diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index b91c0c7..9ab2243 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -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 }}" \ No newline at end of file + 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 }}" diff --git a/cmd/client/connection_native.go b/cmd/client/connection_native.go index 87ba918..1391e4c 100644 --- a/cmd/client/connection_native.go +++ b/cmd/client/connection_native.go @@ -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" diff --git a/cmd/client/connection_wasm.go b/cmd/client/connection_wasm.go index 2c3b6d1..8df3cd4 100644 --- a/cmd/client/connection_wasm.go +++ b/cmd/client/connection_wasm.go @@ -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 diff --git a/cmd/client/game_render.go b/cmd/client/game_render.go index 56a8d8b..846a3c0 100644 --- a/cmd/client/game_render.go +++ b/cmd/client/game_render.go @@ -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}) } } diff --git a/cmd/client/main.go b/cmd/client/main.go index 8d84e14..0c9791d 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -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() diff --git a/cmd/client/web/index.html b/cmd/client/web/index.html index 28cdbb1..59ef593 100644 --- a/cmd/client/web/index.html +++ b/cmd/client/web/index.html @@ -56,6 +56,7 @@ @@ -227,6 +228,16 @@ + + + + + +