// ========================================== // NETZWERK LOGIK (WEBSOCKET + RTT SYNC) // ========================================== /* GLOBALE VARIABLEN (aus state.js): - socket - obstacleBuffer, platformBuffer - currentLatencyMs, pingInterval - isGameRunning, isGameOver - score, currentTick */ function connectGame() { // Alte Verbindung schließen if (socket) { socket.close(); } // 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); }; // --- 2. NACHRICHT VOM SERVER --- socket.onmessage = (event) => { try { const msg = JSON.parse(event.data); // A. PONG (Latenzmessung) if (msg.type === "pong") { const now = Date.now(); const sentTime = msg.ts; // Server schickt unseren Timestamp zurück // Round Trip Time (Hin + Zurück) const rtt = now - sentTime; // One Way Latency (Latenz in eine Richtung) const latency = rtt / 2; // 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 { // 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; } } 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 (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); } }; // --- 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 })); } } // ========================================== // 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 matchedServerIndices = new Set(); // 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 // Speed Schätzung (gleiche Formel wie in logic.js) let debugSpeed = 15.0 + (score / 1000.0) * 1.5; if (debugSpeed > 36) debugSpeed = 36; const speedPerMs = debugSpeed / msPerTick; // Pixel, die das Objekt wegen Ping weiter "links" sein müsste const latencyPx = currentLatencyMs * speedPerMs; // 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; } }); // 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"; } report.push({ "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 }); }); // 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"; 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 })); } }