/* Observer Effect — shared clock logic for the player screen and the Handler's panel. Classic script (no modules) so the pages also open straight from disk for a quick look. Everything hangs off the global `OE`. */ (function () { "use strict"; // In-world second-of-day for the moment Azathoth achieves communion. const COMMUNION = 79417; // 22:03:37 // The scenario's beats, in second-of-day. `pulse: true` marks the live // on-site shudders the player screen reacts to; the rest are scene markers // and useful jump targets for the Handler. const EVENTS = [ { t: 36000, label: "Array activated", time: "10:00:00" }, { t: 55735, label: "Power surge", time: "15:28:55" }, { t: 59682, label: "Phantom signal", time: "16:34:42" }, { t: 61200, label: "Agents arrive", time: "17:00:00" }, { t: 63629, label: "Pulse", time: "17:40:29", pulse: true }, { t: 67576, label: "Takagawa awakens", time: "18:46:16", pulse: true }, { t: 71523, label: "Klinger appears", time: "19:52:03", pulse: true }, { t: 75470, label: "Klinger's rampage", time: "20:57:50", pulse: true }, { t: COMMUNION, label: "COMMUNION", time: "22:03:37", communion: true }, ]; const RESET_POINTS = { 1: 61200, 2: 67576, 3: 75470, 4: COMMUNION }; const ROMAN = { 1: "I", 2: "II", 3: "III", 4: "IV" }; function pad(n) { return String(Math.floor(n)).padStart(2, "0"); } // second-of-day (may exceed a day or go negative) -> "HH:MM:SS", clamped 0..24h. function clock(sec) { let s = Math.max(0, Math.min(86399, Math.floor(sec))); return pad(s / 3600) + ":" + pad((s % 3600) / 60) + ":" + pad(s % 60); } // A signed duration in seconds -> "H:MM:SS". function duration(sec) { const neg = sec < 0; let s = Math.abs(Math.floor(sec)); const h = Math.floor(s / 3600); return (neg ? "-" : "") + h + ":" + pad((s % 3600) / 60) + ":" + pad(s % 60); } /* Holds the latest authoritative snapshot and free-runs from it locally, so the digits keep ticking between the Handler's actions. */ function ClockModel() { let base = RESET_POINTS[1]; let baseAt = performance.now(); let running = false; let rate = 1; let iteration = 1; return { apply(state) { base = state.inworld; baseAt = performance.now(); running = state.running; rate = state.rate; iteration = state.iteration; }, now() { const live = running ? base + ((performance.now() - baseAt) / 1000) * rate : base; return { inworld: live, remaining: COMMUNION - live, running, rate, iteration, }; }, }; } /* Auto-reconnecting WebSocket. `onstate`/`onfx` get parsed messages; `onstatus` gets "open"/"closed" for the link lamp. */ function connect(url, handlers) { let ws; let closed = false; function open() { ws = new WebSocket(url); ws.onopen = () => handlers.onstatus && handlers.onstatus("open"); ws.onclose = () => { handlers.onstatus && handlers.onstatus("closed"); if (!closed) setTimeout(open, 1000); }; ws.onerror = () => ws.close(); ws.onmessage = (e) => { let m; try { m = JSON.parse(e.data); } catch (_) { return; } if (m.type === "state") handlers.onstate && handlers.onstate(m); else if (m.type === "fx") handlers.onfx && handlers.onfx(m); }; } open(); return { send(obj) { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); }, close() { closed = true; if (ws) ws.close(); }, }; } // Default WS endpoint: same host, /ws, matching page's TLS. function defaultWsUrl() { const proto = location.protocol === "https:" ? "wss:" : "ws:"; return proto + "//" + location.host + "/ws"; } window.OE = { COMMUNION, EVENTS, RESET_POINTS, ROMAN, clock, duration, pad, ClockModel, connect, defaultWsUrl, }; })();