1336 lines
53 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Buba — Pastel Dream Dashboard ✦</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&family=JetBrains+Mono:wght@300;400;500&display=swap');
*{margin:0;padding:0;box-sizing:border-box;}
:root{
--pink:#FFB5C8;--lavender:#D4B8FF;--teal:#A8E6CF;
--peach:#FFDAB9;--gold:#FFE5A0;--blue:#B8D4FF;
--coral:#FFB8A8;--mint:#C5E8D0;--text:#555;--text-dim:#888;
}
html,body{width:100%;height:100%;overflow:hidden;background:#FFE4EC;font-family:'Nunito',sans-serif;color:var(--text);}
canvas{display:block;}
#container{position:relative;width:100%;height:100%;}
.hud{position:absolute;pointer-events:none;z-index:10;}
.hud-interactive{pointer-events:auto;}
#hud-tl{top:20px;left:20px;}
#hud-tl h1{font-size:20px;font-weight:800;letter-spacing:1.5px;
background:linear-gradient(135deg,var(--pink),var(--lavender),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
text-shadow:none;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.05));}
#hud-tl .status-row{display:flex;align-items:center;gap:8px;margin-top:6px;font-size:12px;font-family:'JetBrains Mono',monospace;color:var(--text-dim);}
.status-dot{width:8px;height:8px;border-radius:50%;background:var(--teal);box-shadow:0 0 6px var(--teal);transition:all .3s;}
.status-dot.idle{background:#ccc;box-shadow:none;}
#current-task{max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:11px;color:var(--text-dim);margin-top:4px;}
#hud-tr{top:20px;right:20px;text-align:right;}
#clock{font-family:'JetBrains Mono',monospace;font-size:24px;font-weight:300;color:var(--lavender);text-shadow:0 1px 3px rgba(212,184,255,0.3);}
#session-count{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-dim);margin-top:4px;}
#hud-bl{bottom:20px;left:20px;display:flex;gap:10px;flex-wrap:wrap;max-width:400px;}
.stat-card{background:rgba(255,255,255,0.55);border:1px solid rgba(212,184,255,0.2);border-radius:14px;
padding:12px 16px;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);min-width:85px;box-shadow:0 2px 12px rgba(0,0,0,0.04);}
.stat-card .label{font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:4px;font-weight:600;}
.stat-card .value{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:700;}
.stat-card .value.pink{color:var(--pink);}
.stat-card .value.teal{color:#7BCBA2;}
.stat-card .value.lavender{color:var(--lavender);}
.stat-card .value.gold{color:#E8C862;}
#hud-br{bottom:20px;right:20px;width:320px;max-height:260px;}
#activity-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:8px;font-weight:700;}
#activity-feed{display:flex;flex-direction:column;gap:4px;max-height:230px;overflow-y:auto;scrollbar-width:none;}
#activity-feed::-webkit-scrollbar{display:none;}
.activity-item{background:rgba(255,255,255,0.5);border-left:3px solid var(--teal);border-radius:0 10px 10px 0;
padding:8px 10px;font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--text);
line-height:1.4;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);animation:fadeIn .3s ease;box-shadow:0 1px 6px rgba(0,0,0,0.03);}
.activity-item.tool_call{border-left-color:var(--teal);}
.activity-item.user_message{border-left-color:var(--pink);}
.activity-item.buba_reply{border-left-color:var(--lavender);}
.activity-item .type-tag{font-size:9px;font-weight:700;text-transform:uppercase;margin-bottom:2px;}
.activity-item.tool_call .type-tag{color:#7BCBA2;}
.activity-item.user_message .type-tag{color:var(--pink);}
.activity-item.buba_reply .type-tag{color:var(--lavender);}
@keyframes fadeIn{from{opacity:0;transform:translateX(10px);}to{opacity:1;transform:translateX(0);}}
#loading{position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#FFE4EC,#E8DFFF,#DDE8FF);display:flex;
align-items:center;justify-content:center;z-index:100;transition:opacity .8s;flex-direction:column;gap:12px;}
#loading.hidden{opacity:0;pointer-events:none;}
#loading .spinner{width:36px;height:36px;border:3px solid rgba(212,184,255,0.3);
border-top-color:var(--lavender);border-radius:50%;animation:spin 1s linear infinite;}
#loading .load-text{font-size:13px;color:var(--lavender);font-weight:600;letter-spacing:1px;}
@keyframes spin{to{transform:rotate(360deg);}}
</style>
<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/"}}
</script>
</head>
<body>
<div id="container">
<div id="loading"><div class="spinner"></div><div class="load-text">✦ waking up ✦</div></div>
<div class="hud" id="hud-tl">
<h1>✦ Buba Dream Dashboard ✦</h1>
<div class="status-row"><div class="status-dot" id="status-dot"></div><span id="status-text">Initializing...</span></div>
<div id="current-task"></div>
</div>
<div class="hud" id="hud-tr">
<div id="clock">00:00:00</div>
<div id="session-count">— sessions today</div>
</div>
<div class="hud" id="hud-bl">
<div class="stat-card"><div class="label">Sessions</div><div class="value pink" id="stat-sessions"></div></div>
<div class="stat-card"><div class="label">Sub-Agents</div><div class="value teal" id="stat-subagents"></div></div>
<div class="stat-card"><div class="label">Active</div><div class="value lavender" id="stat-active"></div></div>
<div class="stat-card"><div class="label">Today</div><div class="value gold" id="stat-today"></div></div>
</div>
<div class="hud hud-interactive" id="hud-br">
<div id="activity-title">✦ Activity Feed</div>
<div id="activity-feed"></div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
let scene, camera, renderer, controls, clock, composer;
let bubaGroup, bubaParticles, bubaAura;
let buildings = {};
let subagentMeshes = {};
let dataBeams = [];
let dashState = null;
let autoRotate = true;
let autoRotateTimer = null;
let clouds = [];
let sparkles;
let trees = [];
let flowers = [];
let floatingNotes = [];
let smokeParticles = [];
const C = {
pink:0xFFB5C8, lavender:0xD4B8FF, teal:0xA8E6CF,
peach:0xFFDAB9, gold:0xFFE5A0, blue:0xB8D4FF,
coral:0xFFB8A8, mint:0xC5E8D0, white:0xffffff,
skyPink:0xFFE4EC, skyLav:0xE8DFFF, skyBlue:0xDDE8FF
};
const BUILDING_DEFS = [
{ name:'MCP Factory', color:C.teal, pos:[-16,0,-10], height:8, type:'factory' },
{ name:'SOLVR', color:C.gold, pos:[14,0,-12], height:6, type:'cottage' },
{ name:'CannaBri', color:0x7CD8A0, pos:[-17,0,10], height:5, type:'greenhouse' },
{ name:'TheNicheQuiz', color:C.peach, pos:[17,0,8], height:5.5, type:'wizard' },
{ name:'CREdispo', color:C.blue, pos:[10,0,17], height:5, type:'warehouse' },
{ name:'OpenClaw', color:C.pink, pos:[0,0,0], height:7, type:'hub' },
{ name:'OSKV Coaching', color:C.lavender, pos:[-10,0,18], height:4.5, type:'studio' }
];
function init() {
clock = new THREE.Clock();
scene = new THREE.Scene();
// Pastel gradient sky
const skyCanvas = document.createElement('canvas');
skyCanvas.width = 2; skyCanvas.height = 512;
const skyCtx = skyCanvas.getContext('2d');
const grad = skyCtx.createLinearGradient(0, 0, 0, 512);
grad.addColorStop(0, '#DDE8FF');
grad.addColorStop(0.4, '#E8DFFF');
grad.addColorStop(0.75, '#FFE4EC');
grad.addColorStop(1, '#FFF0F5');
skyCtx.fillStyle = grad;
skyCtx.fillRect(0, 0, 2, 512);
const skyTex = new THREE.CanvasTexture(skyCanvas);
skyTex.magFilter = THREE.LinearFilter;
// Sky sphere
const skyGeo = new THREE.SphereGeometry(200, 32, 32);
const skyMat = new THREE.MeshBasicMaterial({ map: skyTex, side: THREE.BackSide });
const skySphere = new THREE.Mesh(skyGeo, skyMat);
scene.add(skySphere);
scene.background = new THREE.Color(0xFFE4EC);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 500);
camera.position.set(30, 25, 30);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
document.getElementById('container').prepend(renderer.domElement);
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.3, 0.6, 0.85
);
composer.addPass(bloom);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2.3;
controls.minDistance = 15;
controls.maxDistance = 80;
controls.target.set(0, 2, 0);
controls.addEventListener('start', () => { autoRotate = false; clearTimeout(autoRotateTimer); });
controls.addEventListener('end', () => { autoRotateTimer = setTimeout(() => autoRotate = true, 5000); });
createLights();
createIsland();
createClouds();
createSparkles();
createTrees();
createFlowers();
createPond();
createPaths();
BUILDING_DEFS.forEach(d => createBuilding(d));
createBuildingLabels();
createBuba();
createFloatingNotes();
renderer.domElement.addEventListener('dblclick', onDoubleClick);
window.addEventListener('resize', onResize);
document.getElementById('loading').classList.add('hidden');
fetchData();
setInterval(fetchData, 5000);
animate();
}
function createLights() {
const hemi = new THREE.HemisphereLight(0xFFE4EC, 0xC5E8D0, 0.8);
scene.add(hemi);
const dir = new THREE.DirectionalLight(0xFFF5EE, 1.0);
dir.position.set(25, 40, 20);
scene.add(dir);
const ambient = new THREE.AmbientLight(0xFFFAFA, 0.6);
scene.add(ambient);
// Warm fill from below
const fill = new THREE.DirectionalLight(0xFFDAB9, 0.3);
fill.position.set(-10, -5, 10);
scene.add(fill);
}
function createIsland() {
// Main island top
const topGeo = new THREE.CylinderGeometry(28, 26, 2.5, 48);
const topMat = new THREE.MeshStandardMaterial({ color: C.mint, roughness: 0.85, metalness: 0.0 });
const island = new THREE.Mesh(topGeo, topMat);
island.position.y = -1.25;
scene.add(island);
// Darker edge ring
const edgeGeo = new THREE.TorusGeometry(27, 0.6, 12, 48);
const edgeMat = new THREE.MeshStandardMaterial({ color: 0x9BCFAB, roughness: 0.7 });
const edge = new THREE.Mesh(edgeGeo, edgeMat);
edge.rotation.x = Math.PI / 2;
edge.position.y = -0.3;
scene.add(edge);
// Bottom of island (dirt)
const bottomGeo = new THREE.CylinderGeometry(26, 18, 5, 48);
const bottomMat = new THREE.MeshStandardMaterial({ color: 0xD4A574, roughness: 0.9 });
const bottom = new THREE.Mesh(bottomGeo, bottomMat);
bottom.position.y = -5;
scene.add(bottom);
// Tiny rock
const rockGeo = new THREE.CylinderGeometry(15, 10, 4, 32);
const rockMat = new THREE.MeshStandardMaterial({ color: 0xC49A6C, roughness: 0.9 });
const rock = new THREE.Mesh(rockGeo, rockMat);
rock.position.y = -8.5;
scene.add(rock);
// Shadow beneath island
const shadowGeo = new THREE.CircleGeometry(22, 32);
const shadowMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.06, side: THREE.DoubleSide });
const shadow = new THREE.Mesh(shadowGeo, shadowMat);
shadow.rotation.x = -Math.PI / 2;
shadow.position.y = -12;
scene.add(shadow);
// Lighter grass patches
for (let i = 0; i < 12; i++) {
const angle = Math.random() * Math.PI * 2;
const r = Math.random() * 22;
const patchGeo = new THREE.CircleGeometry(1.2 + Math.random() * 2, 16);
const patchMat = new THREE.MeshStandardMaterial({ color: 0xD4F0DC, roughness: 0.9 });
const patch = new THREE.Mesh(patchGeo, patchMat);
patch.rotation.x = -Math.PI / 2;
patch.position.set(Math.cos(angle) * r, 0.02, Math.sin(angle) * r);
scene.add(patch);
}
}
function createClouds() {
function makeCloud(x, y, z, scale) {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0xFFFFFF, roughness: 0.9, metalness: 0 });
const offsets = [
[0, 0, 0, 2.5], [-2, 0.3, 0.5, 2], [2, 0.2, -0.3, 2.2],
[-1, 0.8, 0, 1.8], [1, 0.7, 0.5, 1.6]
];
offsets.forEach(o => {
const s = new THREE.Mesh(new THREE.SphereGeometry(o[3], 12, 12), mat);
s.position.set(o[0], o[1], o[2]);
s.scale.set(1, 0.6, 1);
group.add(s);
});
group.position.set(x, y, z);
group.scale.setScalar(scale);
group.userData = { baseX: x, speed: 0.3 + Math.random() * 0.5, range: 15 + Math.random() * 10 };
scene.add(group);
clouds.push(group);
}
makeCloud(-30, 28, -20, 1.2);
makeCloud(25, 32, -15, 1.0);
makeCloud(-10, 35, 25, 0.9);
makeCloud(35, 30, 20, 1.1);
makeCloud(-25, 38, 10, 0.8);
}
function createSparkles() {
const geo = new THREE.BufferGeometry();
const count = 120;
const pos = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
pos[i*3] = (Math.random() - 0.5) * 80;
pos[i*3+1] = 5 + Math.random() * 40;
pos[i*3+2] = (Math.random() - 0.5) * 80;
}
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
const mat = new THREE.PointsMaterial({ color: 0xFFFFFF, size: 0.3, transparent: true, opacity: 0.5, sizeAttenuation: true });
sparkles = new THREE.Points(geo, mat);
scene.add(sparkles);
}
function createTrees() {
const treePositions = [
[-22, 0, -5], [20, 0, -18], [-8, 0, -20], [22, 0, 15], [-20, 0, -16]
];
treePositions.forEach(p => {
const group = new THREE.Group();
// Trunk
const trunkGeo = new THREE.CylinderGeometry(0.3, 0.4, 2.5, 8);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0xA0785A, roughness: 0.8 });
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
trunk.position.y = 1.25;
group.add(trunk);
// Canopy - three spheres
const canopyMat = new THREE.MeshStandardMaterial({ color: 0x8DD9A0, roughness: 0.7 });
const c1 = new THREE.Mesh(new THREE.SphereGeometry(1.8, 12, 12), canopyMat);
c1.position.y = 3.5;
group.add(c1);
const c2 = new THREE.Mesh(new THREE.SphereGeometry(1.4, 10, 10), canopyMat);
c2.position.set(-0.6, 3.0, 0.5);
group.add(c2);
const c3 = new THREE.Mesh(new THREE.SphereGeometry(1.3, 10, 10), canopyMat);
c3.position.set(0.5, 3.2, -0.4);
group.add(c3);
group.position.set(p[0], p[1], p[2]);
scene.add(group);
trees.push(group);
});
}
function createFlowers() {
const flowerColors = [C.pink, C.lavender, C.peach, C.coral, C.gold, 0xFFFFFF];
for (let i = 0; i < 30; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 4 + Math.random() * 22;
const x = Math.cos(angle) * r;
const z = Math.sin(angle) * r;
// Check not too close to buildings
const dist = Math.sqrt(x*x + z*z);
if (dist > 26) continue;
const group = new THREE.Group();
// Stem
const stemGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.5 + Math.random() * 0.3, 4);
const stemMat = new THREE.MeshStandardMaterial({ color: 0x6DBF7A, roughness: 0.7 });
const stem = new THREE.Mesh(stemGeo, stemMat);
stem.position.y = 0.25;
group.add(stem);
// Bloom
const col = flowerColors[Math.floor(Math.random() * flowerColors.length)];
const bloomGeo = new THREE.SphereGeometry(0.12 + Math.random() * 0.08, 8, 8);
const bloomMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.5, emissive: col, emissiveIntensity: 0.15 });
const bloom = new THREE.Mesh(bloomGeo, bloomMat);
bloom.position.y = 0.55;
group.add(bloom);
group.position.set(x, 0, z);
scene.add(group);
flowers.push(group);
}
}
function createPond() {
const pondGeo = new THREE.CircleGeometry(3, 32);
const pondMat = new THREE.MeshStandardMaterial({
color: 0xA0D8EF, roughness: 0.1, metalness: 0.1,
transparent: true, opacity: 0.6
});
const pond = new THREE.Mesh(pondGeo, pondMat);
pond.rotation.x = -Math.PI / 2;
pond.position.set(8, 0.05, -5);
scene.add(pond);
// Pond edge
const edgeGeo = new THREE.TorusGeometry(3, 0.2, 8, 32);
const edgeMat = new THREE.MeshStandardMaterial({ color: 0x9BCFAB, roughness: 0.7 });
const pedge = new THREE.Mesh(edgeGeo, edgeMat);
pedge.rotation.x = Math.PI / 2;
pedge.position.set(8, 0.06, -5);
scene.add(pedge);
}
function createPaths() {
// Simple arced paths connecting buildings to center
const centerX = 0, centerZ = 0;
BUILDING_DEFS.forEach(def => {
if (def.type === 'hub') return;
const pts = [];
const steps = 20;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = centerX + (def.pos[0] - centerX) * t;
const z = centerZ + (def.pos[2] - centerZ) * t;
pts.push(new THREE.Vector3(x, 0.03, z));
}
const pathGeo = new THREE.BufferGeometry().setFromPoints(pts);
const pathMat = new THREE.LineBasicMaterial({ color: 0xE8DDD0, transparent: true, opacity: 0.5 });
const pathLine = new THREE.Line(pathGeo, pathMat);
scene.add(pathLine);
});
// Wider path on ground (simple discs along the way)
BUILDING_DEFS.forEach(def => {
if (def.type === 'hub') return;
const dx = def.pos[0], dz = def.pos[2];
for (let i = 1; i < 5; i++) {
const t = i / 5;
const discGeo = new THREE.CircleGeometry(0.4, 8);
const discMat = new THREE.MeshStandardMaterial({ color: 0xEDE4D8, roughness: 0.9 });
const disc = new THREE.Mesh(discGeo, discMat);
disc.rotation.x = -Math.PI / 2;
disc.position.set(dx * t, 0.03, dz * t);
scene.add(disc);
}
});
}
function createFloatingNotes() {
const symbols = ['♪', '✦', '♡', '☆'];
for (let i = 0; i < 6; i++) {
const canvas = document.createElement('canvas');
canvas.width = 64; canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.font = '40px sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = [
'#FFB5C8','#D4B8FF','#A8E6CF','#FFE5A0','#B8D4FF','#FFB8A8'
][i % 6];
ctx.globalAlpha = 0.7;
ctx.fillText(symbols[i % symbols.length], 32, 46);
const tex = new THREE.CanvasTexture(canvas);
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.6, depthTest: false });
const sprite = new THREE.Sprite(mat);
sprite.scale.set(1.2, 1.2, 1);
const angle = Math.random() * Math.PI * 2;
sprite.position.set(Math.cos(angle) * (3 + Math.random() * 5), 5 + Math.random() * 8, Math.sin(angle) * (3 + Math.random() * 5));
sprite.userData = { baseY: sprite.position.y, speed: 0.5 + Math.random() * 0.5, angle: Math.random() * Math.PI * 2, radius: 2 + Math.random() * 3 };
scene.add(sprite);
floatingNotes.push(sprite);
}
}
function createBuilding(def) {
const group = new THREE.Group();
group.position.set(def.pos[0], 0, def.pos[2]);
group.userData = { name: def.name, baseHeight: def.height, active: false };
const col = new THREE.Color(def.color);
const mat = (c, extra) => new THREE.MeshStandardMaterial({ color: c, roughness: 0.6, metalness: 0.05, ...extra });
// Plot disc
const plotGeo = new THREE.CylinderGeometry(4.5, 4.5, 0.3, 24);
const plotMat = new THREE.MeshStandardMaterial({ color: 0xB5DCC0, roughness: 0.8 });
const plot = new THREE.Mesh(plotGeo, plotMat);
plot.position.y = -0.15;
group.add(plot);
switch(def.type) {
case 'factory': {
// Stacked rounded cylinders like a mushroom tower
const base = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 3, 4, 16), mat(def.color));
base.position.y = 2;
group.add(base);
const mid = new THREE.Mesh(new THREE.CylinderGeometry(2.2, 2.5, 2.5, 16), mat(new THREE.Color(def.color).multiplyScalar(0.9)));
mid.position.y = 5.25;
group.add(mid);
// Dome top
const dome = new THREE.Mesh(new THREE.SphereGeometry(2.2, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2), mat(def.color));
dome.position.y = 6.5;
group.add(dome);
// Windows (dark circles)
for (let i = 0; i < 3; i++) {
const a = (i / 3) * Math.PI * 2;
const win = new THREE.Mesh(new THREE.CircleGeometry(0.4, 12), mat(0x7CC4A8));
win.position.set(Math.cos(a) * 2.51, 2.5 + i * 1.2, Math.sin(a) * 2.51);
win.lookAt(win.position.clone().multiplyScalar(2));
group.add(win);
}
// Chimney
const chimney = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.5, 2, 8), mat(0x8BCFB0));
chimney.position.set(1.5, 7.5, 0);
group.add(chimney);
// Smoke particles
for (let i = 0; i < 5; i++) {
const smoke = new THREE.Mesh(
new THREE.SphereGeometry(0.2 + Math.random() * 0.2, 8, 8),
new THREE.MeshStandardMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.4, roughness: 0.9 })
);
smoke.position.set(1.5 + (Math.random() - 0.5) * 0.3, 8.5 + i * 0.5, (Math.random() - 0.5) * 0.3);
smoke.userData = { baseY: 8.5, speed: 0.3 + Math.random() * 0.5, offset: Math.random() * Math.PI * 2 };
group.add(smoke);
smokeParticles.push(smoke);
}
break;
}
case 'cottage': {
// Rounded box body + dome roof
const body = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 2.8, 3.5, 12), mat(def.color));
body.position.y = 1.75;
group.add(body);
const roof = new THREE.Mesh(new THREE.SphereGeometry(2.8, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2), mat(0xE8C870));
roof.position.y = 3.5;
group.add(roof);
// Door
const door = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 1.2, 8, 1, false, 0, Math.PI), mat(0xC4985A));
door.rotation.y = Math.PI;
door.position.set(0, 0.6, 2.81);
group.add(door);
// Windows
const winL = new THREE.Mesh(new THREE.CircleGeometry(0.35, 12), mat(0xFFF8DC));
winL.position.set(-1.2, 2.5, 2.51);
group.add(winL);
const winR = new THREE.Mesh(new THREE.CircleGeometry(0.35, 12), mat(0xFFF8DC));
winR.position.set(1.2, 2.5, 2.51);
group.add(winR);
break;
}
case 'greenhouse': {
// Half-sphere dome, transparent green
const dome = new THREE.Mesh(
new THREE.SphereGeometry(3, 20, 16, 0, Math.PI * 2, 0, Math.PI / 2),
new THREE.MeshStandardMaterial({ color: 0x88D4A0, roughness: 0.2, metalness: 0.1, transparent: true, opacity: 0.35 })
);
dome.position.y = 0;
group.add(dome);
// Base ring
const ring = new THREE.Mesh(new THREE.TorusGeometry(3, 0.2, 8, 24), mat(0x6DBF7A));
ring.rotation.x = Math.PI / 2;
ring.position.y = 0.1;
group.add(ring);
// Little plants inside
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2;
const r = 1 + Math.random();
const plant = new THREE.Mesh(new THREE.ConeGeometry(0.3, 1 + Math.random() * 0.5, 6), mat(0x5DAF70));
plant.position.set(Math.cos(a) * r, 0.6, Math.sin(a) * r);
group.add(plant);
}
break;
}
case 'wizard': {
// Sphere base + cone roof (wizard hat)
const base = new THREE.Mesh(new THREE.SphereGeometry(2.5, 16, 16), mat(def.color));
base.position.y = 2.5;
base.scale.set(1, 0.8, 1);
group.add(base);
const hat = new THREE.Mesh(new THREE.ConeGeometry(2.2, 4, 16), mat(0xEAB88A));
hat.position.y = 5;
group.add(hat);
// Question mark above: torus + sphere
const qTorus = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.12, 8, 16, Math.PI * 1.5), mat(0xFFEEDD, { emissive: 0xFFDAB9, emissiveIntensity: 0.3 }));
qTorus.position.set(0, 8, 0);
qTorus.rotation.x = 0.2;
group.add(qTorus);
const qDot = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 8), mat(0xFFEEDD, { emissive: 0xFFDAB9, emissiveIntensity: 0.3 }));
qDot.position.set(0, 7.2, 0);
group.add(qDot);
group.userData.qMark = { torus: qTorus, dot: qDot };
break;
}
case 'warehouse': {
// Wide rounded cylinder
const body = new THREE.Mesh(new THREE.CylinderGeometry(3, 3.2, 3, 20), mat(def.color));
body.position.y = 1.5;
group.add(body);
// Flat top with slight dome
const top = new THREE.Mesh(new THREE.SphereGeometry(3, 16, 8, 0, Math.PI * 2, 0, Math.PI / 4), mat(0x9EC4E8));
top.position.y = 3;
group.add(top);
// Semicircle door
const door = new THREE.Mesh(
new THREE.CylinderGeometry(0.7, 0.7, 0.1, 12, 1, false, 0, Math.PI),
mat(0x8AB5D8)
);
door.rotation.x = Math.PI / 2;
door.rotation.z = Math.PI;
door.position.set(0, 0.7, 3.21);
group.add(door);
break;
}
case 'hub': {
// Cute rounded tower, biggest
const body = new THREE.Mesh(new THREE.CylinderGeometry(3, 3.5, 5, 20), mat(def.color));
body.position.y = 2.5;
group.add(body);
const mid = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 3, 2, 20), mat(0xFFC0D0));
mid.position.y = 6;
group.add(mid);
// Glowing sphere on top
const orb = new THREE.Mesh(
new THREE.SphereGeometry(1.5, 20, 20),
new THREE.MeshStandardMaterial({ color: 0xFFC8D8, roughness: 0.3, emissive: 0xFFB5C8, emissiveIntensity: 0.4 })
);
orb.position.y = 8.5;
group.add(orb);
group.userData.orb = orb;
// Star accent
const starCanvas = document.createElement('canvas');
starCanvas.width = 64; starCanvas.height = 64;
const sCtx = starCanvas.getContext('2d');
sCtx.font = '48px sans-serif';
sCtx.textAlign = 'center';
sCtx.fillStyle = '#FFB5C8';
sCtx.fillText('♡', 32, 46);
const starTex = new THREE.CanvasTexture(starCanvas);
const starSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: starTex, transparent: true, opacity: 0.6, depthTest: false }));
starSprite.scale.set(1.5, 1.5, 1);
starSprite.position.set(0, 10.5, 0);
group.add(starSprite);
// Heart window
const heartCanvas = document.createElement('canvas');
heartCanvas.width = 64; heartCanvas.height = 64;
const hCtx = heartCanvas.getContext('2d');
hCtx.font = '36px sans-serif';
hCtx.textAlign = 'center';
hCtx.fillStyle = '#FF8FA8';
hCtx.fillText('♡', 32, 42);
const heartTex = new THREE.CanvasTexture(heartCanvas);
const heartSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: heartTex, transparent: true, opacity: 0.5 }));
heartSprite.scale.set(1.2, 1.2, 1);
heartSprite.position.set(0, 3.5, 3.6);
group.add(heartSprite);
break;
}
case 'studio': {
// Small round building
const body = new THREE.Mesh(new THREE.CylinderGeometry(2, 2.3, 3, 16), mat(def.color));
body.position.y = 1.5;
group.add(body);
const top = new THREE.Mesh(new THREE.SphereGeometry(2, 14, 10, 0, Math.PI * 2, 0, Math.PI / 2), mat(0xC8A8E8));
top.position.y = 3;
group.add(top);
// Easel next to it
const easelLeg1 = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 2.5, 4), mat(0xA0785A));
easelLeg1.position.set(3.5, 1.25, 0);
easelLeg1.rotation.z = 0.15;
group.add(easelLeg1);
const easelLeg2 = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 2.5, 4), mat(0xA0785A));
easelLeg2.position.set(3.5, 1.25, 0.5);
easelLeg2.rotation.z = -0.15;
group.add(easelLeg2);
// Canvas on easel
const canv = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.2, 0.1), mat(0xFFF8F0));
canv.position.set(3.5, 2.4, 0.25);
group.add(canv);
// Colorful splat on canvas
const splat = new THREE.Mesh(new THREE.CircleGeometry(0.3, 8), mat(C.pink, { emissive: C.pink, emissiveIntensity: 0.2 }));
splat.position.set(3.3, 2.3, 0.32);
group.add(splat);
const splat2 = new THREE.Mesh(new THREE.CircleGeometry(0.2, 8), mat(C.gold, { emissive: C.gold, emissiveIntensity: 0.2 }));
splat2.position.set(3.7, 2.5, 0.32);
group.add(splat2);
break;
}
}
// Soft point light per building
const light = new THREE.PointLight(def.color, 0.6, 15);
light.position.y = def.height + 1;
group.add(light);
group.userData.light = light;
group.userData.color = def.color;
scene.add(group);
buildings[def.name] = group;
}
function createBuildingLabels() {
BUILDING_DEFS.forEach(def => {
const group = buildings[def.name];
if (!group) return;
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.font = 'bold 22px "Nunito", sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = '#555555';
ctx.shadowColor = 'rgba(0,0,0,0.1)';
ctx.shadowBlur = 4;
ctx.shadowOffsetY = 1;
ctx.fillText(def.name, 128, 38);
const tex = new THREE.CanvasTexture(canvas);
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.85, depthTest: false });
const sprite = new THREE.Sprite(mat);
sprite.scale.set(7, 1.8, 1);
sprite.position.y = def.height + 3;
group.add(sprite);
group.userData.label = sprite;
});
}
function createBuba() {
bubaGroup = new THREE.Group();
bubaGroup.position.set(0, 0, 0);
const headRadius = 1.6;
const skinColor = 0xFFE8D6;
const hairColor = 0xB08050;
const bodyColor = C.pink;
// Head
const headMat = new THREE.MeshStandardMaterial({ color: skinColor, roughness: 0.5 });
const head = new THREE.Mesh(new THREE.SphereGeometry(headRadius, 24, 24), headMat);
head.scale.set(1, 0.92, 0.95);
head.position.y = 2.8;
bubaGroup.add(head);
// Big sparkly eyes
const eyeWhiteMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.2 });
const irisMat = new THREE.MeshStandardMaterial({ color: 0x7DD4B0, emissive: 0xA8E6CF, emissiveIntensity: 0.2, roughness: 0.3 });
const pupilMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.2 });
const specMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.5 });
function makeEye(x) {
const ew = new THREE.Mesh(new THREE.SphereGeometry(0.46, 16, 16), eyeWhiteMat);
ew.position.set(x, 2.55, 1.3);
ew.scale.set(1, 1.15, 0.5);
bubaGroup.add(ew);
const iris = new THREE.Mesh(new THREE.SphereGeometry(0.3, 14, 14), irisMat);
iris.position.set(x, 2.5, 1.52);
iris.scale.set(1, 1.1, 0.5);
bubaGroup.add(iris);
const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.15, 10, 10), pupilMat);
pupil.position.set(x, 2.48, 1.6);
pupil.scale.set(1, 1.1, 0.5);
bubaGroup.add(pupil);
// Big sparkle highlight
const spec1 = new THREE.Mesh(new THREE.SphereGeometry(0.09, 8, 8), specMat);
spec1.position.set(x + 0.1, 2.65, 1.6);
bubaGroup.add(spec1);
const spec2 = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 6), specMat);
spec2.position.set(x - 0.05, 2.58, 1.62);
bubaGroup.add(spec2);
}
makeEye(-0.5);
makeEye(0.5);
// Tiny smile
const mouthMat = new THREE.MeshStandardMaterial({ color: 0xCC8888, roughness: 0.5 });
const mouth = new THREE.Mesh(new THREE.TorusGeometry(0.15, 0.04, 8, 12, Math.PI), mouthMat);
mouth.position.set(0, 2.05, 1.42);
mouth.rotation.x = 0.2;
bubaGroup.add(mouth);
// Big pink blush circles
const blushMat = new THREE.MeshStandardMaterial({ color: 0xFFAAAA, transparent: true, opacity: 0.4, roughness: 0.8 });
const blushL = new THREE.Mesh(new THREE.SphereGeometry(0.25, 10, 10), blushMat);
blushL.position.set(-0.95, 2.25, 1.2);
blushL.scale.set(1.2, 0.7, 0.3);
bubaGroup.add(blushL);
const blushR = new THREE.Mesh(new THREE.SphereGeometry(0.25, 10, 10), blushMat);
blushR.position.set(0.95, 2.25, 1.2);
blushR.scale.set(1.2, 0.7, 0.3);
bubaGroup.add(blushR);
// Soft brown hair
const hairMat = new THREE.MeshStandardMaterial({ color: hairColor, roughness: 0.7 });
const hairPieces = [
{ p:[0, 4.15, 0], s:[0.9, 0.6, 0.8] },
{ p:[-0.5, 4.1, 0.2], s:[0.7, 0.55, 0.6] },
{ p:[0.5, 4.1, 0.2], s:[0.7, 0.55, 0.6] },
{ p:[0, 4.0, -0.5], s:[0.8, 0.5, 0.7] },
{ p:[-1.3, 3.2, 0], s:[0.5, 0.8, 0.6] },
{ p:[1.3, 3.2, 0], s:[0.5, 0.8, 0.6] },
{ p:[-1.1, 3.6, -0.3], s:[0.55, 0.6, 0.5] },
{ p:[1.1, 3.6, -0.3], s:[0.55, 0.6, 0.5] },
{ p:[0, 3.5, -1.2], s:[1.0, 0.7, 0.5] },
{ p:[-0.6, 3.3, -1.0], s:[0.6, 0.6, 0.5] },
{ p:[0.6, 3.3, -1.0], s:[0.6, 0.6, 0.5] },
{ p:[0, 3.7, 1.1], s:[1.1, 0.35, 0.4] },
];
hairPieces.forEach(h => {
const piece = new THREE.Mesh(new THREE.SphereGeometry(0.5, 10, 10), hairMat);
piece.position.set(h.p[0], h.p[1], h.p[2]);
piece.scale.set(h.s[0], h.s[1], h.s[2]);
bubaGroup.add(piece);
});
// Glasses (round, softer)
const glassMat = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.4, metalness: 0.2 });
const glassL = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.05, 12, 24), glassMat);
glassL.position.set(-0.5, 2.55, 1.35);
bubaGroup.add(glassL);
const glassR = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.05, 12, 24), glassMat);
glassR.position.set(0.5, 2.55, 1.35);
bubaGroup.add(glassR);
const bridge = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.4, 6), glassMat);
bridge.rotation.z = Math.PI / 2;
bridge.position.set(0, 2.55, 1.5);
bubaGroup.add(bridge);
const templeL = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 1.0, 4), glassMat);
templeL.rotation.x = Math.PI / 2;
templeL.position.set(-0.95, 2.55, 0.8);
bubaGroup.add(templeL);
const templeR = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 1.0, 4), glassMat);
templeR.rotation.x = Math.PI / 2;
templeR.position.set(0.95, 2.55, 0.8);
bubaGroup.add(templeR);
// Headphones
const hpMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.4, metalness: 0.3 });
const hpArc = new THREE.Mesh(new THREE.TorusGeometry(1.5, 0.08, 12, 24, Math.PI), hpMat);
hpArc.position.set(0, 3.4, 0);
hpArc.rotation.y = Math.PI / 2;
bubaGroup.add(hpArc);
const cupMat = new THREE.MeshStandardMaterial({ color: C.pink, roughness: 0.4 });
const cupL = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.32, 0.22, 12), cupMat);
cupL.rotation.z = Math.PI / 2;
cupL.position.set(-1.5, 2.0, 0);
bubaGroup.add(cupL);
const cupR = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.32, 0.22, 12), cupMat);
cupR.rotation.z = Math.PI / 2;
cupR.position.set(1.5, 2.0, 0);
bubaGroup.add(cupR);
// Body (soft rose)
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.5, metalness: 0.05 });
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.65, 0.55, 1.0, 10), bodyMat);
body.position.y = 0.9;
bubaGroup.add(body);
const bodyTop = new THREE.Mesh(new THREE.SphereGeometry(0.65, 10, 6, 0, Math.PI * 2, 0, Math.PI / 2), bodyMat);
bodyTop.position.y = 1.4;
bubaGroup.add(bodyTop);
// Arms
const armL = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.15, 0.7, 8), bodyMat);
armL.position.set(-0.9, 1.1, 0); armL.rotation.z = 0.5;
bubaGroup.add(armL);
const handL = new THREE.Mesh(new THREE.SphereGeometry(0.13, 8, 8), new THREE.MeshStandardMaterial({ color: skinColor, roughness: 0.5 }));
handL.position.set(-1.2, 0.85, 0);
bubaGroup.add(handL);
const armR = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.15, 0.7, 8), bodyMat);
armR.position.set(0.9, 1.1, 0); armR.rotation.z = -0.5;
bubaGroup.add(armR);
const handR = new THREE.Mesh(new THREE.SphereGeometry(0.13, 8, 8), new THREE.MeshStandardMaterial({ color: skinColor, roughness: 0.5 }));
handR.position.set(1.2, 0.85, 0);
bubaGroup.add(handR);
// Legs
const legMat = bodyMat;
const legL = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.16, 0.45, 8), legMat);
legL.position.set(-0.3, 0.22, 0);
bubaGroup.add(legL);
const legR = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.16, 0.45, 8), legMat);
legR.position.set(0.3, 0.22, 0);
bubaGroup.add(legR);
// Shoes (soft pastel)
const shoeMat = new THREE.MeshStandardMaterial({ color: C.coral, roughness: 0.5 });
const shoeL = new THREE.Mesh(new THREE.SphereGeometry(0.17, 8, 8), shoeMat);
shoeL.position.set(-0.3, -0.02, 0.05); shoeL.scale.set(1, 0.7, 1.3);
bubaGroup.add(shoeL);
const shoeR = new THREE.Mesh(new THREE.SphereGeometry(0.17, 8, 8), shoeMat);
shoeR.position.set(0.3, -0.02, 0.05); shoeR.scale.set(1, 0.7, 1.3);
bubaGroup.add(shoeR);
// Soft aura ring
const auraGeo = new THREE.TorusGeometry(1.8, 0.12, 8, 32);
const auraMat = new THREE.MeshStandardMaterial({
color: C.pink, emissive: C.pink, emissiveIntensity: 0.3,
transparent: true, opacity: 0.2
});
bubaAura = new THREE.Mesh(auraGeo, auraMat);
bubaAura.rotation.x = Math.PI / 2;
bubaAura.position.y = -0.2;
bubaGroup.add(bubaAura);
// Inner aura
const aura2 = new THREE.Mesh(
new THREE.TorusGeometry(1.2, 0.08, 8, 24),
new THREE.MeshStandardMaterial({ color: C.lavender, emissive: C.lavender, emissiveIntensity: 0.2, transparent: true, opacity: 0.15 })
);
aura2.rotation.x = Math.PI / 2;
aura2.position.y = -0.15;
bubaGroup.add(aura2);
// Label
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 80;
const ctx = canvas.getContext('2d');
ctx.font = 'bold 42px "Nunito", sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = '#FFB5C8';
ctx.shadowColor = 'rgba(255,181,200,0.4)';
ctx.shadowBlur = 10;
ctx.fillText('✦ BUBA ✦', 128, 55);
const tex = new THREE.CanvasTexture(canvas);
const labelMat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
const label = new THREE.Sprite(labelMat);
label.scale.set(5, 1.6, 1);
label.position.y = 5.2;
bubaGroup.add(label);
// Soft point light
const bubaLight = new THREE.PointLight(C.pink, 0.8, 15);
bubaLight.position.y = 3;
bubaGroup.add(bubaLight);
// Sparkle particles (white/gold, gentle)
const pGeo = new THREE.BufferGeometry();
const pCount = 40;
const pPos = new Float32Array(pCount * 3);
const pVel = new Float32Array(pCount * 3);
const pColors = new Float32Array(pCount * 3);
const sparkleColors = [new THREE.Color(0xFFFFFF), new THREE.Color(0xFFE5A0), new THREE.Color(0xFFB5C8), new THREE.Color(0xD4B8FF)];
for (let i = 0; i < pCount; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = 0.5 + Math.random() * 2;
pPos[i*3] = Math.cos(angle) * dist;
pPos[i*3+1] = Math.random() * 5;
pPos[i*3+2] = Math.sin(angle) * dist;
pVel[i*3] = (Math.random() - 0.5) * 0.012;
pVel[i*3+1] = Math.random() * 0.025 + 0.008;
pVel[i*3+2] = (Math.random() - 0.5) * 0.012;
const c = sparkleColors[Math.floor(Math.random() * sparkleColors.length)];
pColors[i*3] = c.r; pColors[i*3+1] = c.g; pColors[i*3+2] = c.b;
}
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
pGeo.setAttribute('color', new THREE.BufferAttribute(pColors, 3));
pGeo.userData = { velocities: pVel };
const pMat = new THREE.PointsMaterial({ size: 0.15, transparent: true, opacity: 0.6, sizeAttenuation: true, vertexColors: true });
bubaParticles = new THREE.Points(pGeo, pMat);
bubaParticles.visible = false;
bubaGroup.add(bubaParticles);
scene.add(bubaGroup);
}
function createSubAgent(id, position) {
const group = new THREE.Group();
group.position.set(position.x, 0, position.z);
const sc = 0.55;
group.scale.set(sc, sc, sc);
const skinColor = 0xFFE8D6;
const bodyColor = C.teal;
const headRadius = 1.3;
const headMat = new THREE.MeshStandardMaterial({ color: skinColor, roughness: 0.5 });
const head = new THREE.Mesh(new THREE.SphereGeometry(headRadius, 20, 20), headMat);
head.scale.set(1, 0.9, 0.93);
head.position.y = 2.6;
group.add(head);
const eyeWhiteMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.2 });
const irisMat = new THREE.MeshStandardMaterial({ color: 0x7DD4B0, emissive: 0xA8E6CF, emissiveIntensity: 0.15 });
function makeSubEye(x) {
const ew = new THREE.Mesh(new THREE.SphereGeometry(0.35, 12, 12), eyeWhiteMat);
ew.position.set(x, 2.4, 1.05); ew.scale.set(1, 1.1, 0.5);
group.add(ew);
const ir = new THREE.Mesh(new THREE.SphereGeometry(0.22, 10, 10), irisMat);
ir.position.set(x, 2.35, 1.2); ir.scale.set(1, 1.1, 0.5);
group.add(ir);
const sp = new THREE.Mesh(new THREE.SphereGeometry(0.06, 6, 6), new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.3 }));
sp.position.set(x + 0.08, 2.5, 1.22);
group.add(sp);
}
makeSubEye(-0.4);
makeSubEye(0.4);
// Blush
const blushMat = new THREE.MeshStandardMaterial({ color: 0xFFAAAA, transparent: true, opacity: 0.35, roughness: 0.8 });
const blL = new THREE.Mesh(new THREE.SphereGeometry(0.18, 8, 8), blushMat);
blL.position.set(-0.75, 2.15, 1.0); blL.scale.set(1, 0.6, 0.3);
group.add(blL);
const blR = new THREE.Mesh(new THREE.SphereGeometry(0.18, 8, 8), blushMat);
blR.position.set(0.75, 2.15, 1.0); blR.scale.set(1, 0.6, 0.3);
group.add(blR);
const mouth = new THREE.Mesh(new THREE.TorusGeometry(0.1, 0.03, 6, 10, Math.PI), new THREE.MeshStandardMaterial({ color: 0xCC8888 }));
mouth.position.set(0, 1.95, 1.1);
group.add(mouth);
// Mint hair
const hairMat = new THREE.MeshStandardMaterial({ color: 0x7DD4B0, roughness: 0.6 });
[{ p:[0,3.6,0],s:[0.7,0.5,0.65]},{p:[-0.4,3.5,0.2],s:[0.55,0.45,0.5]},{p:[0.4,3.5,0.2],s:[0.55,0.45,0.5]},
{p:[0,3.4,-0.6],s:[0.7,0.4,0.5]},{p:[-0.9,2.9,0],s:[0.4,0.6,0.45]},{p:[0.9,2.9,0],s:[0.4,0.6,0.45]}
].forEach(h => {
const piece = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), hairMat);
piece.position.set(h.p[0], h.p[1], h.p[2]);
piece.scale.set(h.s[0], h.s[1], h.s[2]);
group.add(piece);
});
// Body
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.5 });
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.42, 0.8, 8), bodyMat);
body.position.y = 0.9;
group.add(body);
const armL = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.1, 0.5, 6), bodyMat);
armL.position.set(-0.7, 0.95, 0); armL.rotation.z = 0.5;
group.add(armL);
const armR = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.1, 0.5, 6), bodyMat);
armR.position.set(0.7, 0.95, 0); armR.rotation.z = -0.5;
group.add(armR);
const legL = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.1, 0.35, 6), bodyMat);
legL.position.set(-0.2, 0.32, 0);
group.add(legL);
const legR = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.1, 0.35, 6), bodyMat);
legR.position.set(0.2, 0.32, 0);
group.add(legR);
// Soft aura
const aura = new THREE.Mesh(
new THREE.TorusGeometry(1.0, 0.08, 8, 24),
new THREE.MeshStandardMaterial({ color: C.teal, emissive: C.teal, emissiveIntensity: 0.2, transparent: true, opacity: 0.15 })
);
aura.rotation.x = Math.PI / 2;
aura.position.y = -0.1;
group.add(aura);
const light = new THREE.PointLight(C.teal, 0.4, 10);
light.position.y = 3;
group.add(light);
group.userData = { id, spawnTime: clock.getElapsedTime(), active: true, aura };
scene.add(group);
subagentMeshes[id] = group;
return group;
}
function removeSubAgent(id) {
const mesh = subagentMeshes[id];
if (mesh) {
scene.remove(mesh);
mesh.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (child.material.map) child.material.map.dispose();
child.material.dispose();
}
});
delete subagentMeshes[id];
}
}
function createDataBeam(fromPos, toPos, color) {
const midY = Math.max(fromPos.y, toPos.y) + 6;
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(fromPos.x, fromPos.y + 3, fromPos.z),
new THREE.Vector3((fromPos.x + toPos.x) / 2, midY, (fromPos.z + toPos.z) / 2),
new THREE.Vector3(toPos.x, toPos.y + 3, toPos.z)
);
const points = curve.getPoints(30);
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.25 });
const line = new THREE.Line(geo, mat);
scene.add(line);
// Flowing dot — soft glowing
const dotGeo = new THREE.SphereGeometry(0.2, 8, 8);
const dotMat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.3, transparent: true, opacity: 0.7 });
const dot = new THREE.Mesh(dotGeo, dotMat);
scene.add(dot);
const beam = { line, dot, curve, t: 0, speed: 0.004 + Math.random() * 0.004, life: 0, maxLife: 3 + Math.random() * 2 };
dataBeams.push(beam);
return beam;
}
function cleanupBeams(dt) {
for (let i = dataBeams.length - 1; i >= 0; i--) {
const b = dataBeams[i];
b.life += dt;
b.t = (b.t + b.speed) % 1;
const p = b.curve.getPoint(b.t);
b.dot.position.copy(p);
if (b.life > b.maxLife) {
scene.remove(b.line); scene.remove(b.dot);
b.line.geometry.dispose(); b.line.material.dispose();
b.dot.geometry.dispose(); b.dot.material.dispose();
dataBeams.splice(i, 1);
}
}
}
async function fetchData() {
try {
const res = await fetch('/api/state');
if (!res.ok) return;
dashState = await res.json();
updateHUD();
updateWorld();
} catch(e) {}
}
function updateHUD() {
if (!dashState) return;
const s = dashState.stats || {};
const b = dashState.buba || {};
const isActive = b.status === 'active';
document.getElementById('status-dot').className = 'status-dot' + (isActive ? '' : ' idle');
document.getElementById('status-text').textContent = isActive ? '✦ Active' : 'Idle';
document.getElementById('current-task').textContent = b.currentTask || '';
document.getElementById('stat-sessions').textContent = s.totalSessions ?? '—';
document.getElementById('stat-subagents').textContent = s.totalSubagents ?? '—';
document.getElementById('stat-active').textContent = (s.activeSessions ?? 0) + ' / ' + (s.activeSubagents ?? 0);
document.getElementById('stat-today').textContent = s.todaysSessions ?? '—';
document.getElementById('session-count').textContent = (s.todaysSessions || 0) + ' sessions today';
const feed = document.getElementById('activity-feed');
const activities = dashState.activity || [];
feed.innerHTML = '';
activities.slice(-10).reverse().forEach(a => {
const div = document.createElement('div');
div.className = 'activity-item ' + (a.type || '');
const typeLabel = a.type === 'tool_call' ? '✦ Tool' : a.type === 'user_message' ? '♡ Message' : '☆ Reply';
div.innerHTML = `<div class="type-tag">${typeLabel}</div>${(a.content || '').slice(0, 80)}`;
feed.appendChild(div);
});
}
let prevSubagentIds = new Set();
function updateWorld() {
if (!dashState) return;
const isActive = dashState.buba?.status === 'active';
bubaParticles.visible = isActive;
const currentIds = new Set((dashState.subagents || []).filter(s => s.active).map(s => s.id));
for (const id of prevSubagentIds) {
if (!currentIds.has(id)) removeSubAgent(id);
}
for (const sa of (dashState.subagents || []).filter(s => s.active)) {
if (!subagentMeshes[sa.id]) {
const bKeys = Object.keys(buildings);
const target = buildings[bKeys[Math.floor(Math.random() * bKeys.length)]];
const pos = target.position.clone();
pos.x += (Math.random() - 0.5) * 6;
pos.z += (Math.random() - 0.5) * 6;
createSubAgent(sa.id, pos);
if (bubaGroup) createDataBeam(bubaGroup.position, pos, C.teal);
}
}
prevSubagentIds = currentIds;
if (isActive && Math.random() < 0.3 && dataBeams.length < 4) {
const bKeys = Object.keys(buildings);
const target = buildings[bKeys[Math.floor(Math.random() * bKeys.length)]];
const colors = [C.pink, C.teal, C.lavender, C.peach];
createDataBeam(bubaGroup.position, target.position, colors[Math.floor(Math.random() * colors.length)]);
}
}
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
document.getElementById('clock').textContent = `${h}:${m}:${s}`;
}
setInterval(updateClock, 1000);
updateClock();
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onDoubleClick(e) {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const meshes = [];
Object.values(buildings).forEach(g => { g.traverse(c => { if (c.isMesh) meshes.push(c); }); });
const intersects = raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
let target = intersects[0].object;
while (target.parent && !target.userData.name) target = target.parent;
if (target.userData.name || target.parent?.userData?.name) {
const bGroup = target.userData.name ? target : target.parent;
const pos = bGroup.position;
animateCameraTo(pos.x + 8, pos.y + 10, pos.z + 8, pos.x, pos.y + 3, pos.z);
}
}
}
function animateCameraTo(cx, cy, cz, tx, ty, tz) {
autoRotate = false;
clearTimeout(autoRotateTimer);
const start = { cx: camera.position.x, cy: camera.position.y, cz: camera.position.z,
tx: controls.target.x, ty: controls.target.y, tz: controls.target.z };
const dur = 1000;
const t0 = performance.now();
function step(now) {
const t = Math.min((now - t0) / dur, 1);
const ease = t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3) / 2;
camera.position.set(
start.cx + (cx - start.cx) * ease,
start.cy + (cy - start.cy) * ease,
start.cz + (cz - start.cz) * ease
);
controls.target.set(
start.tx + (tx - start.tx) * ease,
start.ty + (ty - start.ty) * ease,
start.tz + (tz - start.tz) * ease
);
if (t < 1) requestAnimationFrame(step);
else autoRotateTimer = setTimeout(() => autoRotate = true, 4000);
}
requestAnimationFrame(step);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta();
const t = clock.getElapsedTime();
// Auto-rotation (slow & gentle)
if (autoRotate) {
const angle = t * 0.04;
camera.position.x += Math.cos(angle) * 0.015;
camera.position.z += Math.sin(angle) * 0.015;
}
controls.update();
// Cloud drift
clouds.forEach(c => {
c.position.x = c.userData.baseX + Math.sin(t * 0.05 * c.userData.speed) * c.userData.range;
c.position.y += Math.sin(t * 0.3 + c.position.x) * 0.001;
});
// Sparkle twinkle
if (sparkles) {
sparkles.material.opacity = 0.3 + Math.sin(t * 2) * 0.2;
sparkles.rotation.y = t * 0.002;
}
// Floating notes
floatingNotes.forEach(n => {
n.position.y = n.userData.baseY + Math.sin(t * n.userData.speed) * 1.0;
n.position.x += Math.cos(t * 0.3 + n.userData.angle) * 0.003;
n.material.opacity = 0.3 + Math.sin(t * 0.8 + n.userData.angle) * 0.2;
n.material.rotation = Math.sin(t * 0.5) * 0.2;
});
// Smoke from factory chimney
smokeParticles.forEach(s => {
s.position.y = s.userData.baseY + Math.abs(Math.sin(t * s.userData.speed + s.userData.offset)) * 2;
s.position.x += Math.sin(t * 0.5 + s.userData.offset) * 0.003;
s.material.opacity = 0.35 - (s.position.y - s.userData.baseY) * 0.1;
if (s.material.opacity < 0) s.material.opacity = 0;
});
// Buba floating
if (bubaGroup) {
bubaGroup.position.y = Math.sin(t * 1.2) * 0.3 + 1.2;
bubaGroup.rotation.y = Math.sin(t * 0.25) * 0.06;
if (bubaAura) {
bubaAura.material.opacity = 0.15 + Math.sin(t * 1.5) * 0.05;
bubaAura.scale.setScalar(1 + Math.sin(t * 1.5) * 0.04);
bubaAura.rotation.z = t * 0.3;
}
}
// Buba sparkles
if (bubaParticles && bubaParticles.visible) {
const pos = bubaParticles.geometry.attributes.position.array;
const vel = bubaParticles.geometry.userData.velocities;
for (let i = 0; i < pos.length / 3; i++) {
pos[i*3] += vel[i*3];
pos[i*3+1] += vel[i*3+1];
pos[i*3+2] += vel[i*3+2];
if (pos[i*3+1] > 6) {
const angle = Math.random() * Math.PI * 2;
const dist = 0.5 + Math.random() * 2;
pos[i*3] = Math.cos(angle) * dist;
pos[i*3+1] = -0.5 + Math.random();
pos[i*3+2] = Math.sin(angle) * dist;
}
}
bubaParticles.geometry.attributes.position.needsUpdate = true;
}
// Building gentle breathing
Object.values(buildings).forEach(b => {
const breathe = 1 + Math.sin(t * 0.8 + b.position.x * 0.3) * 0.015;
b.scale.set(breathe, breathe, breathe);
if (b.userData.light) b.userData.light.intensity = 0.5 + Math.sin(t * 1.2 + b.position.x) * 0.15;
// Question mark float
if (b.userData.qMark) {
b.userData.qMark.torus.position.y = 8 + Math.sin(t * 1.5) * 0.3;
b.userData.qMark.dot.position.y = 7.2 + Math.sin(t * 1.5) * 0.3;
b.userData.qMark.torus.rotation.y = t * 0.5;
}
// Hub orb pulse
if (b.userData.orb) {
b.userData.orb.scale.setScalar(1 + Math.sin(t * 2) * 0.08);
b.userData.orb.material.emissiveIntensity = 0.3 + Math.sin(t * 1.5) * 0.15;
}
});
// Trees gentle sway
trees.forEach(tr => {
tr.rotation.z = Math.sin(t * 0.5 + tr.position.x) * 0.02;
});
// Sub-agent bobbing
Object.values(subagentMeshes).forEach(sa => {
sa.position.y = Math.sin(t * 1.8 + sa.userData.spawnTime) * 0.25 + 0.6;
sa.rotation.y = Math.sin(t * 0.4 + sa.userData.spawnTime) * 0.2;
if (sa.userData.aura) {
sa.userData.aura.material.opacity = 0.1 + Math.sin(t * 2 + sa.userData.spawnTime) * 0.05;
sa.userData.aura.rotation.z = t * 0.4;
}
});
cleanupBeams(dt);
composer.render();
}
init();
</script>
</body>
</html>