diff --git a/simulation.go b/simulation.go index 6531c7b..aa543d4 100644 --- a/simulation.go +++ b/simulation.go @@ -7,11 +7,15 @@ import ( ) func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) { + // State laden posY := parseOr(vals["pos_y"], PlayerYBase) velY := parseOr(vals["vel_y"], 0.0) score := int(parseOr(vals["score"], 0)) - rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64) + // NEU: Wir laden die bisher vergangene Zeit (Ticks) + ticksAlive := int(parseOr(vals["total_ticks"], 0)) + + rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64) godLives := int(parseOr(vals["p_god_lives"], 0)) hasBat := vals["p_has_bat"] == "1" bootTicks := int(parseOr(vals["p_boot_ticks"], 0)) @@ -28,9 +32,14 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st playerDead := false for i := 0; i < totalTicks; i++ { - currentSpeed := BaseSpeed + (float64(score) / 1000) - if currentSpeed > 10.0 { - currentSpeed = 10.0 + // WICHTIG: Wir erhöhen die Zeit + ticksAlive++ + + // LOGIK FIX: Geschwindigkeit basiert jetzt auf ZEIT (Ticks), nicht Score! + // 3000 Ticks = ca. 50 Sekunden. Da wird es schneller. + currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5 + if currentSpeed > 12.0 { + currentSpeed = 12.0 } currentJumpPower := JumpPower @@ -39,6 +48,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st bootTicks-- } + // Input Verarbeitung didJump := false isCrouching := false for _, inp := range inputs { @@ -52,6 +62,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st } } + // Physik isGrounded := posY >= PlayerYBase-1.0 currentHeight := PlayerHeight if isCrouching { @@ -77,6 +88,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st hitboxY = posY + (PlayerHeight - currentHeight) } + // Obstacles nextObstacles := []ActiveObstacle{} rightmostX := 0.0 @@ -87,10 +99,10 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st continue } + // Passed Check (Verhindert Hängenbleiben an "toten" Objekten) paddingX := 10.0 - realRightEdge := obs.X + obs.Width - paddingX - - if realRightEdge < 55.0 { + if obs.X+obs.Width-paddingX < 55.0 { + // Schon vorbei -> Keine Kollision mehr prüfen nextObstacles = append(nextObstacles, obs) if obs.X+obs.Width > rightmostX { rightmostX = obs.X + obs.Width @@ -105,16 +117,14 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-5.0 - - oLeft := obs.X + paddingX - oRight := obs.X + obs.Width - paddingX + currentSpeed + oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-5.0 isCollision := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom if isCollision { if obs.Type == "coin" { - score += 2000 + score += 2000 // Score Bonus macht das Spiel nicht mehr kaputt! continue } else if obs.Type == "powerup" { if obs.ID == "p_god" { @@ -147,17 +157,18 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st } obstacles = nextObstacles + // Spawning if rightmostX < GameWidth-10.0 { - rawGap := 400.0 + rng.NextRange(0, 500) - gap := float64(int(rawGap)) + gap := float64(int(400.0 + rng.NextRange(0, 500))) spawnX := rightmostX + gap if spawnX < GameWidth { spawnX = GameWidth } - isBossPhase := (score % 1500) > 1200 - var possibleDefs []ObstacleDef + // LOGIK FIX: Boss Phase basiert auf ZEIT (Ticks) + isBossPhase := (ticksAlive % 1500) > 1200 + var possibleDefs []ObstacleDef for _, d := range defaultConfig.Obstacles { if isBossPhase { if d.ID == "principal" || d.ID == "trashcan" { @@ -167,7 +178,8 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st if d.ID == "principal" { continue } - if d.ID == "eraser" && score < 500 { + // Eraser kommt ab Tick 3000 (ca. 50 sekunden) + if d.ID == "eraser" && ticksAlive < 3000 { continue } possibleDefs = append(possibleDefs, d) @@ -175,7 +187,6 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st } def := rng.PickDef(possibleDefs) - if def != nil && def.CanTalk { if rng.NextFloat() > 0.7 { rng.NextFloat() @@ -186,23 +197,17 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st if def.Type == "powerup" && rng.NextFloat() > 0.1 { def = nil } - if def != nil { spawnY := GroundY - def.Height - def.YOffset obstacles = append(obstacles, ActiveObstacle{ - ID: def.ID, - Type: def.Type, - X: spawnX, - Y: spawnY, - Width: def.Width, - Height: def.Height, + ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height, }) } } } if !playerDead { - score++ + score++ // Basis-Score läuft normal weiter } else { break } @@ -216,6 +221,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{ "score": score, + "total_ticks": ticksAlive, // NEU: Speichern "pos_y": fmt.Sprintf("%f", posY), "vel_y": fmt.Sprintf("%f", velY), "rng_state": rng.State, diff --git a/static/js/config.js b/static/js/config.js index 6e430f3..5eefcc0 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -10,6 +10,9 @@ const CHUNK_SIZE = 60; const TARGET_FPS = 60; const MS_PER_TICK = 1000 / TARGET_FPS; +const DEBUG_SYNC = true; +const SYNC_TOLERANCE = 5.0; + // RNG Klasse class PseudoRNG { constructor(seed) { diff --git a/static/js/logic.js b/static/js/logic.js index 9e865d8..3a051ca 100644 --- a/static/js/logic.js +++ b/static/js/logic.js @@ -1,70 +1,84 @@ function updateGameLogic() { - // 1. Speed Berechnung (Sync mit Server!) - let currentSpeed = BASE_SPEED + (score / 1000); - if (currentSpeed > 10.0) currentSpeed = 10.0; - - - // 2. Input & Sprung - if (isCrouching) inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" }); - - const originalHeight = 50; const crouchHeight = 25; - player.h = isCrouching ? crouchHeight : originalHeight; - let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y; - - // Jump Power Check - let jumpP = JUMP_POWER; - if (bootTicks > 0) { - jumpP = HIGH_JUMP_POWER; - bootTicks--; + // 1. Input Logging (Ducken) + if (isCrouching) { + inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" }); } - // Physik + // 2. Geschwindigkeit (Basiert auf ZEIT/Ticks, nicht Score!) + // Formel: Start bei 5, erhöht sich alle 3000 Ticks (ca. 50 Sek) um 0.5 + let currentSpeed = 5 + (currentTick / 3000.0) * 0.5; + if (currentSpeed > 12.0) currentSpeed = 12.0; // Max Speed Cap + + // 3. Spieler Physik & Größe + const originalHeight = 50; + const crouchHeight = 25; + + player.h = isCrouching ? crouchHeight : originalHeight; + + // Visuelle Korrektur Y (damit Füße am Boden bleiben) + let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y; + + // Schwerkraft player.vy += GRAVITY; - if (isCrouching && !player.grounded) player.vy += 2.0; + if (isCrouching && !player.grounded) player.vy += 2.0; // Schneller fallen (Fast Fall) + player.y += player.vy; + // Boden-Kollision if (player.y + originalHeight >= GROUND_Y) { - player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true; - } else { player.grounded = false; } + player.y = GROUND_Y - originalHeight; + player.vy = 0; + player.grounded = true; + } else { + player.grounded = false; + } - - // 3. Obstacles + // 4. Hindernisse Bewegen & Kollision let nextObstacles = []; let rightmostX = 0; for (let obs of obstacles) { obs.x -= currentSpeed; + // Hitbox für aktuellen Frame const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h }; if (checkCollision(playerHitbox, obs)) { - // TYPE CHECK + // A. MÜNZE (+2000 Punkte) if (obs.def.type === "coin") { score += 2000; - continue; + continue; // Entfernen (Eingesammelt) } + // B. POWERUP (Aktivieren) else if (obs.def.type === "powerup") { if (obs.def.id === "p_god") godModeLives = 3; if (obs.def.id === "p_bat") hasBat = true; - if (obs.def.id === "p_boot") bootTicks = 600; - continue; + if (obs.def.id === "p_boot") bootTicks = 600; // ca. 10 Sekunden + continue; // Entfernen (Eingesammelt) } + // C. GEGNER / HINDERNIS else { - // HINDERNIS + // Schläger vs Lehrer if (hasBat && obs.def.type === "teacher") { - hasBat = false; - continue; // Zerstört! + hasBat = false; // Schläger kaputt + continue; // Lehrer weg } + // Godmode vs Alles if (godModeLives > 0) { - godModeLives--; - continue; // Überlebt! + godModeLives--; // Ein Leben weg + continue; // Hindernis ignoriert } + // Tod player.color = "darkred"; - if (!isGameOver) { sendChunk(); gameOver("Kollision"); } + if (!isGameOver) { + sendChunk(); + gameOver("Kollision"); + } } } + // Objekt behalten wenn noch im Bild if (obs.x + obs.def.width > -100) { nextObstacles.push(obs); if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width; @@ -72,42 +86,52 @@ function updateGameLogic() { } obstacles = nextObstacles; - // 4. Spawning (Sync mit Go!) + // 5. Spawning (Basiert auf ZEIT/Ticks) if (rightmostX < GAME_WIDTH - 10 && gameConfig) { const gap = Math.floor(400 + rng.nextRange(0, 500)); - let spawnX = rightmostX + gap; if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH; + let spawnX = rightmostX + gap; + if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH; - const isBossPhase = (score % 1500) > 1200; + // Boss Phase abhängig von Zeit (nicht Score, wegen Münzen!) + const isBossPhase = (currentTick % 1500) > 1200; let possibleObs = []; gameConfig.obstacles.forEach(def => { if (isBossPhase) { + // Boss Phase: Nur Principal und Trashcan if (def.id === "principal" || def.id === "trashcan") possibleObs.push(def); } else { + // Normal Phase if (def.id === "principal") return; - if (def.id === "eraser" && score < 500) return; + // Eraser erst ab Tick 3000 (ca. 50 Sek) + if (def.id === "eraser" && currentTick < 3000) return; possibleObs.push(def); } }); + // Zufälliges Objekt wählen let def = rng.pick(possibleObs); - // Speech Sync + // RNG Sync: Sprechblasen let speech = null; if (def && def.canTalk) { + // WICHTIG: Reihenfolge muss 1:1 wie in Go sein if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines); } - // Powerup Rarity Sync (Muss exakt wie Go sein: 10% Chance) + // RNG Sync: Powerup Seltenheit (nur 10% Chance) if (def && def.type === "powerup") { if (rng.nextFloat() > 0.1) def = null; } + // Hinzufügen if (def) { const yOffset = def.yOffset || 0; obstacles.push({ - x: spawnX, y: GROUND_Y - def.height - yOffset, - def: def, speech: speech + x: spawnX, + y: GROUND_Y - def.height - yOffset, + def: def, + speech: speech }); } } diff --git a/static/js/network.js b/static/js/network.js index a832434..627787d 100644 --- a/static/js/network.js +++ b/static/js/network.js @@ -2,6 +2,9 @@ async function sendChunk() { const ticksToSend = currentTick - lastSentTick; if (ticksToSend <= 0) return; + + const snapshotobstacles = JSON.parse(JSON.stringify(obstacles)); + const payload = { sessionId: sessionID, inputs: [...inputLog], @@ -20,15 +23,29 @@ async function sendChunk() { const data = await res.json(); - if (data.serverObs) serverObstacles = data.serverObs; + // Update für visuelles Debugging + if (data.serverObs) { + serverObstacles = data.serverObs; + + // --- NEU: DEBUG MODUS VERGLEICH --- + if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) { + compareState(snapshotobstacles, data.serverObs); + } + // ---------------------------------- + } if (data.status === "dead") { - console.error("SERVER TOT", data); + console.error("💀 SERVER KILL", data); gameOver("Vom Server gestoppt"); } else { const sScore = data.verifiedScore; - if (Math.abs(score - sScore) > 200) score = sScore; + // Score Korrektur + if (Math.abs(score - sScore) > 200) { + console.warn(`⚠️ SCORE DRIFT: Client=${score} Server=${sScore}`); + score = sScore; + } } + } catch (e) { console.error("Netzwerkfehler:", e); } @@ -89,4 +106,76 @@ async function loadStartScreenLeaderboard() { }); listEl.innerHTML = html; } catch (e) {} +} + +function compareState(clientObs, serverObs) { + // 1. Anzahl prüfen + if (clientObs.length !== serverObs.length) { + console.error(`🚨 ANZAHL MISMATCH! Client: ${clientObs.length}, Server: ${serverObs.length}`); + } + + const report = []; + const maxLen = Math.max(clientObs.length, serverObs.length); + let hasMajorDrift = false; + + for (let i = 0; i < maxLen; i++) { + const cli = clientObs[i]; + const srv = serverObs[i]; + + let drift = 0; + let status = "✅ OK"; + + // Client Objekt vorbereiten + let cID = "---"; + let cX = 0; + if (cli) { + cID = cli.def.id; // Struktur beachten: cli.def.id + cX = cli.x; + } + + // Server Objekt vorbereiten + let sID = "---"; + let sX = 0; + if (srv) { + sID = srv.id; // Struktur vom Server: srv.id + sX = srv.x; + } + + // Vergleich + if (cli && srv) { + // IDs unterschiedlich? (z.B. Tisch vs Lehrer) + if (cID !== sID) { + status = "❌ ID ERROR"; + hasMajorDrift = true; + } else { + drift = cX - sX; + if (Math.abs(drift) > SYNC_TOLERANCE) { + status = "⚠️ DRIFT"; + hasMajorDrift = true; + } + } + } else { + status = "❌ MISSING"; + hasMajorDrift = true; + } + + // In Tabelle eintragen + report.push({ + Index: i, + Status: status, + "C-ID": cID, + "S-ID": sID, + "C-Pos": cX.toFixed(1), + "S-Pos": sX.toFixed(1), + "Drift (px)": drift.toFixed(2) + }); + } + + // Nur loggen, wenn Fehler da sind oder alle 5 Sekunden (Tick 300) + if (hasMajorDrift || currentTick % 300 === 0) { + if (hasMajorDrift) console.warn("--- SYNC PROBLEME GEFUNDEN ---"); + else console.log("--- Sync Check (Routine) ---"); + + console.table(report); // Das erstellt eine super lesbare Tabelle im Browser + } } \ No newline at end of file