/** * State reader — hardcoded JSON for Phase 2, WebSocket in Phase 3. * * Provides Timmy's current state to the scene. In Phase 2 this is a * static default; the WebSocket path is stubbed for future use. */ const DEFAULTS = { timmyState: { mood: "focused", activity: "Pondering the arcane arts", energy: 0.6, confidence: 0.7, }, activeThreads: [], recentEvents: [], concerns: [], visitorPresent: false, updatedAt: new Date().toISOString(), version: 1, }; export class StateReader { constructor() { this.state = { ...DEFAULTS }; this.listeners = []; this._ws = null; } /** Subscribe to state changes. */ onChange(fn) { this.listeners.push(fn); } /** Notify all listeners. */ _notify() { for (const fn of this.listeners) { try { fn(this.state); } catch (e) { console.warn("State listener error:", e); } } } /** Try to connect to the world WebSocket for live updates. */ connect() { const proto = location.protocol === "https:" ? "wss:" : "ws:"; const url = `${proto}//${location.host}/api/world/ws`; try { this._ws = new WebSocket(url); this._ws.onopen = () => { const dot = document.getElementById("connection-dot"); if (dot) dot.classList.add("connected"); }; this._ws.onclose = () => { const dot = document.getElementById("connection-dot"); if (dot) dot.classList.remove("connected"); }; this._ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === "world_state" || msg.type === "timmy_state") { if (msg.timmyState) this.state.timmyState = msg.timmyState; if (msg.mood) { this.state.timmyState.mood = msg.mood; this.state.timmyState.activity = msg.activity || ""; this.state.timmyState.energy = msg.energy ?? 0.5; } this._notify(); } } catch (e) { /* ignore parse errors */ } }; } catch (e) { console.warn("WebSocket unavailable — using static state"); } } /** Current mood string. */ get mood() { return this.state.timmyState.mood; } /** Current activity string. */ get activity() { return this.state.timmyState.activity; } /** Energy level 0-1. */ get energy() { return this.state.timmyState.energy; } }