home-server/services/observer-effect/site/common.js

135 lines
4 KiB
JavaScript
Raw Normal View History

2026-06-15 21:22:41 -07:00
/* 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,
};
})();