Files
timmy-tower/the-matrix/js/effects.js

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();
}