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.
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -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/
|
||||||
@@ -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
|
||||||
@@ -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 }}"
|
||||||
@@ -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
@@ -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"]
|
||||||
@@ -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)**
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+5
-5
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: game-assets-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
@@ -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