feat(integrations): add retro doom web engine and agy proxy nginx config

This commit is contained in:
Antigravity committed 2026-09-22 09:25:23 +00:00
1 parent b8db5a123d
commit 50eadb4b5e
15 files changed
+6397

No files matched your search

+728
View File
@@ -0,0 +1,728 @@
/**
* DOOM 3D - Retro Sound Synthesizer & Music Sequencer (Web Audio API)
* Generates authentic 90s DOOM-inspired sound effects and the iconic E1M1 metal riff.
*/
class SoundSystem {
constructor() {
this.ctx = null;
this.masterGain = null;
this.sfxGain = null;
this.musicGain = null;
this.initialized = false;
this.sfxVolume = 0.8;
this.musicVolume = 0.5;
this.masterVolume = 1.0;
this.musicPlaying = false;
this.musicStep = 0;
this.musicTimer = null;
this.musicBpm = 135;
}
init() {
if (this.initialized) return;
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = this.masterVolume;
this.masterGain.connect(this.ctx.destination);
this.sfxGain = this.ctx.createGain();
this.sfxGain.gain.value = this.sfxVolume;
this.sfxGain.connect(this.masterGain);
this.musicGain = this.ctx.createGain();
this.musicGain.gain.value = this.musicVolume;
this.musicGain.connect(this.masterGain);
this.initialized = true;
this.startMusic();
} catch (e) {
console.warn('Web Audio API not supported or blocked:', e);
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
if (!this.initialized) {
this.init();
}
}
setMasterVolume(val) {
this.masterVolume = Math.max(0, Math.min(1, val));
if (this.masterGain) this.masterGain.gain.setValueAtTime(this.masterVolume, this.ctx.currentTime);
}
setSfxVolume(val) {
this.sfxVolume = Math.max(0, Math.min(1, val));
if (this.sfxGain) this.sfxGain.gain.setValueAtTime(this.sfxVolume, this.ctx.currentTime);
}
setMusicVolume(val) {
this.musicVolume = Math.max(0, Math.min(1, val));
if (this.musicGain) this.musicGain.gain.setValueAtTime(this.musicVolume, this.ctx.currentTime);
}
// Helper: Create White Noise buffer
createNoiseBuffer(duration = 1) {
if (!this.ctx) return null;
const bufferSize = this.ctx.sampleRate * duration;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
// Distortion curve for heavy metal tone
makeDistortionCurve(amount = 50) {
const k = typeof amount === 'number' ? amount : 50;
const n_samples = 44100;
const curve = new Float32Array(n_samples);
const deg = Math.PI / 180;
for (let i = 0; i < n_samples; ++i) {
const x = (i * 2) / n_samples - 1;
curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
}
return curve;
}
// ----------------------------------------------------
// WEAPON SOUND EFFECTS
// ----------------------------------------------------
playFist() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Swoosh
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(120, t);
osc.frequency.exponentialRampToValueAtTime(40, t + 0.15);
gain.gain.setValueAtTime(0.6, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.16);
// Impact punch
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.12);
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, t);
filter.frequency.exponentialRampToValueAtTime(100, t + 0.12);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(0.8, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
playPistol() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Punch osc
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(320, t);
osc.frequency.exponentialRampToValueAtTime(45, t + 0.18);
oGain.gain.setValueAtTime(0.7, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.18);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.19);
// Noise crack
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.2);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(1800, t);
filter.frequency.exponentialRampToValueAtTime(300, t + 0.2);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(0.9, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
playShotgun() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Low boom
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(200, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.35);
oGain.gain.setValueAtTime(1.0, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.36);
// Heavy blast noise
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.35);
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(2800, t);
filter.frequency.exponentialRampToValueAtTime(400, t + 0.35);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(1.2, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
// Pump action slide (clack-clack) after 0.35s
setTimeout(() => {
if (!this.ctx) return;
const t2 = this.ctx.currentTime;
const p1 = this.ctx.createOscillator();
const pGain = this.ctx.createGain();
p1.type = 'square';
p1.frequency.setValueAtTime(400, t2);
p1.frequency.setValueAtTime(650, t2 + 0.06);
pGain.gain.setValueAtTime(0.3, t2);
pGain.gain.exponentialRampToValueAtTime(0.01, t2 + 0.15);
p1.connect(pGain);
pGain.connect(this.sfxGain);
p1.start(t2);
p1.stop(t2 + 0.16);
}, 320);
}
playChaingun() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(260 + Math.random() * 40, t);
osc.frequency.exponentialRampToValueAtTime(50, t + 0.12);
oGain.gain.setValueAtTime(0.65, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.13);
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.12);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(2200, t);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(0.7, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
playRocketLaunch() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Swoosh tone
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, t);
osc.frequency.exponentialRampToValueAtTime(450, t + 0.25);
osc.frequency.exponentialRampToValueAtTime(120, t + 0.5);
oGain.gain.setValueAtTime(0.7, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.5);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.51);
// Rocket booster hiss
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.4);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(800, t);
filter.frequency.linearRampToValueAtTime(1600, t + 0.4);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(0.8, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
playExplosion(volumeMultiplier = 1.0) {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Sub bass boom
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(160, t);
osc.frequency.exponentialRampToValueAtTime(25, t + 0.7);
oGain.gain.setValueAtTime(1.2 * volumeMultiplier, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.7);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.71);
// Crumbling explosion noise
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.8);
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1400, t);
filter.frequency.exponentialRampToValueAtTime(120, t + 0.8);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(1.4 * volumeMultiplier, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.8);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
playPlasma() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(950, t);
osc.frequency.exponentialRampToValueAtTime(180, t + 0.16);
oGain.gain.setValueAtTime(0.7, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.16);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.17);
const osc2 = this.ctx.createOscillator();
const oGain2 = this.ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(1400, t);
osc2.frequency.exponentialRampToValueAtTime(300, t + 0.14);
oGain2.gain.setValueAtTime(0.5, t);
oGain2.gain.exponentialRampToValueAtTime(0.01, t + 0.14);
osc2.connect(oGain2);
oGain2.connect(this.sfxGain);
osc2.start(t);
osc2.stop(t + 0.15);
}
playBFGCharge() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, t);
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.8);
gain.gain.setValueAtTime(0.1, t);
gain.gain.linearRampToValueAtTime(0.9, t + 0.8);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.82);
}
playBFGFire() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Big apocalyptic energy discharge
const osc = this.ctx.createOscillator();
const oGain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(60, t + 0.9);
oGain.gain.setValueAtTime(1.3, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.9);
osc.connect(oGain);
oGain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.92);
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(1.0);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(3000, t);
filter.frequency.exponentialRampToValueAtTime(200, t + 1.0);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(1.5, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 1.0);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
// ----------------------------------------------------
// MONSTER & WORLD SOUND EFFECTS
// ----------------------------------------------------
playEnemyAlert(type = 'zombie') {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
if (type === 'pinky' || type === 'caco') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(140, t);
osc.frequency.linearRampToValueAtTime(220, t + 0.15);
osc.frequency.exponentialRampToValueAtTime(60, t + 0.4);
gain.gain.setValueAtTime(0.7, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.42);
} else {
// Zombie / Imp grunt
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, t);
osc.frequency.exponentialRampToValueAtTime(90, t + 0.28);
gain.gain.setValueAtTime(0.6, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.28);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.3);
}
}
playEnemyPain() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(180, t);
osc.frequency.exponentialRampToValueAtTime(70, t + 0.18);
gain.gain.setValueAtTime(0.6, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.18);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.19);
}
playEnemyDeath() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(130, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.45);
gain.gain.setValueAtTime(0.8, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.45);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.46);
// Gurgle
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.35);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(600, t);
const nGain = this.ctx.createGain();
nGain.gain.setValueAtTime(0.7, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35);
noise.connect(filter);
filter.connect(nGain);
nGain.connect(this.sfxGain);
noise.start(t);
}
playFireball() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.3);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(1200, t);
filter.frequency.exponentialRampToValueAtTime(300, t + 0.3);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.8, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.3);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.sfxGain);
noise.start(t);
}
playItemPickup(type = 'ammo') {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
if (type === 'health' || type === 'armor') {
osc.frequency.setValueAtTime(440, t);
osc.frequency.setValueAtTime(554.37, t + 0.06);
osc.frequency.setValueAtTime(659.25, t + 0.12);
gain.gain.setValueAtTime(0.5, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.26);
} else if (type === 'weapon' || type === 'key') {
// Fanfare chime
osc.frequency.setValueAtTime(330, t);
osc.frequency.setValueAtTime(440, t + 0.08);
osc.frequency.setValueAtTime(660, t + 0.16);
osc.frequency.setValueAtTime(880, t + 0.24);
gain.gain.setValueAtTime(0.7, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.45);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.46);
} else {
// Ammo / click
osc.frequency.setValueAtTime(600, t);
osc.frequency.setValueAtTime(900, t + 0.05);
gain.gain.setValueAtTime(0.4, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.13);
}
}
playDoor(open = true) {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
const startFreq = open ? 90 : 150;
const endFreq = open ? 160 : 70;
osc.frequency.setValueAtTime(startFreq, t);
osc.frequency.linearRampToValueAtTime(endFreq, t + 0.6);
gain.gain.setValueAtTime(0.4, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6);
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(450, t);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.62);
}
playPlayerPain() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(200, t);
osc.frequency.exponentialRampToValueAtTime(80, t + 0.22);
gain.gain.setValueAtTime(0.9, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.22);
osc.connect(gain);
gain.connect(this.sfxGain);
osc.start(t);
osc.stop(t + 0.23);
}
playSlimeHurt() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const noise = this.ctx.createBufferSource();
noise.buffer = this.createNoiseBuffer(0.2);
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(3200, t);
filter.Q.setValueAtTime(3, t);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.5, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.sfxGain);
noise.start(t);
}
// ----------------------------------------------------
// E1M1 RETRO SYNTHESIZER SOUNDTRACK
// ----------------------------------------------------
startMusic() {
if (this.musicPlaying) return;
this.musicPlaying = true;
this.musicStep = 0;
// E1M1 Notes in Hz (E Minor riff)
const E1 = 41.20;
const E2 = 82.41;
const G2 = 98.00;
const A2 = 110.00;
const Bb2 = 116.54;
const B2 = 123.47;
const D3 = 146.83;
const E3 = 164.81;
// 32-step bass riff pattern for classic E1M1
// E E E3 E E D3 E E | C3 E E Bb2 E E B2 C3
const riffNotes = [
E2, E2, E3, E2, E2, D3, E2, E2,
E3, E2, E2, D3, E2, E2, Bb2, B2,
E2, E2, E3, E2, E2, D3, E2, E2,
E3, E2, E2, G2, E2, A2, Bb2, B2
];
const stepDuration = (60 / this.musicBpm) / 4; // 16th notes
const scheduleStep = () => {
if (!this.musicPlaying || !this.ctx) return;
const now = this.ctx.currentTime;
const note = riffNotes[this.musicStep % riffNotes.length];
const isHeavyBeat = (this.musicStep % 4 === 0);
const isSnareBeat = (this.musicStep % 8 === 4);
// Synth Distortion Bass/Guitar
const osc = this.ctx.createOscillator();
const dist = this.ctx.createWaveShaper();
const filter = this.ctx.createBiquadFilter();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(note, now);
dist.curve = this.makeDistortionCurve(30);
dist.oversample = '4x';
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1400, now);
filter.frequency.exponentialRampToValueAtTime(350, now + stepDuration * 0.9);
gain.gain.setValueAtTime(0.35, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + stepDuration * 0.95);
osc.connect(dist);
dist.connect(filter);
filter.connect(gain);
gain.connect(this.musicGain);
osc.start(now);
osc.stop(now + stepDuration);
// Kick Drum on quarter notes
if (isHeavyBeat) {
const kick = this.ctx.createOscillator();
const kGain = this.ctx.createGain();
kick.type = 'sine';
kick.frequency.setValueAtTime(140, now);
kick.frequency.exponentialRampToValueAtTime(35, now + 0.12);
kGain.gain.setValueAtTime(0.7, now);
kGain.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
kick.connect(kGain);
kGain.connect(this.musicGain);
kick.start(now);
kick.stop(now + 0.13);
}
// Snare Drum on 2 and 4
if (isSnareBeat) {
const snare = this.ctx.createBufferSource();
snare.buffer = this.createNoiseBuffer(0.15);
const sFilter = this.ctx.createBiquadFilter();
sFilter.type = 'highpass';
sFilter.frequency.setValueAtTime(1000, now);
const sGain = this.ctx.createGain();
sGain.gain.setValueAtTime(0.45, now);
sGain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
snare.connect(sFilter);
sFilter.connect(sGain);
sGain.connect(this.musicGain);
snare.start(now);
}
// Closed Hi-hat every 8th note
if (this.musicStep % 2 === 0) {
const hat = this.ctx.createBufferSource();
hat.buffer = this.createNoiseBuffer(0.04);
const hFilter = this.ctx.createBiquadFilter();
hFilter.type = 'highpass';
hFilter.frequency.setValueAtTime(7000, now);
const hGain = this.ctx.createGain();
hGain.gain.setValueAtTime(0.15, now);
hGain.gain.exponentialRampToValueAtTime(0.01, now + 0.04);
hat.connect(hFilter);
hFilter.connect(hGain);
hGain.connect(this.musicGain);
hat.start(now);
}
this.musicStep++;
this.musicTimer = setTimeout(scheduleStep, stepDuration * 1000);
};
scheduleStep();
}
stopMusic() {
this.musicPlaying = false;
if (this.musicTimer) {
clearTimeout(this.musicTimer);
this.musicTimer = null;
}
}
toggleMusic() {
if (this.musicPlaying) {
this.stopMusic();
return false;
} else {
this.startMusic();
return true;
}
}
}
window.SoundSystem = SoundSystem;
+689
View File
@@ -0,0 +1,689 @@
/**
* DOOM 3D - Enemies & Monster AI System
* 5 Iconic Doom Demons: Zombieman, Shotgunner, Imp, Pinky Demon, Cacodemon.
* 3D Billboards, multi-state animations, line-of-sight tracking, attacks, and blood FX.
*/
class DoomEnemies {
constructor(engine, player, sound, map) {
this.engine = engine;
this.player = player;
this.sound = sound;
this.map = map;
this.enemies = [];
this.monsterProjectiles = [];
this.textures = {};
this.generateMonsterTextures();
this.spawnMapEnemies();
}
// ----------------------------------------------------
// PROCEDURAL HIGH-DEFINITION RETRO SPRITE TEXTURES
// ----------------------------------------------------
generateMonsterTextures() {
this.textures.zombie = this.createZombieSprites();
this.textures.shotgunner = this.createShotgunnerSprites();
this.textures.imp = this.createImpSprites();
this.textures.pinky = this.createPinkySprites();
this.textures.caco = this.createCacoSprites();
}
createZombieSprites() {
return {
walk1: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ff3300', 'rifle', 1),
walk2: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ff3300', 'rifle', 2),
attack: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ffea00', 'rifle_fire', 1),
pain: this.drawHumanoidSprite('#882222', '#c06060', '#ffffff', 'rifle', 1),
dead: this.drawDeadCorpseSprite('#3d6b2c', '#990000')
};
}
createShotgunnerSprites() {
return {
walk1: this.drawHumanoidSprite('#22252a', '#9098a0', '#ff0000', 'shotgun', 1),
walk2: this.drawHumanoidSprite('#22252a', '#9098a0', '#ff0000', 'shotgun', 2),
attack: this.drawHumanoidSprite('#22252a', '#9098a0', '#ffaa00', 'shotgun_fire', 1),
pain: this.drawHumanoidSprite('#6b1a1a', '#aa4444', '#ffffff', 'shotgun', 1),
dead: this.drawDeadCorpseSprite('#22252a', '#bb0000')
};
}
createImpSprites() {
return {
walk1: this.drawImpSprite(1),
walk2: this.drawImpSprite(2),
attack: this.drawImpSprite(3), // casting fireball
pain: this.drawImpSprite(4),
dead: this.drawDeadCorpseSprite('#6e4428', '#aa1100')
};
}
createPinkySprites() {
return {
walk1: this.drawPinkySprite(1),
walk2: this.drawPinkySprite(2),
attack: this.drawPinkySprite(3), // wide jaws bite
pain: this.drawPinkySprite(4),
dead: this.drawDeadCorpseSprite('#a82838', '#880000')
};
}
createCacoSprites() {
return {
walk1: this.drawCacoSprite(1),
walk2: this.drawCacoSprite(2),
attack: this.drawCacoSprite(3), // blue lightning maw
pain: this.drawCacoSprite(4),
dead: this.drawDeadCorpseSprite('#c41c1c', '#00d4ff')
};
}
drawHumanoidSprite(armorColor, skinColor, eyeColor, weaponType, frame) {
const c = document.createElement('canvas');
c.width = 160;
c.height = 180;
const ctx = c.getContext('2d');
// Shadow under feet
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.ellipse(80, 165, 38, 12, 0, 0, Math.PI * 2);
ctx.fill();
// Boots / Legs
ctx.fillStyle = '#181b16';
if (frame === 1) {
ctx.fillRect(58, 105, 20, 58);
ctx.fillRect(82, 105, 20, 58);
} else {
ctx.fillRect(48, 105, 22, 58);
ctx.fillRect(90, 105, 22, 58);
}
// Torso / Tactical Armor Vest
ctx.fillStyle = armorColor;
ctx.fillRect(50, 48, 60, 60);
ctx.fillStyle = '#1c201a';
ctx.fillRect(56, 54, 48, 18); // Chest plate detail
ctx.fillRect(54, 94, 52, 10); // Utility belt
// Muscular / Zombie Flesh Head
ctx.fillStyle = skinColor;
ctx.fillRect(62, 16, 36, 34);
// Glowing Demonic Eyes
ctx.fillStyle = eyeColor;
ctx.fillRect(68, 26, 8, 7);
ctx.fillRect(84, 26, 8, 7);
// Sinister Mouth
ctx.fillStyle = '#330000';
ctx.fillRect(72, 38, 16, 6);
// Zombie Helmet / Hair
ctx.fillStyle = '#33382e';
ctx.fillRect(58, 10, 44, 12);
// Weapon
if (weaponType.includes('rifle')) {
ctx.fillStyle = '#111316';
ctx.fillRect(75, 62, 60, 14);
ctx.fillStyle = '#444c54';
ctx.fillRect(82, 58, 24, 8);
if (weaponType === 'rifle_fire') {
ctx.fillStyle = '#ffea00';
ctx.beginPath();
ctx.arc(142, 69, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(142, 69, 10, 0, Math.PI * 2);
ctx.fill();
}
} else if (weaponType.includes('shotgun')) {
ctx.fillStyle = '#1a1c20';
ctx.fillRect(70, 64, 70, 18);
ctx.fillStyle = '#7a3e1d'; // Wooden pump
ctx.fillRect(92, 70, 24, 14);
if (weaponType === 'shotgun_fire') {
ctx.fillStyle = '#ffaa00';
ctx.beginPath();
ctx.arc(148, 73, 26, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(148, 73, 14, 0, Math.PI * 2);
ctx.fill();
}
}
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter;
return tex;
}
drawImpSprite(pose) {
const c = document.createElement('canvas');
c.width = 160;
c.height = 180;
const ctx = c.getContext('2d');
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.ellipse(80, 165, 42, 12, 0, 0, Math.PI * 2);
ctx.fill();
// Brown Spiky Demon Body
ctx.fillStyle = pose === 4 ? '#8a2d1d' : '#6b4124';
// Torso
ctx.fillRect(52, 45, 56, 62);
// Demonic Spikes on shoulders & head
ctx.fillStyle = '#e8d8b8';
ctx.fillRect(42, 38, 12, 14);
ctx.fillRect(106, 38, 12, 14);
ctx.fillRect(56, 8, 10, 18);
ctx.fillRect(94, 8, 10, 18);
// Head
ctx.fillStyle = pose === 4 ? '#9c3622' : '#7c4d2d';
ctx.fillRect(58, 18, 44, 35);
// Glowing Fiery Red Eyes
ctx.fillStyle = '#ff0000';
ctx.fillRect(66, 28, 8, 8);
ctx.fillRect(86, 28, 8, 8);
ctx.fillStyle = '#ffff00';
ctx.fillRect(68, 30, 4, 4);
ctx.fillRect(88, 30, 4, 4);
// Sharp Demonic Fangs
ctx.fillStyle = '#ffffff';
for (let i = 0; i < 4; i++) {
ctx.fillRect(68 + i * 7, 42, 4, 6);
}
// Legs with clawed hooves
ctx.fillStyle = '#523018';
if (pose === 1) {
ctx.fillRect(52, 107, 22, 56);
ctx.fillRect(86, 107, 22, 56);
} else if (pose === 2) {
ctx.fillRect(44, 107, 22, 56);
ctx.fillRect(94, 107, 22, 56);
} else {
ctx.fillRect(50, 107, 24, 56);
ctx.fillRect(86, 107, 24, 56);
}
// Fireball casting pose
if (pose === 3) {
// Glowing Fireball in hands
ctx.fillStyle = '#ff5500';
ctx.beginPath();
ctx.arc(125, 75, 24, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(125, 75, 14, 0, Math.PI * 2);
ctx.fill();
}
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter;
return tex;
}
drawPinkySprite(frame) {
const c = document.createElement('canvas');
c.width = 180;
c.height = 180;
const ctx = c.getContext('2d');
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.ellipse(90, 165, 52, 14, 0, 0, Math.PI * 2);
ctx.fill();
// Massive Demonic Muscle Beast (Pinky)
ctx.fillStyle = frame === 4 ? '#781822' : '#b82e44';
ctx.fillRect(40, 40, 100, 70);
// Demon Horns
ctx.fillStyle = '#e6d8c8';
ctx.fillRect(28, 22, 18, 24);
ctx.fillRect(134, 22, 18, 24);
// Huge Demon Jaws & Snout
ctx.fillStyle = '#8a1d2e';
ctx.fillRect(55, 50, 70, 55);
// Glowing Yellow Eyes
ctx.fillStyle = '#ffff33';
ctx.fillRect(60, 55, 12, 10);
ctx.fillRect(108, 55, 12, 10);
// Terrifying Sharp Teeth
ctx.fillStyle = '#ffffff';
for (let i = 0; i < 6; i++) {
ctx.fillRect(62 + i * 10, 85, 6, 12);
if (frame === 3) {
// Open jaw
ctx.fillRect(62 + i * 10, 70, 6, 8);
}
}
// Heavy Stomping Legs
ctx.fillStyle = '#6e1422';
if (frame === 1) {
ctx.fillRect(42, 110, 32, 54);
ctx.fillRect(106, 110, 32, 54);
} else {
ctx.fillRect(30, 110, 32, 54);
ctx.fillRect(118, 110, 32, 54);
}
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter;
return tex;
}
drawCacoSprite(frame) {
const c = document.createElement('canvas');
c.width = 180;
c.height = 180;
const ctx = c.getContext('2d');
// Giant Red Floating Sphere Demon
ctx.fillStyle = '#c41c1c';
ctx.beginPath();
ctx.arc(90, 90, 72, 0, Math.PI * 2);
ctx.fill();
// Spikes on head
ctx.fillStyle = '#6e0c0c';
for (let i = 0; i < 8; i++) {
const angle = Math.PI + (i * Math.PI) / 7;
const hx = 90 + Math.cos(angle) * 70;
const hy = 90 + Math.sin(angle) * 70;
ctx.fillRect(hx - 6, hy - 6, 12, 12);
}
// Massive Emerald Eye
ctx.fillStyle = '#00ff55';
ctx.beginPath();
ctx.arc(90, 65, 24, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#003311';
ctx.beginPath();
ctx.arc(90, 65, 10, 0, Math.PI * 2);
ctx.fill();
// Gaping Maw
ctx.fillStyle = '#111116';
ctx.fillRect(50, 105, 80, 34);
if (frame === 3) {
// Blue Lightning Ball inside mouth
ctx.fillStyle = '#00d4ff';
ctx.beginPath();
ctx.arc(90, 122, 22, 0, Math.PI * 2);
ctx.fill();
} else {
// Teeth
ctx.fillStyle = '#ffffff';
for (let i = 0; i < 7; i++) {
ctx.fillRect(55 + i * 10, 105, 6, 10);
ctx.fillRect(55 + i * 10, 128, 6, 10);
}
}
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter;
return tex;
}
drawDeadCorpseSprite(bodyColor, bloodColor) {
const c = document.createElement('canvas');
c.width = 160;
c.height = 90;
const ctx = c.getContext('2d');
// Blood Pool on Floor
ctx.fillStyle = bloodColor;
ctx.beginPath();
ctx.ellipse(80, 58, 68, 24, 0, 0, Math.PI * 2);
ctx.fill();
// Collapsed Demonic Body
ctx.fillStyle = bodyColor;
ctx.fillRect(35, 36, 95, 28);
// Skull / Slain Head
ctx.fillStyle = '#d0a080';
ctx.fillRect(20, 42, 20, 18);
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter;
return tex;
}
// ----------------------------------------------------
// SPAWNING ENEMIES
// ----------------------------------------------------
spawnMapEnemies() {
const spawns = (this.map && this.map.spawns) ? this.map.spawns.enemies : (window.game && window.game.map ? window.game.map.spawns.enemies : []);
if (!spawns || spawns.length === 0) return;
for (let i = 0; i < spawns.length; i++) {
const s = spawns[i];
this.createEnemy(s.type, s.x, s.y, s.z);
}
}
createEnemy(type, x, y, z) {
let maxHp = 30;
let speed = 4.5;
let scaleW = 2.4;
let scaleH = 2.6;
let attackRange = 28;
let isFlying = false;
if (type === 'zombie') {
maxHp = 30;
speed = 3.6;
attackRange = 30;
} else if (type === 'shotgunner') {
maxHp = 45;
speed = 4.0;
attackRange = 24;
} else if (type === 'imp') {
maxHp = 65;
speed = 4.6;
attackRange = 35;
} else if (type === 'pinky') {
maxHp = 140;
speed = 7.5;
attackRange = 2.8;
scaleW = 2.8;
scaleH = 2.8;
} else if (type === 'caco') {
maxHp = 220;
speed = 4.0;
attackRange = 35;
scaleW = 3.4;
scaleH = 3.4;
isFlying = true;
}
// Sprite Material - depthTest true, depthWrite false for proper transparency
const spriteMat = new THREE.SpriteMaterial({
map: this.textures[type].walk1,
transparent: true,
depthTest: true,
depthWrite: false
});
const sprite = new THREE.Sprite(spriteMat);
const spawnY = isFlying ? (y + 3.5) : (y + scaleH / 2);
sprite.position.set(x, spawnY, z);
sprite.scale.set(scaleW, scaleH, 1);
this.engine.scene.add(sprite);
const enemyObj = {
type,
mesh: sprite,
material: spriteMat,
hp: maxHp,
maxHp: maxHp,
speed: speed,
scaleW: scaleW,
scaleH: scaleH,
radius: scaleW * 0.45,
attackRange: attackRange,
isFlying: isFlying,
state: 'idle',
stateTimer: 0,
animTimer: 0,
animFrame: 1,
attackCooldown: 0.8 + Math.random() * 0.8,
isDead: false,
takeDamage: (dmg, hitPos) => this.damageEnemy(enemyObj, dmg, hitPos)
};
this.enemies.push(enemyObj);
return enemyObj;
}
damageEnemy(e, amount, hitPos) {
if (e.isDead) return;
e.hp -= amount;
if (this.sound) this.sound.playEnemyPain();
// Alert enemy and nearby monsters!
e.state = 'chase';
this.alertNearbyMonsters(e.mesh.position, 35);
if (e.hp <= 0) {
this.killEnemy(e);
} else {
// Pain stagger frame
e.state = 'pain';
e.stateTimer = 0.22;
e.material.map = this.textures[e.type].pain;
}
}
killEnemy(e) {
e.isDead = true;
e.state = 'dead';
e.mesh.scale.set(e.scaleW * 1.3, e.scaleH * 0.5, 1);
if (!e.isFlying) {
e.mesh.position.y = Math.max(0.1, e.mesh.position.y - e.scaleH * 0.35);
}
e.material.map = this.textures[e.type].dead;
if (this.sound) this.sound.playEnemyDeath();
// Drop loot
if (e.type === 'shotgunner') {
if (window.game && window.game.items) {
window.game.items.spawnItem('shotgun', e.mesh.position.x, e.mesh.position.y, e.mesh.position.z);
}
} else if (e.type === 'zombie') {
if (window.game && window.game.items && Math.random() > 0.4) {
window.game.items.spawnItem('ammo_clip', e.mesh.position.x, e.mesh.position.y, e.mesh.position.z);
}
}
}
alertNearbyMonsters(pos, radius) {
for (let i = 0; i < this.enemies.length; i++) {
const other = this.enemies[i];
if (!other.isDead && other.state === 'idle') {
if (other.mesh.position.distanceTo(pos) < radius) {
other.state = 'chase';
if (this.sound) this.sound.playEnemyAlert(other.type);
}
}
}
}
// ----------------------------------------------------
// ENEMY AI & PROJECTILES UPDATE LOOP
// ----------------------------------------------------
update(delta) {
const playerPos = this.player.position;
// 1. Update Demon AI
for (let i = 0; i < this.enemies.length; i++) {
const e = this.enemies[i];
if (e.isDead) continue;
const dist = e.mesh.position.distanceTo(playerPos);
// Alert enemy if player is close or in line of sight
if (e.state === 'idle' && dist < 28) {
e.state = 'chase';
if (this.sound) this.sound.playEnemyAlert(e.type);
}
// State Handling
if (e.state === 'pain') {
e.stateTimer -= delta;
if (e.stateTimer <= 0) {
e.state = 'chase';
}
} else if (e.state === 'attack') {
e.stateTimer -= delta;
if (e.stateTimer <= 0) {
e.state = 'chase';
e.attackCooldown = 1.2 + Math.random() * 0.8;
}
} else if (e.state === 'chase') {
e.attackCooldown -= delta;
// Move towards player
const dir = new THREE.Vector3().subVectors(playerPos, e.mesh.position);
if (!e.isFlying) dir.y = 0;
dir.normalize();
// Check collision with other enemies to prevent stacking
const moveDist = e.speed * delta;
e.mesh.position.addScaledVector(dir, moveDist);
// Animation walking frames
e.animTimer += delta * 6;
if (e.animTimer >= 1.0) {
e.animTimer = 0;
e.animFrame = e.animFrame === 1 ? 2 : 1;
e.material.map = (e.animFrame === 1) ? this.textures[e.type].walk1 : this.textures[e.type].walk2;
}
// Attack Player if within range and cooldown is ready
if (dist <= e.attackRange && e.attackCooldown <= 0) {
this.triggerEnemyAttack(e);
}
}
}
// 2. Update Monster Flying Projectiles (Fireballs & Plasma balls)
for (let i = this.monsterProjectiles.length - 1; i >= 0; i--) {
const p = this.monsterProjectiles[i];
p.mesh.position.addScaledVector(p.velocity, delta);
p.light.position.copy(p.mesh.position);
p.life -= delta;
// Check hit against player
const pDist = p.mesh.position.distanceTo(this.player.position);
if (pDist < 1.4) {
this.player.takeDamage(p.damage, p.mesh.position);
this.engine.createExplosionFX(p.mesh.position, p.isCaco ? 0x00d4ff : 0xff5500);
this.destroyMonsterProjectile(i);
continue;
}
if (p.life <= 0) {
this.destroyMonsterProjectile(i);
}
}
}
triggerEnemyAttack(e) {
e.state = 'attack';
e.stateTimer = 0.5;
e.material.map = this.textures[e.type].attack;
const playerPos = this.player.position;
if (e.type === 'pinky') {
// Melee Bite Attack
if (e.mesh.position.distanceTo(playerPos) < 3.2) {
this.player.takeDamage(18, e.mesh.position);
}
} else if (e.type === 'zombie') {
// Hitscan rifle shot
if (this.sound) this.sound.playPistol();
this.engine.createMuzzleLight(e.mesh.position, 0xffcc00);
this.player.takeDamage(8, e.mesh.position);
} else if (e.type === 'shotgunner') {
// Hitscan shotgun blast
if (this.sound) this.sound.playShotgun();
this.engine.createMuzzleLight(e.mesh.position, 0xff9900);
this.player.takeDamage(18, e.mesh.position);
} else if (e.type === 'imp') {
// Launch Fireball projectile
this.spawnFireball(e.mesh.position, playerPos, 16, 22.0);
} else if (e.type === 'caco') {
// Launch Blue Lightning Plasma projectile
this.spawnCacoBall(e.mesh.position, playerPos, 28, 25.0);
}
}
spawnFireball(from, to, damage, speed) {
const geo = new THREE.SphereGeometry(0.35, 8, 8);
const mat = new THREE.MeshBasicMaterial({ color: 0xff4400 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(from);
this.engine.scene.add(mesh);
const light = new THREE.PointLight(0xff5500, 3.0, 10, 2);
light.position.copy(from);
this.engine.scene.add(light);
const target = new THREE.Vector3(to.x, to.y + 1.2, to.z);
const dir = new THREE.Vector3().subVectors(target, from).normalize();
this.monsterProjectiles.push({
mesh,
light,
velocity: dir.multiplyScalar(speed),
damage,
life: 4.0,
isCaco: false
});
}
spawnCacoBall(from, to, damage, speed) {
const geo = new THREE.SphereGeometry(0.45, 8, 8);
const mat = new THREE.MeshBasicMaterial({ color: 0x00d4ff });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(from);
this.engine.scene.add(mesh);
const light = new THREE.PointLight(0x00d4ff, 3.5, 12, 2);
light.position.copy(from);
this.engine.scene.add(light);
const target = new THREE.Vector3(to.x, to.y + 1.2, to.z);
const dir = new THREE.Vector3().subVectors(target, from).normalize();
this.monsterProjectiles.push({
mesh,
light,
velocity: dir.multiplyScalar(speed),
damage,
life: 4.0,
isCaco: true
});
}
destroyMonsterProjectile(index) {
const p = this.monsterProjectiles[index];
this.engine.scene.remove(p.mesh);
this.engine.scene.remove(p.light);
this.monsterProjectiles.splice(index, 1);
}
}
window.DoomEnemies = DoomEnemies;
+852
View File
@@ -0,0 +1,852 @@
/**
* DOOM 3D - Procedural Texture Generator, Particle System & Three.js Engine Setup
* High performance WebGL engine with authentic 90s DOOM textures and visual effects.
*/
class DoomEngine {
constructor(canvasContainer) {
this.container = canvasContainer;
this.scene = null;
this.camera = null;
this.renderer = null;
this.clock = new THREE.Clock();
// Procedural Textures Cache
this.textures = {};
this.materials = {};
// Particle System
this.particles = [];
this.decals = [];
this.animatedTextures = [];
// Lights
this.lights = [];
this.dynamicLights = [];
this.init();
}
init() {
// 1. Scene & Atmospheric Fog
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x1a0505);
this.scene.fog = new THREE.FogExp2(0x1a0505, 0.015);
// 2. Camera
const aspect = window.innerWidth / window.innerHeight;
this.camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
this.camera.position.set(0, 1.8, 0);
// 3. Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.container.appendChild(this.renderer.domElement);
// 4. Generate Procedural Textures & Materials
this.generateTextures();
this.buildSkybox();
// 5. Global Lighting
this.setupLighting();
// 6. Window Resize Handler
window.addEventListener('resize', () => this.onResize());
}
onResize() {
if (!this.camera || !this.renderer) return;
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
setupLighting() {
// Ambient base light (dark demonic tone)
const ambient = new THREE.AmbientLight(0x382222, 0.9);
this.scene.add(ambient);
this.lights.push(ambient);
// Sun / Directional red overhead light
const sun = new THREE.DirectionalLight(0xff6644, 0.8);
sun.position.set(30, 60, 20);
sun.castShadow = true;
sun.shadow.mapSize.width = 1024;
sun.shadow.mapSize.height = 1024;
sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 200;
sun.shadow.camera.left = -50;
sun.shadow.camera.right = 50;
sun.shadow.camera.top = 50;
sun.shadow.camera.bottom = -50;
this.scene.add(sun);
this.lights.push(sun);
}
addFlickeringLight(x, y, z, color = 0xffaa44, intensity = 2.0, distance = 25) {
const light = new THREE.PointLight(color, intensity, distance, 2);
light.position.set(x, y, z);
this.scene.add(light);
this.dynamicLights.push({
light,
baseIntensity: intensity,
flickerSpeed: 10 + Math.random() * 20,
phase: Math.random() * 10
});
return light;
}
// ----------------------------------------------------
// PROCEDURAL TEXTURE GENERATION
// ----------------------------------------------------
generateTextures() {
this.textures.metal = this.createMetalTexture();
this.textures.tech = this.createTechScreenTexture();
this.textures.brick = this.createBrickTexture();
this.textures.hex = this.createHexMetalTexture();
this.textures.grate = this.createGratingTexture();
this.textures.slime = this.createSlimeTexture();
this.textures.doorRegular = this.createDoorTexture('normal');
this.textures.doorBlue = this.createDoorTexture('blue');
this.textures.doorRed = this.createDoorTexture('red');
this.textures.doorYellow = this.createDoorTexture('yellow');
this.textures.ceiling = this.createCeilingTexture();
this.textures.barrel = this.createBarrelTexture();
// Standard Doom Materials
this.materials.metal = new THREE.MeshStandardMaterial({ map: this.textures.metal, roughness: 0.6, metalness: 0.4 });
this.materials.tech = new THREE.MeshStandardMaterial({ map: this.textures.tech, roughness: 0.4, metalness: 0.2, emissive: 0x112233, emissiveIntensity: 0.4 });
this.materials.brick = new THREE.MeshStandardMaterial({ map: this.textures.brick, roughness: 0.85, metalness: 0.1 });
this.materials.hex = new THREE.MeshStandardMaterial({ map: this.textures.hex, roughness: 0.5, metalness: 0.6 });
this.materials.grate = new THREE.MeshStandardMaterial({ map: this.textures.grate, roughness: 0.7, metalness: 0.5 });
this.materials.slime = new THREE.MeshStandardMaterial({ map: this.textures.slime.texture, roughness: 0.2, metalness: 0.1, emissive: 0x00ff22, emissiveIntensity: 0.6 });
this.materials.doorRegular = new THREE.MeshStandardMaterial({ map: this.textures.doorRegular, roughness: 0.5, metalness: 0.5 });
this.materials.doorBlue = new THREE.MeshStandardMaterial({ map: this.textures.doorBlue, roughness: 0.5, metalness: 0.5, emissive: 0x0044ff, emissiveIntensity: 0.3 });
this.materials.doorRed = new THREE.MeshStandardMaterial({ map: this.textures.doorRed, roughness: 0.5, metalness: 0.5, emissive: 0xff0022, emissiveIntensity: 0.3 });
this.materials.doorYellow = new THREE.MeshStandardMaterial({ map: this.textures.doorYellow, roughness: 0.5, metalness: 0.5, emissive: 0xffbb00, emissiveIntensity: 0.3 });
this.materials.ceiling = new THREE.MeshStandardMaterial({ map: this.textures.ceiling, roughness: 0.7, metalness: 0.3 });
this.materials.barrel = new THREE.MeshStandardMaterial({ map: this.textures.barrel, roughness: 0.6, metalness: 0.3 });
}
createMetalTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
// Steel base
ctx.fillStyle = '#484440';
ctx.fillRect(0, 0, 256, 256);
// Heavy steel panel borders
ctx.strokeStyle = '#22201e';
ctx.lineWidth = 4;
ctx.strokeRect(4, 4, 248, 248);
ctx.strokeRect(10, 10, 114, 114);
ctx.strokeRect(132, 10, 114, 114);
ctx.strokeRect(10, 132, 114, 114);
ctx.strokeRect(132, 132, 114, 114);
// Rivets / Bolts
ctx.fillStyle = '#1c1b18';
const rivetPoints = [
[16, 16], [118, 16], [16, 118], [118, 118],
[138, 16], [240, 16], [138, 118], [240, 118],
[16, 138], [118, 138], [16, 240], [118, 240],
[138, 138], [240, 138], [138, 240], [240, 240]
];
rivetPoints.forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#6a6560';
ctx.fillRect(x - 1, y - 1, 2, 2);
ctx.fillStyle = '#1c1b18';
});
// Grime and scratches
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
for (let i = 0; i < 600; i++) {
ctx.fillRect(Math.random() * 256, Math.random() * 256, Math.random() * 3 + 1, Math.random() * 3 + 1);
}
// Rust patches
ctx.fillStyle = 'rgba(120, 60, 20, 0.25)';
ctx.fillRect(10, 110, 80, 14);
ctx.fillRect(150, 10, 20, 90);
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
createTechScreenTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
// Dark tech casing
ctx.fillStyle = '#202428';
ctx.fillRect(0, 0, 256, 256);
// Terminal Screen 1 (Top Blue Terminal)
ctx.fillStyle = '#081420';
ctx.fillRect(16, 16, 224, 100);
ctx.strokeStyle = '#3a4450';
ctx.lineWidth = 3;
ctx.strokeRect(16, 16, 224, 100);
// Oscilloscope / Data wave
ctx.strokeStyle = '#00ffff';
ctx.lineWidth = 2;
ctx.beginPath();
for (let x = 20; x < 230; x += 5) {
const y = 66 + Math.sin(x * 0.1) * 20 + (Math.random() * 6 - 3);
if (x === 20) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Terminal Text lines
ctx.fillStyle = '#44ff88';
ctx.font = '10px monospace';
ctx.fillText('UAC MAINFRAME // LEVEL 1', 24, 34);
ctx.fillStyle = '#00aaff';
ctx.fillText('SYSTEM STATUS: CRITICAL', 24, 48);
ctx.fillStyle = '#ff4444';
ctx.fillText('REACTOR OVERHEAT: 88.4%', 24, 98);
// Lower Control Panel with LED buttons and sliders
ctx.fillStyle = '#16181b';
ctx.fillRect(16, 128, 224, 112);
ctx.strokeStyle = '#40454c';
ctx.strokeRect(16, 128, 224, 112);
// Blinking LEDs
const ledColors = ['#ff0000', '#00ff00', '#ffff00', '#00e1ff', '#ff00ff'];
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 7; col++) {
ctx.fillStyle = ledColors[(row * 7 + col) % ledColors.length];
ctx.fillRect(28 + col * 28, 142 + row * 26, 14, 14);
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.strokeRect(28 + col * 28, 142 + row * 26, 14, 14);
}
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
createBrickTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
// Dark demonic brick mortar
ctx.fillStyle = '#180e0c';
ctx.fillRect(0, 0, 256, 256);
// Red/brown stone bricks
const brickH = 32;
const brickW = 64;
const rows = 8;
const cols = 4;
for (let r = 0; r < rows; r++) {
const offset = (r % 2) * (brickW / 2);
for (let c = -1; c <= cols; c++) {
const x = c * brickW + offset + 2;
const y = r * brickH + 2;
const shade = 70 + Math.floor(Math.random() * 40);
const redShade = shade + 40;
ctx.fillStyle = `rgb(${redShade}, ${Math.floor(shade * 0.4)}, ${Math.floor(shade * 0.35)})`;
ctx.fillRect(x, y, brickW - 4, brickH - 4);
// Brick cracks & texture
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(x + Math.random() * 20, y + 4, Math.random() * 25 + 5, 2);
ctx.fillRect(x + 4, y + Math.random() * 15, 2, Math.random() * 10 + 4);
}
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
createHexMetalTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
ctx.fillStyle = '#26292d';
ctx.fillRect(0, 0, 256, 256);
// Hexagonal grid
const size = 32;
const h = size * Math.sqrt(3);
const drawHex = (x, y, radius) => {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const hx = x + radius * Math.cos(angle);
const hy = y + radius * Math.sin(angle);
if (i === 0) ctx.moveTo(hx, hy);
else ctx.lineTo(hx, hy);
}
ctx.closePath();
};
for (let row = -1; row < 10; row++) {
for (let col = -1; col < 10; col++) {
const x = col * size * 1.5;
const y = row * h + ((col % 2) * h / 2);
ctx.fillStyle = (row + col) % 2 === 0 ? '#34383e' : '#2b2f34';
drawHex(x, y, size * 0.9);
ctx.fill();
ctx.strokeStyle = '#181a1d';
ctx.lineWidth = 2;
ctx.stroke();
// Center glowing dot
if ((row + col) % 3 === 0) {
ctx.fillStyle = '#ff6600';
ctx.beginPath();
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
ctx.fill();
}
}
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
createGratingTexture() {
const c = document.createElement('canvas');
c.width = 128;
c.height = 128;
const ctx = c.getContext('2d');
ctx.fillStyle = '#1c1c1f';
ctx.fillRect(0, 0, 128, 128);
// Grate lines
ctx.fillStyle = '#3a3a40';
for (let i = 0; i < 128; i += 16) {
ctx.fillRect(i, 0, 4, 128);
ctx.fillRect(0, i, 128, 4);
}
// Shadow pits
ctx.fillStyle = '#0a0a0c';
for (let x = 4; x < 128; x += 16) {
for (let y = 4; y < 128; y += 16) {
ctx.fillRect(x + 2, y + 2, 8, 8);
}
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
createSlimeTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
const animObj = {
canvas: c,
ctx: ctx,
texture: tex,
time: 0,
update: (delta) => {
animObj.time += delta * 2;
const t = animObj.time;
// Radioactive green gradient
ctx.fillStyle = '#154408';
ctx.fillRect(0, 0, 256, 256);
// Bubbles and ripples
for (let i = 0; i < 16; i++) {
const bx = 128 + Math.cos(t * 0.8 + i * 1.3) * 90 + Math.sin(i * 4) * 20;
const by = 128 + Math.sin(t * 0.9 + i * 1.5) * 90 + Math.cos(i * 3) * 20;
const rad = 10 + Math.sin(t * 3 + i) * 6;
const grad = ctx.createRadialGradient(bx, by, 2, bx, by, rad);
grad.addColorStop(0, '#88ff22');
grad.addColorStop(0.7, '#22aa00');
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(bx, by, rad, 0, Math.PI * 2);
ctx.fill();
}
// Swirling slime streaks
ctx.strokeStyle = 'rgba(100, 255, 30, 0.3)';
ctx.lineWidth = 6;
ctx.beginPath();
for (let x = 0; x < 256; x += 10) {
const y = 128 + Math.sin(x * 0.05 + t) * 40 + Math.cos(x * 0.02 - t) * 30;
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
tex.needsUpdate = true;
}
};
this.animatedTextures.push(animObj);
return animObj;
}
createDoorTexture(type = 'normal') {
const c = document.createElement('canvas');
c.width = 256;
c.height = 512;
const ctx = c.getContext('2d');
// Blast door base
ctx.fillStyle = '#3a3a40';
ctx.fillRect(0, 0, 256, 512);
// Outer heavy steel frame
ctx.strokeStyle = '#18181a';
ctx.lineWidth = 12;
ctx.strokeRect(6, 6, 244, 500);
// Center seam split
ctx.strokeStyle = '#111';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(128, 12);
ctx.lineTo(128, 500);
ctx.stroke();
// Hazard Stripes at top & bottom
const drawHazard = (yStart, h) => {
ctx.save();
ctx.beginPath();
ctx.rect(12, yStart, 232, h);
ctx.clip();
ctx.fillStyle = '#ffaa00';
ctx.fillRect(12, yStart, 232, h);
ctx.fillStyle = '#111';
for (let x = -50; x < 300; x += 30) {
ctx.beginPath();
ctx.moveTo(x, yStart);
ctx.lineTo(x + 20, yStart);
ctx.lineTo(x - 10, yStart + h);
ctx.lineTo(x - 30, yStart + h);
ctx.closePath();
ctx.fill();
}
ctx.restore();
};
drawHazard(12, 40);
drawHazard(460, 40);
// Center Keycard Lock Indicator
ctx.fillStyle = '#1e2024';
ctx.fillRect(88, 220, 80, 72);
ctx.strokeStyle = '#555';
ctx.lineWidth = 3;
ctx.strokeRect(88, 220, 80, 72);
if (type === 'blue') {
ctx.fillStyle = '#0088ff';
ctx.fillRect(96, 230, 64, 52);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px monospace';
ctx.fillText('BLUE', 110, 260);
} else if (type === 'red') {
ctx.fillStyle = '#ff1122';
ctx.fillRect(96, 230, 64, 52);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px monospace';
ctx.fillText('RED', 114, 260);
} else if (type === 'yellow') {
ctx.fillStyle = '#ffbb00';
ctx.fillRect(96, 230, 64, 52);
ctx.fillStyle = '#111111';
ctx.font = 'bold 12px monospace';
ctx.fillText('YELW', 110, 260);
} else {
// Normal door UAC symbol
ctx.fillStyle = '#888';
ctx.font = 'bold 16px monospace';
ctx.fillText('UAC', 112, 262);
}
const tex = new THREE.CanvasTexture(c);
return tex;
}
createCeilingTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
ctx.fillStyle = '#222428';
ctx.fillRect(0, 0, 256, 256);
// Ceiling grid
ctx.strokeStyle = '#121316';
ctx.lineWidth = 6;
ctx.strokeRect(0, 0, 256, 256);
// Fluorescent light strip
ctx.fillStyle = '#e8f4ff';
ctx.fillRect(40, 112, 176, 32);
ctx.strokeStyle = '#556677';
ctx.lineWidth = 2;
ctx.strokeRect(40, 112, 176, 32);
// Light grill
ctx.strokeStyle = '#8899aa';
for (let x = 50; x < 210; x += 15) {
ctx.beginPath();
ctx.moveTo(x, 112);
ctx.lineTo(x, 144);
ctx.stroke();
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
createBarrelTexture() {
const c = document.createElement('canvas');
c.width = 256;
c.height = 256;
const ctx = c.getContext('2d');
// Toxic green barrel metal
ctx.fillStyle = '#3a6624';
ctx.fillRect(0, 0, 256, 256);
// Metal ribs
ctx.fillStyle = '#224412';
ctx.fillRect(0, 20, 256, 16);
ctx.fillRect(0, 120, 256, 16);
ctx.fillRect(0, 220, 256, 16);
// Biohazard / Radiation Symbol
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(128, 70, 30, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.arc(128, 70, 8, 0, Math.PI * 2);
ctx.fill();
for (let i = 0; i < 3; i++) {
const angle = (i * 2 * Math.PI) / 3;
ctx.beginPath();
ctx.arc(128, 70, 25, angle - 0.5, angle + 0.5);
ctx.lineTo(128, 70);
ctx.fill();
}
// Toxic Slime leak
ctx.fillStyle = '#88ff00';
ctx.fillRect(80, 140, 16, 80);
ctx.fillRect(160, 160, 22, 60);
const tex = new THREE.CanvasTexture(c);
return tex;
}
// ----------------------------------------------------
// RETRO DOOM SKYBOX
// ----------------------------------------------------
buildSkybox() {
const c = document.createElement('canvas');
c.width = 1024;
c.height = 512;
const ctx = c.getContext('2d');
// Dark apocalyptic red/purple gradient
const grad = ctx.createLinearGradient(0, 0, 0, 512);
grad.addColorStop(0, '#100208');
grad.addColorStop(0.3, '#4a0814');
grad.addColorStop(0.65, '#9e1e12');
grad.addColorStop(0.85, '#e04a1b');
grad.addColorStop(1, '#ff8833');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 1024, 512);
// Stars
ctx.fillStyle = '#ffffff';
for (let i = 0; i < 150; i++) {
ctx.fillRect(Math.random() * 1024, Math.random() * 250, Math.random() * 2 + 1, Math.random() * 2 + 1);
}
// Burning Hell Moon
const moonGrad = ctx.createRadialGradient(750, 140, 10, 750, 140, 60);
moonGrad.addColorStop(0, '#ffffff');
moonGrad.addColorStop(0.3, '#ffcc44');
moonGrad.addColorStop(0.7, '#ff3300');
moonGrad.addColorStop(1, 'transparent');
ctx.fillStyle = moonGrad;
ctx.beginPath();
ctx.arc(750, 140, 60, 0, Math.PI * 2);
ctx.fill();
// Jagged Volcanic Mountains silhouette
ctx.fillStyle = '#1c0406';
ctx.beginPath();
ctx.moveTo(0, 512);
let curY = 360;
for (let x = 0; x <= 1024; x += 32) {
curY = 320 + Math.sin(x * 0.02) * 60 + Math.cos(x * 0.05) * 40 + (Math.random() * 20 - 10);
ctx.lineTo(x, curY);
}
ctx.lineTo(1024, 512);
ctx.closePath();
ctx.fill();
const skyTexture = new THREE.CanvasTexture(c);
skyTexture.mapping = THREE.EquirectangularReflectionMapping;
// Big Dome Sphere
const skyGeo = new THREE.SphereGeometry(400, 32, 24);
const skyMat = new THREE.MeshBasicMaterial({
map: skyTexture,
side: THREE.BackSide,
depthWrite: false
});
const skyMesh = new THREE.Mesh(skyGeo, skyMat);
this.scene.add(skyMesh);
this.skyboxMesh = skyMesh;
}
// ----------------------------------------------------
// PARTICLE & EXPLOSION ENGINE
// ----------------------------------------------------
spawnBloodBurst(pos, normal = new THREE.Vector3(0, 1, 0), count = 18, color = 0xcc0000) {
for (let i = 0; i < count; i++) {
const size = 0.08 + Math.random() * 0.12;
const geo = new THREE.BoxGeometry(size, size, size);
const mat = new THREE.MeshBasicMaterial({ color: color });
const p = new THREE.Mesh(geo, mat);
p.position.copy(pos);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 6 + normal.x * 3,
Math.random() * 5 + 2,
(Math.random() - 0.5) * 6 + normal.z * 3
);
this.scene.add(p);
this.particles.push({
mesh: p,
velocity: vel,
gravity: -16,
lifetime: 0.6 + Math.random() * 0.4,
age: 0
});
}
}
spawnSparks(pos, normal = new THREE.Vector3(0, 1, 0), count = 12) {
for (let i = 0; i < count; i++) {
const size = 0.04 + Math.random() * 0.05;
const geo = new THREE.BoxGeometry(size, size, size);
const mat = new THREE.MeshBasicMaterial({ color: 0xffcc33 });
const p = new THREE.Mesh(geo, mat);
p.position.copy(pos);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 8 + normal.x * 4,
Math.random() * 6 + 1 + normal.y * 3,
(Math.random() - 0.5) * 8 + normal.z * 4
);
this.scene.add(p);
this.particles.push({
mesh: p,
velocity: vel,
gravity: -12,
lifetime: 0.25 + Math.random() * 0.25,
age: 0
});
}
}
spawnExplosion(pos, radius = 3.5, color = 0xff6600) {
// 1. Expanding Core Sphere
const geo = new THREE.SphereGeometry(0.5, 12, 8);
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.9 });
const core = new THREE.Mesh(geo, mat);
core.position.copy(pos);
this.scene.add(core);
// 2. Dynamic Explosion Flash Light
const expLight = new THREE.PointLight(color, 6.0, radius * 4, 1.5);
expLight.position.copy(pos);
this.scene.add(expLight);
this.particles.push({
mesh: core,
light: expLight,
isExplosionCore: true,
maxRadius: radius,
lifetime: 0.45,
age: 0
});
// 3. Smoke & Fire Debris
for (let i = 0; i < 24; i++) {
const size = 0.2 + Math.random() * 0.35;
const dGeo = new THREE.BoxGeometry(size, size, size);
const dMat = new THREE.MeshBasicMaterial({
color: Math.random() > 0.4 ? 0xff4400 : 0x444444,
transparent: true,
opacity: 0.9
});
const d = new THREE.Mesh(dGeo, dMat);
d.position.copy(pos);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 14,
Math.random() * 10 + 2,
(Math.random() - 0.5) * 14
);
this.scene.add(d);
this.particles.push({
mesh: d,
velocity: vel,
gravity: -8,
lifetime: 0.7 + Math.random() * 0.5,
age: 0
});
}
}
spawnPlasmaImpact(pos, normal = new THREE.Vector3(0, 1, 0), color = 0x00d4ff) {
const light = new THREE.PointLight(color, 3.0, 8, 2);
light.position.copy(pos);
this.scene.add(light);
for (let i = 0; i < 10; i++) {
const size = 0.08 + Math.random() * 0.08;
const geo = new THREE.BoxGeometry(size, size, size);
const mat = new THREE.MeshBasicMaterial({ color: color });
const p = new THREE.Mesh(geo, mat);
p.position.copy(pos);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 7 + normal.x * 3,
Math.random() * 5 + 1,
(Math.random() - 0.5) * 7 + normal.z * 3
);
this.scene.add(p);
this.particles.push({
mesh: p,
velocity: vel,
gravity: -5,
lifetime: 0.3 + Math.random() * 0.2,
age: 0
});
}
setTimeout(() => {
this.scene.remove(light);
}, 150);
}
// ----------------------------------------------------
// UPDATE GAME LOOP
// ----------------------------------------------------
update(delta) {
// 1. Update animated textures (Toxic Slime)
for (let i = 0; i < this.animatedTextures.length; i++) {
this.animatedTextures[i].update(delta);
}
// 2. Flickering dynamic lights
const t = this.clock.getElapsedTime();
for (let i = 0; i < this.dynamicLights.length; i++) {
const dl = this.dynamicLights[i];
const noise = Math.sin(t * dl.flickerSpeed + dl.phase) * 0.35 + (Math.random() * 0.2 - 0.1);
dl.light.intensity = Math.max(0.2, dl.baseIntensity + noise);
}
// 3. Keep Skybox centered on player camera
if (this.skyboxMesh && this.camera) {
this.skyboxMesh.position.copy(this.camera.position);
}
// 4. Update Particle physics & lifetimes
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.age += delta;
if (p.age >= p.lifetime) {
this.scene.remove(p.mesh);
if (p.light) this.scene.remove(p.light);
this.particles.splice(i, 1);
continue;
}
const progress = p.age / p.lifetime;
if (p.isExplosionCore) {
const scale = 1 + progress * (p.maxRadius * 2);
p.mesh.scale.set(scale, scale, scale);
p.mesh.material.opacity = 1.0 - progress;
if (p.light) {
p.light.intensity = 6.0 * (1.0 - progress);
}
} else if (p.velocity) {
p.velocity.y += p.gravity * delta;
p.mesh.position.addScaledVector(p.velocity, delta);
if (p.mesh.material.transparent) {
p.mesh.material.opacity = 1.0 - progress;
}
}
}
}
render() {
this.renderer.render(this.scene, this.camera);
}
}
window.DoomEngine = DoomEngine;
+270
View File
@@ -0,0 +1,270 @@
/**
* DOOM 3D - Authentic Retro Status Bar & HUD
* LED segmented numbers, animated Doomguy face canvas reacting to HP/pain/godmode,
* weapon slot matrix, keycard inventory, ticker notifications, and damage screen flashes.
*/
class DoomHUD {
constructor(player) {
this.player = player;
// Container elements
this.hudContainer = document.getElementById('doom-hud');
this.damageOverlay = document.getElementById('damage-flash');
this.ticker = document.getElementById('hud-ticker');
this.faceCanvas = document.getElementById('doomguy-face');
this.faceCtx = this.faceCanvas ? this.faceCanvas.getContext('2d') : null;
// Doomguy Face animation states
this.faceLookDir = 0; // -1 = left, 0 = forward, 1 = right
this.faceTimer = 0;
this.painTimer = 0;
this.grinTimer = 0;
// Notification ticker
this.tickerTimer = 0;
// Flash states
this.flashAlpha = 0;
this.flashColor = 'rgba(255, 0, 0, ';
this.init();
}
init() {
if (this.faceCanvas) {
this.faceCanvas.width = 64;
this.faceCanvas.height = 72;
}
}
showNotification(text) {
if (!this.ticker) return;
this.ticker.innerText = text.toUpperCase();
this.ticker.style.opacity = '1';
this.tickerTimer = 3.5;
}
triggerDamageFlash(color = 'red') {
this.flashAlpha = 0.55;
this.painTimer = 0.5;
if (color === 'green') {
this.flashColor = 'rgba(50, 255, 0, ';
} else if (color === 'yellow') {
this.flashColor = 'rgba(255, 220, 0, ';
} else {
this.flashColor = 'rgba(220, 0, 0, ';
}
}
triggerGrin() {
this.grinTimer = 1.2;
}
showGameOver() {
const overlay = document.getElementById('game-over-modal');
if (overlay) overlay.classList.remove('hidden');
}
hideGameOver() {
const overlay = document.getElementById('game-over-modal');
if (overlay) overlay.classList.add('hidden');
}
// ----------------------------------------------------
// DOOMGUY FACE RENDERING (Dynamic Procedural Pixel Art)
// ----------------------------------------------------
renderDoomguyFace(delta) {
if (!this.faceCtx) return;
const ctx = this.faceCtx;
ctx.clearRect(0, 0, 64, 72);
const hp = this.player.health;
const isDead = this.player.isDead;
const godMode = this.player.godMode;
// Update look direction timer
this.faceTimer += delta;
if (this.faceTimer > 1.8) {
this.faceTimer = 0;
const r = Math.random();
this.faceLookDir = r < 0.3 ? -1 : (r > 0.7 ? 1 : 0);
}
if (this.painTimer > 0) this.painTimer -= delta;
if (this.grinTimer > 0) this.grinTimer -= delta;
// 1. Background / Skin Base
let skinTone = '#d49b78';
if (hp <= 40) skinTone = '#c28766';
if (hp <= 20) skinTone = '#b07255';
if (isDead) skinTone = '#808a70'; // Pale zombie/corpse skin
// Jaw / Head Contour
ctx.fillStyle = skinTone;
ctx.fillRect(12, 10, 40, 52);
ctx.fillRect(16, 62, 32, 6);
// Brown Hair & Crewcut
ctx.fillStyle = '#4a2c18';
ctx.fillRect(10, 4, 44, 14);
ctx.fillRect(8, 12, 6, 20);
ctx.fillRect(50, 12, 6, 20);
if (isDead) {
// Dead Doomguy: X-eyes or blank white eyes + blood
ctx.fillStyle = '#111';
ctx.fillRect(18, 30, 10, 4);
ctx.fillRect(36, 30, 10, 4);
ctx.fillStyle = '#aa0000';
ctx.fillRect(26, 44, 12, 18);
ctx.fillRect(16, 52, 32, 6);
return;
}
// 2. Eyes
const eyeOffset = this.faceLookDir * 2;
ctx.fillStyle = '#ffffff';
ctx.fillRect(18, 28, 10, 8);
ctx.fillRect(36, 28, 10, 8);
// Pupils
if (godMode) {
// Glowing Yellow God Mode Eyes
ctx.fillStyle = '#ffff00';
ctx.fillRect(18, 28, 10, 8);
ctx.fillRect(36, 28, 10, 8);
} else {
ctx.fillStyle = '#3a6688'; // Blue eyes
ctx.fillRect(21 + eyeOffset, 30, 4, 4);
ctx.fillRect(39 + eyeOffset, 30, 4, 4);
}
// Eyebrows
ctx.fillStyle = '#3a2010';
if (this.painTimer > 0) {
// Pain / Anguished eyebrows
ctx.fillRect(16, 24, 12, 3);
ctx.fillRect(36, 24, 12, 3);
} else if (this.grinTimer > 0) {
// Evil smirk eyebrows
ctx.fillRect(16, 26, 12, 3);
ctx.fillRect(36, 23, 12, 3);
} else {
// Normal stern eyebrows
ctx.fillRect(16, 25, 12, 3);
ctx.fillRect(36, 25, 12, 3);
}
// 3. Nose
ctx.fillStyle = '#a66e4e';
ctx.fillRect(29, 36, 6, 12);
if (hp <= 60) {
// Bloody nose
ctx.fillStyle = '#aa0000';
ctx.fillRect(29, 44, 6, 6);
ctx.fillRect(31, 50, 3, 6);
}
// 4. Mouth / Expressions
if (this.painTimer > 0) {
// Ouch open screaming mouth
ctx.fillStyle = '#111';
ctx.fillRect(24, 52, 16, 10);
ctx.fillStyle = '#fff';
ctx.fillRect(26, 52, 12, 3);
} else if (this.grinTimer > 0) {
// Evil grin
ctx.fillStyle = '#111';
ctx.fillRect(22, 53, 20, 6);
ctx.fillStyle = '#fff';
ctx.fillRect(24, 54, 16, 3);
} else if (hp <= 30) {
// Bruised grimace
ctx.fillStyle = '#441111';
ctx.fillRect(24, 54, 16, 4);
} else {
// Classic Doomguy smirk
ctx.fillStyle = '#6b3e28';
ctx.fillRect(24, 54, 16, 4);
}
// 5. Blood wounds for low health
if (hp <= 80) {
ctx.fillStyle = '#aa0000';
ctx.fillRect(44, 18, 4, 8); // Forehead cut
}
if (hp <= 40) {
ctx.fillStyle = '#aa0000';
ctx.fillRect(14, 38, 8, 10); // Cheek wound
ctx.fillRect(40, 56, 6, 8);
}
if (hp <= 20) {
ctx.fillStyle = '#880000';
ctx.fillRect(16, 28, 14, 10); // Black eye
}
}
// ----------------------------------------------------
// UPDATE STATUS BAR METRICS
// ----------------------------------------------------
update(delta) {
// 1. Ticker fadeout
if (this.tickerTimer > 0) {
this.tickerTimer -= delta;
if (this.tickerTimer <= 0 && this.ticker) {
this.ticker.style.opacity = '0';
}
}
// 2. Damage Screen Flash fadeout
if (this.flashAlpha > 0) {
this.flashAlpha = Math.max(0, this.flashAlpha - delta * 2.0);
if (this.damageOverlay) {
this.damageOverlay.style.backgroundColor = `${this.flashColor}${this.flashAlpha})`;
}
}
// 3. Render Animated Doomguy Face
this.renderDoomguyFace(delta);
// 4. Update HUD Numerical Values
const currentWeapon = (window.game && window.game.weapons) ? window.game.weapons.current : null;
const ammoCount = (currentWeapon && currentWeapon.ammoType) ? this.player.ammo[currentWeapon.ammoType] : '---';
const ammoElem = document.getElementById('stat-ammo');
const hpElem = document.getElementById('stat-health');
const armorElem = document.getElementById('stat-armor');
if (ammoElem) ammoElem.innerText = ammoCount;
if (hpElem) hpElem.innerText = `${Math.max(0, this.player.health)}%`;
if (armorElem) armorElem.innerText = `${this.player.armor}%`;
// 5. Update ARMS Numbers (1-7)
if (window.game && window.game.weapons) {
const weapons = window.game.weapons;
for (let i = 0; i < 7; i++) {
const armSlot = document.getElementById(`arm-slot-${i + 1}`);
if (armSlot) {
const isUnlocked = weapons.unlocked[i];
const isCurrent = (weapons.currentIndex === i);
armSlot.className = `arm-num ${isUnlocked ? 'active' : 'inactive'} ${isCurrent ? 'selected' : ''}`;
}
}
}
// 6. Update KEYS Indicators (Blue, Red, Yellow)
const blueKeyElem = document.getElementById('key-blue');
const redKeyElem = document.getElementById('key-red');
const yellowKeyElem = document.getElementById('key-yellow');
if (blueKeyElem) blueKeyElem.className = `key-slot ${this.player.keysInventory.blue ? 'has-key' : ''}`;
if (redKeyElem) redKeyElem.className = `key-slot ${this.player.keysInventory.red ? 'has-key' : ''}`;
if (yellowKeyElem) yellowKeyElem.className = `key-slot ${this.player.keysInventory.yellow ? 'has-key' : ''}`;
}
}
window.DoomHUD = DoomHUD;
+335
View File
@@ -0,0 +1,335 @@
/**
* DOOM 3D - Pickups, Keycards & Explosive Barrels System
* 3D rotating items, bobbing animations, and chain reaction explosive toxic barrels.
*/
class DoomItems {
constructor(engine, player, sound, map) {
this.engine = engine;
this.player = player;
this.sound = sound;
this.map = map;
this.items = [];
this.barrels = [];
this.itemTextures = {};
this.generateItemTextures();
this.spawnMapItems();
this.spawnMapBarrels();
}
generateItemTextures() {
this.itemTextures.medikit = this.createMedikitTexture();
this.itemTextures.armorGreen = this.createArmorTexture('#22aa22');
this.itemTextures.armorBlue = this.createArmorTexture('#0088ff');
this.itemTextures.ammoClip = this.createAmmoTexture('CLIP');
this.itemTextures.ammoShells = this.createAmmoTexture('SHELLS');
this.itemTextures.ammoRocket = this.createAmmoTexture('ROCKETS');
this.itemTextures.ammoCell = this.createAmmoTexture('CELLS');
}
createMedikitTexture() {
const c = document.createElement('canvas');
c.width = 64;
c.height = 64;
const ctx = c.getContext('2d');
ctx.fillStyle = '#eeeeee';
ctx.fillRect(0, 0, 64, 64);
ctx.fillStyle = '#cc0000';
// Red cross
ctx.fillRect(24, 10, 16, 44);
ctx.fillRect(10, 24, 44, 16);
const tex = new THREE.CanvasTexture(c);
return tex;
}
createArmorTexture(color) {
const c = document.createElement('canvas');
c.width = 64;
c.height = 64;
const ctx = c.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(8, 8, 48, 48);
ctx.fillStyle = '#ffffff';
ctx.fillRect(18, 18, 28, 28);
const tex = new THREE.CanvasTexture(c);
return tex;
}
createAmmoTexture(label) {
const c = document.createElement('canvas');
c.width = 64;
c.height = 64;
const ctx = c.getContext('2d');
ctx.fillStyle = '#3a3a30';
ctx.fillRect(0, 0, 64, 64);
ctx.fillStyle = '#ddaa00';
ctx.font = 'bold 12px monospace';
ctx.fillText(label, 6, 36);
const tex = new THREE.CanvasTexture(c);
return tex;
}
// ----------------------------------------------------
// SPAWNING ITEMS & KEYCARDS
// ----------------------------------------------------
spawnMapItems() {
const spawns = (this.map && this.map.spawns) ? this.map.spawns.items : (window.game && window.game.map ? window.game.map.spawns.items : []);
if (!spawns || spawns.length === 0) return;
for (let i = 0; i < spawns.length; i++) {
const s = spawns[i];
this.spawnItem(s.type, s.x, s.y, s.z);
}
}
spawnItem(type, x, y, z) {
let mesh = null;
let light = null;
let scale = 1.0;
if (type.startsWith('key_')) {
// 3D Keycard with colored glowing point light
const colorName = type.replace('key_', '');
let colorHex = 0x0088ff;
if (colorName === 'red') colorHex = 0xff1122;
if (colorName === 'yellow') colorHex = 0xffbb00;
const geo = new THREE.BoxGeometry(0.8, 1.2, 0.15);
const mat = new THREE.MeshStandardMaterial({
color: colorHex,
emissive: colorHex,
emissiveIntensity: 0.8
});
mesh = new THREE.Mesh(geo, mat);
light = new THREE.PointLight(colorHex, 2.5, 6, 2);
light.position.set(x, y + 0.6, z);
this.engine.scene.add(light);
} else if (type === 'soulsphere') {
// Blue glowing soulsphere
const geo = new THREE.SphereGeometry(0.6, 16, 16);
const mat = new THREE.MeshStandardMaterial({
color: 0x00d4ff,
emissive: 0x00d4ff,
emissiveIntensity: 0.9,
transparent: true,
opacity: 0.85
});
mesh = new THREE.Mesh(geo, mat);
light = new THREE.PointLight(0x00d4ff, 3.5, 10, 2);
light.position.set(x, y + 0.6, z);
this.engine.scene.add(light);
} else if (type === 'medikit') {
const geo = new THREE.BoxGeometry(0.8, 0.6, 0.8);
const mat = new THREE.MeshStandardMaterial({ map: this.itemTextures.medikit });
mesh = new THREE.Mesh(geo, mat);
} else if (type.startsWith('armor_')) {
const isBlue = type.includes('blue');
const geo = new THREE.BoxGeometry(0.9, 0.9, 0.9);
const mat = new THREE.MeshStandardMaterial({
map: isBlue ? this.itemTextures.armorBlue : this.itemTextures.armorGreen,
emissive: isBlue ? 0x0044ff : 0x008800,
emissiveIntensity: 0.3
});
mesh = new THREE.Mesh(geo, mat);
} else {
// Ammo / Weapon box
const geo = new THREE.BoxGeometry(0.7, 0.5, 0.7);
const mat = new THREE.MeshStandardMaterial({ map: this.itemTextures.ammoClip });
mesh = new THREE.Mesh(geo, mat);
}
mesh.position.set(x, y + 0.4, z);
mesh.castShadow = true;
this.engine.scene.add(mesh);
const itemObj = {
type,
mesh,
light,
baseY: y + 0.4,
rotationSpeed: 1.5,
bobTimer: Math.random() * 5,
isCollected: false
};
this.items.push(itemObj);
return itemObj;
}
// ----------------------------------------------------
// EXPLOSIVE TOXIC BARRELS
// ----------------------------------------------------
spawnMapBarrels() {
const spawns = (this.map && this.map.spawns) ? this.map.spawns.barrels : (window.game && window.game.map ? window.game.map.spawns.barrels : []);
if (!spawns || spawns.length === 0) return;
for (let i = 0; i < spawns.length; i++) {
const s = spawns[i];
this.spawnBarrel(s.x, s.y, s.z);
}
}
spawnBarrel(x, y, z) {
const geo = new THREE.CylinderGeometry(0.55, 0.55, 1.4, 16);
const mat = this.engine.materials.barrel;
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y + 0.7, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.engine.scene.add(mesh);
// Add to map colliders
const min = new THREE.Vector3(x - 0.55, y, z - 0.55);
const max = new THREE.Vector3(x + 0.55, y + 1.4, z + 0.55);
const box = new THREE.Box3(min, max);
this.engine.scene.colliders = this.engine.scene.colliders || [];
const barrelObj = {
mesh,
box,
hp: 20,
isExploded: false,
takeDamage: (dmg) => this.damageBarrel(barrelObj, dmg)
};
this.barrels.push(barrelObj);
return barrelObj;
}
damageBarrel(b, amount) {
if (b.isExploded) return;
b.hp -= amount;
if (b.hp <= 0) {
this.explodeBarrel(b);
}
}
explodeBarrel(b) {
b.isExploded = true;
const pos = b.mesh.position;
this.sound.playExplosion(1.5);
this.engine.spawnExplosion(pos, 5.5, 0x88ff00); // Toxic radioactive green blast!
// Remove mesh
this.engine.scene.remove(b.mesh);
// Area Damage & Chain Reactions
if (window.game && window.game.weapons) {
window.game.weapons.applySplashDamage(pos, 180, 5.5);
}
}
// ----------------------------------------------------
// ITEM COLLECTION & UPDATE
// ----------------------------------------------------
update(delta) {
const playerPos = this.player.position;
for (let i = this.items.length - 1; i >= 0; i--) {
const item = this.items[i];
if (item.isCollected) continue;
// Rotation & Bobbing Animation
item.bobTimer += delta * 3.0;
item.mesh.rotation.y += item.rotationSpeed * delta;
item.mesh.position.y = item.baseY + Math.sin(item.bobTimer) * 0.15;
if (item.light) {
item.light.position.copy(item.mesh.position);
}
// Player proximity pickup check
const dist = item.mesh.position.distanceTo(playerPos);
if (dist < 1.6) {
this.collectItem(item, i);
}
}
}
collectItem(item, index) {
let collected = false;
const type = item.type;
if (type === 'medikit') {
collected = this.player.heal(25);
if (collected) this.sound.playItemPickup('health');
} else if (type === 'soulsphere') {
collected = this.player.heal(100, true);
if (collected) {
this.sound.playItemPickup('weapon');
if (window.game && window.game.hud) window.game.hud.showNotification('SOULSPHERE! +100 HP');
}
} else if (type === 'armor_green') {
collected = this.player.addArmor(100);
if (collected) this.sound.playItemPickup('armor');
} else if (type === 'armor_blue') {
collected = this.player.addArmor(200, true);
if (collected) this.sound.playItemPickup('armor');
} else if (type === 'ammo_clip') {
collected = this.player.addAmmo('bullets', 10);
if (collected) this.sound.playItemPickup('ammo');
} else if (type === 'ammo_box') {
collected = this.player.addAmmo('bullets', 50);
if (collected) this.sound.playItemPickup('ammo');
} else if (type === 'ammo_shells') {
collected = this.player.addAmmo('shells', 8);
if (collected) this.sound.playItemPickup('ammo');
} else if (type === 'ammo_rocket') {
collected = this.player.addAmmo('rockets', 5);
if (collected) this.sound.playItemPickup('ammo');
} else if (type === 'ammo_cell') {
collected = this.player.addAmmo('cells', 40);
if (collected) this.sound.playItemPickup('ammo');
} else if (type === 'shotgun') {
this.player.addAmmo('shells', 8);
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(2);
collected = true;
this.sound.playItemPickup('weapon');
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE SHOTGUN!');
} else if (type === 'chaingun') {
this.player.addAmmo('bullets', 40);
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(3);
collected = true;
this.sound.playItemPickup('weapon');
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE CHAINGUN!');
} else if (type === 'rocket_launcher') {
this.player.addAmmo('rockets', 5);
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(4);
collected = true;
this.sound.playItemPickup('weapon');
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE ROCKET LAUNCHER!');
} else if (type === 'plasma') {
this.player.addAmmo('cells', 50);
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(5);
collected = true;
this.sound.playItemPickup('weapon');
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE PLASMA RIFLE!');
} else if (type === 'bfg') {
this.player.addAmmo('cells', 100);
if (window.game && window.game.weapons) window.game.weapons.unlockWeapon(6);
collected = true;
this.sound.playItemPickup('weapon');
if (window.game && window.game.hud) window.game.hud.showNotification('YOU GOT THE BFG 9000! OH YES.');
} else if (type.startsWith('key_')) {
const keyColor = type.replace('key_', '');
this.player.keysInventory[keyColor] = true;
collected = true;
this.sound.playItemPickup('key');
if (window.game && window.game.hud) window.game.hud.showNotification(`PICKED UP THE ${keyColor.toUpperCase()} KEYCARD`);
}
if (collected) {
item.isCollected = true;
this.engine.scene.remove(item.mesh);
if (item.light) this.engine.scene.remove(item.light);
this.items.splice(index, 1);
}
}
}
window.DoomItems = DoomItems;
+6
View File
@@ -0,0 +1,6 @@
/**
* @license
* Copyright 2010-2021 Three.js Authors
* SPDX-License-Identifier: MIT
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="128",n=100,i=300,r=301,s=302,a=303,o=304,l=306,c=307,h=1e3,u=1001,d=1002,p=1003,m=1004,f=1005,g=1006,v=1007,y=1008,x=1009,_=1012,w=1014,b=1015,M=1016,S=1020,T=1022,E=1023,A=1026,L=1027,R=33776,C=33777,P=33778,D=33779,I=35840,N=35841,B=35842,z=35843,F=37492,O=37496,H=2300,G=2301,U=2302,k=2400,V=2401,W=2402,j=2500,q=2501,X=3e3,Y=3001,Z=3007,J=3002,Q=3004,K=3005,$=3006,tt=7680,et=35044,nt=35048,it="300 es";class rt{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e<i;e++)n[e].call(this,t);t.target=null}}}const st=[];for(let t=0;t<256;t++)st[t]=(t<16?"0":"")+t.toString(16);let at=1234567;const ot=Math.PI/180,lt=180/Math.PI;function ct(){const t=4294967295*Math.random()|0,e=4294967295*Math.random()|0,n=4294967295*Math.random()|0,i=4294967295*Math.random()|0;return(st[255&t]+st[t>>8&255]+st[t>>16&255]+st[t>>24&255]+"-"+st[255&e]+st[e>>8&255]+"-"+st[e>>16&15|64]+st[e>>24&255]+"-"+st[63&n|128]+st[n>>8&255]+"-"+st[n>>16&255]+st[n>>24&255]+st[255&i]+st[i>>8&255]+st[i>>16&255]+st[i>>24&255]).toUpperCase()}function ht(t,e,n){return Math.max(e,Math.min(n,t))}function ut(t,e){return(t%e+e)%e}function dt(t,e,n){return(1-n)*t+n*e}function pt(t){return 0==(t&t-1)&&0!==t}function mt(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))}function ft(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}var gt=Object.freeze({__proto__:null,DEG2RAD:ot,RAD2DEG:lt,generateUUID:ct,clamp:ht,euclideanModulo:ut,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:dt,damp:function(t,e,n,i){return dt(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(ut(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){return void 0!==t&&(at=t%2147483647),at=16807*at%2147483647,(at-1)/2147483646},degToRad:function(t){return t*ot},radToDeg:function(t){return t*lt},isPowerOfTwo:pt,ceilPowerOfTwo:mt,floorPowerOfTwo:ft,setQuaternionFromProperEuler:function(t,e,n,i,r){const s=Math.cos,a=Math.sin,o=s(n/2),l=a(n/2),c=s((e+i)/2),h=a((e+i)/2),u=s((e-i)/2),d=a((e-i)/2),p=s((i-e)/2),m=a((i-e)/2);switch(r){case"XYX":t.set(o*h,l*u,l*d,o*c);break;case"YZY":t.set(l*d,o*h,l*u,o*c);break;case"ZXZ":t.set(l*u,l*d,o*h,o*c);break;case"XZX":t.set(o*h,l*m,l*p,o*c);break;case"YXY":t.set(l*p,o*h,l*m,o*c);break;case"ZYZ":t.set(l*m,l*p,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}}});class vt{constructor(t=0,e=0){this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t,e){return void 0!==e?(console.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this)}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t,e){return void 0!==e?(console.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this)}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=tLine truncated
+179
View File
@@ -0,0 +1,179 @@
/**
* DOOM 3D - Master Game Manager & Application Entry Point
* Orchestrates loop, state transitions, settings, audio sync, and rendering.
*/
class DoomGame {
constructor() {
this.container = document.getElementById('game-container');
this.state = 'start'; // 'start', 'playing', 'paused', 'gameover'
this.sound = new SoundSystem();
this.engine = new DoomEngine(this.container);
this.map = new DoomMap(this.engine, this.sound);
this.player = new DoomPlayer(this.engine, this.sound);
this.weapons = new DoomWeapons(this.engine, this.player, this.sound);
this.enemies = new DoomEnemies(this.engine, this.player, this.sound, this.map);
this.items = new DoomItems(this.engine, this.player, this.sound, this.map);
this.hud = new DoomHUD(this.player);
this.mobile = new DoomMobileControls(this.player);
this.lastTime = performance.now();
this.crtFilterEnabled = true;
this.setupUI();
this.loop = this.loop.bind(this);
requestAnimationFrame(this.loop);
}
setupUI() {
// Start Game Button
const startBtn = document.getElementById('btn-start-game');
if (startBtn) {
startBtn.addEventListener('click', () => {
this.startGame();
});
}
// Restart Game Button
const restartBtn = document.getElementById('btn-restart');
if (restartBtn) {
restartBtn.addEventListener('click', () => {
this.restartGame();
});
}
// Pause Menu Resume Button
const resumeBtn = document.getElementById('btn-resume');
if (resumeBtn) {
resumeBtn.addEventListener('click', () => {
this.resumeGame();
});
}
// Settings Modal Open/Close
const settingsBtn = document.getElementById('btn-settings');
const settingsModal = document.getElementById('settings-modal');
const closeSettingsBtn = document.getElementById('btn-close-settings');
if (settingsBtn && settingsModal) {
settingsBtn.addEventListener('click', () => {
settingsModal.classList.remove('hidden');
});
}
if (closeSettingsBtn && settingsModal) {
closeSettingsBtn.addEventListener('click', () => {
settingsModal.classList.add('hidden');
});
}
// Settings Sliders
const sfxSlider = document.getElementById('slider-sfx');
const musicSlider = document.getElementById('slider-music');
const sensSlider = document.getElementById('slider-sens');
const crtCheck = document.getElementById('toggle-crt');
if (sfxSlider) {
sfxSlider.addEventListener('input', (e) => {
this.sound.setSfxVolume(parseFloat(e.target.value));
});
}
if (musicSlider) {
musicSlider.addEventListener('input', (e) => {
this.sound.setMusicVolume(parseFloat(e.target.value));
});
}
if (sensSlider) {
sensSlider.addEventListener('input', (e) => {
this.player.mouseSensitivity = parseFloat(e.target.value) * 0.002;
});
}
if (crtCheck) {
crtCheck.addEventListener('change', (e) => {
this.toggleCRTFilter(e.target.checked);
});
}
// ESC key to toggle pause
window.addEventListener('keydown', (e) => {
if (e.code === 'Escape') {
if (this.state === 'playing') {
this.pauseGame();
} else if (this.state === 'paused') {
this.resumeGame();
}
}
});
}
startGame() {
this.state = 'playing';
const startOverlay = document.getElementById('start-screen');
if (startOverlay) startOverlay.classList.add('hidden');
this.sound.init();
this.player.requestPointerLock();
this.hud.showNotification('ENTER THE TECH BASE. RIP AND TEAR!');
}
pauseGame() {
this.state = 'paused';
this.player.exitPointerLock();
const pauseOverlay = document.getElementById('pause-menu');
if (pauseOverlay) pauseOverlay.classList.remove('hidden');
}
resumeGame() {
this.state = 'playing';
const pauseOverlay = document.getElementById('pause-menu');
if (pauseOverlay) pauseOverlay.classList.add('hidden');
this.player.requestPointerLock();
}
restartGame() {
this.hud.hideGameOver();
this.player.respawn(this.map.spawns.player);
this.state = 'playing';
this.player.requestPointerLock();
this.hud.showNotification('RESPAWNED! GET READY TO FIGHT.');
}
toggleCRTFilter(enabled) {
this.crtFilterEnabled = enabled;
const scanlineElem = document.getElementById('crt-scanlines');
if (scanlineElem) {
if (enabled) scanlineElem.classList.remove('hidden');
else scanlineElem.classList.add('hidden');
}
}
// ----------------------------------------------------
// MAIN GAME LOOP
// ----------------------------------------------------
loop(currentTime) {
requestAnimationFrame(this.loop);
const delta = Math.min(0.1, (currentTime - this.lastTime) / 1000.0);
this.lastTime = currentTime;
if (this.state === 'playing') {
// Update all game subsystems
this.player.update(delta);
this.map.update(delta);
this.weapons.update(delta);
this.enemies.update(delta);
this.items.update(delta);
this.engine.update(delta);
this.hud.update(delta);
}
// Always render 3D Scene
this.engine.render();
}
}
// Global bootstrap
window.addEventListener('DOMContentLoaded', () => {
window.game = new DoomGame();
});
+458
View File
@@ -0,0 +1,458 @@
/**
* DOOM 3D - Multi-Level 3D Map & Architecture
* Multi-story buildings, stairs, ramps, catwalks, toxic slime pits,
* elevators, flickering lights, and keycard-locked blast doors.
*/
class DoomMap {
constructor(engine, sound) {
this.engine = engine;
this.sound = sound;
// Level Geometry Colliders
this.colliders = [];
this.slimeZones = [];
this.doors = [];
this.elevators = [];
this.spawns = {
player: new THREE.Vector3(0, 0, 0),
enemies: [],
items: [],
barrels: []
};
this.buildMap();
}
getColliders() {
return this.colliders;
}
isInSlimePit(pos) {
for (let i = 0; i < this.slimeZones.length; i++) {
const sz = this.slimeZones[i];
if (pos.x >= sz.min.x && pos.x <= sz.max.x &&
pos.z >= sz.min.z && pos.z <= sz.max.z &&
pos.y >= sz.min.y - 0.5 && pos.y <= sz.max.y + 1.5) {
return true;
}
}
return false;
}
// Helper to add Box Mesh + Collider Box
addBlock(x, y, z, w, h, d, material, castShadow = true, receiveShadow = true) {
const geo = new THREE.BoxGeometry(w, h, d);
const mesh = new THREE.Mesh(geo, material);
mesh.position.set(x, y + h / 2, z);
mesh.castShadow = castShadow;
mesh.receiveShadow = receiveShadow;
this.engine.scene.add(mesh);
// AABB collider
const min = new THREE.Vector3(x - w / 2, y, z - d / 2);
const max = new THREE.Vector3(x + w / 2, y + h, z + d / 2);
const box = new THREE.Box3(min, max);
this.colliders.push(box);
return mesh;
}
// Helper for Stairs (multiple small step boxes)
addStaircase(startX, startY, startZ, width, totalHeight, depth, numSteps, dir = 'z') {
const stepH = totalHeight / numSteps;
const stepD = depth / numSteps;
for (let i = 0; i < numSteps; i++) {
const h = stepH * (i + 1);
let curX = startX;
let curY = startY;
let curZ = startZ;
if (dir === 'z') {
curZ = startZ + (i * stepD);
this.addBlock(curX, curY, curZ, width, h, stepD + 0.1, this.engine.materials.metal);
} else if (dir === '-z') {
curZ = startZ - (i * stepD);
this.addBlock(curX, curY, curZ, width, h, stepD + 0.1, this.engine.materials.metal);
} else if (dir === 'x') {
curX = startX + (i * stepD);
this.addBlock(curX, curY, curZ, stepD + 0.1, h, width, this.engine.materials.metal);
} else if (dir === '-x') {
curX = startX - (i * stepD);
this.addBlock(curX, curY, curZ, stepD + 0.1, h, width, this.engine.materials.metal);
}
}
}
// ----------------------------------------------------
// LEVEL CONSTRUCTION (Inspired by E1M1 & E1M2)
// ----------------------------------------------------
buildMap() {
const mat = this.engine.materials;
// ==========================================
// 1. STARTING ROOM (Hangar / Briefing Room)
// ==========================================
// Floor
this.addBlock(0, -1, 0, 24, 1, 24, mat.metal);
// Ceiling
this.addBlock(0, 7, 0, 24, 1, 24, mat.ceiling);
// North Wall (with door opening in middle)
this.addBlock(-8, 0, -12, 8, 7, 2, mat.tech);
this.addBlock(8, 0, -12, 8, 7, 2, mat.tech);
this.addBlock(0, 5, -12, 8, 2, 2, mat.tech); // door lintel
// South Wall
this.addBlock(0, 0, 12, 24, 7, 2, mat.brick);
// West Wall
this.addBlock(-12, 0, 0, 2, 7, 24, mat.tech);
// East Wall
this.addBlock(12, 0, 0, 2, 7, 24, mat.metal);
// Starting Room Pillars & Consoles
this.addBlock(-5, 0, -4, 2, 7, 2, mat.hex);
this.addBlock(5, 0, -4, 2, 7, 2, mat.hex);
this.addBlock(0, 0, 6, 6, 1.4, 2, mat.tech);
// Flickering light in start room
this.engine.addFlickeringLight(0, 5.5, 0, 0xffeedd, 2.2, 18);
// Spawn points in start room
this.spawns.player.set(0, 0, 6);
this.spawns.items.push({ type: 'medikit', x: -9, y: 0, z: -8 });
this.spawns.items.push({ type: 'armor_green', x: 9, y: 0, z: -8 });
this.spawns.items.push({ type: 'shotgun', x: 0, y: 0, z: 2 });
this.spawns.items.push({ type: 'ammo_shells', x: -2, y: 0, z: 2 });
this.spawns.items.push({ type: 'ammo_shells', x: 2, y: 0, z: 2 });
// Door 1: Start Room to Main Corridor (Sliding blast door)
this.createDoor(0, 0, -12, 7.8, 5, 1.2, 'normal', null);
// ==========================================
// 2. CENTRAL CORRIDOR & CROSSROAD
// ==========================================
// Floor & Ceiling
this.addBlock(0, -1, -26, 12, 1, 26, mat.grate);
this.addBlock(0, 7, -26, 12, 1, 26, mat.ceiling);
// Walls
this.addBlock(-6, 0, -26, 1, 7, 26, mat.metal);
this.addBlock(6, 0, -26, 1, 7, 26, mat.metal);
this.engine.addFlickeringLight(0, 5.5, -24, 0xff8833, 2.0, 16);
this.spawns.enemies.push({ type: 'zombie', x: -2, y: 0, z: -20 });
this.spawns.enemies.push({ type: 'zombie', x: 2, y: 0, z: -24 });
this.spawns.items.push({ type: 'ammo_clip', x: 4, y: 0, z: -28 });
// ==========================================
// 3. TOXIC SLIME ARENA & COURTYARD (Outdoor)
// ==========================================
// Floor around slime
this.addBlock(-25, -1, -65, 60, 1, 50, mat.brick);
// Toxic Slime Basin (recessed pool in middle)
this.addBlock(-25, -2.2, -65, 34, 1, 26, mat.slime);
// Slime Hazard Zone definition
this.slimeZones.push(new THREE.Box3(
new THREE.Vector3(-42, -2.5, -78),
new THREE.Vector3(-8, 0.2, -52)
));
// Stepping stone platforms across the slime river
this.addBlock(-32, -0.8, -65, 4, 1.2, 4, mat.hex);
this.addBlock(-25, -0.8, -65, 4, 1.2, 4, mat.hex);
this.addBlock(-18, -0.8, -65, 4, 1.2, 4, mat.hex);
// Slime pit lighting
this.engine.addFlickeringLight(-25, 2.0, -65, 0x33ff22, 3.5, 30);
// Courtyard Perimeter Walls
this.addBlock(-55, 0, -65, 2, 12, 50, mat.brick); // West high wall
this.addBlock(5, 0, -65, 2, 12, 50, mat.brick); // East high wall
this.addBlock(-25, 0, -90, 60, 12, 2, mat.brick); // North high wall
// Blue Keycard Island on the slime pit!
this.spawns.items.push({ type: 'key_blue', x: -25, y: 0.6, z: -65 });
this.spawns.enemies.push({ type: 'imp', x: -35, y: 0, z: -80 });
this.spawns.enemies.push({ type: 'imp', x: -15, y: 0, z: -80 });
this.spawns.enemies.push({ type: 'shotgunner', x: -25, y: 0, z: -84 });
this.spawns.barrels.push({ x: -10, y: 0, z: -75 });
this.spawns.barrels.push({ x: -8, y: 0, z: -75 });
this.spawns.barrels.push({ x: -38, y: 0, z: -75 });
// ==========================================
// 4. MULTI-STORY TECH TOWER & CATWALKS (Level 2 & 3)
// ==========================================
// Staircase leading from Courtyard (y=0) to 2nd Floor Catwalk (y=6)
this.addStaircase(-2, 0, -45, 6, 6, 16, 15, '-z');
// 2nd Floor Catwalk (y=6)
this.addBlock(20, 5, -65, 28, 1, 50, mat.grate);
this.addBlock(20, 15, -65, 28, 1, 50, mat.ceiling);
// Catwalk Railing/Balcony facing courtyard (with opening for elevator at z=-45)
this.addBlock(6, 6, -72, 1, 1.8, 34, mat.metal);
this.addBlock(6, 6, -37, 1, 1.8, 16, mat.metal);
// East Wall
this.addBlock(34, 6, -65, 2, 9, 50, mat.tech);
// North Wall
this.addBlock(20, 6, -90, 30, 9, 2, mat.tech);
// Catwalk Monsters & Items
this.spawns.enemies.push({ type: 'shotgunner', x: 18, y: 6, z: -55 });
this.spawns.enemies.push({ type: 'pinky', x: 22, y: 6, z: -75 });
this.spawns.items.push({ type: 'chaingun', x: 28, y: 6, z: -80 });
this.spawns.items.push({ type: 'ammo_box', x: 28, y: 6, z: -76 });
// Catwalk Light
this.engine.addFlickeringLight(20, 11, -65, 0x4488ff, 2.8, 22);
// ==========================================
// 5. BLUE KEYCARD ARMORED VAULT (Secret Armory)
// ==========================================
// Door at North of Catwalk (x=20, z=-90)
this.createDoor(20, 6, -90, 8, 6, 1.2, 'blue', 'blue');
// Secret Armory Room behind Blue Door
this.addBlock(20, 5, -105, 20, 1, 28, mat.hex);
this.addBlock(20, 15, -105, 20, 1, 28, mat.ceiling);
this.addBlock(10, 6, -105, 1, 9, 28, mat.tech);
this.addBlock(30, 6, -105, 1, 9, 28, mat.tech);
this.addBlock(20, 6, -119, 20, 9, 2, mat.tech);
// Armory Rewards
this.spawns.items.push({ type: 'plasma', x: 20, y: 6, z: -110 });
this.spawns.items.push({ type: 'ammo_cell', x: 16, y: 6, z: -110 });
this.spawns.items.push({ type: 'ammo_cell', x: 24, y: 6, z: -110 });
this.spawns.items.push({ type: 'soulsphere', x: 20, y: 6, z: -115 });
this.spawns.items.push({ type: 'key_red', x: 20, y: 6, z: -105 }); // Red Keycard inside armory!
this.engine.addFlickeringLight(20, 12, -105, 0x00aaff, 3.2, 18);
// ==========================================
// 6. NUCLEAR REACTOR CHAMBER (Guarded by Red Door)
// ==========================================
// Door leading from West Courtyard (x=-55, z=-65)
this.createDoor(-55, 0, -65, 1.2, 6, 8, 'red', 'red');
// Nuclear Reactor Room
this.addBlock(-80, -1, -65, 48, 1, 48, mat.metal);
this.addBlock(-80, 16, -65, 48, 1, 48, mat.ceiling);
this.addBlock(-104, 0, -65, 2, 16, 48, mat.brick);
this.addBlock(-80, 0, -89, 48, 16, 2, mat.brick);
this.addBlock(-80, 0, -41, 48, 16, 2, mat.brick);
// Central Glowing Reactor Core
this.addBlock(-80, 0, -65, 10, 12, 10, mat.slime);
this.slimeZones.push(new THREE.Box3(
new THREE.Vector3(-86, -1, -71),
new THREE.Vector3(-74, 1, -59)
));
this.engine.addFlickeringLight(-80, 7, -65, 0x00ff22, 4.5, 35);
// Reactor Boss Monsters & Flying Cacodemons!
this.spawns.enemies.push({ type: 'caco', x: -75, y: 5, z: -60 });
this.spawns.enemies.push({ type: 'caco', x: -85, y: 7, z: -70 });
this.spawns.enemies.push({ type: 'pinky', x: -90, y: 0, z: -65 });
this.spawns.enemies.push({ type: 'shotgunner', x: -70, y: 0, z: -50 });
// Reactor Loot & Yellow Keycard
this.spawns.items.push({ type: 'rocket_launcher', x: -92, y: 0, z: -65 });
this.spawns.items.push({ type: 'ammo_rocket', x: -92, y: 0, z: -61 });
this.spawns.items.push({ type: 'ammo_rocket', x: -92, y: 0, z: -69 });
this.spawns.items.push({ type: 'armor_blue', x: -80, y: 0, z: -80 });
this.spawns.items.push({ type: 'key_yellow', x: -80, y: 0, z: -50 });
// ==========================================
// 7. FINAL BOSS ARENA / EXIT VAULT (Yellow Door)
// ==========================================
// Yellow Blast Door at North of Reactor room (x=-80, z=-89)
this.createDoor(-80, 0, -89, 8, 6, 1.2, 'yellow', 'yellow');
// BFG Sanctum & Exit Chamber
this.addBlock(-80, -1, -115, 32, 1, 48, mat.brick);
this.addBlock(-80, 18, -115, 32, 1, 48, mat.ceiling);
this.addBlock(-96, 0, -115, 2, 18, 48, mat.brick);
this.addBlock(-64, 0, -115, 2, 18, 48, mat.brick);
this.addBlock(-80, 0, -139, 32, 18, 2, mat.tech);
// BFG 9000 Altar
this.addBlock(-80, 0, -125, 4, 1.5, 4, mat.hex);
this.spawns.items.push({ type: 'bfg', x: -80, y: 1.8, z: -125 });
this.spawns.items.push({ type: 'ammo_cell', x: -75, y: 0, z: -125 });
this.spawns.items.push({ type: 'ammo_cell', x: -85, y: 0, z: -125 });
this.spawns.items.push({ type: 'soulsphere', x: -80, y: 0, z: -132 });
// Final Arena Demonic Horde
this.spawns.enemies.push({ type: 'caco', x: -80, y: 6, z: -120 });
this.spawns.enemies.push({ type: 'pinky', x: -75, y: 0, z: -115 });
this.spawns.enemies.push({ type: 'pinky', x: -85, y: 0, z: -115 });
this.spawns.enemies.push({ type: 'imp', x: -72, y: 0, z: -130 });
this.spawns.enemies.push({ type: 'imp', x: -88, y: 0, z: -130 });
this.engine.addFlickeringLight(-80, 10, -125, 0xff0022, 4.0, 30);
// Motorized Elevator Platform in Courtyard (moves between ground y=0 and catwalk y=6)
this.createElevator(12, 0, -45, 6, 6, -0.6, 5.2, 2.5);
}
// ----------------------------------------------------
// INTERACTIVE DOORS
// ----------------------------------------------------
createDoor(x, y, z, w, h, d, type = 'normal', requiredKey = null) {
let mat = this.engine.materials.doorRegular;
if (type === 'blue') mat = this.engine.materials.doorBlue;
if (type === 'red') mat = this.engine.materials.doorRed;
if (type === 'yellow') mat = this.engine.materials.doorYellow;
const geo = new THREE.BoxGeometry(w, h, d);
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y + h / 2, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.engine.scene.add(mesh);
const min = new THREE.Vector3(x - w / 2, y, z - d / 2);
const max = new THREE.Vector3(x + w / 2, y + h, z + d / 2);
const box = new THREE.Box3(min, max);
this.colliders.push(box);
const doorObj = {
mesh,
box,
x, y, z, w, h, d,
type,
requiredKey,
state: 'closed', // 'closed', 'opening', 'open', 'closing'
openHeight: h * 0.95,
currentOffset: 0,
autoCloseTimer: 0
};
this.doors.push(doorObj);
return doorObj;
}
interactWithDoors(playerPos, keysInventory) {
for (let i = 0; i < this.doors.length; i++) {
const door = this.doors[i];
const dist = playerPos.distanceTo(door.mesh.position);
if (dist < 5.5) {
if (door.requiredKey && !keysInventory[door.requiredKey]) {
if (window.game && window.game.hud) {
window.game.hud.showNotification(`YOU NEED THE ${door.requiredKey.toUpperCase()} KEY`);
}
if (this.sound) this.sound.playSlimeHurt();
continue;
}
if (door.state === 'closed' || door.state === 'closing') {
door.state = 'opening';
if (this.sound) this.sound.playDoor(true);
}
}
}
}
// ----------------------------------------------------
// MOTORIZED ELEVATOR PLATFORMS
// ----------------------------------------------------
createElevator(x, y, z, w, d, minY, maxY, speed = 2.0) {
const geo = new THREE.BoxGeometry(w, 0.8, d);
const mesh = new THREE.Mesh(geo, this.engine.materials.hex);
mesh.position.set(x, y + 0.4, z);
mesh.receiveShadow = true;
this.engine.scene.add(mesh);
const min = new THREE.Vector3(x - w / 2, y, z - d / 2);
const max = new THREE.Vector3(x + w / 2, y + 0.8, z + d / 2);
const box = new THREE.Box3(min, max);
this.colliders.push(box);
this.elevators.push({
mesh,
box,
x, z, w, d,
currentY: y,
minY,
maxY,
speed,
dir: 1, // 1 = up, -1 = down
pauseTimer: 0
});
}
// ----------------------------------------------------
// MAP GAME LOOP UPDATE
// ----------------------------------------------------
update(delta) {
// 1. Update Doors
for (let i = 0; i < this.doors.length; i++) {
const door = this.doors[i];
if (door.state === 'opening') {
door.currentOffset += delta * 4.0;
if (door.currentOffset >= door.openHeight) {
door.currentOffset = door.openHeight;
door.state = 'open';
door.autoCloseTimer = 5.0; // stay open for 5 sec
}
door.mesh.position.y = (door.y + door.h / 2) + door.currentOffset;
// Move collider out of way
door.box.min.y = door.y + door.currentOffset;
door.box.max.y = door.y + door.h + door.currentOffset;
} else if (door.state === 'open') {
door.autoCloseTimer -= delta;
if (door.autoCloseTimer <= 0) {
door.state = 'closing';
if (this.sound) this.sound.playDoor(false);
}
} else if (door.state === 'closing') {
door.currentOffset -= delta * 4.0;
if (door.currentOffset <= 0) {
door.currentOffset = 0;
door.state = 'closed';
}
door.mesh.position.y = (door.y + door.h / 2) + door.currentOffset;
door.box.min.y = door.y + door.currentOffset;
door.box.max.y = door.y + door.h + door.currentOffset;
}
}
// 2. Update Motorized Elevators
for (let i = 0; i < this.elevators.length; i++) {
const el = this.elevators[i];
if (el.pauseTimer > 0) {
el.pauseTimer -= delta;
continue;
}
el.currentY += el.dir * el.speed * delta;
if (el.currentY >= el.maxY) {
el.currentY = el.maxY;
el.dir = -1;
el.pauseTimer = 3.0;
} else if (el.currentY <= el.minY) {
el.currentY = el.minY;
el.dir = 1;
el.pauseTimer = 3.0;
}
el.mesh.position.y = el.currentY + 0.4;
el.box.min.y = el.currentY;
el.box.max.y = el.currentY + 0.8;
// Lift player if player is on elevator
if (window.game && window.game.player) {
const p = window.game.player;
if (p.position.x >= el.box.min.x && p.position.x <= el.box.max.x &&
p.position.z >= el.box.min.z && p.position.z <= el.box.max.z &&
Math.abs(p.position.y - el.box.max.y) < 0.25) {
p.position.y = el.box.max.y;
}
}
}
}
}
window.DoomMap = DoomMap;
+214
View File
@@ -0,0 +1,214 @@
/**
* DOOM 3D - Mobile Touch Controls
* Virtual thumbstick for movement, swipe aiming, and action buttons.
*/
class DoomMobileControls {
constructor(player) {
this.player = player;
this.container = document.getElementById('mobile-controls');
this.joystickBase = document.getElementById('joystick-base');
this.joystickKnob = document.getElementById('joystick-knob');
this.lookZone = document.getElementById('mobile-look-zone');
this.touchMoveId = null;
this.touchLookId = null;
this.joystickCenter = { x: 0, y: 0 };
this.maxRadius = 50;
this.lastLookX = 0;
this.lastLookY = 0;
this.init();
}
init() {
const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
if (isTouch && this.container) {
this.container.classList.remove('hidden');
}
this.setupJoystick();
this.setupLookZone();
this.setupActionButtons();
}
setupJoystick() {
if (!this.joystickBase || !this.joystickKnob) return;
const onTouchStart = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (this.touchMoveId === null) {
this.touchMoveId = touch.identifier;
const rect = this.joystickBase.getBoundingClientRect();
this.joystickCenter.x = rect.left + rect.width / 2;
this.joystickCenter.y = rect.top + rect.height / 2;
this.handleJoystickMove(touch.clientX, touch.clientY);
}
}
};
const onTouchMove = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (touch.identifier === this.touchMoveId) {
this.handleJoystickMove(touch.clientX, touch.clientY);
}
}
};
const onTouchEnd = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (touch.identifier === this.touchMoveId) {
this.touchMoveId = null;
this.joystickKnob.style.transform = 'translate(-50%, -50%)';
this.player.keys.forward = false;
this.player.keys.backward = false;
this.player.keys.left = false;
this.player.keys.right = false;
}
}
};
this.joystickBase.addEventListener('touchstart', onTouchStart, { passive: false });
window.addEventListener('touchmove', onTouchMove, { passive: false });
window.addEventListener('touchend', onTouchEnd, { passive: false });
window.addEventListener('touchcancel', onTouchEnd, { passive: false });
}
handleJoystickMove(clientX, clientY) {
const dx = clientX - this.joystickCenter.x;
const dy = clientY - this.joystickCenter.y;
const dist = Math.hypot(dx, dy);
const clampedDist = Math.min(dist, this.maxRadius);
const angle = Math.atan2(dy, dx);
const knobX = Math.cos(angle) * clampedDist;
const knobY = Math.sin(angle) * clampedDist;
this.joystickKnob.style.transform = `translate(calc(-50% + ${knobX}px), calc(-50% + ${knobY}px))`;
// Normalize inputs (-1 to 1)
const normX = knobX / this.maxRadius;
const normY = knobY / this.maxRadius;
const threshold = 0.25;
this.player.keys.forward = normY < -threshold;
this.player.keys.backward = normY > threshold;
this.player.keys.left = normX < -threshold;
this.player.keys.right = normX > threshold;
}
setupLookZone() {
if (!this.lookZone) return;
this.lookZone.addEventListener('touchstart', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (this.touchLookId === null) {
this.touchLookId = touch.identifier;
this.lastLookX = touch.clientX;
this.lastLookY = touch.clientY;
}
}
}, { passive: false });
window.addEventListener('touchmove', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (touch.identifier === this.touchLookId) {
const dx = touch.clientX - this.lastLookX;
const dy = touch.clientY - this.lastLookY;
this.lastLookX = touch.clientX;
this.lastLookY = touch.clientY;
// Apply rotation to player camera
const sensitivity = this.player.mouseSensitivity * 1.5;
this.player.euler.y -= dx * sensitivity;
this.player.euler.x -= dy * sensitivity;
const maxPitch = Math.PI / 2 - 0.02;
this.player.euler.x = Math.max(-maxPitch, Math.min(maxPitch, this.player.euler.x));
this.player.camera.quaternion.setFromEuler(this.player.euler);
}
}
}, { passive: false });
const endLook = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (touch.identifier === this.touchLookId) {
this.touchLookId = null;
}
}
};
window.addEventListener('touchend', endLook, { passive: false });
window.addEventListener('touchcancel', endLook, { passive: false });
}
setupActionButtons() {
const btnFire = document.getElementById('btn-mobile-fire');
const btnJump = document.getElementById('btn-mobile-jump');
const btnUse = document.getElementById('btn-mobile-use');
const btnNext = document.getElementById('btn-mobile-next');
const btnPrev = document.getElementById('btn-mobile-prev');
if (btnFire) {
btnFire.addEventListener('touchstart', (e) => {
e.preventDefault();
this.player.keys.shoot = true;
if (window.game && window.game.weapons) {
window.game.weapons.triggerAttack();
}
});
btnFire.addEventListener('touchend', (e) => {
e.preventDefault();
this.player.keys.shoot = false;
});
}
if (btnJump) {
btnJump.addEventListener('touchstart', (e) => {
e.preventDefault();
this.player.keys.jump = true;
});
btnJump.addEventListener('touchend', (e) => {
e.preventDefault();
this.player.keys.jump = false;
});
}
if (btnUse) {
btnUse.addEventListener('touchstart', (e) => {
e.preventDefault();
this.player.checkInteractions();
});
}
if (btnNext) {
btnNext.addEventListener('touchstart', (e) => {
e.preventDefault();
if (window.game && window.game.weapons) {
window.game.weapons.nextWeapon();
}
});
}
if (btnPrev) {
btnPrev.addEventListener('touchstart', (e) => {
e.preventDefault();
if (window.game && window.game.weapons) {
window.game.weapons.prevWeapon();
}
});
}
}
}
window.DoomMobileControls = DoomMobileControls;
+498
View File
@@ -0,0 +1,498 @@
/**
* DOOM 3D - First Person Player Controller & Physics
* Smooth movement, Pointer Lock look, step-climbing physics, stats, inventory and cheat codes.
*/
class DoomPlayer {
constructor(engine, sound) {
this.engine = engine;
this.sound = sound;
this.camera = engine.camera;
// Position & Physics
this.position = new THREE.Vector3(0, 2.0, 0);
this.velocity = new THREE.Vector3();
this.euler = new THREE.Euler(0, 0, 0, 'YXZ');
this.isGrounded = false;
this.height = 1.8;
this.radius = 0.5;
this.stepHeight = 0.95;
// Speeds
this.walkSpeed = 11.0;
this.runSpeed = 19.0;
this.jumpForce = 12.0;
this.gravity = 26.0;
this.mouseSensitivity = 0.0022;
// Controls Input State
this.keys = {
forward: false,
backward: false,
left: false,
right: false,
sprint: false,
jump: false,
interact: false,
shoot: false
};
this.isPointerLocked = false;
// Player Stats
this.maxHealth = 100;
this.health = 100;
this.maxArmor = 100;
this.armor = 0;
this.isDead = false;
this.godMode = false;
this.noclip = false;
// Ammo Reserves
this.ammo = {
bullets: 50,
shells: 0,
rockets: 0,
cells: 0
};
this.maxAmmo = {
bullets: 200,
shells: 50,
rockets: 50,
cells: 300
};
// Keys & Inventory
this.keysInventory = {
blue: false,
red: false,
yellow: false
};
// Head Bobbing
this.bobTimer = 0;
this.bobOffset = new THREE.Vector2();
// Slime Damage Timer
this.slimeTimer = 0;
// Cheat Input Buffer
this.cheatBuffer = '';
this.setupEventListeners();
}
setupEventListeners() {
// Keyboard down
window.addEventListener('keydown', (e) => {
this.onKeyDown(e);
});
// Keyboard up
window.addEventListener('keyup', (e) => {
this.onKeyUp(e);
});
// Mouse move (Pointer Lock)
document.addEventListener('mousemove', (e) => {
if (!this.isPointerLocked || this.isDead) return;
const movementX = e.movementX || e.mozMovementX || e.webkitMovementX || 0;
const movementY = e.movementY || e.mozMovementY || e.webkitMovementY || 0;
this.euler.y -= movementX * this.mouseSensitivity;
this.euler.x -= movementY * this.mouseSensitivity;
// Clamp pitch look angle to [-89 deg, +89 deg]
const maxPitch = Math.PI / 2 - 0.02;
this.euler.x = Math.max(-maxPitch, Math.min(maxPitch, this.euler.x));
this.camera.quaternion.setFromEuler(this.euler);
});
// Pointer lock state changes
document.addEventListener('pointerlockchange', () => {
this.isPointerLocked = (document.pointerLockElement === this.engine.renderer.domElement);
});
// Canvas click request pointer lock
this.engine.renderer.domElement.addEventListener('click', () => {
if (!this.isPointerLocked && !this.isDead) {
this.requestPointerLock();
}
});
// Mouse click to shoot
window.addEventListener('mousedown', (e) => {
if (e.button === 0 && this.isPointerLocked && !this.isDead) {
this.keys.shoot = true;
if (window.game && window.game.weapons) {
window.game.weapons.triggerAttack();
}
}
});
window.addEventListener('mouseup', (e) => {
if (e.button === 0) {
this.keys.shoot = false;
}
});
}
requestPointerLock() {
try {
this.engine.renderer.domElement.requestPointerLock();
if (this.sound) this.sound.resume();
} catch (e) {
console.warn('Pointer lock request error:', e);
}
}
exitPointerLock() {
try {
document.exitPointerLock();
} catch (e) {}
}
onKeyDown(e) {
const code = e.code;
if (code === 'KeyW' || code === 'ArrowUp') this.keys.forward = true;
if (code === 'KeyS' || code === 'ArrowDown') this.keys.backward = true;
if (code === 'KeyA' || code === 'ArrowLeft') this.keys.left = true;
if (code === 'KeyD' || code === 'ArrowRight') this.keys.right = true;
if (code === 'ShiftLeft' || code === 'ShiftRight') this.keys.sprint = true;
if (code === 'Space') {
this.keys.jump = true;
e.preventDefault();
}
if (code === 'KeyE' || code === 'Space') {
this.keys.interact = true;
this.checkInteractions();
}
// Weapon hotkeys (1-7)
if (window.game && window.game.weapons) {
if (code === 'Digit1') window.game.weapons.selectWeapon(0); // Fists
if (code === 'Digit2') window.game.weapons.selectWeapon(1); // Pistol
if (code === 'Digit3') window.game.weapons.selectWeapon(2); // Shotgun
if (code === 'Digit4') window.game.weapons.selectWeapon(3); // Chaingun
if (code === 'Digit5') window.game.weapons.selectWeapon(4); // Rocket Launcher
if (code === 'Digit6') window.game.weapons.selectWeapon(5); // Plasma Gun
if (code === 'Digit7') window.game.weapons.selectWeapon(6); // BFG 9000
}
// Classic DOOM Cheats
this.checkCheatCode(e.key);
}
onKeyUp(e) {
const code = e.code;
if (code === 'KeyW' || code === 'ArrowUp') this.keys.forward = false;
if (code === 'KeyS' || code === 'ArrowDown') this.keys.backward = false;
if (code === 'KeyA' || code === 'ArrowLeft') this.keys.left = false;
if (code === 'KeyD' || code === 'ArrowRight') this.keys.right = false;
if (code === 'ShiftLeft' || code === 'ShiftRight') this.keys.sprint = false;
if (code === 'Space') this.keys.jump = false;
if (code === 'KeyE') this.keys.interact = false;
}
checkCheatCode(char) {
if (!char || char.length > 1) return;
this.cheatBuffer = (this.cheatBuffer + char.toLowerCase()).slice(-8);
// IDDQD (God Mode)
if (this.cheatBuffer.endsWith('iddqd')) {
this.godMode = !this.godMode;
this.health = 100;
if (window.game && window.game.hud) {
window.game.hud.showNotification(this.godMode ? 'DEGNAMELESS INVULNERABILITY ON' : 'DEGNAMELESS INVULNERABILITY OFF');
}
if (this.sound) this.sound.playItemPickup('weapon');
}
// IDKFA (All Weapons, Full Ammo & Keys)
else if (this.cheatBuffer.endsWith('idkfa')) {
this.ammo.bullets = this.maxAmmo.bullets;
this.ammo.shells = this.maxAmmo.shells;
this.ammo.rockets = this.maxAmmo.rockets;
this.ammo.cells = this.maxAmmo.cells;
this.armor = 200;
this.keysInventory.blue = true;
this.keysInventory.red = true;
this.keysInventory.yellow = true;
if (window.game && window.game.weapons) {
window.game.weapons.unlocked = [true, true, true, true, true, true, true];
}
if (window.game && window.game.hud) {
window.game.hud.showNotification('VERY HAPPY AMMO ADDED');
}
if (this.sound) this.sound.playItemPickup('weapon');
}
// IDCLIP (Noclip)
else if (this.cheatBuffer.endsWith('idclip')) {
this.noclip = !this.noclip;
if (window.game && window.game.hud) {
window.game.hud.showNotification(this.noclip ? 'NOCLIP ON' : 'NOCLIP OFF');
}
}
}
checkInteractions() {
if (!window.game || !window.game.map) return;
window.game.map.interactWithDoors(this.position, this.keysInventory);
}
// ----------------------------------------------------
// DAMAGE & COMBAT LOGIC
// ----------------------------------------------------
takeDamage(amount, sourcePos = null) {
if (this.isDead || this.godMode) return;
// Armor absorbs 1/3 to 1/2 of damage
let dmgToHealth = amount;
if (this.armor > 0) {
const absorb = Math.min(this.armor, Math.floor(amount * 0.5));
this.armor -= absorb;
dmgToHealth = amount - absorb;
}
this.health = Math.max(0, this.health - dmgToHealth);
if (this.sound) {
this.sound.playPlayerPain();
}
if (window.game && window.game.hud) {
window.game.hud.triggerDamageFlash();
}
if (this.health <= 0) {
this.die();
}
}
heal(amount, isMega = false) {
if (this.isDead) return false;
const cap = isMega ? 200 : this.maxHealth;
if (this.health >= cap) return false;
this.health = Math.min(cap, this.health + amount);
return true;
}
addArmor(amount, isMega = false) {
if (this.isDead) return false;
const cap = isMega ? 200 : this.maxArmor;
if (this.armor >= cap) return false;
this.armor = Math.min(cap, this.armor + amount);
return true;
}
addAmmo(type, count) {
if (!this.ammo[type]) return false;
if (this.ammo[type] >= this.maxAmmo[type]) return false;
this.ammo[type] = Math.min(this.maxAmmo[type], this.ammo[type] + count);
return true;
}
die() {
this.isDead = true;
this.health = 0;
if (window.game && window.game.hud) {
window.game.hud.showGameOver();
}
}
respawn(spawnPos) {
this.isDead = false;
this.health = 100;
this.armor = 0;
this.ammo.bullets = 50;
this.ammo.shells = 0;
this.ammo.rockets = 0;
this.ammo.cells = 0;
this.position.copy(spawnPos || new THREE.Vector3(0, 2, 0));
this.velocity.set(0, 0, 0);
this.euler.set(0, 0, 0, 'YXZ');
this.camera.quaternion.setFromEuler(this.euler);
}
// ----------------------------------------------------
// PHYSICS & COLLISION UPDATE
// ----------------------------------------------------
update(delta) {
if (this.isDead) {
// Death cam sinking to floor
if (this.camera.position.y > this.position.y - 1.2) {
this.camera.position.y -= delta * 3;
}
return;
}
// 1. Calculate horizontal movement direction relative to camera yaw
const moveDir = new THREE.Vector3();
if (this.keys.forward) moveDir.z -= 1;
if (this.keys.backward) moveDir.z += 1;
if (this.keys.left) moveDir.x -= 1;
if (this.keys.right) moveDir.x += 1;
if (moveDir.lengthSq() > 0) {
moveDir.normalize();
// Rotate move direction by camera's horizontal yaw (euler.y)
moveDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.euler.y);
}
// Speed
const curSpeed = this.keys.sprint ? this.runSpeed : this.walkSpeed;
const targetVelX = moveDir.x * curSpeed;
const targetVelZ = moveDir.z * curSpeed;
// Smooth acceleration/damping
this.velocity.x += (targetVelX - this.velocity.x) * Math.min(1, delta * 14);
this.velocity.z += (targetVelZ - this.velocity.z) * Math.min(1, delta * 14);
// 2. Jump & Gravity
if (!this.noclip) {
if (this.keys.jump && this.isGrounded) {
this.velocity.y = this.jumpForce;
this.isGrounded = false;
}
this.velocity.y -= this.gravity * delta;
} else {
this.velocity.y = 0;
if (this.keys.jump) this.position.y += curSpeed * delta;
if (this.keys.sprint) this.position.y -= curSpeed * delta;
}
// 3. Collision Resolution against Level Geometry
if (!this.noclip && window.game && window.game.map) {
this.resolveCollisions(delta);
} else {
this.position.addScaledVector(this.velocity, delta);
}
// 4. Update Camera position with Head Bobbing
const isMoving = (this.keys.forward || this.keys.backward || this.keys.left || this.keys.right) && this.isGrounded;
if (isMoving) {
const bobSpeed = this.keys.sprint ? 16 : 11;
this.bobTimer += delta * bobSpeed;
this.bobOffset.x = Math.cos(this.bobTimer * 0.5) * 0.05;
this.bobOffset.y = Math.sin(this.bobTimer) * 0.08;
} else {
this.bobOffset.x *= 0.85;
this.bobOffset.y *= 0.85;
}
this.camera.position.set(
this.position.x + this.bobOffset.x,
this.position.y + this.height + this.bobOffset.y,
this.position.z
);
// 5. Slime Pit Hazard Damage
if (window.game && window.game.map) {
if (window.game.map.isInSlimePit(this.position)) {
this.slimeTimer += delta;
if (this.slimeTimer >= 0.6) {
this.slimeTimer = 0;
this.takeDamage(6);
if (this.sound) this.sound.playSlimeHurt();
}
} else {
this.slimeTimer = 0;
}
}
}
resolveCollisions(delta) {
const colliders = window.game.map.getColliders();
// Horizontal move step X
this.position.x += this.velocity.x * delta;
this.collideHorizontal(colliders, 'x');
// Horizontal move step Z
this.position.z += this.velocity.z * delta;
this.collideHorizontal(colliders, 'z');
// Vertical move step Y
this.position.y += this.velocity.y * delta;
this.collideVertical(colliders);
}
collideHorizontal(colliders, axis) {
const pMinX = this.position.x - this.radius;
const pMaxX = this.position.x + this.radius;
const pMinZ = this.position.z - this.radius;
const pMaxZ = this.position.z + this.radius;
const pMinY = this.position.y;
const pMaxY = this.position.y + this.height;
for (let i = 0; i < colliders.length; i++) {
const box = colliders[i];
// Check AABB overlap
if (pMaxX > box.min.x && pMinX < box.max.x &&
pMaxZ > box.min.z && pMinZ < box.max.z &&
pMaxY > box.min.y && pMinY < box.max.y) {
// Check if step-climbing is possible (stairs, curbs, low ledges)
const stepUp = box.max.y - pMinY;
if (stepUp > 0 && stepUp <= this.stepHeight && (this.isGrounded || this.velocity.y <= 1.0)) {
this.position.y = box.max.y + 0.02;
this.isGrounded = true;
this.velocity.y = 0;
continue;
}
// Push back on axis
if (axis === 'x') {
if (this.velocity.x > 0) {
this.position.x = box.min.x - this.radius;
} else if (this.velocity.x < 0) {
this.position.x = box.max.x + this.radius;
}
this.velocity.x = 0;
} else if (axis === 'z') {
if (this.velocity.z > 0) {
this.position.z = box.min.z - this.radius;
} else if (this.velocity.z < 0) {
this.position.z = box.max.z + this.radius;
}
this.velocity.z = 0;
}
}
}
}
collideVertical(colliders) {
const pMinX = this.position.x - this.radius;
const pMaxX = this.position.x + this.radius;
const pMinZ = this.position.z - this.radius;
const pMaxZ = this.position.z + this.radius;
const pMinY = this.position.y;
const pMaxY = this.position.y + this.height;
let onGround = false;
for (let i = 0; i < colliders.length; i++) {
const box = colliders[i];
if (pMaxX > box.min.x && pMinX < box.max.x &&
pMaxZ > box.min.z && pMinZ < box.max.z &&
pMaxY > box.min.y && pMinY < box.max.y) {
if (this.velocity.y < 0) {
// Falling onto platform / floor
this.position.y = box.max.y;
this.velocity.y = 0;
onGround = true;
} else if (this.velocity.y > 0) {
// Hitting ceiling
this.position.y = box.min.y - this.height;
this.velocity.y = 0;
}
}
}
this.isGrounded = onGround;
}
}
window.DoomPlayer = DoomPlayer;
+1342
View File
File diff suppressed because it is too large. Load diff