all repos — felt @ f4513a28f7de843bcef4def5f98ce85545148a49

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

fix dice decoding, token editor can create clientside tokens!
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmSmeXgACgkQO3+8IhRO
Y5gdBA//QDBo4+/gBbiB6fA+sXgMY63kCdjAva769qBbN56GDhEEfBVsISRVW9NP
6YFIp9JslQV88bPw43/Bi5z9oQxka+oCnTBwuLFL9n6M3xjrcIgFXt+HkeeaFvAj
sUmXcJ0zs5qYaw9vzI6gVewtb9jlFwxHwNeRmi8QkYiGY3UOG5IJY8GTaIi1DY3A
bvewvzJV8n6UV5CeLY8/rL/ARt4xX5XbT/Sg0a1oQ3ZCJCpX2wOVc8uzOA9E7T/y
bEH29wTr/1gdU2+Inuf3PylOm77LcLztPGeUzScg9YATB7/v4KMqEDaeMkAoOkeI
4N+CjEo0BtB4b2aX9vgOC8Ogfvgit4t1rHlURNG2ZX8SmbcxKLUMRzgfBODisJui
b3bXyVPbesbpWUTPi2wgX59h1VwDsp2Ypzb+IxtbX9ZDuEhKB68Fy/w+bczWNw2V
ejaLIJJtiJHKWKhQ/ExeVkjbe3KhGYPtfIXIgHfy6EKF5uH9AHjg1cCEr/2QY3Ob
e3OTHkA2Mbw+Rl073CyOAH6FJknwnZdom760O35vLe1co4E5NO73aEyZZTh5gBC8
fi1bhV0T6IxOaW/qIMzvwHHKGX/oafYFPddIcJG8Ub8Bha+kvbhZFVuCEBnvWMU3
BYyt8IlGaInGyRJaqrXhkXd+LGhT7EZH7z2MxQ/bBdSzqEQRbSc=
=xJkN
-----END PGP SIGNATURE-----
commit

f4513a28f7de843bcef4def5f98ce85545148a49

parent

8dfa99c8bcfbfd13b312db2654ad6b20386c9b9b

M static/admin.jsstatic/admin.js

@@ -7,6 +7,15 @@ const createTokenForm = document.getElementById("createTokenForm");

const tokenWrapper = document.getElementById("adminWrapper_tokens"); const newTableName = document.getElementById("newTableName"); const newTablePass = document.getElementById("newTablePass"); +const tokenSpriteDropdown = document.getElementById("token_combobox"); +const tokenName = document.getElementById("token_name"); +const tokenWidth = document.getElementById("token_width"); +const tokenHeight = document.getElementById("token_height"); +const tokenCX = document.getElementById("token_cx"); +const tokenCY = document.getElementById("token_cy"); +const previewZone = document.getElementById("tokenPreview_zone"); +const tokenAspect = document.getElementById("tokenKeepAspect"); +const aspectLockLabel = document.getElementById("aspectLockLabel"); async function getTable(name, pass) { try {

@@ -50,7 +59,7 @@ infoHtml += "<label>Maps couldn't be retrieved</label>";

} adminZone.innerHTML = infoHtml; - let tokenListHTML = "<input id='token_img_upload' type='file'/><button onclick='uploadTokenImg()'>Upload Token</button><br/>"; + let tokenListHTML = "<input id='token_img_upload' type='file'/><button onclick='uploadTokenImg()'>Upload Sprite</button><br/>"; if (tokenImgs.ok) { tokenListHTML += "<label>Available Sprites</label>"; const tokens = await tokenImgs.json();

@@ -60,15 +69,19 @@ const parts = t.split("/");

tokenListHTML += `<li>${parts[parts.length - 1]} <a href="${t}" target="_blank">view</a> <button onclick="deleteImg('${t}')">Delete</button></li>\n` } tokenListHTML += "</ul>"; + fillSpriteDropdown(tokens); } else { tokenListHTML += "<label>Sprites couldn't be retrieved</label>" } spriteZone.innerHTML = tokenListHTML; + + tokenWrapper.style.display = "inline"; // also, we have to fill and toggle the tokens window + } else { console.log(res.status); }

@@ -77,6 +90,144 @@ setErr(`${err.name}: ${err.message}`);

} } +function fillSpriteDropdown(tokens) { + let options = "<option value=''>select</option>"; + for (const t of tokens) { + const parts = t.split("/"); + const o = `<option value="${t}">${parts[parts.length - 1]}</option>\n`; + options += o; + } + tokenSpriteDropdown.innerHTML = options; +} + +function previewSprite(source) { + if (source) { + switch (source.id) { + case "token_combobox": + reinitializeSpritePreview(); + break; + case "token_cx": + case "token_cy": + drawTokenOrigin(); + break; + default: + scaleSpritePreview(source); + console.log("default case"); + } + } +} + +function toggleAspectLock() { + if (tokenKeepAspect.checked) { + aspectLockLabel.innerHTML = "&#128274;" + } else { + aspectLockLabel.innerHTML = "&#128275;" + } +} + +function scaleSpritePreview(source) { + if (mapImg && mapImg._image) { + console.log(mapImg); + const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; + const keepAspect = tokenAspect.checked; + const img = previewZone.children[0]; + if (img) { + if (!keepAspect || !source) { + img.width = Number(tokenWidth.value) * scaleFactor; + img.height = Number(tokenHeight.value) * scaleFactor; + } else { + + const currentAspect = img.width/img.height; + switch (source.id) { + case "token_width": + img.width = Number(tokenWidth.value) * scaleFactor; + img.height = (img.clientWidth / img.naturalWidth) * img.naturalHeight; + tokenHeight.value = Number(tokenWidth.value)/currentAspect; + break; + case "token_height": + img.height = Number(tokenHeight.value) * scaleFactor; + img.width = (img.clientHeight / img.naturalHeight) * img.naturalWidth; + tokenWidth.value = currentAspect * Number(tokenHeight.value); + break; + } + } + tokenCX.value = Number(tokenWidth.value)/2; + tokenCY.value = Number(tokenHeight.value)/2; + drawTokenOrigin(); + } + } +} + +function drawTokenOrigin() { + const img = previewZone.children[0]; + const x = Number(tokenWidth.value) / Number(tokenCX.value); + const y = Number(tokenHeight.value) / Number(tokenCY.value); + const origin = {x: img.width/x, y: img.height/y}; + const originImg = document.createElement("img"); + originImg.src="/table/origin.png"; + originImg.style.position = "absolute"; + originImg.style.left = (origin.x - 2) + "px"; + originImg.style.top = (origin.y - 2) + "px"; + if (previewZone.children.length > 1) { + previewZone.replaceChild(originImg, previewZone.children[1]); + } else { + previewZone.appendChild(originImg); + } + + +} + +function reinitializeSpritePreview() { + const img = document.createElement("img"); + img.src = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; + const tokenNameParts = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].text.split("."); + tokenNameParts.pop(); + tokenName.value = tokenNameParts.join("."); + img.onload = () => { + const w = img.naturalWidth; + const h = img.naturalHeight; + + tokenWidth.value = w; + tokenHeight.value = h; + scaleSpritePreview(); + } + if (previewZone.children.length) { + previewZone.replaceChild(img, previewZone.children[0]); + } else { + previewZone.appendChild(img); + } +} + +function createToken() { + const w = Number(tokenWidth.value); + const h = Number(tokenHeight.value); + const oX = Number(tokenCX.value); + const oY = Number(tokenCY.value); + const img = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; + const name = tokenName.value; + + console.log("creating token"); + if (!isNaN(w) && !isNaN(h) && !isNaN(oX) && !isNaN(oY) && img && name) { + console.log("all green"); + const self = { + sz: [w, h], + m: L.marker(getCascadingPos(), { + icon: L.icon({ + iconUrl: img, + iconSize: [w,h], + }), + title: name, + draggable: true, + autoPan: true + }), + }; + tokens.push(self); + self.m.addTo(map); + resizeMarkers(); + + } +} + function publishAuxMsg() { const txtArea = document.getElementById("auxMsgZone"); if (txtArea != null) {

@@ -223,8 +374,8 @@

if (adminUsrInput && adminPassInput) { adminToken = await getAdminToken(adminUsrInput.value, adminPassInput.value); if (adminToken) { - adminWrapper.style.display="inline"; getTables(); + adminWrapper.style.display="inline"; adminZone.style.display = "block"; } else { setErr("Incorrect credentials");
M static/index.htmlstatic/index.html

@@ -89,14 +89,15 @@ <div id="adminWrapper_tokens">

<details id="admin_token_win" class="ui_win admin_win"><summary>tokens</summary> <button onclick="setTokenCreateFormVisible(true)">New Token</button> <form onsubmit="return false" id="createTokenForm"> - <label>Sprite<select id="token_combobox"></select></label><br/> - <label>Name<input id="newToken_name"/></label><br/> - <label>Width<input type="number" id="newToken_width" min="1" max="9999"/></label><br/> - <label>Height<input type="number" id="newToken_height" min="1" max="9999"/></label><br/> - <label>cX<input type="number" id="newToken_cx" min="0" max="9999"/></label><br/> - <label>cY<input type="number" id="newToken_cy" min="0" max="9999"/></label><br/> + <label>Sprite<select id="token_combobox" onchange="previewSprite(this)"></select></label><br/> + <label>Name<input id="token_name"/></label><br/> + + <label>Width<input type="number" id="token_width" min="1" max="9999" onchange="previewSprite(this)"/></label><label id="aspectLockLabel" for="tokenKeepAspect">&#128274;</label><input type="checkbox" checked id="tokenKeepAspect" onchange="toggleAspectLock()"/><br/> + <label>Height<input type="number" id="token_height" min="1" max="9999" onchange="previewSprite(this)"/></label><br/> + <label>cX<input type="number" id="token_cx" min="0" max="9999" onchange="previewSprite(this)"/></label><br/> + <label>cY<input type="number" id="token_cy" min="0" max="9999" onchange="previewSprite(this)"/></label><br/> <div id="tokenPreview_zone"></div> - <button type="submit" onlcick="createToken()">Create</button> + <button type="submit" onclick="createToken()">Create</button> <button onclick="setTokenCreateFormVisible(false)">Cancel</button> </form> <div id="tokenZone"></div>
M static/map.jsstatic/map.js

@@ -1,18 +1,19 @@

let map = null; let mapImg = null; let tokens = []; +const worldBounds = [[180, -180],[-180, 180]]; function initializeMap(mapImgUrl) { if (!map) { map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple }); - map.on("zoomend", resizeMarkers); + map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();}); } if (mapImg) { mapImg.removeFrom(map); } - mapImg = L.imageOverlay(mapImgUrl, [[-180, 180],[180, -180]]); + mapImg = L.imageOverlay(mapImgUrl, worldBounds); mapImg.addTo(map); - map.setMaxBounds([[-180,180],[180,-180]]); + map.setMaxBounds(worldBounds); map.setView([0,0], 2); while (tokens.some(t=>t)) { tokens[0].m.removeFrom(map);

@@ -28,6 +29,14 @@ const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth;

icon.options.iconSize = [scaleFactor * t.sz[0], scaleFactor * t.sz[1]]; t.m.setIcon(icon); }); +} + +function getCascadingPos() { + const topLeft = worldBounds[0]; + const n = tokens.length; + topLeft[1] += (n+1)*5; + topLeft[0] -= (n+1)*5; + return topLeft; } function addToken(token) {
M static/socket.jsstatic/socket.js

@@ -19,6 +19,7 @@ return n < 10 ? "0" + n : String(n);

} function formatDice(r) { + console.log(r); const date = new Date(r.timestamp) const p = document.createElement("p"); const month = date.getMonth() + 1;

@@ -51,7 +52,6 @@

function setAuxMsg(msg) { const auxDiv = document.getElementById("aux"); if (auxDiv) { - console.log("eeee"); auxDiv.innerText = msg; } }

@@ -111,7 +111,6 @@ }

}); conn.addEventListener("open", e => { // TODO: add message to let user know they are at the table - console.info("socket connected"); tabletop = document.getElementById("tabletop"); if (tabletop) { tabletop.style.display = "block";

@@ -132,6 +131,9 @@ })

table = data; makeUpToDate(table); } else { + if (data.diceRoll) { + data.diceRoll.roll = Uint8Array.from(atob(data.diceRoll.roll), c => c.charCodeAt(0)); + } makeUpToDate(data); }
M static/style.cssstatic/style.css

@@ -169,4 +169,12 @@ }

.leaflet-container { background: transparent; +} + +#tokenKeepAspect { + display: none; +} + +#tokenPreview_zone { + position: relative; }
M static/util.jsstatic/util.js

@@ -30,7 +30,6 @@ const username = document.getElementById("name_entry");

if (username) { const cookies = document.cookie.split(";") cookies.forEach(c=>{ - console.log(c); if (c.trim().startsWith("username=")) { username.value = c.trim().split("=")[1]; }