1 Commits

Author SHA1 Message Date
Arindy
7e9b26499a adds color to name in results
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 4m31s
2025-02-10 00:42:20 +01:00
15 changed files with 205 additions and 881 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -11,8 +11,6 @@ jobs:
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v4
@@ -20,14 +18,8 @@ jobs:
distribution: 'zulu'
java-version: '21'
- name: new Version
if: github.ref_name == 'main'
run: |
./mvnw -B --no-transfer-progress versions:set -DremoveSnapshot
- name: Build Runner
run: |
./mvnw -B --no-transfer-progress clean verify -Pnative -Dquarkus.native.remote-container-build=true -Djgitver.skip=true
run: ./mvnw --no-transfer-progress clean verify -Pnative -Dquarkus.native.remote-container-build=true
- name: Add coverage to PR
id: jacoco
@@ -39,32 +31,10 @@ jobs:
min-coverage-changed-files: 60
title: ${{ env.REPO }} Coverage
- id: version
name: Version
run: echo "VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)" >> ${GITHUB_OUTPUT}
- name: Build Container
if: github.ref_name == 'main'
run: |
echo ${{secrets.PACKAGES_TOKEN}} | docker login --username ${{ secrets.PACKAGES_USER }} --password-stdin git.arindy.de
docker build -f src/main/docker/Dockerfile.native-micro -t git.arindy.de/arindy/dice-tower:latest -t git.arindy.de/arindy/dice-tower:${{ steps.version.outputs.VERSION }} .
docker push git.arindy.de/arindy/dice-tower:${{ steps.version.outputs.VERSION }}
docker push git.arindy.de/arindy/dice-tower:latest
run: docker build -f src/main/docker/Dockerfile.native-micro -t ${{ env.REPO }} .
- name: Deploy
if: github.ref_name == 'main'
run: "docker compose up -d"
- name: create tag
if: github.ref_name == 'main'
run: |
git config user.email "ci@git.arindy.de"
git config user.name "gitea"
git add ./pom.xml
git commit -m "[no ci] release ${{ steps.version.outputs.VERSION }}"
./mvnw -B --no-transfer-progress clean validate -Pnew-snapshot
git add ./pom.xml
git commit -m "[no ci] prepare new Version"
git tag ${{ steps.version.outputs.VERSION }} -m "release ${{ steps.version.outputs.VERSION }}"
git push origin main
git push origin ${{ steps.version.outputs.VERSION }}

View File

@@ -1,84 +1,2 @@
<h1 align="center">
Dice-Tower
</h1>
# dice-tower
<h4 align="center">... they see them rolling ...</h4>
<p align="center">
<a href="#key-features">Key Features</a> •
<a href="#start-container">Start Container</a> •
<a href="#how-to-build-from-scratch">How To Build from scratch</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)
---
## Start Container
You can start dice-tower with docker compose
Create a `compose.yml`-File with following content:
```yaml
services:
api:
container_name: dice-tower
image: git.arindy.de/arindy/dice-tower:latest
restart: always
ports:
- "8080:8080"
```
Run the container with:
```bash
docker compose up -d
```
---
## How To Build from scratch
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

@@ -1,7 +1,7 @@
services:
api:
container_name: dice-tower
image: git.arindy.de/arindy/dice-tower:latest
image: dice-tower
restart: always
networks:
- reverse_proxy

46
pom.xml
View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>de.arindy</groupId>
<artifactId>dice-tower</artifactId>
<version>1.0.3-SNAPSHOT</version>
<version>1.0.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
@@ -21,13 +21,6 @@
<rxjava-version>2.2.8</rxjava-version>
</properties>
<scm>
<connection>scm:git:https://git.arindy.de/arindy/dice-tower.git</connection>
<developerConnection>scm:git:https://git.arindy.de/arindy/dice-tower.git</developerConnection>
<url>https://git.arindy.de/arindy/dice-tower</url>
<tag>HEAD</tag>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
@@ -274,42 +267,5 @@
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
<profile>
<id>new-snapshot</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>next-version</id>
<goals>
<goal>parse-version</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.18.0</version>
<executions>
<execution>
<id>release-version</id>
<phase>validate</phase>
<goals>
<goal>set</goal>
</goals>
<configuration>
<newVersion>${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}-SNAPSHOT</newVersion>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -26,6 +26,9 @@ final class DiceResource(@Context val sse: Sse) {
@POST
@Consumes(MediaType.APPLICATION_JSON)
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.room = id.split(":")[0]
data.user = id.split(":")[1]
@@ -38,17 +41,6 @@ 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
@Path("/results")
@Consumes(MediaType.APPLICATION_JSON)
@@ -63,7 +55,9 @@ final class DiceResource(@Context val sse: Sse) {
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
fun stream(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
if (!sseBroadcasters.containsKey(id)) {
sseBroadcasters[id] = sse.newBroadcaster()
}
sseBroadcasters[id]?.register(sseEventSink)
}
@@ -77,17 +71,7 @@ final class DiceResource(@Context val sse: Sse) {
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
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

@@ -1,35 +0,0 @@
package de.arindy.dicetower
import io.quarkus.qute.TemplateInstance
import io.quarkus.runtime.annotations.RegisterForReflection
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.GET
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.QueryParam
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.sse.OutboundSseEvent
import jakarta.ws.rs.sse.Sse
import jakarta.ws.rs.sse.SseBroadcaster
import jakarta.ws.rs.sse.SseEventSink
import org.eclipse.microprofile.config.inject.ConfigProperty
import java.util.UUID
@Path("/")
@ApplicationScoped
final class IndexResource() {
@ConfigProperty(name = "quarkus.application.version")
private lateinit var version: String
@GET
@Produces(MediaType.TEXT_HTML)
fun get(): TemplateInstance {
return Templates.index(version)
}
}

View File

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

View File

@@ -0,0 +1,18 @@
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,9 +6,7 @@ import io.quarkus.qute.TemplateInstance
@CheckedTemplate
object Templates {
@JvmStatic
external fun overlay(diceid: String, scale: Int?, clearAfter: Long?): TemplateInstance
external fun overlay(diceid: String): TemplateInstance
@JvmStatic
external fun results(room: String, name: String?, user: String?): TemplateInstance
@JvmStatic
external fun index(version: String): TemplateInstance
external fun results(room: String): TemplateInstance
}

View File

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

View File

@@ -1,618 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dice-Tower</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="/META-INF/resources/vendor/color-picker.js"></script>
<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 {
background-color: #333333;
color: black;
margin-top: 10px;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
.content {
padding: 0 18px;
display: none;
overflow: hidden;
background-color: #999999;
color: #000;
border-radius: 10px;
}
button {
padding: 10px;
border: #333333 3px solid;
border-radius: 10px;
background: #333333;
color: #fff
}
button:hover {
background: #444444;
}
button:active {
background: #222222;
}
input {
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>
</head>
<body class="w3-theme-l1">
<div class="w3-container w3-content"
style="height: 95vh; display: flex; flex-direction: column; justify-content: space-between; padding: 25px">
<h1 style="text-align: center">Dice-Tower</h1>
<div class="w3-panel w3-theme-l4 w3-card w3-display-container"
style="padding: 25px; text-align: center; margin-bottom: auto;">
<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-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 id="overlay-urls">
<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>
<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">
<label for="theme">Theme </label>
<select name="theme" id="theme" style="margin: 25px">
<option value="default">Default</option>
<option value="blueGreenMetal">Blue-Green Metal</option>
<option value="diceOfRolling">Dice of Rolling</option>
<option value="gemstone">Gemstone</option>
<option value="gemstoneMarble">Marble Gemstone</option>
<option value="rock">Rock</option>
<option value="rust">Rust</option>
<option value="smooth">Smooth</option>
<option value="wooden">Wooden</option>
</select>
<color-picker id="themeColor"></color-picker>
<div id="dice-box"></div>
<button style="margin: 10px" id="preview">Preview 🔎</button>
<button style="margin: 10px" onclick="saveDice()">Save 💾</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 style="margin: 20px 25px" id="all-results">
<label for="resultSwitch">Show all results </label>
<label class="switch">
<input type="checkbox" id="resultSwitch">
<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 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>
<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() {
return window.location.protocol + '//' + window.location.hostname + (window.location.port?.length > 0 ? ':' + window.location.port : '');
}
function start(event) {
console.log(document.getElementById('gm').checked)
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('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('start').hidden = true;
document.getElementById('dice-tower').hidden = false;
document.getElementById('name').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('all-results').hidden = !document.getElementById('gm').checked;
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) {
if ((!event || event.keyCode === 13) && document.getElementById('command').value?.length > 0) {
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + ':' + localStorage.getItem(`userId`))
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: document.getElementById('name').value,
command: document.getElementById('command').value,
themeColor: document.getElementById('themeColor').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");
for (let i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
const content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
this.children[0].innerHTML = "🞃"
} else {
content.style.display = "block";
this.children[0].innerHTML = "🞁"
}
});
}
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")) {
localStorage.setItem("userId", self.crypto.randomUUID());
}
})
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);
}
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>
</body>
<footer class="w3-theme-l1 w3-center w3-padding-16">
Version {version} ::
<a href="https://git.arindy.de/arindy/dice-tower" target="_blank" class="w3-hover-text-black">Dice-Tower on my
GitTea</a>
</footer>
</html>

View File

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

View File

@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dice-Tower</title>
<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">
<script src="/vendor/color-picker.js"></script>
<style>
.collapsible {
background-color: grey;
color: black;
cursor: pointer;
margin-top: 10px;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
.content {
padding: 0 18px;
display: none;
overflow: hidden;
background-color: lightgrey;
}
button {
padding: 10px;
}
input {
margin: 10px;
}
</style>
</head>
<body class="w3-theme-l4">
<div class="w3-container w3-content" style="height: 100vh">
<h1>Dice-Tower</h1>
<p>Welcome to Dice-Tower</p>
<div class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px">
<label for="name" id="nameLabel">Name </label><input type="text" id="name" style="width: 75%" required onkeyup="start(event)"/><br/>
<label for="room" id="roomLabel">Room </label><input type="text" id="room" style="width: 75%" required onkeyup="start(event)"/><br/>
</div>
<div id="dice-tower" hidden class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px">
<button type="button" class="collapsible" style="color: white; font-weight: bold">Overlay URLs</button>
<div class="content">
<label for="overlayId">Dice-Overlay </label><input type="text" readonly id="overlayId" style="width: 75%"/><br/>
<label for="resultsId">Results-Overlay </label><input type="text" readonly id="resultsId" style="width: 75%"/><br/>
</div>
<button type="button" class="collapsible" style="color: white; font-weight: bold">Customize Dice</button>
<div class="content">
<label for="theme">Theme </label>
<select name="theme" id="theme" style="margin: 25px">
<option value="default">Default</option>
<option value="blueGreenMetal">Blue-Green Metal</option>
<option value="diceOfRolling">Dice of Rolling</option>
<option value="gemstone">Gemstone</option>
<option value="gemstoneMarble">Marble Gemstone</option>
<option value="rock">Rock</option>
<option value="rust">Rust</option>
<option value="smooth">Smooth</option>
<option value="wooden">Wooden</option>
</select>
<br/>
<color-picker value="#cd72fe" id="themeColor"></color-picker><br/>
</div><br/>
<p>Example Commands: "1d6", "2d8 1d100", "1d4 and 1d6", "2d20 & 1d2, "5d6+10"</p>
<label for="command">Command </label><input type="text" id="command" onkeyup="roll(event)"/>
<button hidden id="roll" onclick="roll()">Roll</button>
</div>
<button id="start" onclick="start()">Start</button>
<div id="results" hidden class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px; overflow: hidden; height: 50vh;">
<h2>Results</h2>
<iframe id="resultFrame" title="results" style="width: 100%; height: 85%; overflow: hidden" onload="this.height=this.contentWindow.document.body.scrollHeight;"></iframe>
</div>
</div>
<script>
function url() {
return window.location.protocol + '//' + window.location.hostname + (window.location.port?.length > 0 ? ':' + window.location.port : '');
}
function start(event) {
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');
document.getElementById('resultsId').value = url() + '/overlay/' + document.getElementById('room').value + '/results';
document.getElementById('resultFrame').src = url() + '/overlay/' + document.getElementById('room').value + '/results';
document.getElementById('roll').hidden = false;
document.getElementById('start').hidden = true;
document.getElementById('dice-tower').hidden = false;
document.getElementById('name').hidden = true;
document.getElementById('room').hidden = true;
document.getElementById('results').hidden = false;
document.getElementById('nameLabel').innerHTML = 'Name: <strong>' + document.getElementById('name').value + '</strong>';
document.getElementById('roomLabel').innerHTML = 'Room: <strong>' + document.getElementById('room').value + '</strong>';
}
}
function roll(event) {
if((!event || event.keyCode === 13) && document.getElementById('command').value?.length > 0) {
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + ':' + localStorage.getItem(`userId`))
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: document.getElementById('name').value,
command: document.getElementById('command').value,
themeColor: document.getElementById('themeColor').value,
theme: document.getElementById('theme').value
} ))
}
}
let coll = document.getElementsByClassName("collapsible");
for (let i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
localStorage.setItem("theme", document.getElementById('theme').value)
localStorage.setItem("themeColor", document.getElementById('themeColor').value)
const content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
} else {
content.style.display = "block";
}
});
}
document.addEventListener("DOMContentLoaded", async() => {
if (!localStorage.getItem("userId")) {
localStorage.setItem("userId", self.crypto.randomUUID());
}
if (localStorage.getItem("theme")) {
document.getElementById('theme').value = localStorage.getItem("theme")
}
if (localStorage.getItem("themeColor")) {
document.getElementById('themeColor').setColor(localStorage.getItem("themeColor"));
}
})
</script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<title>Results</title>
</head>
<body>
<div id="results" style="font-size: x-large">
<div id="results" style="padding: 25px; font-size: x-large">
</div>
<script type="module">
function url() {
@@ -14,9 +14,8 @@
const evtSource = new EventSource(url() + '/dice/{room}/results');
evtSource.addEventListener('message', function (event) {
let data = JSON.parse(event.data);
if ("{name}" === "all" && "{user}" === "all" || "{name}" === data.name && "{user}" === data.user || "{name}" === "all" && "{user}" === data.user || "{name}" === data.name && "{user}" === "all") {
let name = document.getElementById(data.user + '-' + data.name) ?? document.createElement('div');
name.id = data.user + '-' + data.name;
let name = document.getElementById(data.user) ?? document.createElement('div');
name.id = data.user;
name.replaceChildren(...[]);
let node = document.createElement('p');
let resultText = ''
@@ -31,10 +30,9 @@
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: 2px 2px 10px ' + data.themeColor + ';">' + data.name + ':</strong> 🎲 ' + resultText
node.innerHTML = '<strong style="text-shadow: 1px 1px 3px black; color: ' + data.themeColor + ';">' + data.name + ':</strong> &#127922; ' + resultText
name.appendChild(node)
document.getElementById('results').insertBefore(name, document.getElementById('results').firstChild);
}
document.getElementById('results').appendChild(name);
})
</script>
</body>