"use strict" /* global document, window */ const location = document.location const performance = window.performance import { Connection, ms_ns, node, q, s_ns, session_id, session_url } from "./shared.js" let conn function setup_url() { const sid = session_id() || "" location.hash = sid } function prettynum(n) { let i = Math.abs(n) | 0 let s = [] while (true) { const m = i % 1000 i = (i / 1000) | 0 if (i === 0) { s.unshift(String(m)) break } else { s.unshift(String(m).padStart(3, "0")) } } return (n < 0 ? "-" : "") + s.join("'") } function assert(expected, got) { if (expected !== got) { throw Error("Assertion failed.") } } function test_prettynum() { assert("0", prettynum(0)) assert("1", prettynum(1)) assert("1'000", prettynum(1000)) assert("36'000", prettynum(36000)) assert("0", prettynum(-0)) assert("-1", prettynum(-1)) assert("-1'000", prettynum(-1000)) assert("-36'000", prettynum(-36000)) assert("-36'600", prettynum(-36600)) } test_prettynum() const player_list = q("#players") function _redraw_clients(clients, monitored) { // Remove all gone players. for (const el of player_list.querySelectorAll(".player")) { if (!monitored.includes(el.dataset.cid)) { el.remove() } } const player_tpl = q("template#player").content.firstElementChild for (const cid of monitored) { const c = clients[cid] ?? { id: cid, name: "", points: 0, tokens: 0 } let player_el = q(`.player[data-cid='${c.id}']`) if (player_el) { // Ensure the element is at the correct position. player_list.appendChild(player_el) } else { player_el = node(player_tpl.cloneNode(true), { data: { cid: c.id }, appendTo: player_list, }) } if (q(".points", player_el).textContent !== prettynum(c.points)) { q(".points.fg", player_el).classList.add("big") q(".points.bg", player_el).classList.add("big") setTimeout(() => { q(".points.fg", player_el).classList.remove("big") q(".points.bg", player_el).classList.remove("big") }, 200) } q(".name", player_el).textContent = c.name q(".points.fg", player_el).textContent = prettynum(c.points) q(".points.bg", player_el).textContent = prettynum(c.points) q(".tokens", player_el).textContent = "🔴 ".repeat(c.tokens) } } function debounce(func, ms) { let timer return function (...args) { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { timer = null func(...args) }, ms) } } const redraw_clients = debounce(_redraw_clients, 200) let highlighted = false function highlight(client_id, until_ns) { if (highlighted) { return false } const timeout_ms = (until_ns - conn.servertime_now_ns()) / ms_ns if (timeout_ms <= 10) { console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms) return false } for (const li of player_list.children) { if (li.dataset.cid === client_id) { li.classList.add("buzzing") highlighted = true setTimeout(() => { highlighted = false li.classList.remove("buzzing") }, timeout_ms) return true } } } function setup_ws() { let clients = {}, me, monitored = [], sounds = {} const sid = session_id() const overlay = q("#text-overlay") conn = new Connection() conn.on("helo", () => { conn.send("name", "Monitor") }) conn.on("id", ({ value }) => { me = value }) conn.on("buzz", ({ value }) => { const { time: buzztime_ns, client: client_id } = value const duration_ns = 12 * s_ns const until_ns = buzztime_ns + duration_ns const is_new_buzz = highlight(client_id, until_ns) if (is_new_buzz && sounds[client_id]) { sounds[client_id].play() } }) conn.on("clients", ({ value }) => { clients = Object.fromEntries(value.clients.map((c) => [c.id, c])) redraw_clients(clients, monitored) }) conn.on("client", ({ value: { client } }) => { clients[client.id] = client redraw_clients(clients, monitored) }) conn.on( "control", ({ value: { payload: { action, ...args }, }, }) => { if (action === "monitor" && Array.isArray(args.targets)) { monitored = args.targets redraw_clients(clients, monitored) } else if (action === "text" && args.text !== undefined) { if (args.text.length) { overlay.textContent = args.text overlay.classList.remove("hidden") } else { overlay.classList.add("hidden") } } else if (action === "sound" && args.sound) { const { client_id, sound } = args sounds[client_id] = new Audio(`sounds/${sound}.mp3`) } else if (action === "double") { q("#board").contentWindow.postMessage("double", "*") } }, ) conn.connect(session_url(sid)) } setup_url() setup_ws()