This commit was merged in pull request #102.
This commit is contained in:
202
the-matrix/js/effects.js
vendored
202
the-matrix/js/effects.js
vendored
@@ -5,10 +5,196 @@ 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);
|
||||
@@ -76,4 +262,18 @@ export function disposeEffects() {
|
||||
}
|
||||
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();
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
getTimmyGroup, applySlap, getCameraShakeStrength,
|
||||
TIMMY_WORLD_POS,
|
||||
} from './agents.js';
|
||||
import { initEffects, updateEffects, disposeEffects } from './effects.js';
|
||||
import { initEffects, updateEffects, disposeEffects, updateJobIndicators } from './effects.js';
|
||||
import { initUI, updateUI } from './ui.js';
|
||||
import { initInteraction, disposeInteraction, registerSlapTarget } from './interaction.js';
|
||||
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
|
||||
@@ -81,6 +81,7 @@ function buildWorld(firstInit, stateSnapshot) {
|
||||
|
||||
updateEffects(now);
|
||||
updateAgents(now);
|
||||
updateJobIndicators(now);
|
||||
updateUI({
|
||||
fps: currentFps,
|
||||
agentCount: getAgentCount(),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { setAgentState, setSpeechBubble, applyAgentStates, setMood } from './agents.js';
|
||||
import * as THREE from 'three';
|
||||
import { scene } from './world.js'; // Import the scene
|
||||
import { setAgentState, setSpeechBubble, applyAgentStates, setMood, TIMMY_WORLD_POS } from './agents.js';
|
||||
import { appendSystemMessage, appendDebateMessage, showCostTicker, updateCostTicker } from './ui.js';
|
||||
import { sentiment } from './edge-worker-client.js';
|
||||
import { setLabelState } from './hud-labels.js';
|
||||
import { createJobIndicator, dissolveJobIndicator } from './effects.js';
|
||||
|
||||
function resolveWsUrl() {
|
||||
const explicit = import.meta.env.VITE_WS_URL;
|
||||
@@ -19,6 +22,10 @@ let reconnectTimer = null;
|
||||
let visitorId = null;
|
||||
const RECONNECT_DELAY_MS = 5000;
|
||||
|
||||
// Map to keep track of active job indicator positions for offsetting
|
||||
const _jobIndicatorOffsets = new Map();
|
||||
let _nextJobOffsetIndex = 0;
|
||||
|
||||
export function initWebSocket(_scene) {
|
||||
visitorId = crypto.randomUUID();
|
||||
connect();
|
||||
@@ -95,6 +102,21 @@ function handleMessage(msg) {
|
||||
setLabelState(msg.agentId, 'active');
|
||||
}
|
||||
appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} started`);
|
||||
|
||||
// Spawn 3D job indicator
|
||||
if (msg.jobId && msg.category) {
|
||||
const offsetMultiplier = _jobIndicatorOffsets.size; // Simple way to spread them out
|
||||
const indicatorPosition = TIMMY_WORLD_POS.clone().add(
|
||||
new THREE.Vector3(
|
||||
(offsetMultiplier % 2 === 0 ? 1 : -1) * (Math.floor(offsetMultiplier / 2) + 1) * 0.7, // Alternate left/right
|
||||
3.5, // Height above Timmy
|
||||
-0.5
|
||||
)
|
||||
);
|
||||
const indicator = createJobIndicator(msg.category, msg.jobId, indicatorPosition);
|
||||
scene.add(indicator);
|
||||
_jobIndicatorOffsets.set(msg.jobId, indicatorPosition); // Store position, not index, for cleaner removal
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -105,6 +127,12 @@ function handleMessage(msg) {
|
||||
setLabelState(msg.agentId, 'idle');
|
||||
}
|
||||
appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} complete`);
|
||||
|
||||
// Dissolve 3D job indicator
|
||||
if (msg.jobId) {
|
||||
dissolveJobIndicator(msg.jobId, scene);
|
||||
_jobIndicatorOffsets.delete(msg.jobId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
let scene, camera, renderer;
|
||||
export let scene, camera, renderer;
|
||||
const _worldObjects = [];
|
||||
|
||||
export function initWorld(existingCanvas) {
|
||||
|
||||
Reference in New Issue
Block a user