diff --git a/static/world/index.html b/static/world/index.html index cd3a99a2..d2aec708 100644 --- a/static/world/index.html +++ b/static/world/index.html @@ -55,7 +55,7 @@ camera.position.set(0, 2.0, 4.5); // --- Build scene elements --- - const { crystalBall, fireLight } = buildRoom(scene); + const { crystalBall, crystalLight, fireLight, candleLights } = buildRoom(scene); const wizard = createWizard(); scene.add(wizard.group); const familiar = createFamiliar(); @@ -93,13 +93,24 @@ familiar.update(dt); controls.update(); - // Crystal ball subtle rotation + // Crystal ball subtle rotation + pulsing glow crystalBall.rotation.y += dt * 0.3; + const pulse = 0.3 + Math.sin(Date.now() * 0.002) * 0.15; + crystalLight.intensity = pulse; + crystalBall.material.emissiveIntensity = pulse * 0.5; // Fireplace flicker fireLight.intensity = 1.2 + Math.sin(Date.now() * 0.005) * 0.15 + Math.sin(Date.now() * 0.013) * 0.1; + // Candle flicker — each offset slightly for variety + const now = Date.now(); + for (let i = 0; i < candleLights.length; i++) { + candleLights[i].intensity = 0.4 + + Math.sin(now * 0.007 + i * 2.1) * 0.1 + + Math.sin(now * 0.019 + i * 1.3) * 0.05; + } + renderer.render(scene, camera); } animate(); diff --git a/static/world/scene.js b/static/world/scene.js index 8dbf3183..c79355a4 100644 --- a/static/world/scene.js +++ b/static/world/scene.js @@ -7,10 +7,13 @@ import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"; -const WALL_COLOR = 0x1a1a2e; +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. @@ -33,6 +36,7 @@ export function buildRoom(scene) { 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); @@ -105,6 +109,8 @@ export function buildRoom(scene) { 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); @@ -121,10 +127,97 @@ export function buildRoom(scene) { base.position.set(0.15, 0.9, -0.3); scene.add(base); - // Crystal ball inner glow - const innerLight = new THREE.PointLight(0x88ccff, 0.3, 2); - innerLight.position.copy(crystalBall.position); - scene.add(innerLight); + // 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 --- @@ -150,5 +243,5 @@ export function buildRoom(scene) { overhead.position.set(0, 3.5, 0); scene.add(overhead); - return { crystalBall, fireLight }; + return { crystalBall, crystalLight, fireLight, candleLights }; }