2021-02-02 00:42:02 +01:00
|
|
|
"use strict"
|
|
|
|
|
|
|
|
|
|
/* global document, window */
|
|
|
|
|
const location = document.location
|
|
|
|
|
const performance = window.performance
|
|
|
|
|
|
2021-03-06 20:57:29 +01:00
|
|
|
import {
|
|
|
|
|
Connection,
|
|
|
|
|
isEmpty,
|
|
|
|
|
ms_ns,
|
|
|
|
|
node,
|
|
|
|
|
q,
|
|
|
|
|
s_ns,
|
|
|
|
|
session_id,
|
|
|
|
|
session_url,
|
|
|
|
|
} from "./shared.js"
|
2021-02-02 00:42:02 +01:00
|
|
|
|
2021-02-02 23:05:00 +01:00
|
|
|
let conn
|
2021-02-02 00:42:02 +01:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2021-02-02 21:43:58 +01:00
|
|
|
const player_list = q("#players")
|
2021-02-04 00:04:03 +01:00
|
|
|
function _redraw_clients(clients, monitored) {
|
2021-02-02 23:05:00 +01:00
|
|
|
// Remove all gone players.
|
|
|
|
|
for (const el of player_list.querySelectorAll(".player")) {
|
2021-02-04 00:04:03 +01:00
|
|
|
if (!monitored.includes(el.dataset.cid)) {
|
2021-02-02 23:05:00 +01:00
|
|
|
el.remove()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-02 19:56:48 +01:00
|
|
|
const player_tpl = q("template#player").content.firstElementChild
|
2021-02-04 00:04:03 +01:00
|
|
|
for (const cid of monitored) {
|
2021-03-02 20:06:03 +01:00
|
|
|
const c = clients[cid] ?? { id: cid, name: "", points: 0, tokens: 0 }
|
2021-02-02 23:05:00 +01:00
|
|
|
let player_el = q(`.player[data-cid='${c.id}']`)
|
2021-02-04 00:04:03 +01:00
|
|
|
if (player_el) {
|
|
|
|
|
// Ensure the element is at the correct position.
|
|
|
|
|
player_list.appendChild(player_el)
|
|
|
|
|
} else {
|
2021-02-02 23:05:00 +01:00
|
|
|
player_el = node(player_tpl.cloneNode(true), {
|
|
|
|
|
data: { cid: c.id },
|
|
|
|
|
appendTo: player_list,
|
|
|
|
|
})
|
2021-02-04 00:04:03 +01:00
|
|
|
}
|
|
|
|
|
if (q(".points", player_el).textContent !== prettynum(c.points)) {
|
2021-02-02 23:05:00 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2021-02-02 19:56:48 +01:00
|
|
|
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)
|
2021-02-02 21:02:30 +01:00
|
|
|
q(".tokens", player_el).textContent = "🔴 ".repeat(c.tokens)
|
2021-02-02 00:42:02 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-02 23:05:00 +01:00
|
|
|
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)
|
|
|
|
|
|
2021-02-02 00:42:02 +01:00
|
|
|
let highlighted = false
|
|
|
|
|
function highlight(client_id, until_ns) {
|
|
|
|
|
if (highlighted) {
|
2021-02-25 22:54:56 +01:00
|
|
|
return false
|
2021-02-02 00:42:02 +01:00
|
|
|
}
|
2021-02-02 21:02:30 +01:00
|
|
|
const timeout_ms = (until_ns - conn.servertime_now_ns()) / ms_ns
|
2021-02-02 00:42:02 +01:00
|
|
|
if (timeout_ms <= 10) {
|
|
|
|
|
console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms)
|
2021-02-25 22:54:56 +01:00
|
|
|
return false
|
2021-02-02 00:42:02 +01:00
|
|
|
}
|
|
|
|
|
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)
|
2021-02-25 22:54:56 +01:00
|
|
|
return true
|
2021-02-02 00:42:02 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setup_ws() {
|
2021-02-04 00:04:03 +01:00
|
|
|
let clients = {},
|
|
|
|
|
me,
|
2021-03-06 20:57:29 +01:00
|
|
|
monitored = [], // list of client IDs
|
|
|
|
|
sounds = {}, // mapping of client ID to Audio instance
|
|
|
|
|
buzzes = {} // mapping of client ID to buzz time
|
2021-02-02 00:42:02 +01:00
|
|
|
const sid = session_id()
|
2021-03-02 20:06:03 +01:00
|
|
|
const overlay = q("#text-overlay")
|
2021-02-02 21:02:30 +01:00
|
|
|
conn = new Connection()
|
|
|
|
|
conn.on("helo", () => {
|
|
|
|
|
conn.send("name", "Monitor")
|
2021-02-02 00:42:02 +01:00
|
|
|
})
|
2021-02-02 21:02:30 +01:00
|
|
|
conn.on("id", ({ value }) => {
|
|
|
|
|
me = value
|
|
|
|
|
})
|
|
|
|
|
conn.on("buzz", ({ value }) => {
|
2021-03-06 20:57:29 +01:00
|
|
|
const collect_duration_ms = 500
|
|
|
|
|
const highlight_duration_ns = 12 * s_ns
|
|
|
|
|
// Collect buzzes to find the true winner.
|
|
|
|
|
if (isEmpty(buzzes)) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// Show winner in UI.
|
|
|
|
|
const winner_id = Object.entries(buzzes).sort(([x, a], [y, b]) => a - b)[0][0]
|
|
|
|
|
buzzes = {}
|
|
|
|
|
const until_ns = buzztime_ns + highlight_duration_ns
|
|
|
|
|
const is_new_buzz = highlight(winner_id, until_ns)
|
|
|
|
|
if (is_new_buzz && sounds[winner_id]) {
|
|
|
|
|
sounds[winner_id].play()
|
|
|
|
|
}
|
|
|
|
|
}, collect_duration_ms)
|
|
|
|
|
}
|
2021-02-02 21:02:30 +01:00
|
|
|
const { time: buzztime_ns, client: client_id } = value
|
2021-03-06 20:57:29 +01:00
|
|
|
if (!(client_id in buzzes)) {
|
|
|
|
|
buzzes[client_id] = buzztime_ns
|
2021-02-25 22:54:56 +01:00
|
|
|
}
|
2021-02-02 21:02:30 +01:00
|
|
|
})
|
|
|
|
|
conn.on("clients", ({ value }) => {
|
2021-02-04 00:04:03 +01:00
|
|
|
clients = Object.fromEntries(value.clients.map((c) => [c.id, c]))
|
|
|
|
|
redraw_clients(clients, monitored)
|
2021-02-02 21:02:30 +01:00
|
|
|
})
|
|
|
|
|
conn.on("client", ({ value: { client } }) => {
|
2021-02-04 00:04:03 +01:00
|
|
|
clients[client.id] = client
|
|
|
|
|
redraw_clients(clients, monitored)
|
2021-02-02 21:02:30 +01:00
|
|
|
})
|
2021-03-02 20:06:03 +01:00
|
|
|
conn.on(
|
|
|
|
|
"control",
|
|
|
|
|
({
|
|
|
|
|
value: {
|
|
|
|
|
payload: { action, ...args },
|
|
|
|
|
},
|
|
|
|
|
}) => {
|
|
|
|
|
if (action === "monitor" && Array.isArray(args.targets)) {
|
2021-03-06 20:58:12 +01:00
|
|
|
monitored = args.targets.filter(Boolean)
|
2021-03-02 20:06:03 +01:00
|
|
|
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`)
|
2021-03-02 20:08:28 +01:00
|
|
|
} else if (action === "double") {
|
|
|
|
|
q("#board").contentWindow.postMessage("double", "*")
|
2021-02-24 23:52:33 +01:00
|
|
|
}
|
2021-03-02 20:06:03 +01:00
|
|
|
},
|
|
|
|
|
)
|
2021-02-02 21:02:30 +01:00
|
|
|
conn.connect(session_url(sid))
|
2021-02-02 00:42:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setup_url()
|
|
|
|
|
setup_ws()
|