279 lines
9.6 KiB
JavaScript
279 lines
9.6 KiB
JavaScript
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();
|
|
} |