120 lines
4.5 KiB
JavaScript
120 lines
4.5 KiB
JavaScript
/* Handler's control panel. Sends authenticated commands to the relay and
|
|
mirrors the live clock so the Handler sees exactly what the players see. */
|
|
(function () {
|
|
"use strict";
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
// --- control key (persisted locally) --------------------------------------
|
|
const tokenInput = $("token");
|
|
tokenInput.value = localStorage.getItem("oe-token") || "";
|
|
function saveToken() {
|
|
localStorage.setItem("oe-token", tokenInput.value.trim());
|
|
flashHint("key set");
|
|
}
|
|
$("saveToken").addEventListener("click", saveToken);
|
|
tokenInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") saveToken();
|
|
});
|
|
let hintTimer = null;
|
|
function flashHint(text) {
|
|
const el = $("tokenHint");
|
|
el.textContent = text;
|
|
clearTimeout(hintTimer);
|
|
hintTimer = setTimeout(() => (el.textContent = ""), 1500);
|
|
}
|
|
|
|
// --- connection -----------------------------------------------------------
|
|
const model = OE.ClockModel();
|
|
const conn = OE.connect(OE.defaultWsUrl(), {
|
|
onstatus(s) {
|
|
$("lampLink").classList.toggle("lamp--on", s === "open");
|
|
},
|
|
onstate(state) {
|
|
model.apply(state);
|
|
syncRateButtons(state.rate);
|
|
},
|
|
});
|
|
window.addEventListener("beforeunload", () => conn.close());
|
|
|
|
function send(obj) {
|
|
const token = (localStorage.getItem("oe-token") || "").trim();
|
|
if (!token) {
|
|
flashHint("no control key set");
|
|
tokenInput.focus();
|
|
return;
|
|
}
|
|
conn.send(Object.assign({ token }, obj));
|
|
}
|
|
|
|
// --- wire fixed-command buttons (play/pause/pulse/communion) ---------------
|
|
document.querySelectorAll("[data-cmd]").forEach((btn) => {
|
|
btn.addEventListener("click", () => send({ cmd: btn.dataset.cmd }));
|
|
});
|
|
|
|
// --- rate -----------------------------------------------------------------
|
|
document.querySelectorAll(".rate").forEach((btn) => {
|
|
btn.addEventListener("click", () => send({ cmd: "rate", rate: Number(btn.dataset.rate) }));
|
|
});
|
|
function syncRateButtons(rate) {
|
|
document.querySelectorAll(".rate").forEach((b) => {
|
|
b.classList.toggle("active", Number(b.dataset.rate) === rate);
|
|
});
|
|
}
|
|
|
|
// --- iterations -----------------------------------------------------------
|
|
document.querySelectorAll(".iter").forEach((btn) => {
|
|
btn.addEventListener("click", () => send({ cmd: "iteration", n: Number(btn.dataset.iter) }));
|
|
});
|
|
|
|
// --- timeline jumps -------------------------------------------------------
|
|
const jumps = $("jumps");
|
|
OE.EVENTS.forEach((ev) => {
|
|
const b = document.createElement("button");
|
|
b.type = "button";
|
|
b.className = "btn jump" + (ev.communion ? " btn--red" : ev.pulse ? " btn--amber" : "");
|
|
b.innerHTML = "<b>" + ev.time + "</b><span>" + ev.label + "</span>";
|
|
b.addEventListener("click", () => send({ cmd: "set", inworld: ev.t }));
|
|
jumps.appendChild(b);
|
|
});
|
|
|
|
// manual time -> seconds-of-day
|
|
$("manualJump").addEventListener("click", () => {
|
|
const parts = $("manualTime").value.split(":").map(Number);
|
|
if (parts.length >= 2 && parts.every((n) => !Number.isNaN(n))) {
|
|
const sec = (parts[0] || 0) * 3600 + (parts[1] || 0) * 60 + (parts[2] || 0);
|
|
send({ cmd: "set", inworld: sec });
|
|
}
|
|
});
|
|
|
|
// --- broadcast message ----------------------------------------------------
|
|
$("sendMsg").addEventListener("click", () => {
|
|
const text = $("msg").value.trim();
|
|
if (text) send({ cmd: "msg", text });
|
|
});
|
|
$("clearMsg").addEventListener("click", () => send({ cmd: "msg", text: null }));
|
|
$("msg").addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") $("sendMsg").click();
|
|
});
|
|
|
|
// player-screen link points at clock.html next to this page
|
|
$("clockLink").href = new URL("clock.html", location.href).href;
|
|
|
|
// --- mirror loop ----------------------------------------------------------
|
|
function nextBeat(inworld) {
|
|
const upcoming = OE.EVENTS.filter((e) => e.t > inworld + 0.5);
|
|
return upcoming.length ? upcoming[0] : null;
|
|
}
|
|
function frame() {
|
|
const s = model.now();
|
|
$("mClock").textContent = OE.clock(s.inworld);
|
|
$("mIter").textContent = OE.ROMAN[s.iteration] || s.iteration;
|
|
$("mCountdown").textContent = s.remaining <= 0 ? "00:00:00" : OE.duration(s.remaining);
|
|
$("mRun").textContent = s.running ? "RUNNING" : "PAUSED";
|
|
$("mRun").classList.toggle("live", s.running);
|
|
const nb = nextBeat(s.inworld);
|
|
$("mNext").textContent = nb ? nb.time + " " + nb.label : "—";
|
|
requestAnimationFrame(frame);
|
|
}
|
|
requestAnimationFrame(frame);
|
|
})();
|