import * as THREE from 'three'; let dustParticles = null; let dustPositions = null; let dustVelocities = null; const DUST_COUNT = 600; // Job Indicators const _activeJobIndicators = new Map(); const INDICATOR_Y_OFFSET = 3.5; // Height above Timmy const INDICATOR_X_OFFSET = 1.0; // Offset from Timmy's center for multiple jobs const JOB_INDICATOR_DEFS = { writing: { create: () => { // Quill (cone for feather, cylinder for handle) const quillGroup = new THREE.Group(); const featherGeo = new THREE.ConeGeometry(0.15, 0.6, 4); const featherMat = new THREE.MeshStandardMaterial({ color: 0xc8c4bc, roughness: 0.8 }); const feather = new THREE.Mesh(featherGeo, featherMat); feather.position.y = 0.3; feather.rotation.x = Math.PI / 8; quillGroup.add(feather); const handleGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.4, 8); const handleMat = new THREE.MeshStandardMaterial({ color: 0x3d2506, roughness: 0.7 }); const handle = new THREE.Mesh(handleGeo, handleMat); handle.position.y = -0.2; quillGroup.add(handle); return quillGroup; }, color: 0xe8d5a0, // parchment-like }, coding: { create: () => { // Brackets (simple box geometry) const bracketsGroup = new THREE.Group(); const bracketMat = new THREE.MeshStandardMaterial({ color: 0x5599dd, emissive: 0x224466, emissiveIntensity: 0.3, roughness: 0.4 }); const bracketGeo = new THREE.BoxGeometry(0.05, 0.3, 0.05); const br1 = new THREE.Mesh(bracketGeo, bracketMat); br1.position.set(-0.1, 0.0, 0); bracketsGroup.add(br1); const br2 = br1.clone(); br2.position.set(0.1, 0.0, 0); bracketsGroup.add(br2); const crossbarGeo = new THREE.BoxGeometry(0.25, 0.05, 0.05); const crossbar1 = new THREE.Mesh(crossbarGeo, bracketMat); crossbar1.position.set(0, 0.125, 0); bracketsGroup.add(crossbar1); const crossbar2 = crossbar1.clone(); crossbar2.position.set(0, -0.125, 0); bracketsGroup.add(crossbar2); return bracketsGroup; }, color: 0x5599dd, // code-editor blue }, research: { create: () => { // Spider (simple sphere body, cylinder legs) - very simplified const spiderGroup = new THREE.Group(); const bodyMat = new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.9 }); const body = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 8), bodyMat); spiderGroup.add(body); const legMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 }); const legGeo = new THREE.CylinderGeometry(0.015, 0.015, 0.4, 4); const legPositions = [ [0.18, 0.0, 0.08, Math.PI / 4], [-0.18, 0.0, 0.08, -Math.PI / 4], [0.22, 0.0, -0.05, Math.PI / 2], [-0.22, 0.0, -0.05, -Math.PI / 2], [0.18, 0.0, -0.18, 3 * Math.PI / 4], [-0.18, 0.0, -0.18, -3 * Math.PI / 4], ]; legPositions.forEach(([x, y, z, rotY]) => { const leg = new THREE.Mesh(legGeo, legMat); leg.position.set(x, y - 0.1, z); leg.rotation.z = Math.PI / 2; leg.rotation.y = rotY; spiderGroup.add(leg); }); return spiderGroup; }, color: 0x8b0000, // dark red, investigative }, creative: { create: () => { // Lightbulb (sphere with small cylinder base) const bulbGroup = new THREE.Group(); const bulbMat = new THREE.MeshStandardMaterial({ color: 0xffddaa, emissive: 0xffaa00, emissiveIntensity: 0.8, transparent: true, opacity: 0.9, roughness: 0.1 }); const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.2, 16, 12), bulbMat); bulbGroup.add(bulb); const baseMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.6 }); const base = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.15, 8), baseMat); base.position.y = -0.25; bulbGroup.add(base); return bulbGroup; }, color: 0xffaa00, // bright idea yellow }, analysis: { create: () => { // Magnifying glass (torus for rim, plane for lens) const magGroup = new THREE.Group(); const rimMat = new THREE.MeshStandardMaterial({ color: 0xbb9900, roughness: 0.4, metalness: 0.7 }); const rim = new THREE.Mesh(new THREE.TorusGeometry(0.2, 0.03, 8, 20), rimMat); magGroup.add(rim); const handleMat = new THREE.MeshStandardMaterial({ color: 0x3d2506, roughness: 0.7 }); const handle = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.4, 6), handleMat); handle.position.set(0.25, -0.25, 0); handle.rotation.z = Math.PI / 4; magGroup.add(handle); const lensMat = new THREE.MeshPhysicalMaterial({ color: 0xaaffff, transmission: 0.8, roughness: 0.1, transparent: true }); const lens = new THREE.Mesh(new THREE.CircleGeometry(0.17, 16), lensMat); // Lens is a plane, so it will be rotated to face the camera or just set its position // For simplicity, make it a thin cylinder or sphere segment to give it depth const lensGeo = new THREE.CylinderGeometry(0.17, 0.17, 0.02, 16); const thinLens = new THREE.Mesh(lensGeo, lensMat); magGroup.add(thinLens); return magGroup; }, color: 0x88ddff, // clear blue, analytic }, other: { // Generic glowing orb create: () => { const orbMat = new THREE.MeshStandardMaterial({ color: 0x800080, emissive: 0x550055, emissiveIntensity: 0.8, roughness: 0.2 }); return new THREE.Mesh(new THREE.SphereGeometry(0.2, 16, 16), orbMat); }, color: 0x800080, // purple }, }; export function initEffects(scene) { initDustMotes(scene); } // Map to hold job indicator objects by jobId const jobIndicators = new Map(); export function createJobIndicator(category, jobId, position) { const def = JOB_INDICATOR_DEFS[category] || JOB_INDICATOR_DEFS.other; const indicatorGroup = new THREE.Group(); indicatorGroup.userData.jobId = jobId; indicatorGroup.userData.category = category; const object = def.create(); object.scale.setScalar(0.7); // Make indicators a bit smaller indicatorGroup.add(object); // Add a subtle glowing point light to the indicator const pointLight = new THREE.PointLight(def.color, 0.8, 3); indicatorGroup.add(pointLight); indicatorGroup.position.copy(position); jobIndicators.set(jobId, indicatorGroup); return indicatorGroup; } export function updateJobIndicators(time) { const t = time * 0.001; jobIndicators.forEach(indicator => { // Simple bobbing motion indicator.position.y += Math.sin(t * 2.5 + indicator.userData.jobId.charCodeAt(0)) * 0.002; // Rotation indicator.rotation.y += 0.01; }); } export function dissolveJobIndicator(jobId, scene) { const indicator = jobIndicators.get(jobId); if (indicator) { // TODO: Implement particle dissolve effect here // For now, just remove and dispose scene.remove(indicator); if (indicator.children.length > 0) { const object = indicator.children[0]; if (object.geometry) object.geometry.dispose(); if (object.material) { if (Array.isArray(object.material)) object.material.forEach(m => m.dispose()); else object.material.dispose(); } } indicator.children.forEach(child => { if (child.isLight) child.dispose(); }); jobIndicators.delete(jobId); } } function initDustMotes(scene) { const geo = new THREE.BufferGeometry(); const positions = new Float32Array(DUST_COUNT * 3); const colors = new Float32Array(DUST_COUNT * 3); const velocities = new Float32Array(DUST_COUNT); for (let i = 0; i < DUST_COUNT; i++) { positions[i * 3] = (Math.random() - 0.5) * 22; positions[i * 3 + 1] = Math.random() * 10; positions[i * 3 + 2] = (Math.random() - 0.5) * 16 - 2; velocities[i] = 0.008 + Math.random() * 0.012; const roll = Math.random(); if (roll < 0.6) { colors[i * 3] = 0.9 + Math.random() * 0.1; colors[i * 3 + 1] = 0.7 + Math.random() * 0.2; colors[i * 3 + 2] = 0.3 + Math.random() * 0.3; } else { const b = 0.3 + Math.random() * 0.5; colors[i * 3] = 0; colors[i * 3 + 1] = b; colors[i * 3 + 2] = 0; } } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); dustPositions = positions; dustVelocities = velocities; const mat = new THREE.PointsMaterial({ size: 0.06, vertexColors: true, transparent: true, opacity: 0.55, sizeAttenuation: true, }); dustParticles = new THREE.Points(geo, mat); scene.add(dustParticles); } export function updateEffects(time) { if (!dustParticles) return; const t = time * 0.001; for (let i = 0; i < DUST_COUNT; i++) { dustPositions[i * 3 + 1] += dustVelocities[i]; dustPositions[i * 3] += Math.sin(t * 0.5 + i * 0.1) * 0.002; if (dustPositions[i * 3 + 1] > 10) { dustPositions[i * 3 + 1] = 0; dustPositions[i * 3] = (Math.random() - 0.5) * 22; dustPositions[i * 3 + 2] = (Math.random() - 0.5) * 16 - 2; } } dustParticles.geometry.attributes.position.needsUpdate = true; } export function disposeEffects() { if (dustParticles) { dustParticles.geometry.dispose(); dustParticles.material.dispose(); dustParticles = null; } dustPositions = null; dustVelocities = null; jobIndicators.forEach(indicator => { if (indicator.children.length > 0) { const object = indicator.children[0]; if (object.geometry) object.geometry.dispose(); if (object.material) { if (Array.isArray(object.material)) object.material.forEach(m => m.dispose()); else object.material.dispose(); } } indicator.children.forEach(child => { if (child.isLight) child.dispose(); }); }); jobIndicators.clear(); }