303 lines
7.5 KiB
JavaScript
303 lines
7.5 KiB
JavaScript
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 + ")" : "")}<br>[${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 + `<li><a href="#" onclick="initSpritePreviewById('${t.t.id}')">${t.t.name}</a><button onclick="toggleActive('${t.t.id}');return false">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\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 = "";
|
|
}
|
|
|