Private
Public Access
1
0

Merge pull request 'bug fixes' (#8) from add-new-player-skin into main
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m10s

Reviewed-on: #8
This commit is contained in:
2025-11-26 10:10:10 +00:00
4 changed files with 191 additions and 69 deletions

View File

@@ -7,11 +7,15 @@ import (
) )
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) { func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) {
// State laden
posY := parseOr(vals["pos_y"], PlayerYBase) posY := parseOr(vals["pos_y"], PlayerYBase)
velY := parseOr(vals["vel_y"], 0.0) velY := parseOr(vals["vel_y"], 0.0)
score := int(parseOr(vals["score"], 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)) godLives := int(parseOr(vals["p_god_lives"], 0))
hasBat := vals["p_has_bat"] == "1" hasBat := vals["p_has_bat"] == "1"
bootTicks := int(parseOr(vals["p_boot_ticks"], 0)) 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 playerDead := false
for i := 0; i < totalTicks; i++ { for i := 0; i < totalTicks; i++ {
currentSpeed := BaseSpeed + (float64(score) / 1000) // WICHTIG: Wir erhöhen die Zeit
if currentSpeed > 10.0 { ticksAlive++
currentSpeed = 10.0
// 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 currentJumpPower := JumpPower
@@ -39,6 +48,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
bootTicks-- bootTicks--
} }
// Input Verarbeitung
didJump := false didJump := false
isCrouching := false isCrouching := false
for _, inp := range inputs { 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 isGrounded := posY >= PlayerYBase-1.0
currentHeight := PlayerHeight currentHeight := PlayerHeight
if isCrouching { if isCrouching {
@@ -77,6 +88,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
hitboxY = posY + (PlayerHeight - currentHeight) hitboxY = posY + (PlayerHeight - currentHeight)
} }
// Obstacles
nextObstacles := []ActiveObstacle{} nextObstacles := []ActiveObstacle{}
rightmostX := 0.0 rightmostX := 0.0
@@ -87,10 +99,10 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
continue continue
} }
// Passed Check (Verhindert Hängenbleiben an "toten" Objekten)
paddingX := 10.0 paddingX := 10.0
realRightEdge := obs.X + obs.Width - paddingX if obs.X+obs.Width-paddingX < 55.0 {
// Schon vorbei -> Keine Kollision mehr prüfen
if realRightEdge < 55.0 {
nextObstacles = append(nextObstacles, obs) nextObstacles = append(nextObstacles, obs)
if obs.X+obs.Width > rightmostX { if obs.X+obs.Width > rightmostX {
rightmostX = obs.X + obs.Width 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 pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX
pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-5.0 pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-5.0
oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX
oLeft := obs.X + paddingX
oRight := obs.X + obs.Width - paddingX + currentSpeed
oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-5.0 oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-5.0
isCollision := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom isCollision := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom
if isCollision { if isCollision {
if obs.Type == "coin" { if obs.Type == "coin" {
score += 2000 score += 2000 // Score Bonus macht das Spiel nicht mehr kaputt!
continue continue
} else if obs.Type == "powerup" { } else if obs.Type == "powerup" {
if obs.ID == "p_god" { if obs.ID == "p_god" {
@@ -147,17 +157,18 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
obstacles = nextObstacles obstacles = nextObstacles
// Spawning
if rightmostX < GameWidth-10.0 { if rightmostX < GameWidth-10.0 {
rawGap := 400.0 + rng.NextRange(0, 500) gap := float64(int(400.0 + rng.NextRange(0, 500)))
gap := float64(int(rawGap))
spawnX := rightmostX + gap spawnX := rightmostX + gap
if spawnX < GameWidth { if spawnX < GameWidth {
spawnX = GameWidth spawnX = GameWidth
} }
isBossPhase := (score % 1500) > 1200 // LOGIK FIX: Boss Phase basiert auf ZEIT (Ticks)
var possibleDefs []ObstacleDef isBossPhase := (ticksAlive % 1500) > 1200
var possibleDefs []ObstacleDef
for _, d := range defaultConfig.Obstacles { for _, d := range defaultConfig.Obstacles {
if isBossPhase { if isBossPhase {
if d.ID == "principal" || d.ID == "trashcan" { 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" { if d.ID == "principal" {
continue continue
} }
if d.ID == "eraser" && score < 500 { // Eraser kommt ab Tick 3000 (ca. 50 sekunden)
if d.ID == "eraser" && ticksAlive < 3000 {
continue continue
} }
possibleDefs = append(possibleDefs, d) possibleDefs = append(possibleDefs, d)
@@ -175,7 +187,6 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
def := rng.PickDef(possibleDefs) def := rng.PickDef(possibleDefs)
if def != nil && def.CanTalk { if def != nil && def.CanTalk {
if rng.NextFloat() > 0.7 { if rng.NextFloat() > 0.7 {
rng.NextFloat() 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 { if def.Type == "powerup" && rng.NextFloat() > 0.1 {
def = nil def = nil
} }
if def != nil { if def != nil {
spawnY := GroundY - def.Height - def.YOffset spawnY := GroundY - def.Height - def.YOffset
obstacles = append(obstacles, ActiveObstacle{ obstacles = append(obstacles, ActiveObstacle{
ID: def.ID, ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height,
Type: def.Type,
X: spawnX,
Y: spawnY,
Width: def.Width,
Height: def.Height,
}) })
} }
} }
} }
if !playerDead { if !playerDead {
score++ score++ // Basis-Score läuft normal weiter
} else { } else {
break break
} }
@@ -216,6 +221,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{ rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"score": score, "score": score,
"total_ticks": ticksAlive, // NEU: Speichern
"pos_y": fmt.Sprintf("%f", posY), "pos_y": fmt.Sprintf("%f", posY),
"vel_y": fmt.Sprintf("%f", velY), "vel_y": fmt.Sprintf("%f", velY),
"rng_state": rng.State, "rng_state": rng.State,

View File

@@ -10,6 +10,9 @@ const CHUNK_SIZE = 60;
const TARGET_FPS = 60; const TARGET_FPS = 60;
const MS_PER_TICK = 1000 / TARGET_FPS; const MS_PER_TICK = 1000 / TARGET_FPS;
const DEBUG_SYNC = true;
const SYNC_TOLERANCE = 5.0;
// RNG Klasse // RNG Klasse
class PseudoRNG { class PseudoRNG {
constructor(seed) { constructor(seed) {

View File

@@ -1,70 +1,84 @@
function updateGameLogic() { function updateGameLogic() {
// 1. Speed Berechnung (Sync mit Server!) // 1. Input Logging (Ducken)
let currentSpeed = BASE_SPEED + (score / 1000); if (isCrouching) {
if (currentSpeed > 10.0) currentSpeed = 10.0; inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" });
// 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--;
} }
// 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; 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; player.y += player.vy;
// Boden-Kollision
if (player.y + originalHeight >= GROUND_Y) { if (player.y + originalHeight >= GROUND_Y) {
player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true; player.y = GROUND_Y - originalHeight;
} else { player.grounded = false; } player.vy = 0;
player.grounded = true;
} else {
player.grounded = false;
}
// 4. Hindernisse Bewegen & Kollision
// 3. Obstacles
let nextObstacles = []; let nextObstacles = [];
let rightmostX = 0; let rightmostX = 0;
for (let obs of obstacles) { for (let obs of obstacles) {
obs.x -= currentSpeed; obs.x -= currentSpeed;
// Hitbox für aktuellen Frame
const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h }; const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
if (checkCollision(playerHitbox, obs)) { if (checkCollision(playerHitbox, obs)) {
// TYPE CHECK // A. MÜNZE (+2000 Punkte)
if (obs.def.type === "coin") { if (obs.def.type === "coin") {
score += 2000; score += 2000;
continue; continue; // Entfernen (Eingesammelt)
} }
// B. POWERUP (Aktivieren)
else if (obs.def.type === "powerup") { else if (obs.def.type === "powerup") {
if (obs.def.id === "p_god") godModeLives = 3; if (obs.def.id === "p_god") godModeLives = 3;
if (obs.def.id === "p_bat") hasBat = true; if (obs.def.id === "p_bat") hasBat = true;
if (obs.def.id === "p_boot") bootTicks = 600; if (obs.def.id === "p_boot") bootTicks = 600; // ca. 10 Sekunden
continue; continue; // Entfernen (Eingesammelt)
} }
// C. GEGNER / HINDERNIS
else { else {
// HINDERNIS // Schläger vs Lehrer
if (hasBat && obs.def.type === "teacher") { if (hasBat && obs.def.type === "teacher") {
hasBat = false; hasBat = false; // Schläger kaputt
continue; // Zerstört! continue; // Lehrer weg
} }
// Godmode vs Alles
if (godModeLives > 0) { if (godModeLives > 0) {
godModeLives--; godModeLives--; // Ein Leben weg
continue; // Überlebt! continue; // Hindernis ignoriert
} }
// Tod
player.color = "darkred"; 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) { if (obs.x + obs.def.width > -100) {
nextObstacles.push(obs); nextObstacles.push(obs);
if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width; if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width;
@@ -72,42 +86,52 @@ function updateGameLogic() {
} }
obstacles = nextObstacles; obstacles = nextObstacles;
// 4. Spawning (Sync mit Go!) // 5. Spawning (Basiert auf ZEIT/Ticks)
if (rightmostX < GAME_WIDTH - 10 && gameConfig) { if (rightmostX < GAME_WIDTH - 10 && gameConfig) {
const gap = Math.floor(400 + rng.nextRange(0, 500)); 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 = []; let possibleObs = [];
gameConfig.obstacles.forEach(def => { gameConfig.obstacles.forEach(def => {
if (isBossPhase) { if (isBossPhase) {
// Boss Phase: Nur Principal und Trashcan
if (def.id === "principal" || def.id === "trashcan") possibleObs.push(def); if (def.id === "principal" || def.id === "trashcan") possibleObs.push(def);
} else { } else {
// Normal Phase
if (def.id === "principal") return; 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); possibleObs.push(def);
} }
}); });
// Zufälliges Objekt wählen
let def = rng.pick(possibleObs); let def = rng.pick(possibleObs);
// Speech Sync // RNG Sync: Sprechblasen
let speech = null; let speech = null;
if (def && def.canTalk) { if (def && def.canTalk) {
// WICHTIG: Reihenfolge muss 1:1 wie in Go sein
if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines); 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 (def && def.type === "powerup") {
if (rng.nextFloat() > 0.1) def = null; if (rng.nextFloat() > 0.1) def = null;
} }
// Hinzufügen
if (def) { if (def) {
const yOffset = def.yOffset || 0; const yOffset = def.yOffset || 0;
obstacles.push({ obstacles.push({
x: spawnX, y: GROUND_Y - def.height - yOffset, x: spawnX,
def: def, speech: speech y: GROUND_Y - def.height - yOffset,
def: def,
speech: speech
}); });
} }
} }

View File

@@ -2,6 +2,9 @@ async function sendChunk() {
const ticksToSend = currentTick - lastSentTick; const ticksToSend = currentTick - lastSentTick;
if (ticksToSend <= 0) return; if (ticksToSend <= 0) return;
const snapshotobstacles = JSON.parse(JSON.stringify(obstacles));
const payload = { const payload = {
sessionId: sessionID, sessionId: sessionID,
inputs: [...inputLog], inputs: [...inputLog],
@@ -20,15 +23,29 @@ async function sendChunk() {
const data = await res.json(); 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") { if (data.status === "dead") {
console.error("SERVER TOT", data); console.error("💀 SERVER KILL", data);
gameOver("Vom Server gestoppt"); gameOver("Vom Server gestoppt");
} else { } else {
const sScore = data.verifiedScore; 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) { } catch (e) {
console.error("Netzwerkfehler:", e); console.error("Netzwerkfehler:", e);
} }
@@ -89,4 +106,76 @@ async function loadStartScreenLeaderboard() {
}); });
listEl.innerHTML = html; listEl.innerHTML = html;
} catch (e) {} } 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
}
} }