all repos — felt @ 9cf0de40cb12f3aecdab616156cd7eb511ddaebc

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

more messageboxes on caught exceptions, update readme, bump to 0.2.2
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmS7bdoACgkQO3+8IhRO
Y5ixZA//YuWmx89c8C4gNKeZ5z6eQ194gmkX9iXZtsDWX9jnVioPsuOuSwNnpHD8
bQ2Yxmy2CFx+xfWpHDMg91FENdByM60DcuTZbAkHYi2DWpmF99F5cFe1b436c3FS
OyJr0Tf9CoJqYShBLUws3vwGko4vY4N6JGFpRNLKadK1tQRcEXk/JApjGYxJcnjP
mHwCFvjblmiNqaFR7+do7GiDlYk7O0t/O0nA2rS1Lsjmj0sBZpaU+narQKmDj5CH
6ZJSZUPYyXdldQbyyCtZQZ9AaqmgDjkx3BvfyjWSF9mKYtujYO9iqJUXy78N/gEt
bQNdAF0VemmPcduD1rN1f99h3+PRBjI8ett5zWGpW1VUMoBJILEpO7wQaOYpUph6
oax7qUNVjszqK2/BtLmpuix1NVxtn/H95gongyVb4CFsYHZzIB6i7nrAAHye2i0h
GnIxbHkFLELWba1eBGv99KbaLYi6B3doNCkL30Xo56n3hMRl30eEAOfJYUOJbYs0
SbgxMx0Cb588O4m98DgE7dfzGpy6x4B2i1QoM3+KQkMOXMPuXcXDRQ+6eC2HvSn6
gJ+ZpnfsHKs/7avnsLdIGV9/kjVIKH54dFI2WssGcvdNmdX9kghsdNtgeaoP8pdq
fy6Tz0tQaXqpXUDlKbDcB5qGerr0qY8XoKZnss/BssGkNn0s3+Y=
=qkGO
-----END PGP SIGNATURE-----
commit

9cf0de40cb12f3aecdab616156cd7eb511ddaebc

parent

7a294373c5340edcfebfec5cd21a2c0ef0cdda8f

M README.mdREADME.md

@@ -2,9 +2,11 @@ # felt

-- virtual tabletop for distributed cooperative storytelling -- +![Screenshot of Felt showing a hexgrid map on which a party of 4 humans and an alien dog thing facing off against two giant centipedes. Various UI windows are overlayed, some of them collapsed, showing the dice roller with dice log, and admin windows showing map and token management interfaces.](./screenshot.jpg) + ## about -Felt is a lightweight webapp written in Go and vanilla Javascript which provides an agnostic virtual tabletop for battle maps, visual puzzles, or any other situation you may need a shared map in a tabletop RPG over voice/video chat. +Felt is a lightweight (~210KB frontend!) webapp written in Go and vanilla Javascript which provides an agnostic virtual tabletop for battle maps, visual puzzles, or any other situation you may need a shared map in a tabletop RPG over voice/video chat. ## usage

@@ -20,7 +22,7 @@ The `status` panel is updated when the admin changes the table's status, and can be used to display initiative order, other battle status, environmental or contextual notes, etc.

The `token select` panel provides a list of every existing token at the table. Clicking the name of the token shows a to-scale preview at the top of the list (with a button to dismiss it), which is resized according to the zoom level of the map. The button to the right of each token's name can place the token on or remove it from the map. -Any user can move any token on the map by dragging it around. The map is pannable and zoomable as well. +Any user can move any token on the map by dragging it around. The map is pannable and zoomable as well. The UI theme is changeable from the drawer at bottom-left of the application, and it persists to `localStorage`. ### admin

@@ -48,8 +50,8 @@ ## build, run, deploy

Requirements: -- go -- docker +- go 1.19 +- docker (for the containerized database) 1. Clone this repository and `cd` into it. 2. `go mod tidy`
M static/admin.jsstatic/admin.js

@@ -65,7 +65,6 @@ } else {

console.log(res.status); } } catch (err) { - console.dir(err) setErr(`${err.name}: ${err.message}`); } }

@@ -162,26 +161,26 @@

if (dropdown && tokenWidth && tokenHeight && tokenCX && tokenCY && preview && dropdown.selectedIndex >= 0) { - tokenCX.value = Math.floor(Number(tokenCX.value)) - tokenCY.value = Math.floor(Number(tokenCY.value)) + tokenCX.value = Math.floor(Number(tokenCX.value)) + tokenCY.value = Math.floor(Number(tokenCY.value)) - const img = preview.children[0]; - const x = Number(tokenWidth.value) / Number(tokenCX.value); - const y = Number(tokenHeight.value) / Number(tokenCY.value); + const img = preview.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"); + 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"; + originImg.src="/table/origin.png"; + originImg.style.position = "absolute"; + originImg.style.left = (origin.x - 2) + "px"; + originImg.style.top = (origin.y - 2) + "px"; - if (preview.children.length > 1) { - preview.replaceChild(originImg, preview.children[1]); - } else { - preview.appendChild(originImg); - } + if (preview.children.length > 1) { + preview.replaceChild(originImg, preview.children[1]); + } else { + preview.appendChild(originImg); + } } }

@@ -252,7 +251,9 @@ setTokenCreateFormVisible(false);

return; } setErr("All token fields are required"); - } catch {} + } catch (err) { + setErr(`${err.name}: ${err.message}`); + } } function destroyToken(id) {

@@ -270,7 +271,7 @@ function previewExistingToken(id) {

try { const existing = tokens.find(t=>t.t.id == id); const dropdown = $("sprite_dropdown"); - if (existing) { + if (existing && dropdown) { $("token_width").value = existing.t.w; $("token_height").value = existing.t.h; $("token_cx").value = existing.t.oX;

@@ -285,7 +286,7 @@ }

reinitializeSpritePreview(true); } } catch (err) { - console.log(err); + setErr(`${err.name}: ${err.message}`); } }

@@ -486,7 +487,9 @@ if (!v) {

$("new_table_name").value = ""; $("new_table_pass").value = ""; } - } catch {} + } catch (err) { + setErr(`${err.name}: ${err.message}`); + } } function setTokenCreateFormVisible(v) {

@@ -502,7 +505,9 @@ }

$("token_creation_form").style.display = v ? "block" : "none"; $("token_list").style.display = v ? "none" : "block"; $("token_admin_preview").innerHTML = ""; - } catch {} + } catch (err) { + setErr(`${err.name}: ${err.message}`); + } } async function createTable() {

@@ -538,8 +543,8 @@ setErr("Table name and passcode must be only alphanumeric and underscores");

} else { setErr("Error creating table"); } - } catch { - setErr("Error creating table"); + } catch (err) { + setErr(`${err.name}: ${err.message}`); } } }
M static/index.htmlstatic/index.html

@@ -6,7 +6,7 @@ <title>Felt</title>

<meta name="viewport" content="width=device-width" /> <link rel="shortcut icon" href="./favicon.png"/> <link href="./leaflet.css?v=1.9.4" rel="stylesheet" /> - <link href="./style.css?v=0.2.0" rel="stylesheet" /> + <link href="./style.css?v=0.2.2" rel="stylesheet" /> </head> <body> <noscript><div id="noscript_container">

@@ -146,13 +146,13 @@ <button onclick="setTheme()">Apply</button><button onclick="resetTheme(defaultTheme)">Reset</button>

</form> </details> <div id="lag" style="display:none;">lag...</div> - <div class="ui_win" id="felt_info"><a href="https://hacklab.nilfm.cc/felt">felt v0.2.1</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LICENSE">license</a>) | built with <a href="https://leafletjs.com">leaflet</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LEAFLET_LICENSE">license</a>) </div> + <div class="ui_win" id="felt_info"><a href="https://hacklab.nilfm.cc/felt">felt v0.2.2</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LICENSE">license</a>) | built with <a href="https://leafletjs.com">leaflet</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LEAFLET_LICENSE">license</a>) </div> </nav> </body> <script src="./leaflet.js?v=1.9.4" type="text/javascript"></script> - <script src="./util.js?v=0.2.1" type="text/javascript"></script> - <script src="./map.js?v=0.2.1" type="text/javascript"></script> - <script src="./socket.js?v=0.2.1" type="text/javascript"></script> - <script src="./dice.js?v=0.2.1" type="text/javascript"></script> - <script src="./admin.js?v=0.2.1" type="text/javascript"></script> + <script src="./util.js?v=0.2.2" type="text/javascript"></script> + <script src="./map.js?v=0.2.2" type="text/javascript"></script> + <script src="./socket.js?v=0.2.2" type="text/javascript"></script> + <script src="./dice.js?v=0.2.2" type="text/javascript"></script> + <script src="./admin.js?v=0.2.2" type="text/javascript"></script> </html>
M static/map.jsstatic/map.js

@@ -57,6 +57,14 @@ });

} +function sortByTokenName(a, b) { + return (a.t.name < b.t.name) + ? -1 + : ((a.t.name > b.t.name) + ? 1 + : 0) +} + function processTokens(tokenChanges) { for (const t of tokenChanges) { const i = tokens.findIndex(tk=>tk.t.id == t.id);

@@ -84,14 +92,7 @@ } else {

if (t.x != null && t.y != null) { const self = NewToken(t); tokens.push(self); - tokens.sort((a,b)=>{ - if (a.t.name < b.t.name) { - return -1; - } else if (a.t.name > b.t.name) { - return 1; - } - return 0; - }); + tokens.sort(sortByTokenName); if (t.active) { self.m.addTo(map); }
M static/style.cssstatic/style.css

@@ -171,7 +171,7 @@ color: var(--fg_color);

} .ui_win ul { - max-height: 10em; + max-height: 16em; overflow: auto; }

@@ -212,7 +212,7 @@ }

.single_btn_list li { display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr auto; } .two_btn_list li {
M templates/error.htmltemplates/error.html

@@ -6,7 +6,7 @@ <meta charset="UTF-8" />

<title>Felt &mdash; Error</title> <meta name="viewport" content="width=device-width" /> <link rel="shortcut icon" href="/table/favicon.png"/> - <link href="/table/style.css?v=0.2.0" rel="stylesheet" /> + <link href="/table/style.css?v=0.2.2" rel="stylesheet" /> </head> <body> <main id="registration">
M templates/register.htmltemplates/register.html

@@ -7,7 +7,7 @@ <meta charset="UTF-8" />

<title>Felt &mdash; Admin Registration</title> <meta name="viewport" content="width=device-width" /> <link rel="shortcut icon" href="/table/favicon.png"/> - <link href="/table/style.css?v=0.2.0" rel="stylesheet" /> + <link href="/table/style.css?v=0.2.2" rel="stylesheet" /> </head> <body> <main id="registration">

@@ -23,5 +23,5 @@ <span class="error">The registration token you provided is invalid;<br/> obtain a new one.</span>

{{end}} </main> </body> -<script src="/table/util.js?v=0.2.1" type="text/javascript"></script> +<script src="/table/util.js?v=0.2.2" type="text/javascript"></script> </html>
M templates/registered.htmltemplates/registered.html

@@ -6,7 +6,7 @@ <meta charset="UTF-8" />

<title>Felt &mdash; Registration Complete</title> <meta name="viewport" content="width=device-width" /> <link rel="shortcut icon" href="/table/favicon.png"/> - <link href="/table/style.css?v=0.2.0" rel="stylesheet" /> + <link href="/table/style.css?v=0.2.2" rel="stylesheet" /> </head> <body> <main id="registration">