Files

459 lines
19 KiB
JavaScript
Executable File

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