/** * The Workshop — Three.js scene bootstrap * * Initializes the 3D world where Timmy lives. * Handles WebSocket connection to tower-hermes backend with * timeout, retry, and clear status display. * * See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema) */ // Future: import * as THREE from 'three'; const HERMES_WS_URL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/tower'; const CONNECT_TIMEOUT_MS = 5000; const RETRY_DELAY_MS = 3000; const MAX_AUTO_RETRIES = 3; const Status = { CONNECTING: 'connecting', ONLINE: 'online', OFFLINE: 'offline' }; const dom = { dot: document.getElementById('status-dot'), text: document.getElementById('status-text'), agents: document.getElementById('agent-count'), retryBtn: document.getElementById('retry-btn'), fallback: document.getElementById('fallback-link'), }; // Signal to inline fallback script that main.js loaded successfully window.__workshopBooted = true; let ws = null; let autoRetries = 0; let connectTimer = null; function setStatus(state, message) { dom.dot.className = 'dot ' + state; dom.text.textContent = message; var isOffline = state === Status.OFFLINE; dom.retryBtn.style.display = isOffline ? 'block' : 'none'; if (dom.fallback) dom.fallback.style.display = isOffline ? 'block' : 'none'; } function setAgentCount(n) { dom.agents.textContent = n; } function cleanup() { clearTimeout(connectTimer); if (ws) { ws.onopen = null; ws.onclose = null; ws.onerror = null; ws.onmessage = null; if (ws.readyState <= WebSocket.OPEN) ws.close(); ws = null; } } function connect() { cleanup(); setStatus(Status.CONNECTING, 'CONNECTING\u2026'); setAgentCount(0); try { ws = new WebSocket(HERMES_WS_URL); } catch (err) { console.error('[Workshop] WebSocket creation failed:', err); onFail(); return; } connectTimer = setTimeout(function () { console.warn('[Workshop] Connection timeout after ' + CONNECT_TIMEOUT_MS + 'ms'); cleanup(); onFail(); }, CONNECT_TIMEOUT_MS); ws.onopen = function () { clearTimeout(connectTimer); autoRetries = 0; setStatus(Status.ONLINE, 'ONLINE'); console.log('[Workshop] Connected to tower-hermes'); }; ws.onmessage = function (evt) { try { var msg = JSON.parse(evt.data); if (typeof msg.agents === 'number') setAgentCount(msg.agents); } catch (_) { // non-JSON messages are ignored } }; ws.onclose = function () { clearTimeout(connectTimer); console.log('[Workshop] Connection closed'); onFail(); }; ws.onerror = function () { clearTimeout(connectTimer); console.error('[Workshop] WebSocket error'); // onclose will fire after this, which calls onFail }; } function onFail() { if (autoRetries < MAX_AUTO_RETRIES) { autoRetries++; setStatus(Status.CONNECTING, 'RETRYING (' + autoRetries + '/' + MAX_AUTO_RETRIES + ')\u2026'); setTimeout(connect, RETRY_DELAY_MS); } else { setStatus(Status.OFFLINE, 'WORKSHOP OFFLINE'); } } // Manual retry resets the counter dom.retryBtn.addEventListener('click', function () { autoRetries = 0; connect(); }); // Boot export function initWorkshop(container) { console.log('[Workshop] Scene container ready:', container.id); connect(); } initWorkshop(document.getElementById('scene'));