package main import ( "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/physics" ) // ApplyInput wendet einen Input auf den vorhergesagten Zustand an // Nutzt die gemeinsame Physik-Engine aus pkg/physics func (g *Game) ApplyInput(input InputState) { // Horizontale Bewegung mit analogem Joystick moveX := 0.0 if input.Left { moveX = -1.0 } else if input.Right { moveX = 1.0 } // Wenn Joystick benutzt wird, überschreibe moveX mit analogem Wert if input.JoyX != 0 { moveX = input.JoyX } // Physik-State vorbereiten state := physics.PlayerPhysicsState{ X: g.predictedX, Y: g.predictedY, VX: g.predictedVX, VY: g.predictedVY, OnGround: g.predictedGround, OnWall: g.predictedOnWall, } // Physik-Input vorbereiten physicsInput := physics.PhysicsInput{ InputX: moveX, Jump: input.Jump, Down: input.Down, } // Kollisions-Checker vorbereiten g.stateMutex.Lock() collisionChecker := &physics.ClientCollisionChecker{ World: g.world, ActiveChunks: g.gameState.WorldChunks, MovingPlatforms: g.gameState.MovingPlatforms, } g.stateMutex.Unlock() // Gemeinsame Physik anwenden (1:1 wie Server) physics.ApplyPhysics(&state, physicsInput, g.currentSpeed, collisionChecker, physics.DefaultPlayerConstants()) // Ergebnis zurückschreiben g.predictedX = state.X g.predictedY = state.Y g.predictedVX = state.VX g.predictedVY = state.VY g.predictedGround = state.OnGround g.predictedOnWall = state.OnWall } // ReconcileWithServer gleicht lokale Prediction mit Server-State ab func (g *Game) ReconcileWithServer(serverState game.PlayerState) { g.predictionMutex.Lock() defer g.predictionMutex.Unlock() // Server-bestätigte Sequenz g.lastServerSeq = serverState.LastInputSeq // Entferne alle bestätigten Inputs for seq := range g.pendingInputs { if seq <= g.lastServerSeq { delete(g.pendingInputs, seq) } } // Temporäre Position für Replay (jetzt MIT Y-Achse) replayX := serverState.X replayY := serverState.Y replayVX := serverState.VX replayVY := serverState.VY replayGround := serverState.OnGround replayOnWall := serverState.OnWall // Replay alle noch nicht bestätigten Inputs mit VOLLER Physik if len(g.pendingInputs) > 0 { for seq := g.lastServerSeq + 1; seq <= g.inputSequence; seq++ { if input, ok := g.pendingInputs[seq]; ok { // Temporär auf Replay-Position setzen oldX, oldY := g.predictedX, g.predictedY oldVX, oldVY := g.predictedVX, g.predictedVY oldGround := g.predictedGround oldOnWall := g.predictedOnWall g.predictedX = replayX g.predictedY = replayY g.predictedVX = replayVX g.predictedVY = replayVY g.predictedGround = replayGround g.predictedOnWall = replayOnWall g.ApplyInput(input) replayX = g.predictedX replayY = g.predictedY replayVX = g.predictedVX replayVY = g.predictedVY replayGround = g.predictedGround replayOnWall = g.predictedOnWall // Zurücksetzen g.predictedX = oldX g.predictedY = oldY g.predictedVX = oldVX g.predictedVY = oldVY g.predictedGround = oldGround g.predictedOnWall = oldOnWall } } } // Berechne Differenz zwischen Client-Prediction und Server-Replay (X und Y) diffX := replayX - g.predictedX diffY := replayY - g.predictedY dist := diffX*diffX + diffY*diffY // Speichere Korrektur-Magnitude für Debug g.correctionX = diffX g.correctionY = diffY // === SMOOTH CORRECTION MIT VISUELLEM OFFSET === // // Strategie: Physics springt sofort zur richtigen Position (korrekte Physik), // aber der angezeigte Wert (predictedX/Y + correctionOffsetX/Y) springt NICHT – // er driftet sanft über ~80ms zur Physics-Position hin. // // Dadurch sehen Spieler keinen "Pushback/Teleport" mehr, selbst wenn der Server // eine größere Korrektur schickt. const ( acceptTolerance = 400.0 // ~20px: keine Korrektur criticalTolerance = 2500.0 // ~50px: sanfte Korrektur ) if dist < acceptTolerance { // Kleine Abweichung (<20px): Client-Position beibehalten, kein Eingriff nötig } else if dist < criticalTolerance { // Mittlere Abweichung (20-50px): sanfte Physics-Korrektur (25%) // Visueller Offset kompensiert die Bewegung → kein sichtbarer Sprung factor := 0.25 corrX := diffX * factor corrY := diffY * factor g.correctionOffsetX -= corrX // Offset gegenläufig zur Physics-Korrektur g.correctionOffsetY -= corrY g.predictedX += corrX g.predictedY += corrY g.correctionCount++ } else { // Große Abweichung (>50px): Physics springt komplett zur Replay-Position. // Visueller Offset hält das Bild an der alten Stelle – Spieler sieht keinen Teleport. g.correctionOffsetX += g.predictedX - replayX // = -(replayX - predictedX) = -diffX g.correctionOffsetY += g.predictedY - replayY g.predictedX = replayX g.predictedY = replayY g.correctionCount++ } // Velocity und Ground-State immer vom Replay übernehmen. // Replay-Werte kommen aus korrekter Server-State + Pending-Input-Replay // und sind physikalisch genauer als reine Client-Prediction über Zeit. g.predictedVX = replayVX g.predictedVY = replayVY g.predictedGround = replayGround g.predictedOnWall = replayOnWall }