Private
Public Access
1
0

add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s

This commit is contained in:
Sebastian Unterschütz
2025-11-29 23:37:57 +01:00
parent 5ce097bbb7
commit 669c783a06
43 changed files with 3001 additions and 878 deletions

View File

@@ -1,199 +1,377 @@
async function sendChunk() {
const ticksToSend = currentTick - lastSentTick;
if (ticksToSend <= 0) return;
// ==========================================
// NETZWERK LOGIK (WEBSOCKET + RTT SYNC)
// ==========================================
/*
GLOBALE VARIABLEN (aus state.js):
- socket
- obstacleBuffer, platformBuffer
- currentLatencyMs, pingInterval
- isGameRunning, isGameOver
- score, currentTick
*/
const snapshotobstacles = JSON.parse(JSON.stringify(obstacles));
function connectGame() {
// Alte Verbindung schließen
if (socket) {
socket.close();
}
const payload = {
sessionId: sessionID,
inputs: [...inputLog],
totalTicks: ticksToSend
// Ping Timer stoppen falls aktiv
if (typeof pingInterval !== 'undefined' && pingInterval) {
clearInterval(pingInterval);
}
// Protokoll automatisch wählen (ws:// oder wss://)
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = proto + "//" + location.host + "/ws";
console.log("Verbinde zu:", url);
socket = new WebSocket(url);
// --- 1. VERBINDUNG GEÖFFNET ---
socket.onopen = () => {
console.log("🟢 WS Verbunden. Spiel startet.");
// Alles zurücksetzen
obstacleBuffer = [];
platformBuffer = [];
obstacles = [];
platforms = [];
currentLatencyMs = 0; // Reset Latenz
isGameRunning = true;
isGameOver = false;
isLoaded = true;
// PING LOOP STARTEN (Jede Sekunde messen)
pingInterval = setInterval(sendPing, 1000);
// Game Loop anwerfen
requestAnimationFrame(gameLoop);
};
inputLog = [];
lastSentTick = currentTick;
// --- 2. NACHRICHT VOM SERVER ---
socket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
try {
const res = await fetch('/api/validate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
// A. PONG (Latenzmessung)
if (msg.type === "pong") {
const now = Date.now();
const sentTime = msg.ts; // Server schickt unseren Timestamp zurück
const data = await res.json();
// Round Trip Time (Hin + Zurück)
const rtt = now - sentTime;
// Update für visuelles Debugging
if (data.serverObs) {
serverObstacles = data.serverObs;
// One Way Latency (Latenz in eine Richtung)
const latency = rtt / 2;
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
compareState(snapshotobstacles, data.serverObs);
}
if (data.powerups) {
const sTick = data.serverTick;
if (lastPowerupTick > sTick) {
// Glätten (Exponential Moving Average), damit Werte nicht springen
// Wenn es der erste Wert ist, nehmen wir ihn direkt.
if (currentLatencyMs === 0) {
currentLatencyMs = latency;
} else {
godModeLives = data.powerups.godLives;
hasBat = data.powerups.hasBat;
bootTicks = data.powerups.bootTicks;
// 90% alter Wert, 10% neuer Wert
currentLatencyMs = (currentLatencyMs * 0.9) + (latency * 0.1);
}
// Optional: Debugging im Log
// console.log(`📡 Ping: ${rtt}ms | Latenz: ${currentLatencyMs.toFixed(1)}ms`);
}
// B. CHUNK (Objekte empfangen)
if (msg.type === "chunk") {
// 1. CLOCK SYNC (Die Zeitmaschine)
// Wenn der Server bei Tick 204 ist und wir bei 182, müssen wir aufholen!
// Wir addieren die geschätzte Latenz (in Ticks) auf die Serverzeit.
// 60 FPS = 16ms/Tick. 20 TPS = 50ms/Tick.
const msPerTick = 1000 / 20; // WICHTIG: Wir laufen auf 20 TPS Basis!
const latencyInTicks = Math.floor(currentLatencyMs / msPerTick);
// Ziel-Zeit: Server-Zeit + Übertragungsweg
const targetTick = msg.serverTick + latencyInTicks;
const drift = targetTick - currentTick;
// Wenn wir mehr als 2 Ticks abweichen -> Korrigieren
if (Math.abs(drift) > 2) {
// console.log(`⏰ Clock Sync: ${currentTick} -> ${targetTick} (Drift: ${drift})`);
currentTick = targetTick; // Harter Sync, damit Physik stimmt
}
// 2. PIXEL KORREKTUR (Sanfter!)
// Wir berechnen den Speed
let sTick = msg.serverTick;
// Formel aus logic.js (Base 15 + Zeit)
let currentSpeedPerTick = 15.0 + (sTick / 1000.0) * 1.5;
if (currentSpeedPerTick > 36) currentSpeedPerTick = 36;
const speedPerMs = currentSpeedPerTick / msPerTick; // Speed pro MS
// Korrektur: Latenz * Speed
// FIX: Wir kappen die Korrektur bei max 100px, damit Objekte nicht "teleportieren".
let dynamicCorrection = (currentLatencyMs * speedPerMs) + 5;
if (dynamicCorrection > 100) dynamicCorrection = 100; // Limit
// Puffer füllen (mit Limit)
if (msg.obstacles) {
msg.obstacles.forEach(o => {
o.x -= dynamicCorrection;
// Init für Interpolation
o.prevX = o.x;
obstacleBuffer.push(o);
});
}
if (msg.platforms) {
msg.platforms.forEach(p => {
p.x -= dynamicCorrection;
p.prevX = p.x;
platformBuffer.push(p);
});
}
if (msg.score !== undefined) score = msg.score;
// Powerups übernehmen (für Anzeige)
if (msg.powerups) {
godModeLives = msg.powerups.godLives;
hasBat = msg.powerups.hasBat;
bootTicks = msg.powerups.bootTicks;
}
}
// Sync Spawning Timer
if (data.NextSpawnTick) {
if (Math.abs(nextSpawnTick - data.nextSpawnTick) > 5) {
console.log("Sync Spawn Timer:", nextSpawnTick, "->", data.NextSpawnTick);
nextSpawnTick = data.nextSpawnTick;
if (msg.type === "init") {
console.log("📩 INIT EMPFANGEN:", msg); // <--- DEBUG LOG
if (msg.sessionId) {
sessionID = msg.sessionId; // Globale Variable setzen
console.log("🔑 Session ID gesetzt auf:", sessionID);
} else {
console.error("❌ INIT FEHLER: Keine sessionId im Paket!", msg);
}
}
}
// C. TOD (Server Authoritative)
if (msg.type === "dead") {
console.log("💀 Server sagt: Game Over");
if (data.status === "dead") {
console.error("💀 SERVER KILL", data);
gameOver("Vom Server gestoppt");
} else {
const sScore = data.verifiedScore;
// Score Korrektur
if (Math.abs(score - sScore) > 200) {
console.warn(`⚠️ SCORE DRIFT: Client=${score} Server=${sScore}`);
score = sScore;
if (msg.score) score = msg.score;
// Verbindung sauber trennen
socket.close();
if (pingInterval) clearInterval(pingInterval);
gameOver("Vom Server gestoppt");
}
if (msg.type === "debug_sync") {
// 1. CLIENT SPEED BERECHNEN (Formel aus logic.js)
// Wir nutzen hier 'score', da logic.js das auch tut
let clientSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
if (clientSpeed > 36.0) clientSpeed = 36.0;
// 2. SERVER SPEED HOLEN
let serverSpeed = msg.currentSpeed || 0;
// 3. DIFF BERECHNEN
let diffSpeed = clientSpeed - serverSpeed;
let speedIcon = Math.abs(diffSpeed) < 0.01 ? "✅" : "❌";
console.group(`📊 SYNC REPORT (Tick: ${currentTick} vs Server: ${msg.serverTick})`);
// --- DER NEUE SPEED CHECK ---
console.log(`🚀 SPEED CHECK: ${speedIcon}`);
console.log(` Client: ${clientSpeed.toFixed(4)} px/tick (Basis: Tick ${currentTick})`);
console.log(` Server: ${serverSpeed.toFixed(4)} px/tick (Basis: Tick ${msg.serverTick})`);
if (Math.abs(diffSpeed) > 0.01) {
console.warn(`⚠️ ACHTUNG: Geschwindigkeiten weichen ab! Diff: ${diffSpeed.toFixed(4)}`);
console.warn("Ursache: Client nutzt 'Score', Server nutzt 'Ticks'. Sind diese synchron?");
}
// -----------------------------
// 1. Hindernisse vergleichen
generateSyncTable("Obstacles", obstacles, msg.obstacles);
// 2. Plattformen vergleichen
generateSyncTable("Platforms", platforms, msg.platforms);
console.groupEnd();
}
} catch (e) {
console.error("Fehler beim Verarbeiten der Nachricht:", e);
}
};
} catch (e) {
console.error("Netzwerkfehler:", e);
// --- 3. VERBINDUNG GETRENNT ---
socket.onclose = () => {
console.log("🔴 WS Verbindung getrennt.");
if (pingInterval) clearInterval(pingInterval);
};
socket.onerror = (error) => {
console.error("WS Fehler:", error);
};
}
// ==========================================
// PING SENDEN
// ==========================================
function sendPing() {
if (socket && socket.readyState === WebSocket.OPEN) {
// Wir senden den aktuellen Zeitstempel
// Der Server muss diesen im "tick" Feld zurückschicken (siehe websocket.go)
socket.send(JSON.stringify({
type: "ping",
tick: Date.now() // Timestamp als Integer
}));
}
}
window.submitScore = async function() {
const nameInput = document.getElementById('playerNameInput');
const name = nameInput.value;
const btn = document.getElementById('submitBtn');
if (!name) return alert("Namen eingeben!");
btn.disabled = true;
try {
const res = await fetch('/api/submit-name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sessionID, name: name })
});
const data = await res.json();
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
myClaims.push({
name: name, score: Math.floor(score / 10), code: data.claimCode,
date: new Date().toLocaleString('de-DE'), sessionId: sessionID
});
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
document.getElementById('inputSection').style.display = 'none';
loadLeaderboard();
} catch (e) {}
};
async function loadLeaderboard() {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json();
let html = "<h3>BESTENLISTE</h3>";
entries.forEach(e => {
const color = e.isMe ? "yellow" : "white";
html += `<div style="display:flex; justify-content:space-between; color:${color}; margin-bottom:5px;">
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
});
document.getElementById('leaderboard').innerHTML = html;
}
async function loadStartScreenLeaderboard() {
try {
const listEl = document.getElementById('startLeaderboardList');
if (!listEl) return;
const res = await fetch('/api/leaderboard');
const entries = await res.json();
if (entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
let html = "";
entries.forEach(e => {
let icon = "#" + e.rank;
if (e.rank === 1) icon = "🥇"; if (e.rank === 2) icon = "🥈"; if (e.rank === 3) icon = "🥉";
html += `<div class="hof-entry"><span><span class="hof-rank">${icon}</span> ${e.name}</span><span class="hof-score">${Math.floor(e.score / 10)}</span></div>`;
});
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}`);
// ==========================================
// INPUT SENDEN
// ==========================================
function sendInput(type, action) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: "input",
input: action
}));
}
}
// Helper für die Tabelle
function generateSyncTable(label, clientList, serverList) {
if (!serverList) serverList = [];
console.log(`--- ${label} Analyse (Ping: ${Math.round(currentLatencyMs)}ms) ---`);
const report = [];
const maxLen = Math.max(clientObs.length, serverObs.length);
let hasMajorDrift = false;
const matchedServerIndices = new Set();
for (let i = 0; i < maxLen; i++) {
const cli = clientObs[i];
const srv = serverObs[i];
// 1. Parameter für Latenz-Korrektur berechnen
// Damit wir wissen: "Wo MÜSSTE das Server-Objekt auf dem Client sein?"
const msPerTick = 50; // Bei 20 TPS
let drift = 0;
let status = "✅ OK";
// Speed Schätzung (gleiche Formel wie in logic.js)
let debugSpeed = 15.0 + (score / 1000.0) * 1.5;
if (debugSpeed > 36) debugSpeed = 36;
// Client Objekt vorbereiten
let cID = "---";
let cX = 0;
if (cli) {
cID = cli.def.id; // Struktur beachten: cli.def.id
cX = cli.x;
}
const speedPerMs = debugSpeed / msPerTick;
// Server Objekt vorbereiten
let sID = "---";
let sX = 0;
if (srv) {
sID = srv.id; // Struktur vom Server: srv.id
sX = srv.x;
}
// Pixel, die das Objekt wegen Ping weiter "links" sein müsste
const latencyPx = currentLatencyMs * speedPerMs;
// 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;
}
// 2. Client Objekte durchgehen
clientList.forEach((cObj) => {
let bestMatch = null;
let bestDist = 9999;
let bestSIdx = -1;
// ID sicherstellen
const cID = cObj.def ? cObj.def.id : (cObj.id || "unknown");
// Passendes Server-Objekt suchen
serverList.forEach((sObj, sIdx) => {
if (matchedServerIndices.has(sIdx)) return;
const sID = sObj.id || "unknown";
// Match Kriterien:
// 1. Gleiche ID (oder Plattform)
// 2. Nähe (Wir vergleichen hier die korrigierte Position!)
const sPosCorrected = sObj.x - latencyPx;
const dist = Math.abs(cObj.x - sPosCorrected);
const isTypeMatch = (label === "Platforms") || (cID === sID);
// Toleranter Suchradius (500px), falls Drift groß ist
if (isTypeMatch && dist < bestDist && dist < 500) {
bestDist = dist;
bestMatch = sObj;
bestSIdx = sIdx;
}
} else {
status = "❌ MISSING";
hasMajorDrift = true;
});
// Datenzeile bauen
let serverXRaw = "---";
let serverXCorrected = "---";
let diffReal = "---";
let status = "👻 GHOST (Client only)";
if (bestMatch) {
matchedServerIndices.add(bestSIdx);
serverXRaw = bestMatch.x;
serverXCorrected = bestMatch.x - latencyPx; // Hier rechnen wir den Ping raus
// Der "Wahrs" Drift: Differenz nach Latenz-Abzug
diffReal = cObj.x - serverXCorrected;
// Status Bestimmung
const absDiff = Math.abs(diffReal);
if (absDiff < 20) status = "✅ PERFECT";
else if (absDiff < 60) status = "🆗 OK";
else if (absDiff < 150) status = "⚠️ DRIFT";
else status = "🔥 BROKEN";
}
// 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)
"ID": cID,
"Client X": Math.round(cObj.x),
"Server X (Raw)": Math.round(serverXRaw),
"Server X (Sim)": Math.round(serverXCorrected), // Wo es sein sollte
"Diff (Real)": typeof diffReal === 'number' ? Math.round(diffReal) : "---",
"Status": status
});
}
});
// 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) ---");
// 3. Fehlende Server Objekte finden
serverList.forEach((sObj, sIdx) => {
if (!matchedServerIndices.has(sIdx)) {
// Prüfen, ob es vielleicht einfach noch unsichtbar ist (Zukunft)
const sPosCorrected = sObj.x - latencyPx;
let status = "❌ MISSING";
console.table(report); // Das erstellt eine super lesbare Tabelle im Browser
if (sPosCorrected > 850) status = "🔮 FUTURE (Buffer)"; // Noch rechts vom Screen
if (sPosCorrected < -100) status = "🗑️ OLD (Server lag)"; // Schon links raus
report.push({
"ID": sObj.id || "?",
"Client X": "---",
"Server X (Raw)": Math.round(sObj.x),
"Server X (Sim)": Math.round(sPosCorrected),
"Diff (Real)": "---",
"Status": status
});
}
});
// 4. Sortieren nach Position (links nach rechts)
report.sort((a, b) => {
const valA = (typeof a["Client X"] === 'number') ? a["Client X"] : a["Server X (Sim)"];
const valB = (typeof b["Client X"] === 'number') ? b["Client X"] : b["Server X (Sim)"];
return valA - valB;
});
if (report.length > 0) console.table(report);
else console.log("Leer.");
}
function sendPhysicsSync(y, vy) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: "sync",
y: y,
vy: vy,
tick: currentTick
}));
}
}