observer effect content
This commit is contained in:
parent
625a847a81
commit
e4ee25295c
23 changed files with 2982 additions and 0 deletions
134
services/observer-effect/site/common.js
Normal file
134
services/observer-effect/site/common.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/* 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,
|
||||
};
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue