Merge pull request 'Full packaged app' (#1) from dev/app-done into main
Reviewed-on: #1
This commit is contained in:
commit
fecfb72765
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