feat(integrations): add retro doom web engine and agy proxy nginx config
This commit is contained in:
Executable
+852
@@ -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;
|
||||
Reference in New Issue
Block a user