Files

853 lines
29 KiB
JavaScript
Executable File

/**
* 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;