"use strict" /* global document, window */ const crypto = window.crypto const location = document.location const performance = window.performance const storage = window.sessionStorage // TODOs // - measure/report latency // - use server reported time to find winner import { clear, Connection, isEmpty, isString, ms_ns, node, off, on, q, s_ns, session_id, session_id_from_url, session_url, } from "./shared.js" const buzzer_key = { code: 0x20, name: "space bar", } let socket, conn, clients = [], me, session_key function hide(e) { e = isString(e) ? q(`#${e}`) : e e.style.display = "none" } function show(e) { e = isString(e) ? q(`#${e}`) : e e.style.display = "block" } function find_client(client_id) { return clients.find((c) => c.id === client_id) } function new_session_id() { if (!crypto) { return Math.random().toString(36).substr(2) } const data = new Uint8Array(10) crypto.getRandomValues(data) return Array.from(data, (v) => v.toString(36)).join("") } function setup_url() { const sid = session_id() || new_session_id() location.hash = sid } const player_list = q("#info ul") function redraw_clients(me, clients) { if (!me) { return } clear(player_list) const player_tpl = q("template#player").content.firstElementChild for (const c of clients) { const player_el = node(player_tpl.cloneNode(true), { data: { cid: c.id }, appendTo: player_list, cls: { me: c.id === me.id, inactive: !c.active, }, }) q(".name", player_el).textContent = c.name q(".points", player_el).textContent = c.points q(".tokens", player_el).textContent = c.tokens } } const highlights = {} function highlight(client_id, until_ns) { if (highlights[client_id]) { return } 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 } for (const li of player_list.children) { if (li.dataset.cid === client_id) { li.classList.add("buzzing") if (!isEmpty(highlights)) { li.classList.add("too-late") } else { li.classList.add("first") } highlights[client_id] = setTimeout(() => { delete highlights[client_id] li.classList.remove("buzzing") li.classList.remove("too-late") li.classList.remove("first") }, timeout_ms) return } } } function setup_ui() { on("focus", (event) => { hide("active") hide("inactive") show("ready") }) on("blur", (event) => { hide("active") show("inactive") hide("ready") }) if (document.hasFocus()) { hide("inactive") } else { hide("ready") } q("#bkey").textContent = buzzer_key.name let buzzing = false on("keydown", (event) => { if (!buzzing && event.keyCode === buzzer_key.code) { buzzing = true conn.send("buzz", conn.servertime_now_ns()) show("active") hide("ready") } }) on("keyup", (event) => { if (event.keyCode === buzzer_key.code) { buzzing = false hide("active") show("ready") } }) const username_el = q("#username") if (storage["my_name"]) { username_el.value = storage["my_name"] } on( "change", (event) => { set_name(event.target.value) }, { target: username_el }, ) } function disable_player_ui() { off("focus") off("blur") off("keydown") off("keyup") node(q("body"), { cls: { player: 0 } }) } function enable_admin_ui() { const body = q("body") if (body.classList.contains("admin")) { return } body.classList.add("admin") const named = Object.fromEntries( Array.from(document.querySelectorAll("[name]")).map((el) => [el.name, el]), ) on( "click", ({ target }) => { if (!target.dataset.eval) { return } eval(target.dataset.eval) }, { target: q("#points-admin") }, ) on( "click", ({ target }) => { let node = target while (!node.dataset.cid && node.parentElement) { node = node.parentElement } if (!target.dataset.eval) { return } const client = find_client(node.dataset.cid) if (!client) { return } eval(target.dataset.eval) }, { target: q("#info .players") }, ) } function set_name(name) { storage["my_name"] = name conn.send("name", name) } function setup_ws() { const sid = session_id() const credentials = { id: storage["my_uid"], key: storage["my_key"] } conn = new Connection() conn.on("helo", () => { if (sid === storage["my_sid"]) { conn.send("login", credentials) } set_name(q("#username").value) }) conn.on("id", ({ value }) => { me = value storage["my_uid"] = me.id storage["my_key"] = me.key storage["my_sid"] = session_id_from_url(me.path) redraw_clients(me, clients) }) conn.on("buzz", ({ value }) => { const { time: buzztime_ns, client: client_id } = value const duration_ns = 3 * s_ns const until_ns = buzztime_ns + duration_ns highlight(client_id, until_ns) }) conn.on("clients", ({ value }) => { clients = value.clients redraw_clients(me, clients) }) conn.on("client", ({ value: { client } }) => { const idx = clients.findIndex((c) => c.id === client.id) if (idx >= 0) { clients[idx] = client } else { clients.push(client) } redraw_clients(me, clients) }) conn.on("session_key", ({ value }) => { session_key = value storage["session_path"] = session_key.path storage["session_key"] = session_key.key disable_player_ui() enable_admin_ui() }) conn.on("control", ({ value }) => { // ignore }) conn.on("error", (msg) => { console.error(`Error: ${msg.value.reason}`) const errorbox = q("#error") q("code", errorbox).textContent = JSON.stringify(msg, null, 2) q("body").classList.add("error") }) conn.connect(session_url(sid)) } setup_url() setup_ui() setup_ws()