1336 lines
53 KiB
HTML
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>
|