Add Docker, Kubernetes configurations, and CI workflows for deployment. Integrate Gin server for API, WebSocket support, and static file hosting. Refactor WebSocket gateway to use Gin router.
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
35
.dockerignore
Normal file
35
.dockerignore
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
server
|
||||||
|
client
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Development
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Dependencies (werden neu gebaut)
|
||||||
|
vendor/
|
||||||
68
.github/workflows/cleanup.yaml
vendored
Normal file
68
.github/workflows/cleanup.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
name: Cleanup Environment
|
||||||
|
on: [delete]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Nur ausführen, wenn ein Branch gelöscht wurde (keine Tags)
|
||||||
|
if: github.event.ref_type == 'branch'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1. Variablen berechnen (MIT FIX FÜR REFS/HEADS & MAIN-CHECK)
|
||||||
|
- name: Prepare Variables
|
||||||
|
run: |
|
||||||
|
# Repo Name klein (z.B. "it232abschied")
|
||||||
|
REPO_LOWER=$(echo "${{ gitea.repository }}" | cut -d'/' -f2 | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
# Branch Name aus Event (z.B. "refs/heads/feature-x")
|
||||||
|
RAW_REF="${{ github.event.ref }}"
|
||||||
|
# "refs/heads/" entfernen
|
||||||
|
BRANCH_CLEAN=${RAW_REF#refs/heads/}
|
||||||
|
# Kleinschreiben & Sonderzeichen
|
||||||
|
BRANCH_LOWER=$(echo "$BRANCH_CLEAN" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
|
||||||
|
|
||||||
|
# Logik synchron zum Deploy:
|
||||||
|
# Main/Master -> Namespace ist nur der Repo-Name
|
||||||
|
# Anderes -> Namespace ist Repo-Branch
|
||||||
|
if [ "$BRANCH_LOWER" = "main" ] || [ "$BRANCH_LOWER" = "master" ]; then
|
||||||
|
TARGET_NS="${REPO_LOWER}"
|
||||||
|
IS_MAIN="true"
|
||||||
|
else
|
||||||
|
TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}"
|
||||||
|
IS_MAIN="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DEBUG: Clean Branch: $BRANCH_LOWER"
|
||||||
|
echo "DEBUG: Target NS: $TARGET_NS"
|
||||||
|
|
||||||
|
echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV
|
||||||
|
echo "IS_MAIN=$IS_MAIN" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 2. Sicherheits-Check: Niemals Main/Master löschen!
|
||||||
|
# Wir prüfen jetzt die Variable IS_MAIN, statt den Namen hart zu codieren
|
||||||
|
- name: Protect Main
|
||||||
|
if: env.IS_MAIN == 'true'
|
||||||
|
run: |
|
||||||
|
echo "❌ ABBRUCH: Der Produktions-Namespace ${{ env.TARGET_NS }} darf nicht gelöscht werden!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# 3. Kubectl einrichten
|
||||||
|
- name: Setup Kubectl
|
||||||
|
run: |
|
||||||
|
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl /usr/local/bin/
|
||||||
|
|
||||||
|
mkdir -p $HOME/.kube
|
||||||
|
echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
|
||||||
|
chmod 600 $HOME/.kube/config
|
||||||
|
|
||||||
|
# Der Trick für interne Kommunikation
|
||||||
|
sed -i 's|server: https://.*:6443|server: https://kubernetes.default.svc:443|g' $HOME/.kube/config
|
||||||
|
|
||||||
|
# 4. Namespace löschen
|
||||||
|
- name: Delete Namespace
|
||||||
|
run: |
|
||||||
|
echo "🗑️ Lösche Namespace: ${{ env.TARGET_NS }}"
|
||||||
|
# Wir löschen den Namespace ohne zu warten (async), das geht schneller
|
||||||
|
kubectl delete namespace ${{ env.TARGET_NS }} --ignore-not-found --wait=false
|
||||||
128
.github/workflows/deploy.yaml
vendored
Normal file
128
.github/workflows/deploy.yaml
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
name: Dynamic Branch Deploy
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.zb-server.de
|
||||||
|
# WICHTIG: Deine echte Haupt-Domain
|
||||||
|
BASE_DOMAIN: escape-from-school.de
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# 1. Code auschecken
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# 2. Variablen vorbereiten (MIT HAUPT-DOMAIN LOGIK)
|
||||||
|
- name: Prepare Environment Variables
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
# 1. Repo und Branch Namen säubern
|
||||||
|
# Voller Pfad für Docker Image (z.B. "user/escape-teacher")
|
||||||
|
FULL_IMAGE_PATH=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
# Nur der Projektname für K8s (z.B. "escape-teacher")
|
||||||
|
REPO_LOWER=$(echo "$FULL_IMAGE_PATH" | cut -d'/' -f2)
|
||||||
|
|
||||||
|
# Branch Name säubern (Sonderzeichen zu Bindestrichen)
|
||||||
|
BRANCH_LOWER=$(echo "${{ gitea.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
|
||||||
|
|
||||||
|
# 2. Logik: Ist es der Haupt-Branch?
|
||||||
|
if [ "$BRANCH_LOWER" = "main" ] || [ "$BRANCH_LOWER" = "master" ]; then
|
||||||
|
# PRODUKTION:
|
||||||
|
# URL ist direkt die Domain (ohne Subdomain)
|
||||||
|
APP_URL="${{ env.BASE_DOMAIN }}"
|
||||||
|
# Namespace ist nur der Projektname (ohne Branch-Suffix)
|
||||||
|
TARGET_NS="${REPO_LOWER}"
|
||||||
|
echo "Mode: PRODUCTION (Root Domain)"
|
||||||
|
else
|
||||||
|
# ENTWICKLUNG:
|
||||||
|
# URL ist repo-branch.domain.de
|
||||||
|
APP_URL="${REPO_LOWER}-${BRANCH_LOWER}.${{ env.BASE_DOMAIN }}"
|
||||||
|
# Namespace ist repo-branch
|
||||||
|
TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}"
|
||||||
|
echo "Mode: DEVELOPMENT (Subdomain)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Image Tag (Commit Hash)
|
||||||
|
IMAGE_TAG="${{ gitea.sha }}"
|
||||||
|
|
||||||
|
# Debug Ausgabe
|
||||||
|
echo "DEBUG: Branch: $BRANCH_LOWER"
|
||||||
|
echo "DEBUG: Namespace: $TARGET_NS"
|
||||||
|
echo "DEBUG: URL: $APP_URL"
|
||||||
|
|
||||||
|
# In Gitea Actions Environment schreiben
|
||||||
|
echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV
|
||||||
|
echo "REPO_NAME=$REPO_LOWER" >> $GITHUB_ENV
|
||||||
|
echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV
|
||||||
|
echo "APP_URL=$APP_URL" >> $GITHUB_ENV
|
||||||
|
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 3. Kaniko Build
|
||||||
|
- name: Build and Push with Kaniko
|
||||||
|
uses: aevea/action-kaniko@v0.12.0
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
image: ${{ env.FULL_IMAGE_PATH }}
|
||||||
|
tag: ${{ env.IMAGE_TAG }}
|
||||||
|
cache: true
|
||||||
|
extra_args: --skip-tls-verify-pull --insecure
|
||||||
|
|
||||||
|
# 4. Setup Kubectl (Interner Trick)
|
||||||
|
- name: Setup Kubectl
|
||||||
|
run: |
|
||||||
|
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl /usr/local/bin/
|
||||||
|
|
||||||
|
mkdir -p $HOME/.kube
|
||||||
|
echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
|
||||||
|
chmod 600 $HOME/.kube/config
|
||||||
|
|
||||||
|
# Internal DNS Trick (für Kommunikation innerhalb des Clusters)
|
||||||
|
sed -i 's|server: https://.*:6443|server: https://kubernetes.default.svc:443|g' $HOME/.kube/config
|
||||||
|
|
||||||
|
# 5. Deploy to Kubernetes
|
||||||
|
- name: Deploy to Kubernetes
|
||||||
|
run: |
|
||||||
|
# Namespace erstellen (falls nicht existiert)
|
||||||
|
kubectl create namespace ${{ env.TARGET_NS }} --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
# Vollen Image Pfad bauen
|
||||||
|
FULL_IMAGE_URL="${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.IMAGE_TAG }}"
|
||||||
|
|
||||||
|
# 1. Ingress anpassen (Hier wird die URL eingesetzt!)
|
||||||
|
sed -i "s|\${APP_URL}|${{ env.APP_URL }}|g" k8s/ingress.yaml
|
||||||
|
|
||||||
|
# 2. App Deployment anpassen (Image)
|
||||||
|
sed -i "s|\${IMAGE_NAME}|$FULL_IMAGE_URL|g" k8s/app.yaml
|
||||||
|
|
||||||
|
# Anwenden
|
||||||
|
echo "Deploying Resources to Namespace: ${{ env.TARGET_NS }}"
|
||||||
|
kubectl apply -f k8s/pvc.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
kubectl apply -f k8s/redis.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
kubectl apply -f k8s/app.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
kubectl apply -f k8s/ingress.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
|
||||||
|
# HPA (Autoscaling) nur für Main/Master Branch aktivieren
|
||||||
|
# Wir vergleichen den Namespace mit dem Repo-Namen
|
||||||
|
# Wenn Namespace == RepoName, dann sind wir im Main Branch
|
||||||
|
if [ "${{ env.TARGET_NS }}" == "${{ env.REPO_NAME }}" ]; then
|
||||||
|
echo "Main Branch detected: Applying HPA (Autoscaling)..."
|
||||||
|
kubectl apply -f k8s/hpa.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
else
|
||||||
|
echo "Feature Branch: Skipping HPA."
|
||||||
|
# Optional: HPA löschen, falls es versehentlich da ist
|
||||||
|
kubectl delete hpa escape-game-hpa -n ${{ env.TARGET_NS }} --ignore-not-found
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Force Update (damit das neue Image sicher geladen wird)
|
||||||
|
kubectl rollout restart deployment/escape-game -n ${{ env.TARGET_NS }}
|
||||||
|
|
||||||
|
# 6. Summary
|
||||||
|
- name: Summary
|
||||||
|
run: echo "🚀 Deployed successfully to https://${{ env.APP_URL }}"
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -51,14 +51,6 @@ Thumbs.db
|
|||||||
.env
|
.env
|
||||||
config.local.json
|
config.local.json
|
||||||
|
|
||||||
# Generated assets (if you want to regenerate them)
|
|
||||||
# Uncomment if assets should be generated, not committed
|
|
||||||
# /cmd/client/web/assets/*.png
|
|
||||||
# /cmd/client/web/assets/assets.json
|
|
||||||
|
|
||||||
# Keep chunks but ignore generated ones if needed
|
|
||||||
# /cmd/client/web/assets/chunks/*.json
|
|
||||||
|
|
||||||
# Debug files
|
# Debug files
|
||||||
debug
|
debug
|
||||||
__debug_bin
|
__debug_bin
|
||||||
|
|||||||
45
Dockerfile
Normal file
45
Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Stage 1: Builder
|
||||||
|
FROM golang:1.25.5-alpine AS builder
|
||||||
|
|
||||||
|
# Build-Dependencies
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Dependencies cachen
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Source Code kopieren
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Server binary bauen
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o server ./cmd/server
|
||||||
|
|
||||||
|
# WASM Client bauen
|
||||||
|
RUN GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client
|
||||||
|
|
||||||
|
# Stage 2: Production Image
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates curl tzdata && \
|
||||||
|
addgroup -g 1000 appuser && \
|
||||||
|
adduser -D -u 1000 -G appuser appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Binary und Web-Dateien vom Builder kopieren
|
||||||
|
COPY --from=builder /app/server .
|
||||||
|
COPY --from=builder /app/cmd/client/web ./cmd/client/web
|
||||||
|
|
||||||
|
# User wechseln
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Port für HTTP/WebSocket
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
296
README.md
Normal file
296
README.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# 🏃 Escape From Teacher
|
||||||
|
|
||||||
|
Ein Endless-Runner-Spiel entwickelt als Schulprojekt. Renne vor dem Lehrer davon, sammle Münzen, nutze Power-Ups und klettere an Wänden!
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 📋 Inhaltsverzeichnis
|
||||||
|
|
||||||
|
- [Features](#-features)
|
||||||
|
- [Technologie-Stack](#-technologie-stack)
|
||||||
|
- [Installation](#-installation)
|
||||||
|
- [Spielmodi](#-spielmodi)
|
||||||
|
- [Entwicklung](#-entwicklung)
|
||||||
|
- [Architektur](#-architektur)
|
||||||
|
- [Tools](#-tools)
|
||||||
|
- [Lizenz](#-lizenz)
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### Gameplay
|
||||||
|
- 🏃 **Endless Runner**: Unendliches Spiel mit prozedural generierten Levels
|
||||||
|
- 🧗 **Wall Climbing**: Klettere an Wänden hoch für neue Strategien
|
||||||
|
- 💰 **Coins & Power-Ups**: Sammle Münzen und nutze Power-Ups wie Double Jump und Godmode
|
||||||
|
- 🎯 **Hindernisse**: Weiche bewegenden Plattformen und Objekten aus
|
||||||
|
- ⚡ **Dynamische Schwierigkeit**: Das Spiel wird mit der Zeit schneller
|
||||||
|
|
||||||
|
### Modi
|
||||||
|
- 👤 **Solo-Modus**: Spiele alleine und sammle Highscores
|
||||||
|
- 👥 **Coop-Modus**: Spiele mit Freunden im Team
|
||||||
|
- Team-Namen für Leaderboard
|
||||||
|
- Multiplizierter Score basierend auf lebenden Spielern
|
||||||
|
- Host-Controls für Lobby-Management
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- 🏆 **Leaderboard**: Redis-basiertes globales Leaderboard mit Proof-Codes
|
||||||
|
- 🎮 **Cross-Platform**: Web (WASM) und Desktop (Native)
|
||||||
|
- 🎨 **Level Editor**: Erstelle eigene Levels mit dem integrierten Level Builder
|
||||||
|
- 🔊 **Audio**: Musik und Sound-Effekte mit Lautstärkeregelung
|
||||||
|
- 📱 **Mobile Support**: Touch-Controls für mobile Geräte
|
||||||
|
|
||||||
|
## 🛠 Technologie-Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Go 1.25+**: Server und Game-Logik
|
||||||
|
- **NATS**: Message Broker für Echtzeit-Kommunikation (Legacy)
|
||||||
|
- **WebSocket**: Direkte WebSocket-Verbindungen für niedrige Latenz
|
||||||
|
- **Redis**: Persistentes Leaderboard mit Proof-Code-Validierung
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Go + Ebiten**: Desktop-Client (Native)
|
||||||
|
- **Go + WASM**: Web-Client kompiliert zu WebAssembly
|
||||||
|
- **HTML/CSS/JavaScript**: Web-UI und Menüs
|
||||||
|
|
||||||
|
### Architektur
|
||||||
|
- **Client-Side Prediction**: Flüssiges Gameplay trotz Netzwerk-Latenz
|
||||||
|
- **Server-Authoritative**: Server validiert alle Spielaktionen
|
||||||
|
- **Deterministische Physik**: Gleiche Physik auf Client und Server
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go 1.25 oder höher
|
||||||
|
go version
|
||||||
|
|
||||||
|
# Redis für Leaderboard
|
||||||
|
redis-server --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository klonen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.zb-server.de/ZB-Server/EscapeFromTeacher.git
|
||||||
|
cd EscapeFromTeacher
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assets generieren
|
||||||
|
|
||||||
|
Die Assets müssen aus den Raw-Dateien generiert werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Asset-Builder starten (GUI)
|
||||||
|
go run ./cmd/builder
|
||||||
|
|
||||||
|
# Im GUI: Assets laden, anpassen und speichern
|
||||||
|
# Dies erstellt cmd/client/web/assets/assets.json und die PNG-Dateien
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redis starten (Terminal 1)
|
||||||
|
redis-server
|
||||||
|
|
||||||
|
# Server starten (Terminal 2)
|
||||||
|
go run ./cmd/server
|
||||||
|
```
|
||||||
|
|
||||||
|
Server läuft auf `http://localhost:8080`
|
||||||
|
|
||||||
|
### Clients starten
|
||||||
|
|
||||||
|
#### Web-Client (WASM)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WASM kompilieren
|
||||||
|
GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client
|
||||||
|
|
||||||
|
# Web-Server starten (z.B. mit Python)
|
||||||
|
cd cmd/client/web
|
||||||
|
python3 -m http.server 8000
|
||||||
|
|
||||||
|
# Browser öffnen: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desktop-Client (Native)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/client
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Spielmodi
|
||||||
|
|
||||||
|
### Solo-Modus
|
||||||
|
- Starte direkt aus dem Hauptmenü
|
||||||
|
- Automatischer Room-Erstellen
|
||||||
|
- Score basiert auf zurückgelegter Distanz
|
||||||
|
- Eigener Highscore im Leaderboard
|
||||||
|
|
||||||
|
### Coop-Modus
|
||||||
|
1. **Raum erstellen** (Host):
|
||||||
|
- "RAUM ERSTELLEN" klicken
|
||||||
|
- Team-Namen festlegen (optional)
|
||||||
|
- Raum-Code mit Freunden teilen
|
||||||
|
|
||||||
|
2. **Raum beitreten**:
|
||||||
|
- Raum-Code eingeben
|
||||||
|
- "RAUM BEITRETEN" klicken
|
||||||
|
|
||||||
|
3. **Gameplay**:
|
||||||
|
- Host startet das Spiel wenn alle bereit sind
|
||||||
|
- Team-Score = Distanz × Anzahl lebender Spieler
|
||||||
|
- Beispiel: 100 Tiles mit 3 Spielern = 300 Punkte pro Spieler
|
||||||
|
|
||||||
|
### Steuerung
|
||||||
|
|
||||||
|
**Desktop:**
|
||||||
|
- `Leertaste` / `W` / `↑`: Springen
|
||||||
|
- `A` / `←`: Links bewegen
|
||||||
|
- `D` / `→`: Rechts bewegen
|
||||||
|
- `S` / `↓`: Schneller fallen
|
||||||
|
|
||||||
|
**Mobile:**
|
||||||
|
- Touch-Buttons auf dem Bildschirm
|
||||||
|
|
||||||
|
**Wall Climbing:**
|
||||||
|
- Laufe gegen eine Wand
|
||||||
|
- Halte Richtungstaste gedrückt zum Hochklettern
|
||||||
|
- Lasse los zum Herunterrutschen
|
||||||
|
|
||||||
|
## 🔧 Entwicklung
|
||||||
|
|
||||||
|
### Projekt-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
EscapeFromTeacher/
|
||||||
|
├── cmd/
|
||||||
|
│ ├── server/ # Game Server
|
||||||
|
│ ├── client/ # Desktop & WASM Client
|
||||||
|
│ │ └── web/ # Web Assets & HTML
|
||||||
|
│ ├── builder/ # Asset Builder Tool
|
||||||
|
│ └── levelbuilder/ # Level Editor
|
||||||
|
├── pkg/
|
||||||
|
│ ├── game/ # Gemeinsame Game-Logik
|
||||||
|
│ ├── server/ # Server-spezifische Logik
|
||||||
|
│ └── config/ # Konfiguration
|
||||||
|
└── assets_raw/ # Rohe Asset-Dateien
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neue Features entwickeln
|
||||||
|
|
||||||
|
1. **Server-Logik** in `pkg/server/`
|
||||||
|
2. **Game-Logik** (gemeinsam) in `pkg/game/`
|
||||||
|
3. **Client-Rendering** in `cmd/client/`
|
||||||
|
4. **Web-UI** in `cmd/client/web/`
|
||||||
|
|
||||||
|
### Build-Befehle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server
|
||||||
|
go build -o server ./cmd/server
|
||||||
|
|
||||||
|
# Desktop-Client
|
||||||
|
go build -o client ./cmd/client
|
||||||
|
|
||||||
|
# WASM-Client
|
||||||
|
GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client
|
||||||
|
|
||||||
|
# Level Builder
|
||||||
|
go build -o levelbuilder ./cmd/levelbuilder
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧰 Tools
|
||||||
|
|
||||||
|
### Level Builder
|
||||||
|
|
||||||
|
Erstelle eigene Levels mit dem visuellen Editor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/levelbuilder
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Drag & Drop von Assets
|
||||||
|
- Bewegende Plattformen konfigurieren
|
||||||
|
- Chunk-basiertes Level-Design
|
||||||
|
- JSON-Export in `cmd/client/web/assets/chunks/`
|
||||||
|
|
||||||
|
### Asset Builder
|
||||||
|
|
||||||
|
Konvertiere Raw-Assets zu optimierten Spiel-Assets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/builder
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- PNG-Kompression und -Skalierung
|
||||||
|
- Hitbox-Editor
|
||||||
|
- Asset-Metadaten (Typ, Offsets, etc.)
|
||||||
|
- Generiert `assets.json`
|
||||||
|
|
||||||
|
## 📐 Architektur
|
||||||
|
|
||||||
|
### Client-Server Kommunikation
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server
|
||||||
|
| |
|
||||||
|
|------ WebSocket ------->|
|
||||||
|
| (Input Events) |
|
||||||
|
| |
|
||||||
|
|<----- Broadcast --------|
|
||||||
|
| (Game State) |
|
||||||
|
| |
|
||||||
|
|-- Score Submission ---->|
|
||||||
|
| |
|
||||||
|
|<--- Proof Code ---------|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Game Loop
|
||||||
|
|
||||||
|
**Server (60 FPS):**
|
||||||
|
1. Verarbeite Inputs von allen Clients
|
||||||
|
2. Update Physik (Gravity, Kollision, etc.)
|
||||||
|
3. Update Map (Chunk spawning/despawning)
|
||||||
|
4. Broadcast Game State an alle Clients
|
||||||
|
|
||||||
|
**Client:**
|
||||||
|
1. Sende lokale Inputs an Server
|
||||||
|
2. Empfange Game State vom Server
|
||||||
|
3. Render Game State
|
||||||
|
4. (Optional) Client-Side Prediction für lokalen Spieler
|
||||||
|
|
||||||
|
### Score-System
|
||||||
|
|
||||||
|
**Solo:**
|
||||||
|
- Score = Zurückgelegte Distanz (in Tiles)
|
||||||
|
- Coins: +200 Punkte
|
||||||
|
|
||||||
|
**Coop:**
|
||||||
|
- Distanz-Score akkumuliert pro Tick
|
||||||
|
- Pro Tick: `Score += Anzahl lebender Spieler`
|
||||||
|
- Beispiel: 3 Spieler → +3 Punkte/Tick (180 Punkte/Sekunde)
|
||||||
|
- Coins werden geteilt: Alle Spieler bekommen +200
|
||||||
|
|
||||||
|
## 📄 Lizenz
|
||||||
|
|
||||||
|
Dies ist ein **Schulprojekt**. Kommerzielle Nutzung und Veränderung des Quellcodes sind ausdrücklich untersagt. Alle Rechte liegen bei den Urhebern.
|
||||||
|
|
||||||
|
**Projektleitung & Code:** Sebastian Unterschütz
|
||||||
|
**Musik & Sound Design:** Max E.
|
||||||
|
|
||||||
|
## 🙏 Credits
|
||||||
|
|
||||||
|
- **Ebiten**: Go 2D Game Engine
|
||||||
|
- **NATS**: Message Broker
|
||||||
|
- **Redis**: In-Memory Database
|
||||||
|
- **Go WASM**: WebAssembly Support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Entwickelt als Abschlussprojekt für IT232 (2025/2026)**
|
||||||
88
cmd/server/gin_server.go
Normal file
88
cmd/server/gin_server.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupGinServer erstellt und konfiguriert den Gin-Server
|
||||||
|
func SetupGinServer(ec *nats.EncodedConn, port string) *gin.Engine {
|
||||||
|
// Production mode für bessere Performance
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// Logging Middleware (bereits in gin.Default())
|
||||||
|
// Custom Logger für bessere Übersicht
|
||||||
|
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||||
|
return "🌐 " + param.TimeStamp.Format("2006-01-02 15:04:05") +
|
||||||
|
" | " + param.Method +
|
||||||
|
" | " + string(rune(param.StatusCode)) +
|
||||||
|
" | " + param.Latency.String() +
|
||||||
|
" | " + param.ClientIP +
|
||||||
|
" | " + param.Path + "\n"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Recovery Middleware
|
||||||
|
r.Use(gin.Recovery())
|
||||||
|
|
||||||
|
// Health Check Endpoint
|
||||||
|
r.GET("/health", func(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "EscapeFromTeacher",
|
||||||
|
"version": "1.0.0",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Metrics Endpoint (optional für Kubernetes)
|
||||||
|
r.GET("/metrics", func(c *gin.Context) {
|
||||||
|
mu.RLock()
|
||||||
|
roomCount := len(rooms)
|
||||||
|
playerCount := len(playerSessions)
|
||||||
|
mu.RUnlock()
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"rooms": roomCount,
|
||||||
|
"players": playerCount,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// WebSocket Endpoint
|
||||||
|
r.GET("/ws", func(c *gin.Context) {
|
||||||
|
handleWebSocketGin(c.Writer, c.Request, ec)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Static Files - Serve Web Client
|
||||||
|
r.Static("/assets", "./cmd/client/web/assets")
|
||||||
|
r.StaticFile("/", "./cmd/client/web/index.html")
|
||||||
|
r.StaticFile("/index.html", "./cmd/client/web/index.html")
|
||||||
|
r.StaticFile("/game.js", "./cmd/client/web/game.js")
|
||||||
|
r.StaticFile("/style.css", "./cmd/client/web/style.css")
|
||||||
|
r.StaticFile("/wasm_exec.js", "./cmd/client/web/wasm_exec.js")
|
||||||
|
r.StaticFile("/main.wasm", "./cmd/client/web/main.wasm")
|
||||||
|
r.StaticFile("/background.jpg", "./cmd/client/web/background.jpg")
|
||||||
|
|
||||||
|
// 404 Handler
|
||||||
|
r.NoRoute(func(c *gin.Context) {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "Route not found",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("🚀 Gin-Server konfiguriert auf Port %s", port)
|
||||||
|
log.Printf("📁 Statische Dateien: ./cmd/client/web/")
|
||||||
|
log.Printf("🌐 WebSocket Endpoint: /ws")
|
||||||
|
log.Printf("❤️ Health Check: /health")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebSocketGin verwaltet WebSocket-Verbindungen über Gin
|
||||||
|
func handleWebSocketGin(w http.ResponseWriter, r *http.Request, ec *nats.EncodedConn) {
|
||||||
|
// Verwende die handleWebSocket Funktion aus websocket_gateway.go
|
||||||
|
handleWebSocket(w, r, ec)
|
||||||
|
}
|
||||||
@@ -172,11 +172,11 @@ func main() {
|
|||||||
|
|
||||||
log.Println("✅ Server bereit. Warte auf Spieler...")
|
log.Println("✅ Server bereit. Warte auf Spieler...")
|
||||||
|
|
||||||
// 9. WEBSOCKET-GATEWAY STARTEN (für Browser-Clients)
|
// 9. GIN-SERVER STARTEN (statische Dateien + WebSocket)
|
||||||
go StartWebSocketGateway("8080", ec)
|
router := SetupGinServer(ec, "8080")
|
||||||
|
if err := router.Run(":8080"); err != nil {
|
||||||
// Block forever
|
log.Fatal("❌ Gin-Server Fehler:", err)
|
||||||
select {}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadServerAssets(w *game.World) {
|
func loadServerAssets(w *game.World) {
|
||||||
|
|||||||
72
k8s/app.yaml
Normal file
72
k8s/app.yaml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: escape-game
|
||||||
|
labels:
|
||||||
|
app: escape-game
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: escape-game
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: escape-game
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: server
|
||||||
|
image: ${IMAGE_NAME}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "250m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "1000m"
|
||||||
|
volumeMounts:
|
||||||
|
- name: assets
|
||||||
|
mountPath: /root/cmd/client/web/assets
|
||||||
|
volumes:
|
||||||
|
- name: assets
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: game-assets-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: escape-game
|
||||||
|
labels:
|
||||||
|
app: escape-game
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: escape-game
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
sessionAffinity: ClientIP
|
||||||
|
sessionAffinityConfig:
|
||||||
|
clientIP:
|
||||||
|
timeoutSeconds: 3600
|
||||||
24
k8s/hpa.yaml
Normal file
24
k8s/hpa.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: escape-game-hpa
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: escape-game
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 10
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
25
k8s/ingress.yaml
Normal file
25
k8s/ingress.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: game-ingress
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web, websecure
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: gitea-redirect-https@kubernetescrd
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- ${APP_URL}
|
||||||
|
secretName: game-tls-secret
|
||||||
|
rules:
|
||||||
|
- host: ${APP_URL}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: escape-game
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
11
k8s/pvc.yaml
Normal file
11
k8s/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: game-assets-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
75
k8s/redis.yaml
Normal file
75
k8s/redis.yaml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: redis-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 6379
|
||||||
|
selector:
|
||||||
|
app: redis
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
labels:
|
||||||
|
app: redis
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: redis
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: redis
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: fix-permissions
|
||||||
|
image: busybox
|
||||||
|
command: ["sh", "-c", "chown -R 999:999 /data"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m" # 0.05 CPU Cores
|
||||||
|
limits:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "1000m" # 0.5 CPU Cores
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["redis-cli", "ping"]
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["redis-cli", "ping"]
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: redis-data
|
||||||
Reference in New Issue
Block a user