more messageboxes on caught exceptions, update readme, bump to 0.2.2
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-----
@@ -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`
@@ -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}`); } } }
@@ -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>
@@ -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); }
@@ -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 {
@@ -6,7 +6,7 @@ <meta charset="UTF-8" />
<title>Felt — 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">
@@ -7,7 +7,7 @@ <meta charset="UTF-8" />
<title>Felt — 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>
@@ -6,7 +6,7 @@ <meta charset="UTF-8" />
<title>Felt — 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">