Full packaged app
- LFS (just incase) - WS - Room creation/destruction - Dice Rolling functions - Modifier added correctly
This commit is contained in:
parent
0194875242
commit
47788a19f5
7 changed files with 852 additions and 0 deletions
36
Dockerfile
Normal file
36
Dockerfile
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Build stage
|
||||||
|
FROM golang:bookworm AS builder
|
||||||
|
|
||||||
|
# Install UPX
|
||||||
|
RUN apt-get update && apt-get install -y wget xz-utils && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Download the latest UPX binary directly from GitHub
|
||||||
|
RUN wget https://github.com/upx/upx/releases/download/v5.0.2/upx-5.0.2-amd64_linux.tar.xz
|
||||||
|
RUN tar -xf upx-5.0.2-amd64_linux.tar.xz && mv upx-5.0.2-amd64_linux/upx /usr/local/bin/upx && rm -r upx-5.0.2-amd64_linux upx-5.0.2-amd64_linux.tar.xz
|
||||||
|
|
||||||
|
# Create a simple Go web server
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod files first for better layer caching
|
||||||
|
COPY go.mod ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create necessary directories, build, and compress with UPX
|
||||||
|
RUN mkdir -p /var/sockets
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-s -w -extldflags '-static' -X main.GOMEMLIMIT=50MiB -X runtime.defaultGOGC=150" -trimpath -gcflags="-l=4" -asmflags=-trimpath -o bin/main app/gofudge/main.go
|
||||||
|
RUN upx --best --ultra-brute bin/main
|
||||||
|
RUN chmod +x bin/main
|
||||||
|
|
||||||
|
# Final stage with Chainguard static
|
||||||
|
FROM cgr.dev/chainguard/static:latest
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only the built binary and necessary directories
|
||||||
|
COPY --from=builder /app/bin/main ./bin/main
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
USER nonroot:nonroot
|
||||||
|
CMD ["./bin/main"]
|
||||||
17
LICENSE
Normal file
17
LICENSE
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
Copyright (c) 2025 Riomoo [alister at kamikishi dot net]
|
||||||
|
|
||||||
|
This software is licensed under the Prism Information License (PIL).
|
||||||
|
|
||||||
|
- You are free to use, modify, and distribute this software.
|
||||||
|
- Any derivative work must include this license.
|
||||||
|
- You must also provide a concise explanation of how to operate this software,
|
||||||
|
as well as backends and frontends (if any) that work with this software.
|
||||||
|
- You must credit the original creator of this software.
|
||||||
|
- You may choose to license this software under additional licenses, provided that the terms of the original PIL are still adhered to.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind,
|
||||||
|
express or implied, including but not limited to the warranties of merchantability,
|
||||||
|
fitness for a particular purpose, and non-infringement.
|
||||||
|
|
||||||
|
By using this software, you agree to the terms of the PIL.
|
||||||
|
For more information visit: https://pil.jester-designs.com/
|
||||||
58
README.md
Normal file
58
README.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# GoFudge
|
||||||
|
|
||||||
|
A Fudge Dice rolling room programmed in Go
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
[](https://golang.org/dl/)
|
||||||
|
- The version of **Go** used to test the code in this repository is **1.25.3**.
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
- run the following commands: `go mod tidy; go build -o bin/main ./app/gofudge/main.go` then `./bin/main` to start the server on port 8080.
|
||||||
|
- Visit http://localhost:8080 in your browser.
|
||||||
|
|
||||||
|
- Upon visiting the URL you will be created with a username entry and Create room button. After that you will be in the room.
|
||||||
|
- (If you are hosting this publicly) You can copy the room link in the top right hand corner and share it to anyone. They will be prompted to also pick a username.
|
||||||
|
- From there you may increase/decrease the modifier as needed for the skill you are rolling for.
|
||||||
|
|
||||||
|
### Podman/Docker
|
||||||
|
|
||||||
|
- If you want to use this with Docker, replace all `podman` commands in `run.sh` instances with `docker`
|
||||||
|
- Use `run.sh` script which will start the site on port `12007` and to make it public change the `8080` port in the NGINX config to `12007` or change the port in the `run.sh` script how you like.
|
||||||
|
- To use it locally, same as above but visit http://localhost:12007 instead.
|
||||||
|
|
||||||
|
## Config for NGINX to use as a website:
|
||||||
|
```
|
||||||
|
upstream gofudge {
|
||||||
|
server 127.0.0.1:8080;
|
||||||
|
server [::1]:8080;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::1]:80;
|
||||||
|
server_name fudge.example.com;
|
||||||
|
location / {
|
||||||
|
proxy_pass http://gofudge;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Software Used but not included
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
693
app/gofudge/main.go
Normal file
693
app/gofudge/main.go
Normal file
|
|
@ -0,0 +1,693 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Room struct {
|
||||||
|
ID string
|
||||||
|
Users map[*User]bool
|
||||||
|
History []RollResult
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string
|
||||||
|
Conn *websocket.Conn
|
||||||
|
Room *Room
|
||||||
|
}
|
||||||
|
|
||||||
|
type RollResult struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Dice []int `json:"dice"`
|
||||||
|
Modifier int `json:"modifier"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
rooms = make(map[string]*Room)
|
||||||
|
roomsMu sync.RWMutex
|
||||||
|
upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateRoomID() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
rand.Read(b)
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rollFateDice() []int {
|
||||||
|
dice := make([]int, 4)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
n, _ := rand.Int(rand.Reader, big.NewInt(3))
|
||||||
|
dice[i] = int(n.Int64()) - 1 // -1, 0, or 1
|
||||||
|
}
|
||||||
|
return dice
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrCreateRoom(roomID string) *Room {
|
||||||
|
roomsMu.Lock()
|
||||||
|
defer roomsMu.Unlock()
|
||||||
|
|
||||||
|
if room, exists := rooms[roomID]; exists {
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
|
||||||
|
room := &Room{
|
||||||
|
ID: roomID,
|
||||||
|
Users: make(map[*User]bool),
|
||||||
|
History: []RollResult{},
|
||||||
|
}
|
||||||
|
rooms[roomID] = room
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRoomIfEmpty(roomID string) {
|
||||||
|
roomsMu.Lock()
|
||||||
|
defer roomsMu.Unlock()
|
||||||
|
|
||||||
|
if room, exists := rooms[roomID]; exists {
|
||||||
|
room.mu.RLock()
|
||||||
|
isEmpty := len(room.Users) == 0
|
||||||
|
room.mu.RUnlock()
|
||||||
|
|
||||||
|
if isEmpty {
|
||||||
|
delete(rooms, roomID)
|
||||||
|
log.Printf("Room %s deleted", roomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) broadcast(msg Message) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
data, _ := json.Marshal(msg)
|
||||||
|
for user := range r.Users {
|
||||||
|
user.Conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) addUser(user *User) {
|
||||||
|
r.mu.Lock()
|
||||||
|
r.Users[user] = true
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
// Send room history to new user
|
||||||
|
r.mu.RLock()
|
||||||
|
history := r.History
|
||||||
|
r.mu.RUnlock()
|
||||||
|
|
||||||
|
historyMsg := Message{Type: "history", Data: history}
|
||||||
|
data, _ := json.Marshal(historyMsg)
|
||||||
|
user.Conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
|
||||||
|
// Notify others
|
||||||
|
r.broadcast(Message{
|
||||||
|
Type: "user_joined",
|
||||||
|
Username: user.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) removeUser(user *User) {
|
||||||
|
r.mu.Lock()
|
||||||
|
delete(r.Users, user)
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
r.broadcast(Message{
|
||||||
|
Type: "user_left",
|
||||||
|
Username: user.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tmpl := `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Go Fudge Dice Roller</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #4a9eff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #3a3a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #3a8eef;
|
||||||
|
}
|
||||||
|
.invite-info {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4a9eff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
color: #4a9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Go Fudge Dice Roller</h1>
|
||||||
|
<div class="section">
|
||||||
|
<h2 style="color: #4a9eff; margin-bottom: 20px;">Create New Room</h2>
|
||||||
|
<input type="text" id="username" placeholder="Enter your username">
|
||||||
|
<button onclick="createRoom()">Create Room</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Licensed under the <a href="https://pil.jester-designs.com/" target="_blank">Prism Information License</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function createRoom() {
|
||||||
|
const username = document.getElementById('username').value.trim();
|
||||||
|
if (!username) {
|
||||||
|
alert('Please enter a username');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/api/create-room', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
window.location.href = '/room/' + data.roomId + '?username=' + encodeURIComponent(username);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Write([]byte(tmpl))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRoom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
roomID := r.URL.Path[len("/room/"):]
|
||||||
|
username := r.URL.Query().Get("username")
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
// Show username prompt
|
||||||
|
tmpl := fmt.Sprintf(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Join Room - FATE</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #4a9eff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #3a3a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
color: #4a9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="section">
|
||||||
|
<h1>Join Room</h1>
|
||||||
|
<input type="text" id="username" placeholder="Enter your username">
|
||||||
|
<button onclick="joinRoom()">Join</button>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Licensed under the <a href="https://pil.jester-designs.com/" target="_blank">Prism Information License</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function joinRoom() {
|
||||||
|
const username = document.getElementById('username').value.trim();
|
||||||
|
if (!username) {
|
||||||
|
alert('Please enter a username');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = '/room/%s?username=' + encodeURIComponent(username);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`, roomID)
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Write([]byte(tmpl))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := template.Must(template.New("room").Parse(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>FATE Room</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
h1 { color: #4a9eff; }
|
||||||
|
.invite-link {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
padding: 5px 15px;
|
||||||
|
background: #4a9eff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.roll-section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.roll-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.roll-controls input {
|
||||||
|
flex: 0 0 150px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #3a3a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.roll-controls button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.history {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.roll-item {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.roll-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
.dice {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.die {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.die.plus { color: #4ade80; }
|
||||||
|
.die.minus { color: #f87171; }
|
||||||
|
.die.zero { color: #94a3b8; }
|
||||||
|
.total {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4a9eff;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.notification {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
color: #4a9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Go Fudge Dice Roller</h1>
|
||||||
|
<div class="invite-link">
|
||||||
|
<span id="inviteLink"></span>
|
||||||
|
<button class="copy-btn" onclick="copyInvite()">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-section">
|
||||||
|
<h2 style="margin-bottom: 15px;">Roll Dice</h2>
|
||||||
|
<div class="roll-controls">
|
||||||
|
<input type="number" id="modifier" placeholder="Modifier (±)" value="0">
|
||||||
|
<button onclick="rollDice()">Roll Fuge Dice</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history">
|
||||||
|
<h2 style="margin-bottom: 15px;">Roll History</h2>
|
||||||
|
<div id="rollHistory"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Licensed under the <a href="https://pil.jester-designs.com/" target="_blank">Prism Information License</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const roomId = '{{.RoomID}}';
|
||||||
|
const username = '{{.Username}}';
|
||||||
|
const ws = new WebSocket('ws://' + window.location.host + '/ws?room=' + roomId + '&username=' + encodeURIComponent(username));
|
||||||
|
|
||||||
|
document.getElementById('inviteLink').textContent = window.location.origin + '/room/' + roomId;
|
||||||
|
|
||||||
|
function copyInvite() {
|
||||||
|
navigator.clipboard.writeText(window.location.origin + '/room/' + roomId);
|
||||||
|
alert('Invite link copied!');
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (msg.type === 'history') {
|
||||||
|
displayHistory(msg.data);
|
||||||
|
} else if (msg.type === 'roll') {
|
||||||
|
addRoll(msg.data);
|
||||||
|
} else if (msg.type === 'user_joined') {
|
||||||
|
addNotification(msg.username + ' joined the room');
|
||||||
|
} else if (msg.type === 'user_left') {
|
||||||
|
addNotification(msg.username + ' left the room');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function rollDice() {
|
||||||
|
const modifier = parseInt(document.getElementById('modifier').value) || 0;
|
||||||
|
ws.send(JSON.stringify({ type: 'roll', data: { modifier: modifier } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayHistory(rolls) {
|
||||||
|
const container = document.getElementById('rollHistory');
|
||||||
|
container.innerHTML = '';
|
||||||
|
rolls.forEach(roll => addRoll(roll));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRoll(roll) {
|
||||||
|
const container = document.getElementById('rollHistory');
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'roll-item';
|
||||||
|
|
||||||
|
const diceSum = roll.dice.reduce((a, b) => a + b, 0);
|
||||||
|
const diceHTML = roll.dice.map(d =>
|
||||||
|
'<div class="die ' + (d > 0 ? 'plus' : d < 0 ? 'minus' : 'zero') + '">' +
|
||||||
|
(d > 0 ? '+' : d < 0 ? '−' : '0') + '</div>'
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const modifierText = roll.modifier !== 0 ?
|
||||||
|
' ' + (roll.modifier > 0 ? '+' : '') + roll.modifier : '';
|
||||||
|
|
||||||
|
item.innerHTML =
|
||||||
|
'<div class="roll-header">' +
|
||||||
|
'<strong>' + roll.username + '</strong>' +
|
||||||
|
'<span>' + new Date(roll.timestamp).toLocaleTimeString() + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="dice">' + diceHTML + '</div>' +
|
||||||
|
'<div>Result: ' + diceSum + modifierText + '</div>' +
|
||||||
|
'<div class="total">Total: ' + roll.total + '</div>';
|
||||||
|
|
||||||
|
container.insertBefore(item, container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNotification(text) {
|
||||||
|
const container = document.getElementById('rollHistory');
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'notification';
|
||||||
|
item.textContent = text;
|
||||||
|
container.insertBefore(item, container.firstChild);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
RoomID string
|
||||||
|
Username string
|
||||||
|
}{
|
||||||
|
RoomID: roomID,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCreateRoom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
roomID := generateRoomID()
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"roomId": roomID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
roomID := r.URL.Query().Get("room")
|
||||||
|
username := r.URL.Query().Get("username")
|
||||||
|
|
||||||
|
if roomID == "" || username == "" {
|
||||||
|
http.Error(w, "Missing parameters", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
room := getOrCreateRoom(roomID)
|
||||||
|
user := &User{
|
||||||
|
Username: username,
|
||||||
|
Conn: conn,
|
||||||
|
Room: room,
|
||||||
|
}
|
||||||
|
|
||||||
|
room.addUser(user)
|
||||||
|
defer func() {
|
||||||
|
room.removeUser(user)
|
||||||
|
conn.Close()
|
||||||
|
deleteRoomIfEmpty(roomID)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var msg Message
|
||||||
|
err := conn.ReadJSON(&msg)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type == "roll" {
|
||||||
|
modifier := 0
|
||||||
|
if m, ok := msg.Data.(map[string]interface{}); ok {
|
||||||
|
if mod, ok := m["modifier"].(float64); ok {
|
||||||
|
modifier = int(mod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dice := rollFateDice()
|
||||||
|
sum := 0
|
||||||
|
for _, d := range dice {
|
||||||
|
sum += d
|
||||||
|
}
|
||||||
|
|
||||||
|
result := RollResult{
|
||||||
|
Username: username,
|
||||||
|
Dice: dice,
|
||||||
|
Modifier: modifier,
|
||||||
|
Total: sum + modifier,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
room.mu.Lock()
|
||||||
|
room.History = append(room.History, result)
|
||||||
|
room.mu.Unlock()
|
||||||
|
|
||||||
|
room.broadcast(Message{
|
||||||
|
Type: "roll",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", handleHome)
|
||||||
|
http.HandleFunc("/room/", handleRoom)
|
||||||
|
http.HandleFunc("/api/create-room", handleCreateRoom)
|
||||||
|
http.HandleFunc("/ws", handleWebSocket)
|
||||||
|
|
||||||
|
log.Println("Server starting on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module go-fate
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require github.com/gorilla/websocket v1.5.3
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
41
run.sh
Executable file
41
run.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Define variables for your image and container name.
|
||||||
|
IMAGE_NAME="localhost/gofudge:latest"
|
||||||
|
CONTAINER_NAME="gofudge"
|
||||||
|
|
||||||
|
# Step 1: Build the new container image.
|
||||||
|
echo "Building new image: $IMAGE_NAME..."
|
||||||
|
podman build --force-rm -t "$IMAGE_NAME" .
|
||||||
|
|
||||||
|
# Check if the build was successful before proceeding.
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Image build failed. Exiting script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Check if the container is already running.
|
||||||
|
if podman container exists "$CONTAINER_NAME"; then
|
||||||
|
echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..."
|
||||||
|
# Step 3: Stop the existing container.
|
||||||
|
podman stop "$CONTAINER_NAME"
|
||||||
|
# Step 4: Remove the stopped container.
|
||||||
|
podman rm "$CONTAINER_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 5: Run a new container from the newly built image.
|
||||||
|
echo "Starting new container from image: $IMAGE_NAME..."
|
||||||
|
podman run -d --name "$CONTAINER_NAME" --memory=75m --restart unless-stopped -p 12007:8080 "$IMAGE_NAME"
|
||||||
|
|
||||||
|
# Check if the new container started successfully.
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to start new container. Exiting script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 6: Clean up old, unused images.
|
||||||
|
# This command removes any images that are not being used by a container.
|
||||||
|
echo "Cleaning up old images..."
|
||||||
|
podman image prune --force
|
||||||
|
|
||||||
|
echo "Update and cleanup complete!"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue