const tableKey = { name: "", passcode: "" } let conn = null; let offline = false; const msgQ = []; let q = false; function dial() { // get tableKey from UI const tblNameInput = $("input_table_name"); const tblPassInput = $("input_table_pass"); const joinTblBtn = $("table_join"); const leaveTblBtn = $("table_leave"); if (tblNameInput && tblPassInput && tblNameInput.value && tblPassInput.value) { if (isTableKeyValid(tblNameInput.value, tblPassInput.value)) { tableKey.name = tblNameInput.value; tableKey.passcode = tblPassInput.value; if (conn) { conn.close(1000); } const wsProto = location.protocol == "https:" ? "wss" : "ws"; conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, `${tableKey.name}.${tableKey.passcode}`); conn.addEventListener("close", e => { offline = true; if (e.code == 1006 && e.wasClean) { setErr("Table not found - check the name and passcode are correct"); } else if (e.code > 1001) { const lagDiv = $("lag"); if (lagDiv) { lagDiv.style.display = "block"; setTimeout(dial, 1000) } } else { tblNameInput.readOnly = false; tblPassInput.readOnly = false; joinTblBtn.style.display = adminToken ? "none" : "inline"; leaveTblBtn.style.display = "none"; tabletop = $("tabletop"); if (tabletop) { tabletop.style.display = "none"; } while (tokens.some(t=>t)) { tokens[0].m.removeFrom(map); tokens.shift(); } if (mapImg) { mapImg.removeFrom(map); mapImg = null; } msgQ = []; } }); conn.addEventListener("open", e => { offline = false; tblNameInput.readOnly = true; tblPassInput.readOnly = true; joinTblBtn.style.display = "none"; leaveTblBtn.style.display = adminToken ? "none" : "inline"; lagDiv = $("lag"); if (lagDiv) { lagDiv.style.display = "none"; } emptyMsgQ(); closeErr(); tabletop = $("tabletop"); if (tabletop) { tabletop.style.display = "block"; } }); conn.addEventListener("error", e => { setErr(`Websocket error`); conn.close(3000); }) conn.addEventListener("message", e => { const data = JSON.parse(e.data); if (data.diceRolls) { // dicerolls are treated as a byte array when marshalling to json, so we have to decode them data.diceRolls.forEach(r=>{ r.roll = Uint8Array.from(atob(r.roll), c => c.charCodeAt(0)) }) makeUpToDate(data); } else { if (data.diceRoll) { data.diceRoll.roll = Uint8Array.from(atob(data.diceRoll.roll), c => c.charCodeAt(0)); } makeUpToDate(data); } console.log(data); }); } else { setErr("Table name and passcode can only be alphanumeric and underscores"); } }else { setErr("Table name and passcode required"); } } function emptyMsgQ() { while (msgQ.some(m=>m)) { publish(msgQ[0]); msgQ.shift(); } } async function publish(msg) { if (offline || q) { msgQ.push(msg); } else { msg.key = tableKey; const res = await fetch('/publish', { method: 'POST', body: JSON.stringify(msg) }); if (!res.ok) { setErr("Failed to publish message"); } } } function makeUpToDate(msg) { if (msg) { // map image has to be set before tokens can be handled! if (msg.mapImg) { setMapImg(msg.mapImg); } if (msg.auxMsg) { setAuxMsg(msg.auxMsg); } if (msg.diceRolls) { logDice(msg.diceRolls); } else if (msg.diceRoll) { logDice(msg.diceRoll); } if (msg.tokens) { updateTokens(msg.tokens); } else if (msg.token) { updateTokens([msg.token]); } } } function fmtLeading(n) { return n < 10 ? "0" + n : String(n); } function formatDice(r) { const date = new Date(r.timestamp) const p = document.createElement("p"); const month = date.getMonth() + 1; const day = date.getDate(); const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); p.innerHTML = `${date.getFullYear()}-${fmtLeading(month)}-${fmtLeading(day)} ${fmtLeading(hours)}:${fmtLeading(minutes)}:${fmtLeading(seconds)} ${r.player} rolled ${r.roll.length}d${r.faces} ${(r.note ? "(" + r.note + ")" : "")}
[${r.roll}] (total ${r.roll.reduce((a,c)=>a+c,0)})`; return p; } function logDice(dice) { const diceLog = $("dice_log"); if (!Array.isArray(dice)) { dice = [ dice ]; } else { if (diceLog) { diceLog.innerHTML = ""; } } if (diceLog) { for(const r of dice) { try{ const p = formatDice(r); diceLog.append(p); diceLog.scrollTop = diceLog.scrollHeight; } catch{} } } } function setAuxMsg(msg) { const auxDiv = $("aux"); if (auxDiv) { auxDiv.innerText = msg || " "; } } function updateTokens(tokens) { // update internal token array and map processTokens(tokens); // update token select window renderTokenSelect(); // if admin, update token master list renderTokenMasterList(); } function renderTokenSelect() { const tokenSelect = $("token_select"); if (tokenSelect) { const scroll = tokenSelect.scrollTop; tokenSelect.innerHTML = tokens.reduce((s, t) => { return s + `
  • ${t.t.name}
  • \n`; }, ""); tokenSelect.scrollTop = scroll; } } // the following few functions aren't socket related but they directly relate to the previous function function initSpritePreviewById(id) { const token = tokens.find(t=>t.t.id == id); let img = null; if (token && id) { img = document.createElement("img"); img.src = token.t.sprite; const hdnTokenId = $("token_preview_id"); if (hdnTokenId) { hdnTokenId.value = id; } img.onload = () => { scaleAltSpritePreview(); } } const previewClearBtn = $("token_preview_clear"); if (previewClearBtn) { previewClearBtn.style.display = (token && id) ? "block" : "none"; } const tokenPreview = $("token_preview"); if (tokenPreview) { tokenPreview.innerHTML = ""; if (img) { tokenPreview.appendChild(img); } } } function dismissPreview() { initSpritePreviewById(null); } function scaleAltSpritePreview() { const hdnTokenId = $("token_preview_id"); const tokenPreview = $("token_preview"); if (tokenPreview && hdnTokenId && mapImg && mapImg._image) { const id = hdnTokenId.value; const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; const img = tokenPreview.children[0]; const token = tokens.find(t=>t.t.id == id); if (img && token) { img.width = token.t.w * scaleFactor; img.height = token.t.h * scaleFactor; } } } function setMapImg(url) { initializeMap(url); } function isTableKeyValid(name, passcode) { const r = /^[a-zA-Z0-9_]+$/; return r.test(name) && r.test(passcode); } function leave() { conn.close(1000); tableKey.name = ""; tableKey.passcode = ""; }