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