/** * Workshop scene — room geometry, lighting, materials. * * A dark stone room with a wooden desk, crystal ball, fireplace glow, * and faint emerald ambient light. This is Timmy's Workshop. */ import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"; const WALL_COLOR = 0x2a2a3e; const FLOOR_COLOR = 0x1a1a1a; const DESK_COLOR = 0x3e2723; const DESK_TOP_COLOR = 0x4e342e; const BOOK_COLORS = [0x8b1a1a, 0x1a3c6e, 0x2e5e3e, 0x6e4b1a, 0x4a1a5e, 0x5e1a2e]; const CANDLE_WAX = 0xe8d8b8; const CANDLE_FLAME = 0xffaa33; /** * Build the room and add it to the given scene. * Returns { crystalBall } for animation. */ export function buildRoom(scene) { // --- Floor --- const floorGeo = new THREE.PlaneGeometry(8, 8); const floorMat = new THREE.MeshStandardMaterial({ color: FLOOR_COLOR, roughness: 0.9, }); const floor = new THREE.Mesh(floorGeo, floorMat); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor); // --- Back wall --- const wallGeo = new THREE.PlaneGeometry(8, 4); const wallMat = new THREE.MeshStandardMaterial({ color: WALL_COLOR, roughness: 0.95, metalness: 0.05, }); const backWall = new THREE.Mesh(wallGeo, wallMat); backWall.position.set(0, 2, -4); scene.add(backWall); // --- Side walls --- const leftWall = new THREE.Mesh(wallGeo, wallMat); leftWall.position.set(-4, 2, 0); leftWall.rotation.y = Math.PI / 2; scene.add(leftWall); const rightWall = new THREE.Mesh(wallGeo, wallMat); rightWall.position.set(4, 2, 0); rightWall.rotation.y = -Math.PI / 2; scene.add(rightWall); // --- Desk --- // Table top const topGeo = new THREE.BoxGeometry(1.8, 0.08, 0.9); const topMat = new THREE.MeshStandardMaterial({ color: DESK_TOP_COLOR, roughness: 0.6, }); const tableTop = new THREE.Mesh(topGeo, topMat); tableTop.position.set(0, 0.85, -0.3); tableTop.castShadow = true; scene.add(tableTop); // Legs const legGeo = new THREE.BoxGeometry(0.08, 0.85, 0.08); const legMat = new THREE.MeshStandardMaterial({ color: DESK_COLOR, roughness: 0.7, }); const offsets = [ [-0.8, -0.35], [0.8, -0.35], [-0.8, 0.05], [0.8, 0.05], ]; for (const [x, z] of offsets) { const leg = new THREE.Mesh(legGeo, legMat); leg.position.set(x, 0.425, z - 0.3); scene.add(leg); } // --- Scrolls / papers on desk (simple flat boxes) --- const paperGeo = new THREE.BoxGeometry(0.3, 0.005, 0.2); const paperMat = new THREE.MeshStandardMaterial({ color: 0xd4c5a0, roughness: 0.9, }); const paper1 = new THREE.Mesh(paperGeo, paperMat); paper1.position.set(-0.4, 0.895, -0.35); paper1.rotation.y = 0.15; scene.add(paper1); const paper2 = new THREE.Mesh(paperGeo, paperMat); paper2.position.set(0.5, 0.895, -0.2); paper2.rotation.y = -0.3; scene.add(paper2); // --- Crystal ball --- const ballGeo = new THREE.SphereGeometry(0.12, 16, 14); const ballMat = new THREE.MeshPhysicalMaterial({ color: 0x88ccff, roughness: 0.05, metalness: 0.0, transmission: 0.9, thickness: 0.3, transparent: true, opacity: 0.7, emissive: new THREE.Color(0x88ccff), emissiveIntensity: 0.3, }); const crystalBall = new THREE.Mesh(ballGeo, ballMat); crystalBall.position.set(0.15, 1.01, -0.3); scene.add(crystalBall); // Crystal ball base const baseGeo = new THREE.CylinderGeometry(0.08, 0.1, 0.04, 8); const baseMat = new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.3, metalness: 0.5, }); const base = new THREE.Mesh(baseGeo, baseMat); base.position.set(0.15, 0.9, -0.3); scene.add(base); // Crystal ball inner glow (pulsing) const crystalLight = new THREE.PointLight(0x88ccff, 0.3, 2); crystalLight.position.copy(crystalBall.position); scene.add(crystalLight); // --- Bookshelf (right wall) --- const shelfMat = new THREE.MeshStandardMaterial({ color: DESK_COLOR, roughness: 0.7, }); // Bookshelf frame — tall backing panel const shelfBack = new THREE.Mesh( new THREE.BoxGeometry(1.4, 2.2, 0.06), shelfMat ); shelfBack.position.set(3.0, 1.1, -2.0); scene.add(shelfBack); // Shelves (4 horizontal planks) const shelfGeo = new THREE.BoxGeometry(1.4, 0.04, 0.35); const shelfYs = [0.2, 0.7, 1.2, 1.7]; for (const sy of shelfYs) { const shelf = new THREE.Mesh(shelfGeo, shelfMat); shelf.position.set(3.0, sy, -1.85); scene.add(shelf); } // Side panels const sidePanelGeo = new THREE.BoxGeometry(0.04, 2.2, 0.35); for (const sx of [-0.68, 0.68]) { const side = new THREE.Mesh(sidePanelGeo, shelfMat); side.position.set(3.0 + sx, 1.1, -1.85); scene.add(side); } // Books on shelves — colored boxes const bookGeo = new THREE.BoxGeometry(0.08, 0.28, 0.22); const booksPerShelf = [5, 4, 5, 3]; for (let s = 0; s < shelfYs.length; s++) { const count = booksPerShelf[s]; const startX = 3.0 - (count * 0.12) / 2; for (let b = 0; b < count; b++) { const bookMat = new THREE.MeshStandardMaterial({ color: BOOK_COLORS[(s * 3 + b) % BOOK_COLORS.length], roughness: 0.8, }); const book = new THREE.Mesh(bookGeo, bookMat); book.position.set( startX + b * 0.14, shelfYs[s] + 0.16, -1.85 ); // Slight random tilt for character book.rotation.z = (Math.random() - 0.5) * 0.08; scene.add(book); } } // --- Candles --- const candleLights = []; const candlePositions = [ [-0.6, 0.89, -0.15], // desk left [0.7, 0.89, -0.4], // desk right [3.0, 1.78, -1.85], // bookshelf top ]; const candleGeo = new THREE.CylinderGeometry(0.02, 0.025, 0.12, 6); const candleMat = new THREE.MeshStandardMaterial({ color: CANDLE_WAX, roughness: 0.9, }); for (const [cx, cy, cz] of candlePositions) { // Wax cylinder const candle = new THREE.Mesh(candleGeo, candleMat); candle.position.set(cx, cy + 0.06, cz); scene.add(candle); // Flame — tiny emissive sphere const flameGeo = new THREE.SphereGeometry(0.015, 6, 4); const flameMat = new THREE.MeshBasicMaterial({ color: CANDLE_FLAME }); const flame = new THREE.Mesh(flameGeo, flameMat); flame.position.set(cx, cy + 0.13, cz); scene.add(flame); // Warm point light const candleLight = new THREE.PointLight(0xff8833, 0.4, 3); candleLight.position.set(cx, cy + 0.15, cz); scene.add(candleLight); candleLights.push(candleLight); } // --- Lighting --- // Fireplace glow (warm, off-screen stage left) const fireLight = new THREE.PointLight(0xff6622, 1.2, 8); fireLight.position.set(-3.5, 1.2, -1.0); fireLight.castShadow = true; fireLight.shadow.mapSize.width = 512; fireLight.shadow.mapSize.height = 512; scene.add(fireLight); // Secondary warm fill const fillLight = new THREE.PointLight(0xff8844, 0.3, 6); fillLight.position.set(-2.0, 0.5, 1.0); scene.add(fillLight); // Emerald ambient const ambient = new THREE.AmbientLight(0x00b450, 0.15); scene.add(ambient); // Faint overhead to keep things readable const overhead = new THREE.PointLight(0x887766, 0.2, 8); overhead.position.set(0, 3.5, 0); scene.add(overhead); return { crystalBall, crystalLight, fireLight, candleLights }; }