Private
Public Access
1
0

fix game
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Failing after 8m17s

This commit is contained in:
Sebastian Unterschütz
2026-03-21 20:32:53 +01:00
parent f7df54431c
commit 188e9c2cc2
6 changed files with 420 additions and 101 deletions

View File

@@ -10,9 +10,11 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# 1. Code auschecken
# 1. Code auschecken (fetch-depth: 2 für git diff gegen den vorherigen Commit)
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 2
# 2. Variablen vorbereiten (MIT HAUPT-DOMAIN LOGIK)
- name: Prepare Environment Variables
@@ -21,37 +23,33 @@ jobs:
# 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 (Commit Hash) und stabiler Branch-Tag
IMAGE_TAG="${{ gitea.sha }}"
BRANCH_TAG="${TARGET_NS}-latest"
# Debug Ausgabe
echo "DEBUG: Branch: $BRANCH_LOWER"
echo "DEBUG: Namespace: $TARGET_NS"
echo "DEBUG: URL: $APP_URL"
echo "DEBUG: Branch-Tag: $BRANCH_TAG"
# In Gitea Actions Environment schreiben
echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV
@@ -59,9 +57,34 @@ jobs:
echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV
echo "APP_URL=$APP_URL" >> $GITHUB_ENV
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
echo "BRANCH_TAG=$BRANCH_TAG" >> $GITHUB_ENV
# 3. Kaniko Build
# 3. Prüfen ob ein Image-Rebuild nötig ist
- name: Detect Source Changes
run: |
# Beim allerersten Commit gibt es kein HEAD~1 → immer bauen
if ! git rev-parse HEAD~1 >/dev/null 2>&1; then
echo "NEEDS_BUILD=true" >> $GITHUB_ENV
echo "→ Erster Commit, Image wird gebaut"
exit 0
fi
CHANGED=$(git diff --name-only HEAD~1 HEAD)
echo "Geänderte Dateien:"
echo "$CHANGED"
# Rebuild wenn Source-Code, Dockerfile oder Assets geändert wurden
if echo "$CHANGED" | grep -qE '(^Dockerfile$|^go\.(mod|sum)$|^(cmd|pkg|assets_raw)/)'; then
echo "NEEDS_BUILD=true" >> $GITHUB_ENV
echo "→ Quelldateien geändert Image wird neu gebaut"
else
echo "NEEDS_BUILD=false" >> $GITHUB_ENV
echo "→ Keine Quelländerungen Image-Build wird übersprungen"
fi
# 4. Image bauen und pushen (NUR wenn NEEDS_BUILD=true)
- name: Build and Push with Kaniko
if: env.NEEDS_BUILD == 'true'
uses: aevea/action-kaniko@v0.12.0
with:
registry: ${{ env.REGISTRY }}
@@ -72,7 +95,34 @@ jobs:
cache: true
extra_args: --skip-tls-verify-pull --insecure
# 4. Setup Kubectl (Interner Trick)
# 5. Stabilen Branch-Tag aktualisieren (NUR nach erfolgreichem Build)
# Damit weiß der nächste Nicht-Build-Push welches Image er verwenden soll.
- name: Tag as branch-latest
if: env.NEEDS_BUILD == 'true'
run: |
skopeo copy \
--src-creds="${{ gitea.actor }}:${{ secrets.PACKAGE_TOKEN }}" \
--dest-creds="${{ gitea.actor }}:${{ secrets.PACKAGE_TOKEN }}" \
--src-tls-verify=false \
--dest-tls-verify=false \
"docker://${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.IMAGE_TAG }}" \
"docker://${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.BRANCH_TAG }}"
# 6. Deploy-Image festlegen
# - Build passiert: SHA-Tag verwenden (exakt dieser Stand)
# - Build übersprungen: Branch-Latest-Tag verwenden (letztes gebautes Image)
- name: Set Deploy Image
run: |
if [ "$NEEDS_BUILD" = "true" ]; then
DEPLOY_IMAGE="${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.IMAGE_TAG }}"
echo "→ Deploy mit neuem Image: $DEPLOY_IMAGE"
else
DEPLOY_IMAGE="${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.BRANCH_TAG }}"
echo "→ Deploy mit bestehendem Image: $DEPLOY_IMAGE"
fi
echo "DEPLOY_IMAGE=$DEPLOY_IMAGE" >> $GITHUB_ENV
# 7. Setup Kubectl
- 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"
@@ -82,49 +132,51 @@ jobs:
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
# 8. 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!)
# Ingress und App-Manifest anpassen
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
sed -i "s|\${IMAGE_NAME}|${{ env.DEPLOY_IMAGE }}|g" k8s/app.yaml
# Anwenden
echo "Deploying Resources to Namespace: ${{ env.TARGET_NS }}"
echo "Deploying to Namespace: ${{ env.TARGET_NS }} (Image: ${{ env.DEPLOY_IMAGE }})"
kubectl apply -f k8s/pvc.yaml -n ${{ env.TARGET_NS }}
kubectl apply -f k8s/nats.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/cilium-netpol.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
# HPA nur für Main/Master Branch
if [ "${{ env.TARGET_NS }}" == "${{ env.REPO_NAME }}" ]; then
echo "Main Branch detected: Applying HPA (Autoscaling)..."
echo "Main Branch: Applying HPA..."
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 }}
# Pod nur neu starten wenn ein neues Image gebaut wurde
# Bei reinen k8s-Änderungen reicht kubectl apply, kein Restart nötig
if [ "$NEEDS_BUILD" = "true" ]; then
kubectl rollout restart deployment/escape-game -n ${{ env.TARGET_NS }}
fi
# 6. Summary
# 9. Summary
- name: Summary
run: echo "🚀 Deployed successfully to https://${{ env.APP_URL }}"
run: |
if [ "$NEEDS_BUILD" = "true" ]; then
echo "🔨 Image neu gebaut und deployed: ${{ env.DEPLOY_IMAGE }}"
else
echo "⚡ Image-Build übersprungen (keine Quelländerungen)"
echo "📦 Bestehendes Image verwendet: ${{ env.DEPLOY_IMAGE }}"
fi
echo "🚀 Deployed to https://${{ env.APP_URL }}"

View File

@@ -27,6 +27,22 @@ type WebSocketMessage struct {
Payload interface{} `json:"payload"`
}
// disconnectFromServer trennt die bestehende WebSocket-Verbindung sauber
func (g *Game) disconnectFromServer() {
if g.wsConn != nil {
// stopChan schließen ohne Panic falls schon geschlossen
select {
case <-g.wsConn.stopChan:
// bereits geschlossen
default:
close(g.wsConn.stopChan)
}
g.wsConn.conn.Close()
g.wsConn = nil
}
g.connected = false
}
// connectToServer verbindet sich über WebSocket mit dem Gateway (Native Desktop)
func (g *Game) connectToServer() {
serverURL := "ws://localhost:8080/ws"

View File

@@ -25,6 +25,15 @@ type wsConn struct {
connected bool
}
// disconnectFromServer trennt die bestehende WebSocket-Verbindung sauber
func (g *Game) disconnectFromServer() {
if g.wsConn != nil {
g.wsConn.ws.Call("close")
g.wsConn = nil
}
g.connected = false
}
// connectToServer verbindet sich über WebSocket mit dem Gateway
func (g *Game) connectToServer() {
// Automatisch die richtige WebSocket-URL basierend auf der aktuellen Domain

View File

@@ -39,27 +39,27 @@ func (g *Game) UpdateGame() {
// --- 4. INPUT STATE ERSTELLEN ---
joyDir := 0.0
if g.joyActive {
maxDist := g.joyRadius
if maxDist == 0 {
maxDist = 60.0 // Fallback
}
diffX := g.joyStickX - g.joyBaseX
maxDist := 60.0 // Muss mit handleTouchInput() übereinstimmen
// Analoger Wert zwischen -1.0 und 1.0
joyDir = diffX / maxDist
// Clamp zwischen -1 und 1
if joyDir < -1.0 {
joyDir = -1.0
}
if joyDir > 1.0 {
joyDir = 1.0
}
// Deadzone für bessere Kontrolle
// Deadzone
if joyDir > -0.15 && joyDir < 0.15 {
joyDir = 0
}
}
isJoyDown := g.joyActive && (g.joyStickY-g.joyBaseY) > 40
// Down: Joystick nach unten ziehen ODER Down-Button
isJoyDown := g.joyActive && (g.joyStickY-g.joyBaseY) > g.joyRadius*0.5
// Input State zusammenbauen
input := InputState{
@@ -67,10 +67,11 @@ func (g *Game) UpdateGame() {
Left: keyLeft || joyDir < -0.1,
Right: keyRight || joyDir > 0.1,
Jump: keyJump || g.btnJumpActive,
Down: keyDown || isJoyDown,
JoyX: joyDir, // Analoge X-Achse speichern
Down: keyDown || isJoyDown || g.btnDownActive,
JoyX: joyDir,
}
g.btnJumpActive = false
g.btnDownActive = false
// --- 4. INPUT SENDEN (MIT CLIENT PREDICTION) ---
if g.connected {
@@ -130,13 +131,32 @@ func (g *Game) UpdateGame() {
func (g *Game) handleTouchInput() {
touches := ebiten.TouchIDs()
// WICHTIG: Joystick-Base-Position wird jetzt beim Rendering gesetzt (DrawGame)
// um sicherzustellen dass die gleiche Canvas-Höhe verwendet wird!
// Wir verwenden hier nur die gecachte Position.
// Linke Hälfte = Joystick-Zone (55% der Breite, damit auf schmalen Screens etwas Platz bleibt)
halfW := float64(g.lastCanvasWidth) * 0.55
if halfW == 0 {
halfW = float64(ScreenWidth) * 0.55 // Fallback
}
// Reset, wenn keine Finger mehr auf dem Display sind
joyRadius := g.joyRadius
if joyRadius == 0 {
joyRadius = 60.0 // Fallback
}
// Vorab: alle gerade neu gedrückten Touch-IDs sammeln
justPressed := inpututil.JustPressedTouchIDs()
isJustPressed := func(id ebiten.TouchID) bool {
for _, j := range justPressed {
if id == j {
return true
}
}
return false
}
// Reset wenn alle Finger weg
if len(touches) == 0 {
g.joyActive = false
g.btnJumpPressed = false
g.joyStickX = g.joyBaseX
g.joyStickY = g.joyBaseY
return
@@ -148,55 +168,56 @@ func (g *Game) handleTouchInput() {
x, y := ebiten.TouchPosition(id)
fx, fy := float64(x), float64(y)
// 1. RECHTE SEITE: JUMP BUTTON
// Alles rechts der Bildschirmmitte ist "Springen"
if fx > ScreenWidth/2 {
// Prüfen, ob dieser Touch gerade NEU dazu gekommen ist
for _, justID := range inpututil.JustPressedTouchIDs() {
if id == justID {
g.btnJumpActive = true
break
if fx >= halfW {
// ── RECHTE SEITE: Jump und Down ──────────────────────────────────────
g.btnJumpPressed = true // visuelles Feedback solange Finger drauf
if isJustPressed(id) {
// Down-Button: Prüfen ob Finger in der Nähe des Down-Buttons
if g.downBtnR > 0 {
dx := fx - g.downBtnX
dy := fy - g.downBtnY
if dx*dx+dy*dy < g.downBtnR*g.downBtnR*1.5 {
g.btnDownActive = true
continue
}
}
// Sonst: Sprung
g.btnJumpActive = true
}
continue
}
// 2. LINKE SEITE: JOYSTICK
// Wenn wir noch keinen Joystick-Finger haben, prüfen wir, ob dieser Finger startet
// ── LINKE SEITE: Floating Joystick ───────────────────────────────────
// Floating = Basis springt zu der Stelle wo der Finger aufsetzt.
// Kein fester Ausgangspunkt nötig → komfortabler auf allen Screen-Größen.
if !g.joyActive {
// Prüfen ob Touch in der Nähe der Joystick-Basis ist (Radius 150 Toleranz)
dist := math.Sqrt(math.Pow(fx-g.joyBaseX, 2) + math.Pow(fy-g.joyBaseY, 2))
if dist < 150 {
g.joyActive = true
g.joyTouchID = id
}
g.joyActive = true
g.joyTouchID = id
g.joyBaseX = fx
g.joyBaseY = fy
g.joyStickX = fx
g.joyStickY = fy
}
// Wenn das der Joystick-Finger ist -> Stick bewegen
if g.joyActive && id == g.joyTouchID {
joyFound = true
// Vektor berechnen (Wie weit ziehen wir weg?)
dx := fx - g.joyBaseX
dy := fy - g.joyBaseY
dist := math.Sqrt(dx*dx + dy*dy)
maxDist := 60.0 // Maximaler Radius des Sticks
// Begrenzen auf Radius
if dist > maxDist {
scale := maxDist / dist
if dist > joyRadius {
scale := joyRadius / dist
dx *= scale
dy *= scale
}
g.joyStickX = g.joyBaseX + dx
g.joyStickY = g.joyBaseY + dy
}
}
// Wenn der Joystick-Finger losgelassen wurde, Joystick resetten
if !joyFound {
g.joyActive = false
g.btnJumpPressed = false
g.joyStickX = g.joyBaseX
g.joyStickY = g.joyBaseY
}
@@ -460,40 +481,74 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
// 8. TOUCH CONTROLS OVERLAY (nur wenn Tastatur nicht benutzt wurde)
if !g.keyboardUsed {
canvasW, canvasH := screen.Size()
tcW, tcH := screen.Size()
// Cache Canvas-Höhe für Touch-Input (wird in UpdateGame/handleTouchInput verwendet)
g.lastCanvasHeight = canvasH
// Canvas-Größe cachen (für handleTouchInput im nächsten Frame)
g.lastCanvasHeight = tcH
g.lastCanvasWidth = tcW
// A) Joystick Base (unten links, über dem Boden positioniert)
// WICHTIG: Verwende die gleiche Berechnung wie in handleTouchInput()!
g.joyBaseX = 150.0
floorY := GetFloorYFromHeight(canvasH)
g.joyBaseY = floorY - 50.0 // 50px über dem Boden (sichtbar über der Erde)
floorY := GetFloorYFromHeight(tcH)
// Wenn Joystick nicht aktiv, Stick = Base
// Proportionale Größen: basiert auf der kleineren Screen-Dimension
refDim := math.Min(float64(tcW), float64(tcH))
joyR := refDim * 0.13 // Joystick-Außenring (13% der kleineren Dimension)
knobR := joyR * 0.48 // Knob ~halber Joystick-Radius
jumpR := refDim * 0.11 // Sprung-Button
downR := refDim * 0.08 // Down-Button (kleiner)
// Werte für Input-Verarbeitung cachen
g.joyRadius = joyR
// ── A) Floating Joystick ─────────────────────────────────────────────
// Wenn inaktiv: Basis an Default-Position (unten links) zeigen
if !g.joyActive {
g.joyBaseX = float64(tcW) * 0.18
g.joyBaseY = floorY - joyR - 12
g.joyStickX = g.joyBaseX
g.joyStickY = g.joyBaseY
}
baseCol := color.RGBA{80, 80, 80, 50}
vector.DrawFilledCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), 60, baseCol, false)
vector.StrokeCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), 60, 2, color.RGBA{100, 100, 100, 100}, false)
// B) Joystick Knob (relativ zu Base)
knobCol := color.RGBA{100, 100, 100, 80}
// Joystick-Ring (halb transparent, nur wenn aktiv sichtbarer)
ringAlpha := uint8(40)
if g.joyActive {
knobCol = color.RGBA{100, 255, 100, 120}
ringAlpha = 70
}
vector.DrawFilledCircle(screen, float32(g.joyStickX), float32(g.joyStickY), 30, knobCol, false)
vector.DrawFilledCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), float32(joyR), color.RGBA{80, 80, 80, ringAlpha}, false)
vector.StrokeCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), float32(joyR), 2, color.RGBA{120, 120, 120, 90}, false)
// C) Jump Button (unten rechts, über dem Boden positioniert)
jumpX := float32(canvasW) - 150
jumpY := float32(floorY) - 50 // 50px über dem Boden
vector.DrawFilledCircle(screen, jumpX, jumpY, 50, color.RGBA{255, 0, 0, 50}, false)
vector.StrokeCircle(screen, jumpX, jumpY, 50, 2, color.RGBA{255, 0, 0, 100}, false)
text.Draw(screen, "JUMP", basicfont.Face7x13, int(jumpX)-15, int(jumpY)+5, color.RGBA{255, 255, 255, 150})
// Joystick-Knob
knobCol := color.RGBA{180, 180, 180, 100}
if g.joyActive {
knobCol = color.RGBA{80, 220, 80, 160}
}
vector.DrawFilledCircle(screen, float32(g.joyStickX), float32(g.joyStickY), float32(knobR), knobCol, false)
// ── B) Jump Button (rechts) ──────────────────────────────────────────
jumpX := float64(tcW)*0.82
jumpY := floorY - jumpR - 12
jumpFill := color.RGBA{220, 50, 50, 60}
jumpStroke := color.RGBA{255, 80, 80, 130}
if g.btnJumpPressed {
jumpFill = color.RGBA{255, 80, 80, 130} // heller wenn gedrückt
jumpStroke = color.RGBA{255, 160, 160, 200}
}
vector.DrawFilledCircle(screen, float32(jumpX), float32(jumpY), float32(jumpR), jumpFill, false)
vector.StrokeCircle(screen, float32(jumpX), float32(jumpY), float32(jumpR), 2.5, jumpStroke, false)
text.Draw(screen, "JUMP", basicfont.Face7x13,
int(jumpX)-14, int(jumpY)+5, color.RGBA{255, 255, 255, 180})
// ── C) Down/FastFall Button (links vom Jump) ─────────────────────────
downX := float64(tcW)*0.62
downY := floorY - downR - 12
g.downBtnX = downX
g.downBtnY = downY
g.downBtnR = downR
vector.DrawFilledCircle(screen, float32(downX), float32(downY), float32(downR), color.RGBA{50, 120, 220, 55}, false)
vector.StrokeCircle(screen, float32(downX), float32(downY), float32(downR), 2, color.RGBA{80, 160, 255, 120}, false)
text.Draw(screen, "▼", basicfont.Face7x13,
int(downX)-4, int(downY)+5, color.RGBA{200, 220, 255, 180})
}
}

View File

@@ -126,9 +126,15 @@ type Game struct {
joyStickX, joyStickY float64
joyActive bool
joyTouchID ebiten.TouchID
joyRadius float64 // Joystick-Radius (skaliert mit Screen)
btnJumpActive bool
keyboardUsed bool // Wurde Tastatur benutzt?
lastCanvasHeight int // Cache der Canvas-Höhe für Touch-Input
btnJumpPressed bool // Jump wird gerade gehalten (visuelles Feedback)
btnDownActive bool // Down/FastFall-Button gedrückt
keyboardUsed bool // Wurde Tastatur benutzt?
lastCanvasHeight int // Cache der Canvas-Höhe für Touch-Input
lastCanvasWidth int // Cache der Canvas-Breite für Touch-Input
downBtnX, downBtnY float64 // Zentrum des Down-Buttons
downBtnR float64 // Radius des Down-Buttons
// Debug Stats
showDebug bool // Debug-Overlay anzeigen (F3 zum Umschalten)
@@ -844,13 +850,61 @@ func generateRoomCode() string {
return string(code)
}
func (g *Game) connectAndStart() {
// Initiale predicted Position
// resetForNewGame setzt den gesamten Spiel-State zurück ohne die Seite neu zu laden.
// Muss vor jeder neuen Verbindung aufgerufen werden.
func (g *Game) resetForNewGame() {
// Alte Verbindung sauber trennen
g.disconnectFromServer()
// Prediction-State zurücksetzen
g.predictionMutex.Lock()
g.pendingInputs = make(map[uint32]InputState)
g.inputSequence = 0
g.lastServerSeq = 0
g.predictedX = 100
g.predictedY = 200
g.predictedVX = 0
g.predictedVY = 0
g.predictedGround = false
g.predictedOnWall = false
g.currentSpeed = 0
g.correctionOffsetX = 0
g.correctionOffsetY = 0
g.predictionMutex.Unlock()
// KRITISCH: lastRecvSeq zurücksetzen!
// Ohne diesen Reset ignoriert die Out-of-Order-Prüfung alle Nachrichten
// des neuen Spiels (neue Sequenzen < alter lastRecvSeq).
g.lastRecvSeq = 0
// Spieler-State zurücksetzen
g.scoreSubmitted = false
g.lastStatus = ""
g.correctionCount = 0
g.outOfOrderCount = 0
g.totalUpdates = 0
// GameState leeren
g.stateMutex.Lock()
g.gameState = game.GameState{Players: make(map[string]game.PlayerState)}
g.stateMutex.Unlock()
// Leaderboard leeren
g.leaderboardMutex.Lock()
g.leaderboard = make([]game.LeaderboardEntry, 0)
g.leaderboardMutex.Unlock()
// Partikel leeren
g.particlesMutex.Lock()
g.particles = nil
g.particlesMutex.Unlock()
g.lastCollectedCoins = make(map[string]bool)
g.lastCollectedPowerups = make(map[string]bool)
g.lastPlayerStates = make(map[string]game.PlayerState)
}
func (g *Game) connectAndStart() {
g.resetForNewGame()
// Verbindung über plattformspezifische Implementierung
g.connectToServer()

View File

@@ -56,6 +56,7 @@
<div class="legal-bar" style="margin-top: 10px;">
<button class="legal-btn" onclick="showImpressum()">📄 IMPRESSUM</button>
<button class="legal-btn" onclick="showDatenschutz()">🔒 DATENSCHUTZ</button>
<button class="legal-btn" onclick="showSprueche()">💬 SPRÜCHE</button>
</div>
</div>
@@ -227,6 +228,16 @@
</div>
</div>
<!-- SPRÜCHE MENU -->
<div id="spruecheMenu" class="overlay-screen hidden">
<div class="center-box" style="max-width: 900px; width: 95%;">
<h1>💬 LEGENDÄRE SPRÜCHE</h1>
<p style="font-size: 12px; color: #888; margin: -10px 0 15px;">Aus dem Schulalltag gesammelt von Schülern</p>
<div id="spruecheList" class="leaderboard-box" style="max-height: 60vh; overflow-y: auto; text-align: left; padding: 10px;"></div>
<button class="back-btn" onclick="showMainMenu()">← ZURÜCK</button>
</div>
</div>
<!-- LOBBY SCREEN (CO-OP WAITING ROOM) -->
<div id="lobbyScreen" class="overlay-screen hidden">
<div class="center-box">
@@ -288,6 +299,128 @@
</div>
</div>
<!-- Sprüche Logic -->
<script>
const SPRUECHE = [
{ text: "Mobbing ist besser als gar keine sozialen Kontakte.", author: "Ein Lehrer" },
{ text: "Ein Lehrer kauft sich ein Auto, weil ihm die Farbe gefallen hat.", author: "Schulflur-Legende" },
{ text: "Was heißt Strafe auf Englisch? „Richard"?", author: "Ein Lehrer", ctx: "Während er versucht, einem Schüler etwas auf Englisch zu erklären" },
{ text: "Heute ist [alles in der Anwesenheitsliste] richtig eingetragen.", author: "Eine Lehrerin", ctx: "Am Montagmorgen. Ein paar Stunden später fehlte trotzdem jemand in der Liste." },
{ text: "Verstehen Sie überhaupt die Prüfungsfragen? Neh, ach ", author: "Ein Lehrer", ctx: "An einen Schüler" },
{ text: "Tut mir leid, dass ich Sie damit nerve, aber [erzählt 10 Minuten über seine Weiterbildung zu Courage und Rechtsextremismus]. Ein Schüler meldet sich. Ist das eine rechtsextreme Handlung?!", author: "Ein Lehrer" },
{ text: "Können wir nochmal eine LK über Straßennamen schreiben? Ja klar, wenn du das willst.", author: "Schüler & Lehrerin", ctx: "Richard zur Deutschlehrerin im 3. Lehrjahr" },
{ text: "Mitten im WiKu-Unterricht betritt eine Lehrerin den Raum, schaut sich kurz um, schnappt sich den versteckten Wasserkocher vom Fensterstock und geht wieder. Ist das gerade wirklich passiert?", author: "Augenzeugen" },
{ text: "Neues Lieblingswort: Hanebüchen"", author: "Ein Lehrer" },
{ text: "Einem Lehrer fällt die Kinnlade runter, als Reda chinesisch mit den chinesischen Lehrkräften spricht und diese es verstehen.", author: "Augenzeugen" },
{ text: "Ich bin mit dem Staat verheiratet. … Ich hab das nur wegen der Pension gemacht.", author: "Ein Lehrer (Lehrer aus Überzeugung)" },
{ text: "Ich hab 'nen Freund, [Schüler]! Das ist egal.", author: "Eine Lehrerin & Schüler" },
{ text: "Beschreibungen im Unterricht u.a.: Slavenhändler, Mafia, Kopfgeld, Knebelverträge, Voodoo-Zauber (bei Physik und Frontend), Menschen quälen mit Aufgaben, goldene Balkone bauen, geklauter Code … und ein Ast als Zeigestock, mit dem er auf die Tastatur haut.", author: "Ein Lehrer" },
{ text: "Ein Kollege hat vorgeschlagen, bei der Hitze schon um 6:40 Uhr anzufangen. Ich meinte, um diese Zeit ist noch niemand da, nicht einmal ich selber. Dann meinte ich, wir sollten lieber 5:40 Uhr anfangen. Dann kommen noch welche von ihrem Heimweg vorbei.", author: "Ein Lehrer" },
{ text: "Nachdem wir in ihrem Kreuzworträtsel etwa 10 Fehler gefunden haben, gibt sie zu, dass sie es bei einem Gläschen Wein erstellt hat. 1A Unterrichtsvorbereitung.", author: "Eine Lehrerin" },
{ text: "Die „Katastrophenprüfung" im Sommer 2023 und die Entschuldigungsprüfung" im Sommer 2024.", author: "Ein Lehrer" },
{ text: "Das „Pippi"-Diagramm: das UML-Sequenzdiagramm.", author: "Ein Lehrer" },
{ text: "Und wo ist euer Klassenbuch? Mit wem hattet ihr gestern zuletzt Unterricht? Mit [dem Lehrer]. Ah ja. Der hat das gegessen.", author: "Eine Lehrerin" },
{ text: "Cool! Ich hab einen Klassensatz Bücher bestellt bekommen!", author: "Eine Lehrerin", ctx: "Freut sich wie ein Kind" },
{ text: "Ich hör viel zu viel Deutsch Das ist nicht rassistisch gemeint. Äh Redet gefälligst in Englisch!", author: "Eine Lehrerin", ctx: "Im Englischunterricht" },
{ text: "Itze! Äh, äh, nowe! Yes, this is good so.", author: "Eine Lehrerin" },
{ text: "Die anderen listen carefully, bitte.", author: "Eine Lehrerin" },
{ text: "Was mit Beschiss? Mit Beschiss kann ich. [] Ich hab den König gekrönt vom Bescheissen.", author: "Ein Lehrer" },
{ text: "Ein Lehrer erzählt uns die Geschichte von dem Huhn, das aggro wurde, als es einen Waschbären traf das jetzt so viel wert ist wie seine neue Brille.", author: "Schulflur-Legende" },
{ text: "EPK ist BPMN auf Steroiden.", author: "Ein Lehrer" },
{ text: "Richard ist heute nicht da. Was!? Dann wird das ja heute richtig still im Raum. Geil. Das hab ich jetzt nicht gesagt.", author: "Schüler & Lehrerin" },
{ text: "Zu Hausmeistern habe ich immer noch das beste Verhältnis im Gegensatz zu manchen anderen.", author: "Ein Lehrer" },
{ text: "Ein Kollege hat das bestimmt entspannter gemacht. Ich mag Menschen quälen.", author: "Ein Lehrer", ctx: "Zu LF 3" },
{ text: "Morgen sind Schüler unserer polnischen Partnerschule mit in unserem Unterricht. Die wollen von dem Besten lernen also von mir.", author: "Ein Lehrer" },
{ text: "Zwei Lehrer haben sich beim Kaffeetrinken über die Notengebung geeinigt. [Der Kollege] ist da sehr entspannt, weil er Richard nur noch zwei Stunden die Woche hat."", author: "Schulflur-Legende" },
{ text: "Ich bräuchte jetzt wirklich einen Kaffee oder ein Bier. Aber bietet mir bloß nicht was aus eurem Kofferraum an! Nur weil manche Schüler das schon gemacht haben…", author: "Eine Lehrerin", ctx: "Nach ausführlicher Diskussion über Englisch-Noten in der 9. Stunde" },
{ text: "Unterrichtsvorbereitung: „Ich werde es Ihnen beweisen. Hoffentlich habe ich keine zu große Klappe…"", author: "Ein Lehrer" },
{ text: "Ein Facharzt? Das ist ne echt gute Ausrede. Verdammte Scheiße", author: "Ein Lehrer", ctx: "Zum Schüler, der früher gehen musste" },
{ text: "Scheiße, darauf kann ich nicht rumschreiben!", author: "Ein Lehrer", ctx: "Als er mit dem Tablet-Stift auf den Fernseher zugeht" },
{ text: "Can you read number 4? Which number? Please pay a little bit more attension to the lesson!", author: "Richard", ctx: "Während er versucht, die Klasse auf Englisch zu unterrichten, weil die Lehrerin noch nicht da war. Gelächter, weil er selbst sonst nur zu selten aufpasst." },
{ text: "Warum muss ich jetzt wieder Scheiße erklären, die ich net verzapft hab. Lasst mich doch in Ruhe.", author: "Ein Lehrer", ctx: "Als die Klasse mal wieder abschweift" },
{ text: "Dann können Rollstuhlfahrer gleich mit in den Krieg ziehen.", author: "Ein Lehrer" },
{ text: "Mobbing ist 3 Monate durchgängig. Ich kann sie also nicht gar nicht mobben, weil sie immer weg sind.", author: "Ein Lehrer" },
{ text: "Ich spiele kein Schach mehr, seit ich gegen ein (Klein-)Kind verloren hab.", author: "Ein Lehrer" },
{ text: "Sind denn alle schon da? Es sind so wenig gerade. Es sind ja auch nur die A- und C-Klasse da. Vermisst du etwa Richard?", author: "Samuel & Mitschüler" },
{ text: "Spielen Sie God of War"? So sehen sie auch aus.", author: "Ein Lehrer", ctx: "Zu einem Schüler in der ersten Reihe" },
{ text: "Ich hab gesehen, dass jetzt sogar Spiele aus dem Microsoft Store hier funktionieren. Kaum zu glauben.", author: "Ein Lehrer", ctx: "Über die Schultechnik" },
{ text: "Die Antwort soll „ja" sein. Mit genug Reden kann man auch das Gegenteil argumentieren. Mit viel Liebe, ja.", author: "Ein Lehrer", ctx: "Beim Auflösen einer Aufgabe" },
{ text: "Hallo, [Herr Lehrer]! Was macht ihr denn jetzt? Wir gehen auf's Klo. Was macht [Richard] jetzt? Sich ein Loch graben?", author: "Richard, David & ein Lehrer" },
{ text: "Offiziell gibt's das Notenbuch gar nicht. Der könnte von mir sein.", author: "Sebastian & ein Lehrer", ctx: "Über eigenmächtige Eintragungen" },
{ text: "Nicht richtig, aber auch nicht zielführend. Richard: In der Prüfung Aber wenn das nicht die Antwort ist, kann ich doch die IHK verklagen?", author: "Eine Lehrerin & Richard" },
{ text: "Wollen Sie jetzt damit sagen, dass Stuttgart keinen Bahnhof braucht? Nein, aber net so ein Ding. Hätten sie mal lieber die halbe Stadt weggesprengt und einen richtigen Bahnhof hingebaut.", author: "Ein Lehrer" },
{ text: "Es geht darum, Sie drei Jahre hinzuhalten und dann sind Sie eh weg.", author: "Ein Lehrer", ctx: "Zum Prinzip Berufsschule" },
{ text: "Diesen Workaround gibt es hier nicht. Es gibt hier gar kein Problem. Am Tag darauf: Es gibt *** nochmal keine Probleme!", author: "Ein Lehrer", ctx: "Zu den Problemchen" der IT-Infrastruktur" },
{ text: "Wenn man seine Kommentare aus dem Kontext reißt: „Ich denke immer, ich bin doof. Aber das ist so."", author: "Ein Lehrer" },
{ text: "Wollen Sie etwas Lustiges hören? Nein? Dann eben etwas Trauriges", author: "Ein Lehrer", ctx: "Freitagmorgen" },
{ text: "Ein Lehrer schickt einen Schüler, ein Messer aus dem Lehrerzimmer zu holen Tür extra offengelassen. Der Schüler kommt mit Messer heraus, begegnet zwei verdutzt schauenden Lehrern, grüßt freundlichst und geht weiter, als ob er das jeden Tag machen würde.", author: "Schulflur-Legende" },
{ text: "Warum wollen Sie die Schule versichern? Die können Sie sowieso nicht verklagen.", author: "Ein Lehrer" },
{ text: "Das ist das Beste, das uns passieren kann. Dann können wir von null anfangen.", author: "Ein Lehrer", ctx: "Falls der einzige IT-Administrator-Lehrer mal nicht mehr da sein sollte" },
{ text: "Haltet euch sklavisch an die Notation!", author: "Ein Lehrer" },
{ text: "Nur die Paranoiden werden überleben.", author: "Ein Lehrer" },
{ text: "Ein Lehrer wünscht sich, dass die Schüler mehr Angst haben. Aber nicht, dass den Schülern Angst gemacht wird.", author: "Schulflur-Legende" },
{ text: "Ich bin gerade im Größenwahn und es wird immer verrückter.", author: "Ein Lehrer", ctx: "Mitten im Unterricht" },
{ text: "Und was haben Sie heute gemacht? Nichts. Und was soll ich jetzt darauf antworten? Wenn Sie einmal einen Yachthafen haben, ", author: "Ein Lehrer", ctx: "Am Ende des Unterrichts" },
{ text: "Die Schüler haben mich genötigt durchzumachen (damit sie eher gehen können).", author: "Ein Lehrer", ctx: "Nach der Mittagspause" },
{ text: "Programmieren kann so ekelhaft sein, wenn man sich wirklich damit beschäftigt.", author: "Ein Lehrer" },
{ text: "Vorsicht, sauer. Hab ich meinen Kindern geklaut.", author: "Ein Lehrer", ctx: "Beim Austeilen von sauren Gummischlangen" },
{ text: "[Herr Lehrer], Sie sind mein absoluter Lieblingslehrer, Mit was habe ich Ihre Anwesenheit verdient?", author: "Richard & ein Lehrer" },
{ text: "Die Beitragsätze der Sozialversicherungen haben sich geändert. Und ich muss nur kurz mein Auto aufsperren.", author: "Eine Lehrerin", ctx: "Im neuen Kalenderjahr" },
{ text: "Es gibt eine große Zeitspanne, in der quasi keine Lehrer ausgebildet wurden. Und dann gibt es noch solche von der Resterampe wie mich.", author: "Ein Lehrer" },
{ text: "Ja. So viel Pech im Denken kann man gar nicht haben.", author: "Ein Lehrer", ctx: "Als ihn ein Schüler fragte, mit wem er Spaß bei Sinusfunktionen hatte" },
{ text: "Richard; bei dir, das hab ich gleich gelassen und gar keine Note drunter geschrieben.", author: "Eine Lehrerin", ctx: "Beim Zurückgeben freiwilliger Aufgaben" },
{ text: "Die Noten der letzten LK konnten nicht eingetragen werden, weil die Notenbücher verschwunden sind. Reaktion: Die tauchen irgendwann schon wieder auf."", author: "Eine Lehrerin" },
{ text: "Ein Lehrer kommt random in der Mittagspause ins Zimmer und erzählt von seinen kulinarischen Abenteuern u.a. Gammelfisch am Wochenende.", author: "Zeugen" },
{ text: "Samuel: Ich hab für den Weg zum Netto länger gebraucht als die WiKu-LK gerade.", author: "Samuel" },
{ text: "[Schüler], waren Sie gestern da? Nein, ich war krank. Warum? Weil ein Kollege drauf gewettet hat, dass Sie nicht da sind. Ich hab die Wette verloren.", author: "Ein Lehrer" },
{ text: "[Herr Lehrer], wie heißen Sie eigentlich mit Vornamen? „Unwichtig." Aha, Unwichtig [Nachname]." Wie würden Sie sich nennen, wenn Sie sich einen Namen aussuchen könnten? „Ich-hab-zu-tun,-Geh-weg."", author: "Richard & ein Lehrer" },
{ text: "Der Assi = der Assistenzarzt. Hat der Assi Sie gut behandelt?"", author: "Ein Lehrer" },
{ text: "Dass ihr mal wisst, wie eine Prüfung physisch aussieht hier die vom Sommer '26. Die nehmen wir gerne! Nein! Die vom Sommer '25.", author: "Ein Lehrer & Schüler" },
{ text: "Ich kenn einen Autor vom Westermann; der schimpft eigentlich nur.", author: "Ein Lehrer" },
{ text: "[Herr Lehrer], haben Sie ein Messer dabei? Nein, leider nicht.", author: "Schüler & ein Lehrer" },
{ text: "Sag mal, die Amok-Läufer; das sind doch immer Schüler, oder?", author: "Ein Lehrer", ctx: "Zu einem Kollegen" },
{ text: "Das werdet ihr nicht mehr erleben mit den elektronischen Tafeln…", author: "Eine Lehrerin" },
{ text: "Ich darf mich doch, während ich die Aufgabe mache, umentscheiden. Ich muss Sie aber verstehen. Denken Sie doch mal an mich!", author: "Schüler & ein Lehrer" },
{ text: "Wir schauen uns eine Aufgabe aus der Prüfung der Systemintegratoren an. Wir haben ja einen Experten hier sitzen. Richard: So einfach?! Mehr trauen sie Euch nicht zu.", author: "Ein Lehrer & Richard", ctx: "Richard hatte sich in seiner Freistunde in den Unterricht gesetzt" },
{ text: "[Schüler], du hast leider eine 2 in der Arbeit… Haben wir uns trotzdem noch lieb?", author: "Ein Lehrer" },
{ text: "Kann ich Sie ihrem elendigem Schicksal überlassen? (= Bitte gehen Sie.)", author: "Ein Lehrer", ctx: "Zum Stundenbeginn" },
{ text: "Ich war leider zu spät. Zu spät zum Unterricht, oder für was? Zu spät zum Gaffen.", author: "Ein Lehrer", ctx: "Am Tag nach einem Unfall auf der Reichenbacher Straße" },
{ text: "Da wäre ich fast ausm Schlüpper geflogen!", author: "Eine Lehrerin" },
{ text: "Was stört sich die Eiche, wenn sich ein Schwein daran wälzt.", author: "Ein Lehrer" },
{ text: "Ich habe nichts verstanden. Hättest du auch nicht, wenn Ton da gewesen wäre.", author: "Leon & ein Lehrer", ctx: "Bei einem Video mit Tonproblemen" },
{ text: "Ein Lehrer greift an eine gerissene Achillissehne (einer anderen Person), nur um zu wissen, wie sich das anfühlt: „Wie Matsch."", author: "Augenzeugen" },
{ text: "Wo ist Sebastian? Krank. Wo ist euer Klassenbuch? Krank.", author: "Reda" },
{ text: "W"-Fragen sind verpönt! Das geht gar nicht!", author: "Ein Lehrer" },
{ text: "Wenn Richard im Unterricht keine Zeit hat, mit Mitschülern zu reden dann muss eine Lehrerin daran glauben…", author: "Schulflur-Legende" },
{ text: "Jetzt muss ich mir schon die Musterlösung schönsaufen.", author: "Ein Lehrer" },
{ text: "Ich muss die Prüfung nicht schreiben!", author: "Ein Lehrer", ctx: "Bei der Prüfungsvorbereitung" },
{ text: "Werd ich echt alt?", author: "Ein Lehrer", ctx: "Nachdem er die Anfänge von Wörtern nicht mehr gut verstand" },
{ text: "Bei euch hab ich ein gutes Gefühl. Apropos wo ist denn Leon?", author: "Ein Lehrer", ctx: "Als die Prüfung näher rückte" },
{ text: "Ein Lehrer kennt leider alle Bibi & Tina-Filme und erkennt für die Hälfte der Folgen den Titel am Titelbild.", author: "Schulflur-Legende" },
{ text: "Die Geißel Gottes. Ich freue mich, Sie zu sehen…!", author: "Ein Lehrer", ctx: "Zu Richard" },
{ text: "Ja, wir haben ein Kind. Meine Frau war da mehr beteiligt als ich.", author: "Ein Lehrer" },
{ text: "Kind kriegen ist glaub ich schon geiler als auf'm Mount Everest zu steigen.", author: "Ein Lehrer" },
{ text: "Ein Lehrer setzt Richard nach einem unruhigen Stundenstart nach vorne und fordert ihn zur Mitarbeit auf. Danach hat er im Unterricht niemanden mehr, den er rügen konnte.", author: "Augenzeugen" },
{ text: "„Mach nicht den Reda!" = In einen Raum gehen, vor dem Schüler warten, und ihnen noch zurufen, dass da eine andere Klasse drin ist in dem Moment hat er die Tür schon aufgemacht und blickt in die verwirrten Gesichter der fremden Klasse.", author: "Schulflur-Legende" },
];
function showSprueche() {
document.querySelectorAll('.overlay-screen').forEach(el => el.classList.add('hidden'));
document.getElementById('spruecheMenu').classList.remove('hidden');
// Shuffle quotes
const shuffled = [...SPRUECHE].sort(() => Math.random() - 0.5);
const container = document.getElementById('spruecheList');
container.innerHTML = shuffled.map((q, i) => `
<div style="background: rgba(255,255,255,0.05); border-left: 3px solid #fc0; margin: 8px 0; padding: 12px 15px; border-radius: 3px;">
<p style="margin: 0 0 6px; font-family: sans-serif; font-size: 14px; color: #eee; line-height: 1.5;">${q.text.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</p>
${q.ctx ? `<p style="margin: 0 0 4px; font-size: 11px; color: #888; font-style: italic;">${q.ctx.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</p>` : ''}
<p style="margin: 0; font-size: 12px; color: #fc0;"> ${q.author}</p>
</div>
`).join('');
}
</script>
<!-- WASM Execution -->
<script>
// Cache-Busting für JavaScript-Dateien (wird beim Build aktualisiert)