Files
timmy-tower/the-matrix
alexpaynex 9ff5ef683d feat(task-21): Timmy face expressions + emotion engine
## What changed
- the-matrix/js/agents.js — face expression system added to Timmy wizard

## Face geometry (all parented to head — follow head.rotation.z tilt)
- White sclera eyes (MeshStandardMaterial f5f2e8, emissive 0x777777@0.10)
  replace the old flat dark-blue spheres
- Dark pupils (MeshBasicMaterial 0x07070f) as child meshes of each sclera;
  they scale with the parent eye for squint effect
- Mouth arc: TubeGeometry built from QuadraticBezierCurve3; control point
  moves ±0.065 on Y for smile/frown; rebuilt via _buildMouthGeo() only when
  |smileDelta| > 0.016 (throttled to avoid per-frame GC pressure)
- All face meshes are children of `head` — head.rotation.z carries every
  face component naturally with the existing head-tilt animation

## FACE_TARGETS lookup table (lidScale, pupilScale, smileAmount)
- idle  (contemplative): 0.44 / 0.90 / 0.08  — half-lid, neutral
- active (curious):      0.92 / 1.25 / 0.38  — wide eyes + dilated pupils, smile
- thinking (focused):    0.30 / 0.72 / -0.06 — squint + constricted pupils, flat
- working (attentive):   0.22 / 0.80 / 0.18  — very squint, slight grin

## setFaceEmotion(mood) exported API
- Accepts both task-spec names (contemplative|curious|focused|attentive)
  and internal state names (idle|active|thinking|working) via MOOD_ALIASES
- Immediately sets faceTarget; lerp in updateAgents() handles the smooth transition

## Per-frame lerp (rate 0.055/frame) in updateAgents
- lidScale → eyeL.scale.y / eyeR.scale.y (squash for squint)
- pupilScale → pupilL.scale / pupilR.scale (uniform dilation)
- smileAmount → drives TubeGeometry rebuild when drift > 0.016

## Lip-sync while speaking (~1 Hz)
- speechTimer > 0: smileTarget = 0.28 + sin(t*6.283)*0.22
- Returns to mood target when timer expires

## Validation
- Vite build: clean (14 modules, 542 kB, no errors)
- testkit: 27/27 PASS (after server restart to clear rate-limit counters)
2026-03-19 03:09:45 +00:00
..

Timmy Tower World

A Three.js 3D visualization of the Timmy agent network. Agents appear as glowing icosahedra connected by lines, pulsing as they process jobs. A matrix-rain particle effect fills the background.

Quick start

npm install
npm run dev      # Vite dev server with hot reload → http://localhost:5173
npm run build    # Production bundle → dist/
npm run preview  # Serve dist/ locally

Configuration

Set these in a .env.local file (not committed):

VITE_WS_URL=ws://localhost:8080/ws/agents

Leave VITE_WS_URL unset to run in offline/demo mode (agents animate but receive no live updates).

Adding custom agents

Edit one file only: js/agent-defs.js

export const AGENT_DEFS = [
  // existing agents …
  {
    id:        'zeta',      // unique string — matches WebSocket message agentId
    label:     'ZETA',      // displayed in the 3D HUD
    color:     0xff00aa,    // hex integer (0xRRGGBB)
    role:      'observer',  // shown under the label sprite
    direction: 'east',      // cardinal facing direction (north/east/south/west)
    x:         12,          // world-space position (horizontal)
    z:         0,           // world-space position (depth)
  },
];

Nothing else needs to change. agents.js reads positions from x/z, and websocket.js reads colors and labels — both derive everything from AGENT_DEFS.

Architecture

js/
├── agent-defs.js   ← single source of truth: id, label, color, role, position
├── agents.js       ← Three.js scene objects, animation loop
├── effects.js      ← matrix rain particles, starfield
├── interaction.js  ← OrbitControls (pan, zoom, rotate)
├── main.js         ← entry point, rAF loop
├── ui.js           ← DOM HUD overlay (FPS, agent states, chat)
└── websocket.js    ← WebSocket reconnect, message dispatch

WebSocket protocol

The backend sends JSON messages on the agents channel:

type Fields Effect
agent_state agentId, state Update agent visual state
job_started agentId, jobId Increment job counter, pulse
job_completed agentId, jobId Decrement job counter
chat agentId, text Append to chat panel

Agent states: idle (dim pulse) · active (bright pulse + fast ring spin)

Stack

  • Three.js 0.171.0 — 3D rendering
  • Vite 5 — build + dev server
  • crypto.randomUUID() — secure client session IDs (no external library)