33 Commits

Author SHA1 Message Date
0299f89901 Merge pull request 'copies links to clipboard' (#20) from copy-to-clipboard into main
All checks were successful
CI / deploy (push) Successful in 4m28s
Reviewed-on: #20
2025-02-11 11:24:44 +01:00
Arindy
7f7a27a178 copies links to clipboard
All checks were successful
CI / deploy (push) Successful in 4m22s
CI / deploy (pull_request) Successful in 4m18s
2025-02-11 11:15:37 +01:00
d9caee420a Merge pull request 'overwrites dice-overlay sse, so that only the last instance rolls the dice' (#19) from remove-sse-on-close into main
All checks were successful
CI / deploy (push) Successful in 4m21s
Reviewed-on: #19
2025-02-10 21:39:03 +01:00
Arindy
f9893e2a17 overwrites dice-overlay sse, so that only the last instance rolls the dice
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m20s
2025-02-10 21:30:11 +01:00
fdd06978d8 Merge pull request 'show-all-overlays' (#18) from show-all-overlays into main
All checks were successful
CI / deploy (push) Successful in 4m24s
Reviewed-on: #18
2025-02-10 20:21:20 +01:00
Arindy
13f206ae97 splits gm-view from normal-view
All checks were successful
CI / deploy (push) Successful in 4m16s
CI / deploy (pull_request) Successful in 4m20s
2025-02-10 20:08:29 +01:00
Arindy
827b74ebf1 moves overlay notice 2025-02-10 18:41:13 +01:00
80d331cdd3 Merge pull request 'adds buttons for simple commands' (#16) from adds-easy-way-to-roll into main
All checks were successful
CI / deploy (push) Successful in 4m18s
Reviewed-on: #16
2025-02-10 18:39:00 +01:00
Arindy
0e3d88cd4a fixes overlay tooltip
All checks were successful
CI / deploy (push) Successful in 4m14s
CI / deploy (pull_request) Successful in 4m17s
2025-02-10 18:26:44 +01:00
Arindy
cb077b50de adds how-to
All checks were successful
CI / deploy (push) Successful in 4m21s
CI / deploy (pull_request) Successful in 4m19s
2025-02-10 18:19:54 +01:00
Arindy
9def3240b2 styles buttons
All checks were successful
CI / deploy (push) Successful in 4m29s
CI / deploy (pull_request) Successful in 4m22s
2025-02-10 18:01:57 +01:00
Arindy
13eacc886d adds buttons for simple commands
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m17s
2025-02-10 17:46:10 +01:00
af41ff5d78 Merge pull request 'adds a dice preview' (#15) from fix-color-picker into main
All checks were successful
CI / deploy (push) Successful in 4m24s
Reviewed-on: #15
2025-02-10 17:20:26 +01:00
Arindy
153b25e53f rounds elements
All checks were successful
CI / deploy (push) Successful in 4m23s
CI / deploy (pull_request) Successful in 4m13s
2025-02-10 17:04:57 +01:00
Arindy
2e6267e7b4 adds button icons
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 6m22s
2025-02-10 16:58:26 +01:00
Arindy
821e939b3d adds a dice preview
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m15s
2025-02-10 16:40:01 +01:00
7ee16d5f3e Merge pull request 'enter in color picker now sets the color' (#14) from fix-color-picker into main
All checks were successful
CI / deploy (push) Successful in 4m22s
Reviewed-on: #14
2025-02-10 16:05:24 +01:00
Arindy
4643ec107e enter in color picker now sets the color
All checks were successful
CI / deploy (push) Successful in 4m20s
CI / deploy (pull_request) Successful in 4m16s
2025-02-10 15:53:17 +01:00
36b7c290da Merge pull request 'implements timed clearing of dice' (#11) from make-dice-clear into main
All checks were successful
CI / deploy (push) Successful in 4m23s
Reviewed-on: #11
2025-02-10 15:25:06 +01:00
Arindy
b69bdb72ee implements timed clearing of dice
All checks were successful
CI / deploy (push) Successful in 4m20s
CI / deploy (pull_request) Successful in 4m17s
2025-02-10 15:10:03 +01:00
1518783090 Merge pull request 'adds Readme & adjusts Layout of index' (#10) from readme-and-overlay-adjustements into main
All checks were successful
CI / deploy (push) Successful in 4m21s
Reviewed-on: #10
2025-02-10 14:06:46 +01:00
Arindy
39d5107ea4 adds Readme & adjusts Layout of index
All checks were successful
CI / deploy (push) Successful in 4m19s
CI / deploy (pull_request) Successful in 4m22s
2025-02-10 13:57:18 +01:00
bdbc120e61 Merge pull request 'update-styling' (#4) from update-styling into main
All checks were successful
CI / deploy (push) Successful in 4m24s
Reviewed-on: #4
2025-02-10 03:35:38 +01:00
Arindy
9b25a69636 introduces results for individual players
All checks were successful
CI / deploy (pull_request) Successful in 4m23s
CI / deploy (push) Successful in 4m16s
2025-02-10 03:24:20 +01:00
Arindy
7f40d85022 adds scale queryparam to overlay
All checks were successful
CI / deploy (push) Successful in 4m1s
CI / deploy (pull_request) Successful in 3m59s
2025-02-10 02:42:44 +01:00
Arindy
70f0cc99fc fixes concurrent rolling of same user with different name
All checks were successful
CI / deploy (push) Successful in 4m1s
CI / deploy (pull_request) Successful in 4m1s
2025-02-10 02:26:54 +01:00
Arindy
f616726aae saves customizing to name
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m20s
2025-02-10 02:04:58 +01:00
Arindy
d548ae2ea0 moves latest result to top
All checks were successful
CI / deploy (push) Successful in 4m14s
2025-02-10 01:51:18 +01:00
cc370fc3a0 Merge pull request 'saves dice config to localstorage' (#3) from update-styling into main
All checks were successful
CI / deploy (push) Successful in 4m22s
Reviewed-on: #3
2025-02-10 01:20:52 +01:00
Arindy
3a3677c17e refines dark-mode
All checks were successful
CI / deploy (push) Successful in 4m17s
CI / deploy (pull_request) Successful in 4m15s
2025-02-10 01:07:44 +01:00
Arindy
df5d072636 removes frame-border from results on index
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 4m18s
2025-02-10 01:02:10 +01:00
Arindy
9feee85cd1 switches to dark-mode
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 4m42s
2025-02-10 00:56:25 +01:00
Arindy
ce1f43fe98 adds color to name in results
All checks were successful
CI / deploy (push) Successful in 4m17s
CI / deploy (pull_request) Successful in 4m16s
2025-02-10 00:45:38 +01:00
10 changed files with 644 additions and 101 deletions

BIN
.github/media/preview.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,2 +1,63 @@
# dice-tower <h1 align="center">
Dice-Tower
</h1>
<h4 align="center">... they see them rolling ...</h4>
<p align="center">
<a href="#key-features">Key Features</a> •
<a href="#how-to-build">How To Build</a> •
<a href="#credits">Credits</a> •
<a href="#license">License</a>
</p>
![preview](.github/media/preview.gif)
---
## Key Features
* Connect to a room with others
* Configure your dice (theme and color)
* Roll any dice
* See the dice roll (Can be used as a Browser Source in OBS)
* Watch roll results (also available as Browser Source in OBS)
---
## How To Build
To clone and run this application, you'll need `git`, `java21` and `docker`.
```bash
# Clone this repository
$ git clone https://git.arindy.de/arindy/dice-tower.git
# Go into the repository
$ cd dice-tower
# Build the binary
$ ./mvnw clean verify -Pnative
# Build the container
$ docker build -f src/main/docker/Dockerfile.native-micro -t dice-tower .
# run the container in the background
$ docker run --network host -d dice-tower
# Visit the dice-tower in your browser on http://localhost:8080
```
---
## Credits
This software uses the following open source packages:
- [3D-Dice/dice-box](https://github.com/3d-dice/dice-box)
- [3D-Dice/dice-themes](https://github.com/3d-dice/dice-themes)
- [W3.CSS Color Themes](https://www.w3schools.com/w3css/w3css_color_themes.asp)
## License
GPL-3

View File

@@ -26,9 +26,6 @@ final class DiceResource(@Context val sse: Sse) {
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
fun parseCommand(@PathParam("id") id: String, data: RollPayload) { fun parseCommand(@PathParam("id") id: String, data: RollPayload) {
if (!sseBroadcasters.containsKey(id)) {
sseBroadcasters[id] = sse.newBroadcaster()
}
data.roll = data.command.split(" ", "&", "and").filter { it.isNotEmpty() }.map { it.trim() }.toTypedArray<String>() data.roll = data.command.split(" ", "&", "and").filter { it.isNotEmpty() }.map { it.trim() }.toTypedArray<String>()
data.room = id.split(":")[0] data.room = id.split(":")[0]
data.user = id.split(":")[1] data.user = id.split(":")[1]
@@ -41,6 +38,17 @@ final class DiceResource(@Context val sse: Sse) {
} }
} }
@POST
@Path("/register")
@Consumes(MediaType.APPLICATION_JSON)
fun register(@PathParam("id") id: String, data: Any) {
println("id = [${id}], data = [${data}]")
sseBroadcasters["register:$id"]?.broadcast(
eventBuilder.id((UUID.randomUUID()).toString())
.mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build())
}
@POST @POST
@Path("/results") @Path("/results")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@@ -55,9 +63,7 @@ final class DiceResource(@Context val sse: Sse) {
@Path("/stream") @Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS) @Produces(MediaType.SERVER_SENT_EVENTS)
fun stream(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) { fun stream(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
if (!sseBroadcasters.containsKey(id)) {
sseBroadcasters[id] = sse.newBroadcaster() sseBroadcasters[id] = sse.newBroadcaster()
}
sseBroadcasters[id]?.register(sseEventSink) sseBroadcasters[id]?.register(sseEventSink)
} }
@@ -71,7 +77,17 @@ final class DiceResource(@Context val sse: Sse) {
sseBroadcasters[id]?.register(sseEventSink) sseBroadcasters[id]?.register(sseEventSink)
} }
@GET
@Path("/users")
@Produces(MediaType.SERVER_SENT_EVENTS)
fun users(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
if (!sseBroadcasters.containsKey("register:$id")) {
sseBroadcasters["register:$id"] = sse.newBroadcaster()
}
sseBroadcasters["register:$id"]?.register(sseEventSink)
}
@RegisterForReflection @RegisterForReflection
data class Result(val name: String, val user: String, val themeColor: String) {} data class Result(val name: String, val user: String, val themeColor: String)
} }

View File

@@ -5,6 +5,7 @@ import jakarta.ws.rs.GET
import jakarta.ws.rs.Path import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces import jakarta.ws.rs.Produces
import jakarta.ws.rs.QueryParam
import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.MediaType
@Path("overlay/{diceid}") @Path("overlay/{diceid}")
@@ -12,14 +13,14 @@ class OverlayResource {
@GET @GET
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
fun get(@PathParam("diceid") diceid: String): TemplateInstance { fun get(@PathParam("diceid") diceid: String, @QueryParam("scale") scale: Int? = 7, @QueryParam("clearAfter") clearAfter: Long? = -1): TemplateInstance {
return Templates.overlay(diceid) return Templates.overlay(diceid, scale ?: 7, clearAfter ?: -1)
} }
@GET @GET
@Path("/results") @Path("/results")
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
fun results(@PathParam("diceid") room: String): TemplateInstance { fun results(@PathParam("diceid") room: String, @QueryParam("name") name: String?, @QueryParam("user") user: String?): TemplateInstance {
return Templates.results(room) return Templates.results(room, name ?: "all", user ?: "all")
} }
} }

View File

@@ -1,18 +0,0 @@
package de.arindy.dicetower
import io.quarkus.qute.TemplateInstance
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@Path("results/{room}")
class ResultsResource {
@GET
@Produces(MediaType.TEXT_HTML)
fun get(@PathParam("room") room: String): TemplateInstance {
return Templates.results(room)
}
}

View File

@@ -6,7 +6,7 @@ import io.quarkus.qute.TemplateInstance
@CheckedTemplate @CheckedTemplate
object Templates { object Templates {
@JvmStatic @JvmStatic
external fun overlay(diceid: String): TemplateInstance external fun overlay(diceid: String, scale: Int?, clearAfter: Long?): TemplateInstance
@JvmStatic @JvmStatic
external fun results(room: String): TemplateInstance external fun results(room: String, name: String?, user: String?): TemplateInstance
} }

View File

@@ -63,6 +63,7 @@ class ColorPicker extends HTMLElement {
.rgba-display-wrapper { .rgba-display-wrapper {
height: 100%; height: 100%;
padding: 1.25em; padding: 1.25em;
text-align: center;
} }
#rgba-display { #rgba-display {
font-family: "Courier New", monospace; font-family: "Courier New", monospace;
@@ -72,11 +73,11 @@ class ColorPicker extends HTMLElement {
display: inline; display: inline;
border: none; border: none;
border-radius: 0.2em; border-radius: 0.2em;
margin-bottom: 2.5em;
letter-spacing: 0.1em; letter-spacing: 0.1em;
width: 8em; width: 5em;
max-width: 100%; max-width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-align: center;
} }
input[type=range] { input[type=range] {
width: 100%; width: 100%;
@@ -132,13 +133,17 @@ class ColorPicker extends HTMLElement {
this.lightnessInput = this.shadowRoot.getElementById( 'lightness' ); this.lightnessInput = this.shadowRoot.getElementById( 'lightness' );
this.colorPreview = this.shadowRoot.getElementById( 'color-preview' ); this.colorPreview = this.shadowRoot.getElementById( 'color-preview' );
this.rgbaDisplay = this.shadowRoot.getElementById( 'rgba-display' ); this.rgbaDisplay = this.shadowRoot.getElementById( 'rgba-display' );
this.rgbaDisplay.onkeyup = (event) => {
if(event.keyCode === 13) {
this.setColor( this.rgbaDisplay.value );
}
}
// Create a temporary canvas to read color values. // Create a temporary canvas to read color values.
let tempCanvas = document.createElement( 'canvas' ); let tempCanvas = document.createElement( 'canvas' );
tempCanvas.height = 1; tempCanvas.height = 1;
tempCanvas.width = 1; tempCanvas.width = 1;
this.tempCanvasCTX = tempCanvas.getContext( '2d', { willReadFrequently: true } ); this.tempCanvasCTX = tempCanvas.getContext( '2d', { willReadFrequently: true } );
} }

View File

@@ -41,7 +41,6 @@
} }
import DiceBox from "/vendor/dice-box/dice-box.es.js"; import DiceBox from "/vendor/dice-box/dice-box.es.js";
const evtSource = new EventSource(url() + "/dice/{diceid??}/stream"); const evtSource = new EventSource(url() + "/dice/{diceid??}/stream");
const diceBox = new DiceBox("#dice-box", { const diceBox = new DiceBox("#dice-box", {
assetPath: "/vendor/assets/", assetPath: "/vendor/assets/",
theme: 'default', theme: 'default',
@@ -56,13 +55,15 @@
'smooth', 'smooth',
'wooden' 'wooden'
], ],
scale: 7 scale: {scale}
}); });
document.addEventListener("DOMContentLoaded", async() => { document.addEventListener("DOMContentLoaded", async() => {
await diceBox.init() await diceBox.init()
let timeout = 0;
evtSource.addEventListener("message", function (event) { evtSource.addEventListener("message", function (event) {
clearInterval(timeout);
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
diceBox.onRollComplete = (rollResult) => { diceBox.onRollComplete = (rollResult) => {
let httpRequest = new XMLHttpRequest(); let httpRequest = new XMLHttpRequest();
@@ -74,10 +75,12 @@
themeColor: data.themeColor, themeColor: data.themeColor,
results: rollResult, results: rollResult,
} )) } ))
if ({clearAfter} > 0) {
timeout = setTimeout(() => diceBox.clear(), {clearAfter} * 1000)
}
} }
diceBox.roll(data.roll, { theme: data.theme?.length > 0 ? data.theme : 'default', themeColor: data.themeColor.length > 0 ? data.themeColor : '#4545FF' }); diceBox.roll(data.roll, { theme: data.theme?.length > 0 ? data.theme : 'default', themeColor: data.themeColor.length > 0 ? data.themeColor : '#4545FF' });
}) })
}) })
</script> </script>

View File

@@ -4,14 +4,30 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Dice-Tower</title> <title>Dice-Tower</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css"> <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-black.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="/vendor/color-picker.js"></script> <script src="/vendor/color-picker.js"></script>
<style> <style>
.w3-theme-l6 {
color: #000 !important;
background-color: #999999 !important;
border-radius: 10px;
}
.w3-theme-l4 {
color: #fff !important;
background-color: #666666 !important;
border-radius: 10px;
}
.w3-theme-l1 {
color: #fff !important;
background-color: #333333 !important;
border-radius: 10px;
}
.collapsible { .collapsible {
background-color: grey; background-color: #333333;
color: black; color: black;
cursor: pointer;
margin-top: 10px; margin-top: 10px;
padding: 18px; padding: 18px;
width: 100%; width: 100%;
@@ -25,32 +41,265 @@
padding: 0 18px; padding: 0 18px;
display: none; display: none;
overflow: hidden; overflow: hidden;
background-color: lightgrey; background-color: #999999;
color: #000;
border-radius: 10px;
} }
button { button {
padding: 10px; padding: 10px;
border: #333333 3px solid;
border-radius: 10px;
background: #333333;
color: #fff
} }
button:hover {
background: #444444;
}
button:active {
background: #222222;
}
input { input {
margin: 10px; margin: 10px;
} }
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 25px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #333333;
}
input:focus + .slider {
box-shadow: 0 0 1px #333333;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.overlayButton {
background: transparent;
border: none;
width: 5px;
height: 5px;
font-size: large;
color: #000
}
.overlayButton:hover {
background: transparent;
}
.tooltip {
position: fixed;
border-radius: 0.5rem;
padding: 15px;
color: #fff !important;
background-color: #333333dd !important
}
#dice-box {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
background: transparent;
background-size: cover;
}
#dice-box canvas {
width: 100%;
height: 100%;
margin: 20px
}
.checkbox {
position: relative;
padding-left: 100px;
margin-bottom: 12px;
margin-left: 10px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
margin-left: 75px;
position: absolute;
top: 0;
left: 0;
height: 15px;
width: 15px;
background-color: #eee;
}
.checkbox:hover input ~ .checkmark {
background-color: #ccc;
}
/* When the checkbox is checked, add a blue background */
.checkbox input:checked ~ .checkmark {
background-color: #333333;
}
/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
.checkbox input:checked ~ .checkmark:after {
display: block;
}
/* Style the checkmark/indicator */
.checkbox .checkmark:after {
left: 5px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
#snackbar-container {
visibility: hidden;
position: fixed;
width: 100%;
z-index: 1;
bottom: 200px;
display: flex;
justify-content: center;
align-items: center;
}
#snackbar {
visibility: hidden;
text-align: center;
border-radius: 0.5rem;
padding: 15px;
color: #fff !important;
border: #fff 5px solid;
background-color: #333333dd !important
}
#snackbar-container.show {
visibility: visible;
}
#snackbar.show {
visibility: visible;
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
animation: fadein 0.5s, fadeout 0.5s 2.5s;
}
</style> </style>
</head> </head>
<body class="w3-theme-l4"> <body class="w3-theme-l1">
<div class="w3-container w3-content" style="height: 100vh"> <div class="w3-container w3-content"
<h1>Dice-Tower</h1> style="height: 95vh; display: flex; flex-direction: column; justify-content: space-between; padding: 25px">
<p>Welcome to Dice-Tower</p> <h1 style="text-align: center">Dice-Tower</h1>
<div class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px"> <div class="w3-panel w3-theme-l4 w3-card w3-display-container"
<label for="name" id="nameLabel">Name </label><input type="text" id="name" style="width: 75%" required onkeyup="start(event)"/><br/> style="padding: 25px; text-align: center; margin-bottom: auto;">
<label for="room" id="roomLabel">Room </label><input type="text" id="room" style="width: 75%" required onkeyup="start(event)"/><br/> <label for="name" id="nameLabel">Name </label><input type="text" id="name" style="width: 50%; margin-top: 20px"
required onkeyup="start(event)"/><br/>
<label for="room" id="roomLabel">Room </label><input type="text" id="room" style="width: 50%" required
onkeyup="start(event)"/><br/>
<div>
<button id="start" onclick="start()" style="align-self: center; margin-top: 20px">Start 🞂</button>
<label class="checkbox" id="gm-container">Join as GM
<input type="checkbox" id="gm" style="margin-left: 50px">
<span class="checkmark"></span>
</label>
</div> </div>
<div id="dice-tower" hidden class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px"> </div>
<button type="button" class="collapsible" style="color: white; font-weight: bold">Overlay URLs</button> <div id="dice-tower" hidden class="w3-panel w3-theme-l4 w3-card w3-display-container"
style="padding: 25px; margin-bottom: auto">
<button type="button" class="collapsible" style="color: white; font-weight: bold">Overlay URLs <a>🞃</a></button>
<div class="content"> <div class="content">
<label for="overlayId">Dice-Overlay </label><input type="text" readonly id="overlayId" style="width: 75%"/><br/> <div id="overlay-urls">
<label for="resultsId">Results-Overlay </label><input type="text" readonly id="resultsId" style="width: 75%"/><br/> <div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;">
<label for="overlayId" id="overlayLabel">Dice-Overlay </label>
<input type="text" readonly id="overlayId" style="flex-grow: 1" onclick="copyToClipboard(this.id)"/>
<button id="overlay-hint-button" popovertarget="overlay-hint" data-trigger="hover"
class="overlayButton">🛈
</button>
</div> </div>
<button type="button" class="collapsible" style="color: white; font-weight: bold">Customize Dice</button> </div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;"
hidden id="all-results-urls">
<label for="resultsId">All-Results-Overlay </label>
<input type="text" readonly id="resultsId" style="flex-grow: 1" onclick="copyToClipboard(this.id)"/>
<button popovertarget="all-results-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;">
<label for="myResultsId">My-Results-Overlay </label>
<input type="text" readonly id="myResultsId" style="flex-grow: 1" onclick="copyToClipboard(this.id)"/>
<button popovertarget="my-results-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
</div>
<button type="button" class="collapsible" style="color: white; font-weight: bold">Customize Dice <a>🞃</a>
</button>
<div class="content"> <div class="content">
<label for="theme">Theme </label> <label for="theme">Theme </label>
<select name="theme" id="theme" style="margin: 25px"> <select name="theme" id="theme" style="margin: 25px">
@@ -64,44 +313,201 @@
<option value="smooth">Smooth</option> <option value="smooth">Smooth</option>
<option value="wooden">Wooden</option> <option value="wooden">Wooden</option>
</select> </select>
<br/> <color-picker id="themeColor"></color-picker>
<color-picker value="#cd72fe" id="themeColor"></color-picker><br/> <div id="dice-box"></div>
</div><br/>
<p>Example Commands: "1d6", "2d8 1d100", "1d4 and 1d6", "2d20 & 1d2, "5d6+10"</p> <button style="margin: 10px" id="preview">Preview 🔎</button>
<label for="command">Command </label><input type="text" id="command" onkeyup="roll(event)"/> <button style="margin: 10px" onclick="saveDice()">Save 💾</button>
<button hidden id="roll" onclick="roll()">Roll</button> <button popovertarget="save-dice-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin: 20px 25px;">
<label style="font-size: x-large; font-weight: bold;">Roll </label>
<button style="border: transparent; border-radius: 100%; font-size: x-large; font-weight: bold; height: 50px; width: 50px"
onclick="removeDice()">-
</button>
<label style="font-size: x-large; font-weight: bold;" id="dice-amount">1</label>
<button style="border: transparent; border-radius: 100%; font-size: x-large; font-weight: bold; height: 50px; width: 50px"
onclick="addDice()">+
</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d4')">D4</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d6')">D6</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d8')">D8</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d10')">D10</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d12')">D12</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d20')">D20</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d100')">D100</button>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; margin: 20px 25px;">
<label for="command">Command </label>
<input type="text" id="command" style="flex-grow: 1" onkeyup="roll(event)"/>
<button hidden id="roll" onclick="roll()">Roll 🎲</button>
<button popovertarget="command-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div> </div>
<button id="start" onclick="start()">Start</button> <div style="margin: 20px 25px" id="all-results">
<label for="resultSwitch">Show all results </label>
<div id="results" hidden class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px; overflow: hidden; height: 50vh;"> <label class="switch">
<h2>Results</h2> <input type="checkbox" id="resultSwitch">
<iframe id="resultFrame" title="results" style="width: 100%; height: 85%; overflow: hidden" onload="this.height=this.contentWindow.document.body.scrollHeight;"></iframe> <span class="slider"></span>
</label>
</div>
</div>
<div id="results" hidden class="w3-panel w3-theme-l6 w3-card w3-display-container"
style="padding: 25px; flex-grow: 1; margin-bottom: auto">
<iframe id="resultFrame" title="results" style="width: 100%; height: 85%; overflow: hidden; border: 0"
onload="this.height=this.contentWindow.document.body.scrollHeight;"></iframe>
</div> </div>
<div popover id="overlay-hint" class="tooltip">
<p style="color: red; font-weight: bold">Only the last loaded instance of that overlay rolls the dice!</p>
<p>Query Params you can Change:</p>
<ul>
<li><strong>scale</strong> changes the size of the dice (any value over 1)</li>
<li><strong>clearAfter</strong> time until dice are cleared (in seconds; remove param or set -1 to keep the
dice)
</li>
</ul>
</div>
<div popover id="all-results-hint" class="tooltip">
<p>Shows all Results in this room</p>
</div>
<div popover id="my-results-hint" class="tooltip">
<p>Shows only my Results in this room</p>
</div>
<div popover id="save-dice-hint" class="tooltip">
This saves your current theme and theme color for current Name
</div>
<div popover id="command-hint" class="tooltip">
Example Commands: "1d6", "2d8 1d100", "1d4 and 1d6", "2d20 & 1d2, "5d6+10"
</div>
<div style="margin-top: 20px; flex-grow: 1" id="how-to">
<div class="w3-panel w3-theme-l4 w3-card w3-display-container" style="padding: 25px; margin-bottom: auto">
<h2 style="text-align: center">How-To</h2>
<ul>
<li>
Join a room by entering your character name and the name of the room.<br/>
<strong>If you are a GM, make sure to join the room first or let all other players rejoin to get all
Overlay-URLs.</strong>
</li>
<li>Open your Dice-Overlay either in a new Tab or as a browser source in OBS</li>
<ul>
<li style="color: red; font-weight: bold">Only the last loaded instance of that overlay rolls the
dice!
</li>
<li>You can configure your Overlay with query parameters (more information at the info element next
to the link)
</li>
</ul>
<li>Configure your dice</li>
<li>Save your dice configuration</li>
<li>Start rolling</li>
</ul>
</div>
</div>
</div>
<div id="snackbar-container">
<div id="snackbar">.. they see them rolling</div>
</div> </div>
<script> <script>
function addDice() {
let amount = +document.getElementById('dice-amount').innerText
document.getElementById('dice-amount').innerText = amount + 1
}
function removeDice() {
let amount = +document.getElementById('dice-amount').innerText
if (amount > 1) {
document.getElementById('dice-amount').innerText = amount - 1
}
}
function url() { function url() {
return window.location.protocol + '//' + window.location.hostname + (window.location.port?.length > 0 ? ':' + window.location.port : ''); return window.location.protocol + '//' + window.location.hostname + (window.location.port?.length > 0 ? ':' + window.location.port : '');
} }
function start(event) { function start(event) {
if((!event || event.keyCode === 13) && document.getElementById('name').value.length > 0 && document.getElementById('room').value.length > 0) { console.log(document.getElementById('gm').checked)
document.getElementById('overlayId').value = url() + '/overlay/' + document.getElementById('room').value + ':' + localStorage.getItem('userId');
if ((!event || event.keyCode === 13) && document.getElementById('name').value.length > 0 && document.getElementById('room').value.length > 0) {
document.getElementById('overlayId').value = url() + '/overlay/' + document.getElementById('room').value + ':' + localStorage.getItem('userId') + '?scale=7&clearAfter=30';
document.getElementById('resultsId').value = url() + '/overlay/' + document.getElementById('room').value + '/results'; document.getElementById('resultsId').value = url() + '/overlay/' + document.getElementById('room').value + '/results';
document.getElementById('resultFrame').src = url() + '/overlay/' + document.getElementById('room').value + '/results'; document.getElementById('myResultsId').value = document.getElementById('resultsId').value + '?name=' + encodeURIComponent(document.getElementById('name').value) + '&user=' + localStorage.getItem('userId');
document.getElementById('resultFrame').src = document.getElementById('myResultsId').value;
document.getElementById('roll').hidden = false; document.getElementById('roll').hidden = false;
document.getElementById('start').hidden = true; document.getElementById('start').hidden = true;
document.getElementById('dice-tower').hidden = false; document.getElementById('dice-tower').hidden = false;
document.getElementById('name').hidden = true; document.getElementById('name').hidden = true;
document.getElementById('room').hidden = true; document.getElementById('room').hidden = true;
document.getElementById('gm-container').hidden = true;
document.getElementById('how-to').hidden = true;
document.getElementById('results').hidden = false; document.getElementById('results').hidden = false;
document.getElementById('nameLabel').innerHTML = 'Name: <strong>' + document.getElementById('name').value + '</strong>'; document.getElementById('all-results').hidden = !document.getElementById('gm').checked;
document.getElementById('roomLabel').innerHTML = 'Room: <strong>' + document.getElementById('room').value + '</strong>'; document.getElementById('all-results-urls').style.display = document.getElementById('gm').checked ? 'fles' : 'none';
document.getElementById('nameLabel').innerHTML = '<strong style="font-size:x-large;">' + document.getElementById('name').value + '</strong>';
document.getElementById('roomLabel').innerHTML = '<strong style="font-size:medium;">' + document.getElementById('room').value + '</strong>';
document.getElementById('overlayLabel').innerHTML = 'Dice-Overlay for <strong>' + document.getElementById('name').value + '</strong>';
document.title = document.getElementById('name').value + ' - Dice-Tower';
if (localStorage.getItem(document.getElementById('name').value + "-theme")) {
document.getElementById('theme').value = localStorage.getItem(document.getElementById('name').value + "-theme")
}
if (localStorage.getItem(document.getElementById('name').value + "-themeColor")) {
document.getElementById('themeColor').setColor(localStorage.getItem(document.getElementById('name').value + "-themeColor"));
}
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + '/register')
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: document.getElementById('name').value,
overlay: document.getElementById('overlayId').value,
id: document.getElementById('room').value + ':' + localStorage.getItem('userId')
}))
if (document.getElementById('gm').checked) {
document.getElementById('resultSwitch').checked = true;
document.getElementById('resultFrame').src = document.getElementById('resultsId').value;
const evtSource = new EventSource(url() + '/dice/' + document.getElementById('room').value + '/users');
evtSource.addEventListener('message', function (event) {
let data = JSON.parse(event.data);
if (data.id !== document.getElementById('room').value + ':' + localStorage.getItem('userId')) {
let overlays = document.getElementById('overlay-urls');
let newOverlay = document.getElementById(data.id) ?? document.createElement('div');
newOverlay.replaceChildren(...[]);
newOverlay.id = data.id;
newOverlay.style.display = "flex";
newOverlay.style.flexDirection = "row";
newOverlay.style.justifyContent = "space-between";
newOverlay.style.alignItems = "baseline";
let newLabel = document.createElement('label');
newLabel.for = data.id + 'url';
newLabel.innerHTML = "Dice-Overlay for <strong>" + data.name + "</strong>";
let newInput = document.createElement('input');
newInput.type = "text";
newInput.readOnly = true;
newInput.id = data.id + 'url';
newInput.style.flexGrow = '1';
newInput.value = data.overlay;
newInput.onclick = () => copyToClipboard(newInput.id)
let hint = document.getElementById('overlay-hint-button').cloneNode(true);
newOverlay.appendChild(newLabel);
newOverlay.appendChild(newInput);
newOverlay.appendChild(hint);
overlays.appendChild(newOverlay);
}
showPopover();
});
}
} }
} }
function rollEasy(dice) {
document.getElementById('command').value = document.getElementById('dice-amount').innerText + dice;
roll();
}
function roll(event) { function roll(event) {
if((!event || event.keyCode === 13) && document.getElementById('command').value?.length > 0) { if ((!event || event.keyCode === 13) && document.getElementById('command').value?.length > 0) {
let httpRequest = new XMLHttpRequest(); let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + ':' + localStorage.getItem(`userId`)) httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + ':' + localStorage.getItem(`userId`))
httpRequest.setRequestHeader('Content-Type', 'application/json') httpRequest.setRequestHeader('Content-Type', 'application/json')
@@ -110,35 +516,102 @@
command: document.getElementById('command').value, command: document.getElementById('command').value,
themeColor: document.getElementById('themeColor').value, themeColor: document.getElementById('themeColor').value,
theme: document.getElementById('theme').value theme: document.getElementById('theme').value
} )) }))
} }
} }
function saveDice() {
localStorage.setItem(document.getElementById('name').value + "-theme", document.getElementById('theme').value)
localStorage.setItem(document.getElementById('name').value + "-themeColor", document.getElementById('themeColor').value)
}
let coll = document.getElementsByClassName("collapsible"); let coll = document.getElementsByClassName("collapsible");
for (let i = 0; i < coll.length; i++) { for (let i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () { coll[i].addEventListener("click", function () {
localStorage.setItem("theme", document.getElementById('theme').value)
localStorage.setItem("themeColor", document.getElementById('themeColor').value)
const content = this.nextElementSibling; const content = this.nextElementSibling;
if (content.style.display === "block") { if (content.style.display === "block") {
content.style.display = "none"; content.style.display = "none";
this.children[0].innerHTML = "🞃"
} else { } else {
content.style.display = "block"; content.style.display = "block";
this.children[0].innerHTML = "🞁"
} }
}); });
} }
document.addEventListener("DOMContentLoaded", async() => {
function showPopover() {
const popover = document.querySelectorAll("[popovertarget][data-trigger='hover']");
popover.forEach((e) => {
const target = document.querySelector("#" + e.getAttribute("popovertarget"));
e.addEventListener("mouseover", () => {
target.showPopover();
});
e.addEventListener("mouseout", () => {
target.hidePopover();
});
});
}
showPopover();
document.getElementById('resultSwitch').addEventListener('change', function () {
if (!this.checked) {
document.getElementById('resultFrame').src = document.getElementById('myResultsId').value;
} else {
document.getElementById('resultFrame').src = document.getElementById('resultsId').value;
}
})
document.addEventListener("DOMContentLoaded", async () => {
if (!localStorage.getItem("userId")) { if (!localStorage.getItem("userId")) {
localStorage.setItem("userId", self.crypto.randomUUID()); localStorage.setItem("userId", self.crypto.randomUUID());
} }
if (localStorage.getItem("theme")) { })
document.getElementById('theme').value = localStorage.getItem("theme")
function copyToClipboard(id) {
let copyText = document.getElementById(id);
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
showSnackbar("Link copied to clipboard: <br/>" + copyText.value);
} }
if (localStorage.getItem("themeColor")) {
document.getElementById('themeColor').setColor(localStorage.getItem("themeColor")); function showSnackbar(message) {
let snackbar = document.getElementById("snackbar");
let snackbarContainer = document.getElementById("snackbar-container");
snackbar.innerHTML = message;
snackbar.className = "show";
snackbarContainer.className = "show";
setTimeout(function () {
snackbar.className = snackbar.className.replace("show", "");
snackbarContainer.className = snackbarContainer.className.replace("show", "");
}, 5000);
}
</script>
<script type="module">
import DiceBox from "/vendor/dice-box/dice-box.es.js";
document.addEventListener("DOMContentLoaded", async () => {
document.getElementById('preview').onclick = async () => {
document.getElementById('dice-box').replaceChildren(...[])
const diceBox = new DiceBox("#dice-box", {
assetPath: "/vendor/assets/",
theme: document.getElementById('theme').value,
themeColor: document.getElementById('themeColor').value,
scale: 14
});
await diceBox.init()
diceBox.roll(['1d2', '1d4', '1d6', '1d8', '1d10', '1d12', '1d20', '1d100']);
} }
}) })
</script> </script>
</body> </body>
<footer class="w3-theme-l1 w3-center w3-padding-16">
<a href="https://git.arindy.de/arindy/dice-tower" target="_blank" class="w3-hover-text-black">Dice-Tower on my
GitTea</a>
</footer>
</html> </html>

View File

@@ -5,7 +5,7 @@
<title>Results</title> <title>Results</title>
</head> </head>
<body> <body>
<div id="results" style="padding: 25px; font-size: x-large"> <div id="results" style="font-size: x-large">
</div> </div>
<script type="module"> <script type="module">
function url() { function url() {
@@ -14,8 +14,9 @@
const evtSource = new EventSource(url() + '/dice/{room}/results'); const evtSource = new EventSource(url() + '/dice/{room}/results');
evtSource.addEventListener('message', function (event) { evtSource.addEventListener('message', function (event) {
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
let name = document.getElementById(data.user) ?? document.createElement('div'); if ("{name}" === "all" && "{user}" === "all" || "{name}" === data.name && "{user}" === data.user || "{name}" === "all" && "{user}" === data.user || "{name}" === data.name && "{user}" === "all") {
name.id = data.user; let name = document.getElementById(data.user + '-' + data.name) ?? document.createElement('div');
name.id = data.user + '-' + data.name;
name.replaceChildren(...[]); name.replaceChildren(...[]);
let node = document.createElement('p'); let node = document.createElement('p');
let resultText = '' let resultText = ''
@@ -27,12 +28,13 @@
result.rolls.forEach(roll => { result.rolls.forEach(roll => {
values.push(roll.value); values.push(roll.value);
}) })
resultText += '<br/>&ensp; D' + result.sides + ': [' + values.map(value => value === 1 ? '<strong style="color: red">' + value + '</strong>' : value === result.sides ? '<strong style="color: green">' + value + '</strong>' : value).join(' + ') + (result.modifier > 0 ? ' <a style="text-decoration: underline">+' + result.modifier + '</a>': result.modifier < 0 ? ' <a style="text-decoration: underline">' + result.modifier + '</a>': '') + '] = <strong style="font-size: x-large">' + result.value + '</strong> ' resultText += '<br/>&ensp; D' + result.sides + ': [' + values.map(value => value === 1 ? '<strong style="color: red">' + value + '</strong>' : value === result.sides ? '<strong style="color: green">' + value + '</strong>' : value).join(' + ') + (result.modifier > 0 ? ' <a style="text-decoration: underline">+' + result.modifier + '</a>' : result.modifier < 0 ? ' <a style="text-decoration: underline">' + result.modifier + '</a>' : '') + '] = <strong style="font-size: x-large">' + result.value + '</strong> '
}) })
} }
node.innerHTML = '<strong style="text-shadow: 1px 1px 3px black; color: ' + data.themeColor + ';">' + data.name + ':</strong> &#127922; ' + resultText node.innerHTML = '<strong style="text-shadow: 2px 2px 10px ' + data.themeColor + ';">' + data.name + ':</strong> 🎲 ' + resultText
name.appendChild(node) name.appendChild(node)
document.getElementById('results').appendChild(name); document.getElementById('results').insertBefore(name, document.getElementById('results').firstChild);
}
}) })
</script> </script>
</body> </body>