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