2021-01-29 01:20:17 +01:00
|
|
|
"use strict"
|
|
|
|
|
|
2021-01-30 13:41:25 +01:00
|
|
|
/* global document, window */
|
|
|
|
|
const crypto = window.crypto
|
|
|
|
|
const location = document.location
|
2021-01-31 00:19:35 +01:00
|
|
|
const performance = window.performance
|
2021-01-30 13:41:25 +01:00
|
|
|
const storage = window.sessionStorage
|
|
|
|
|
|
2021-01-29 01:20:17 +01:00
|
|
|
// TODOs
|
|
|
|
|
// - measure/report latency
|
|
|
|
|
// - use server reported time to find winner
|
|
|
|
|
|
2021-02-02 00:42:02 +01:00
|
|
|
import {
|
|
|
|
|
clear,
|
2021-02-02 21:02:30 +01:00
|
|
|
Connection,
|
2021-02-02 00:42:02 +01:00
|
|
|
isEmpty,
|
|
|
|
|
isString,
|
|
|
|
|
ms_ns,
|
|
|
|
|
node,
|
|
|
|
|
off,
|
|
|
|
|
on,
|
|
|
|
|
q,
|
|
|
|
|
s_ns,
|
|
|
|
|
session_id,
|
|
|
|
|
session_id_from_url,
|
|
|
|
|
session_url,
|
|
|
|
|
} from "./shared.js"
|
2021-01-29 01:20:17 +01:00
|
|
|
|
|
|
|
|
const buzzer_key = {
|
|
|
|
|
code: 0x20,
|
|
|
|
|
name: "space bar",
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-02 23:05:00 +01:00
|
|
|
let conn,
|
|
|
|
|
clients = []
|
2021-01-29 01:20:17 +01:00
|
|
|
|
|
|
|
|
function hide(e) {
|
2021-03-02 20:02:54 +01:00
|
|
|
e = isString(e) ? q(e) : e
|
2021-01-31 00:19:35 +01:00
|
|
|
e.style.display = "none"
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function show(e) {
|
2021-03-02 20:02:54 +01:00
|
|
|
e = isString(e) ? q(e) : e
|
2021-01-31 00:19:35 +01:00
|
|
|
e.style.display = "block"
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-01 23:07:49 +01:00
|
|
|
function find_client(client_id) {
|
|
|
|
|
return clients.find((c) => c.id === client_id)
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-29 01:20:17 +01:00
|
|
|
function new_session_id() {
|
2021-01-30 13:41:25 +01:00
|
|
|
if (!crypto) {
|
2021-01-29 01:20:17 +01:00
|
|
|
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()
|
2021-02-02 00:42:02 +01:00
|
|
|
location.hash = sid
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-01 23:07:49 +01:00
|
|
|
const player_list = q("#info ul")
|
2021-01-29 01:20:17 +01:00
|
|
|
function redraw_clients(me, clients) {
|
|
|
|
|
if (!me) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-02-01 23:07:49 +01:00
|
|
|
clear(player_list)
|
2021-01-31 00:19:35 +01:00
|
|
|
const player_tpl = q("template#player").content.firstElementChild
|
2021-01-29 01:20:17 +01:00
|
|
|
for (const c of clients) {
|
2021-02-01 23:07:49 +01:00
|
|
|
const player_el = node(player_tpl.cloneNode(true), {
|
2021-01-29 01:20:17 +01:00
|
|
|
data: { cid: c.id },
|
2021-02-01 23:07:49 +01:00
|
|
|
appendTo: player_list,
|
2021-01-31 00:56:30 +01:00
|
|
|
cls: {
|
|
|
|
|
me: c.id === me.id,
|
|
|
|
|
inactive: !c.active,
|
|
|
|
|
},
|
2021-01-29 01:20:17 +01:00
|
|
|
})
|
2021-02-01 23:07:49 +01:00
|
|
|
q(".name", player_el).textContent = c.name
|
|
|
|
|
q(".points", player_el).textContent = c.points
|
|
|
|
|
q(".tokens", player_el).textContent = c.tokens
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const highlights = {}
|
|
|
|
|
function highlight(client_id, until_ns) {
|
|
|
|
|
if (highlights[client_id]) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-02-02 21:02:30 +01:00
|
|
|
const timeout_ms = (until_ns - conn.servertime_now_ns()) / ms_ns
|
2021-01-29 01:20:17 +01:00
|
|
|
if (timeout_ms <= 10) {
|
|
|
|
|
console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms)
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-02-01 23:07:49 +01:00
|
|
|
for (const li of player_list.children) {
|
2021-01-29 01:20:17 +01:00
|
|
|
if (li.dataset.cid === client_id) {
|
|
|
|
|
li.classList.add("buzzing")
|
2021-02-01 23:07:49 +01:00
|
|
|
if (!isEmpty(highlights)) {
|
2021-01-29 01:20:17 +01:00
|
|
|
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() {
|
2021-03-02 20:02:54 +01:00
|
|
|
const buzzbutton = q("#buzzbutton")
|
|
|
|
|
const username = q("#username")
|
|
|
|
|
|
2021-01-29 01:20:17 +01:00
|
|
|
on("focus", (event) => {
|
2021-03-02 20:02:54 +01:00
|
|
|
hide(".inactive")
|
2021-01-29 01:20:17 +01:00
|
|
|
})
|
|
|
|
|
on("blur", (event) => {
|
2021-03-02 20:02:54 +01:00
|
|
|
show(".inactive")
|
2021-01-29 01:20:17 +01:00
|
|
|
})
|
|
|
|
|
if (document.hasFocus()) {
|
2021-03-02 20:02:54 +01:00
|
|
|
hide(".inactive")
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let buzzing = false
|
2021-03-02 20:02:54 +01:00
|
|
|
|
|
|
|
|
function buzz() {
|
|
|
|
|
if (!buzzing) {
|
2021-01-29 01:20:17 +01:00
|
|
|
buzzing = true
|
2021-02-02 21:02:30 +01:00
|
|
|
conn.send("buzz", conn.servertime_now_ns())
|
2021-03-02 20:02:54 +01:00
|
|
|
show(".active")
|
|
|
|
|
hide(".ready")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unbuzz() {
|
|
|
|
|
buzzing = false
|
|
|
|
|
hide(".active")
|
|
|
|
|
show(".ready")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q("#bkey").textContent = buzzer_key.name
|
|
|
|
|
on("keydown", (event) => {
|
|
|
|
|
if (event.target === username) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (event.keyCode === buzzer_key.code) {
|
|
|
|
|
buzz()
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
on("keyup", (event) => {
|
2021-03-02 20:02:54 +01:00
|
|
|
if (event.target === username) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-01-29 01:20:17 +01:00
|
|
|
if (event.keyCode === buzzer_key.code) {
|
2021-03-02 20:02:54 +01:00
|
|
|
unbuzz()
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2021-03-02 20:02:54 +01:00
|
|
|
on(
|
|
|
|
|
"mousedown",
|
|
|
|
|
(event) => {
|
|
|
|
|
buzz()
|
|
|
|
|
},
|
|
|
|
|
{ target: buzzbutton },
|
|
|
|
|
)
|
|
|
|
|
on(
|
|
|
|
|
"mouseup",
|
|
|
|
|
(event) => {
|
|
|
|
|
unbuzz()
|
|
|
|
|
},
|
|
|
|
|
{ target: buzzbutton },
|
|
|
|
|
)
|
|
|
|
|
on(
|
|
|
|
|
"mouseleave",
|
|
|
|
|
(event) => {
|
|
|
|
|
unbuzz()
|
|
|
|
|
},
|
|
|
|
|
{ target: buzzbutton },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const named = Object.fromEntries(
|
|
|
|
|
Array.from(document.querySelectorAll("[name]")).map((el) => [el.name, el]),
|
|
|
|
|
)
|
2021-01-31 00:19:35 +01:00
|
|
|
if (storage["my_name"]) {
|
2021-03-02 20:02:54 +01:00
|
|
|
username.value = storage["my_name"]
|
2021-01-31 00:19:35 +01:00
|
|
|
}
|
2021-02-01 23:07:49 +01:00
|
|
|
on(
|
|
|
|
|
"change",
|
2021-03-02 20:02:54 +01:00
|
|
|
({ target }) => {
|
|
|
|
|
if (!target.dataset.onchange) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
eval(`;{${target.dataset.onchange}};`)
|
|
|
|
|
},
|
|
|
|
|
{ target: q("#userinfo") },
|
|
|
|
|
)
|
|
|
|
|
on(
|
|
|
|
|
"click",
|
|
|
|
|
({ target }) => {
|
|
|
|
|
if (!target.dataset.onclick) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
eval(`;{${target.dataset.onclick}};`)
|
2021-02-01 23:07:49 +01:00
|
|
|
},
|
2021-03-02 20:02:54 +01:00
|
|
|
{ target: q("#userinfo") },
|
2021-02-01 23:07:49 +01:00
|
|
|
)
|
|
|
|
|
}
|
2021-01-29 01:20:17 +01:00
|
|
|
|
2021-02-01 23:07:49 +01:00
|
|
|
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 }) => {
|
2021-02-24 23:52:33 +01:00
|
|
|
if (!target.dataset.onclick) {
|
2021-02-01 23:07:49 +01:00
|
|
|
return
|
|
|
|
|
}
|
2021-02-24 23:52:33 +01:00
|
|
|
eval(`;{${target.dataset.onclick}};`)
|
2021-02-01 23:07:49 +01:00
|
|
|
},
|
|
|
|
|
{ target: q("#points-admin") },
|
|
|
|
|
)
|
2021-02-24 23:52:33 +01:00
|
|
|
on(
|
|
|
|
|
"select",
|
|
|
|
|
({ target }) => {
|
|
|
|
|
if (!target.dataset.onselect) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
eval(`;{${target.dataset.onselect}};`)
|
|
|
|
|
},
|
|
|
|
|
{ target: q("#points-admin") },
|
|
|
|
|
)
|
|
|
|
|
on(
|
|
|
|
|
"change",
|
|
|
|
|
({ target }) => {
|
|
|
|
|
if (!target.dataset.onchange) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
eval(`;{${target.dataset.onchange}};`)
|
|
|
|
|
},
|
|
|
|
|
{ target: q("#points-admin") },
|
|
|
|
|
)
|
|
|
|
|
|
2021-02-25 22:54:56 +01:00
|
|
|
const client_for_target = (target) => {
|
|
|
|
|
let node = target
|
|
|
|
|
while (!node.dataset.cid && node.parentElement) {
|
|
|
|
|
node = node.parentElement
|
|
|
|
|
}
|
|
|
|
|
return find_client(node.dataset.cid)
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-01 23:07:49 +01:00
|
|
|
on(
|
|
|
|
|
"click",
|
|
|
|
|
({ target }) => {
|
2021-02-25 22:54:56 +01:00
|
|
|
const client = client_for_target(target)
|
|
|
|
|
if (!client || !target.dataset.onclick) {
|
2021-02-01 23:07:49 +01:00
|
|
|
return
|
|
|
|
|
}
|
2021-02-25 22:54:56 +01:00
|
|
|
eval(`;{${target.dataset.onclick}};`)
|
|
|
|
|
},
|
|
|
|
|
{ target: q("#info .players") },
|
|
|
|
|
)
|
|
|
|
|
on(
|
|
|
|
|
"change",
|
|
|
|
|
({ target }) => {
|
|
|
|
|
const client = client_for_target(target)
|
|
|
|
|
if (!client || !target.dataset.onchange) {
|
2021-02-01 23:07:49 +01:00
|
|
|
return
|
|
|
|
|
}
|
2021-02-25 22:54:56 +01:00
|
|
|
eval(`;{${target.dataset.onchange}};`)
|
2021-02-01 23:07:49 +01:00
|
|
|
},
|
|
|
|
|
{ target: q("#info .players") },
|
|
|
|
|
)
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-31 00:19:35 +01:00
|
|
|
function set_name(name) {
|
|
|
|
|
storage["my_name"] = name
|
2021-02-02 21:02:30 +01:00
|
|
|
conn.send("name", name)
|
2021-01-31 00:19:35 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-29 01:20:17 +01:00
|
|
|
function setup_ws() {
|
2021-02-02 23:05:00 +01:00
|
|
|
let me, session_key
|
2021-01-29 01:20:17 +01:00
|
|
|
const sid = session_id()
|
2021-01-31 00:19:35 +01:00
|
|
|
const credentials = { id: storage["my_uid"], key: storage["my_key"] }
|
2021-02-02 21:02:30 +01:00
|
|
|
conn = new Connection()
|
|
|
|
|
conn.on("helo", () => {
|
2021-01-31 00:19:35 +01:00
|
|
|
if (sid === storage["my_sid"]) {
|
2021-02-02 21:02:30 +01:00
|
|
|
conn.send("login", credentials)
|
2021-01-31 00:19:35 +01:00
|
|
|
}
|
|
|
|
|
set_name(q("#username").value)
|
2021-01-29 01:20:17 +01:00
|
|
|
})
|
2021-02-02 21:02:30 +01:00
|
|
|
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
|
2021-01-29 01:20:17 +01:00
|
|
|
} else {
|
2021-02-02 21:02:30 +01:00
|
|
|
clients.push(client)
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
2021-02-02 21:02:30 +01:00
|
|
|
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()
|
2021-03-02 20:02:54 +01:00
|
|
|
set_name("Admin")
|
2021-02-02 21:02:30 +01:00
|
|
|
})
|
|
|
|
|
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")
|
2021-01-29 01:20:17 +01:00
|
|
|
})
|
2021-02-02 21:02:30 +01:00
|
|
|
conn.connect(session_url(sid))
|
2021-01-29 01:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setup_url()
|
|
|
|
|
setup_ui()
|
|
|
|
|
setup_ws()
|