forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
248 lines
7.8 KiB
JavaScript
248 lines
7.8 KiB
JavaScript
/**
|
|
* 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 };
|
|
}
|