fix README, SYNC, DATENSCHUTZ
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -88,47 +88,91 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-impressum" class="modal-overlay">
|
||||
<div id="modal-impressum" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<button class="close-modal" onclick="closeModal()">X</button>
|
||||
<h2>Impressum</h2>
|
||||
<p><strong>Angaben gemäß § 5 TMG:</strong></p>
|
||||
<p>
|
||||
<h2>Impressum & Credits</h2>
|
||||
|
||||
<p><strong>Projektleitung & Code:</strong><br>
|
||||
Sebastian Unterschütz<br>
|
||||
Göltzschtalblick 16 <br>
|
||||
08236 Ellefeld
|
||||
Göltzschtalblick 16<br>
|
||||
08236 Ellefeld<br>
|
||||
<small>Kontakt: sebastian@unterschutz.de</small>
|
||||
</p>
|
||||
|
||||
<hr style="border:1px solid #444; margin: 15px 0;">
|
||||
|
||||
<p><strong>🎵 Musik & Sound Design:</strong><br>
|
||||
<span style="color:#ffcc00; font-size:18px;">Max E.</span>
|
||||
</p>
|
||||
|
||||
<p><strong>💻 Quellcode:</strong><br>
|
||||
<a href="https://git.zb-server.de/ZB-Server/it232Abschied" target="_blank" style="color:#2196F3; text-decoration:none;">
|
||||
git.zb-server.de/ZB-Server/it232Abschied
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<hr style="border:1px solid #444; margin: 15px 0;">
|
||||
|
||||
<p><strong>⚖️ Lizenzhinweis:</strong></p>
|
||||
<p style="font-size:12px; color:#aaa;">
|
||||
Dies ist ein Schulprojekt. <br>
|
||||
<strong>Kommerzielle Nutzung und Veränderung des Quellcodes sind ausdrücklich untersagt.</strong><br>
|
||||
Alle Rechte liegen bei den Urhebern.
|
||||
</p>
|
||||
<p>Kontakt: sebastian@unterschuetz.de</p>
|
||||
<p><em>Dies ist ein Schulprojekt ohne kommerzielle Absicht.</em></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-datenschutz" class="modal-overlay">
|
||||
<div id="modal-datenschutz" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<button class="close-modal" onclick="closeModal()">X</button>
|
||||
<h2>Datenschutz</h2>
|
||||
<h2>Datenschutzerklärung</h2>
|
||||
|
||||
<p><strong>1. Allgemeines</strong><br>
|
||||
Dies ist ein Schulprojekt. Wir speichern so wenig Daten wie möglich.</p>
|
||||
<p><strong>1. Datenschutz auf einen Blick</strong><br>
|
||||
Allgemeine Hinweise: Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.</p>
|
||||
|
||||
<p><strong>2. Welche Daten speichern wir?</strong><br>
|
||||
Wenn du einen Highscore einträgst, speichern wir auf unserem Server:
|
||||
<p><strong>2. Verantwortlicher</strong><br>
|
||||
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
||||
Sebastian Unterschütz<br>
|
||||
Göltzschtalblick 16, 08236 Ellefeld<br>
|
||||
E-Mail: sebastian@unterschutz.de<br>
|
||||
<em>(Schulprojekt im Rahmen der IT232)</em></p>
|
||||
|
||||
<p><strong>3. Hosting (Hetzner)</strong><br>
|
||||
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
||||
<strong>Hetzner Online GmbH</strong><br>
|
||||
Industriestr. 25<br>
|
||||
91710 Gunzenhausen<br>
|
||||
Deutschland<br>
|
||||
<br>
|
||||
Serverstandort: <strong>Deutschland</strong> (ausschließlich).<br>
|
||||
Wir haben mit dem Anbieter einen Vertrag zur Auftragsverarbeitung (AVV) geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.</p>
|
||||
|
||||
<p><strong>4. Datenerfassung auf dieser Website</strong></p>
|
||||
|
||||
<p><strong>Server-Log-Dateien</strong><br>
|
||||
Der Provider der Seiten (Hetzner) erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
|
||||
<ul>
|
||||
<li>Deinen gewählten Namen</li>
|
||||
<li>Deinen Punktestand</li>
|
||||
<li>Einen Zeitstempel</li>
|
||||
<li>Einen zufälligen "Beweis-Code"</li>
|
||||
<li>Uhrzeit der Serveranfrage</li>
|
||||
<li>IP-Adresse</li>
|
||||
</ul>
|
||||
</p>
|
||||
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.<br>
|
||||
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an der technisch fehlerfreien Darstellung und der Optimierung seiner Website – hierzu müssen die Server-Log-Files erfasst werden. Die Daten werden nach spätestens 7 Tagen automatisch gelöscht.</p>
|
||||
|
||||
<p><strong>3. Lokale Speicherung (Dein Gerät)</strong><br>
|
||||
Das Spiel nutzt den "LocalStorage" deines Browsers, um deinen persönlichen Rekord und deine gesammelten Beweis-Codes zu speichern. Diese Daten verlassen dein Gerät nicht, außer du sendest sie aktiv ab.</p>
|
||||
<p><strong>Spielstände & Highscores</strong><br>
|
||||
Wenn Sie einen Highscore eintragen, speichern wir in unserer Datenbank:
|
||||
<ul>
|
||||
<li>Den von Ihnen gewählten Namen (Pseudonym empfohlen!)</li>
|
||||
<li>Ihren Punktestand und Zeitstempel</li>
|
||||
<li>Eine Session-ID und einen "Claim-Code" zur Verifizierung</li>
|
||||
</ul>
|
||||
Diese Daten dienen ausschließlich der Darstellung der Bestenliste und der Spielmechanik.</p>
|
||||
|
||||
<p><strong>4. Cookies & Tracking</strong><br>
|
||||
Wir verwenden <strong>keine</strong> Tracking-Cookies, keine Analyse-Tools (wie Google Analytics) und laden keine Schriftarten von fremden Servern.</p>
|
||||
<p><strong>Lokale Speicherung (LocalStorage)</strong><br>
|
||||
Das Spiel speichert Einstellungen (z.B. "Ton aus") und Ihre persönlichen "Claim-Codes" lokal in Ihrem Browser (`LocalStorage`). Diese Daten verlassen Ihr Gerät nicht, außer Sie übermitteln einen Highscore aktiv an den Server. Wir setzen <strong>keine Tracking-Cookies</strong> oder Analyse-Tools (wie Google Analytics) ein.</p>
|
||||
|
||||
<p><strong>5. Deine Rechte</strong><br>
|
||||
Du kannst deine Einträge jederzeit selbstständig über das Menü "Meine Codes" vom Server löschen.</p>
|
||||
<p><strong>5. Ihre Rechte</strong><br>
|
||||
Sie haben jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger und den Zweck der Datenverarbeitung sowie ein Recht auf Berichtigung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit an die im Impressum angegebene Adresse wenden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ SOUNDS.hit.volume = 0.6;
|
||||
SOUNDS.music.loop = true;
|
||||
SOUNDS.music.volume = 0.2;
|
||||
|
||||
// --- STATUS LADEN ---
|
||||
// Wir lesen den String 'true'/'false' aus dem LocalStorage
|
||||
|
||||
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
||||
|
||||
function playSound(name) {
|
||||
@@ -29,15 +28,13 @@ function playSound(name) {
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
|
||||
// --- STATUS SPEICHERN ---
|
||||
|
||||
localStorage.setItem('escape_muted', isMuted);
|
||||
|
||||
// Musik sofort pausieren/starten
|
||||
|
||||
if(isMuted) {
|
||||
SOUNDS.music.pause();
|
||||
} else {
|
||||
// Nur starten, wenn wir schon im Spiel sind (user interaction needed)
|
||||
// Wir fangen Fehler ab, falls der Browser Autoplay blockiert
|
||||
SOUNDS.music.play().catch(()=>{});
|
||||
}
|
||||
|
||||
@@ -45,13 +42,13 @@ function toggleMute() {
|
||||
}
|
||||
|
||||
function startMusic() {
|
||||
// Nur abspielen, wenn NICHT stummgeschaltet
|
||||
|
||||
if(!isMuted) {
|
||||
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Getter für UI
|
||||
|
||||
function getMuteState() {
|
||||
return isMuted;
|
||||
}
|
||||
@@ -1,27 +1,22 @@
|
||||
// ==========================================
|
||||
// SPIEL KONFIGURATION & KONSTANTEN
|
||||
// ==========================================
|
||||
|
||||
// Dimensionen (Muss zum Canvas passen)
|
||||
|
||||
const GAME_WIDTH = 800;
|
||||
const GAME_HEIGHT = 400;
|
||||
|
||||
// Physik (Muss exakt synchron zum Go-Server sein!)
|
||||
const GRAVITY = 1.8;
|
||||
const JUMP_POWER = -20.0; // Vorher -36.0 (Deutlich weniger!)
|
||||
const HIGH_JUMP_POWER = -28.0;// Vorher -48.0 (Boots)
|
||||
const GROUND_Y = 350; // Y-Position des Bodens
|
||||
const JUMP_POWER = -20.0;
|
||||
const HIGH_JUMP_POWER = -28.0;
|
||||
const GROUND_Y = 350;
|
||||
|
||||
|
||||
// Geschwindigkeit
|
||||
const BASE_SPEED = 15.0;
|
||||
|
||||
// Game Loop Einstellungen
|
||||
|
||||
const TARGET_FPS = 20;
|
||||
const MS_PER_TICK = 1000 / TARGET_FPS;
|
||||
const CHUNK_SIZE = 20; // Intervall für Berechnungen (Legacy)
|
||||
const CHUNK_SIZE = 20;
|
||||
|
||||
|
||||
// Debugging
|
||||
// true = Zeigt Hitboxen (Grün) und Server-Daten (Cyan)
|
||||
const DEBUG_SYNC = true;
|
||||
|
||||
function lerp(a, b, t) {
|
||||
|
||||
@@ -3,41 +3,31 @@
|
||||
// ==========================================
|
||||
|
||||
function handleInput(action, active) {
|
||||
// 1. Game Over Reset
|
||||
if (isGameOver) {
|
||||
if(active) location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. JUMP LOGIK
|
||||
if (action === "JUMP" && active) {
|
||||
// Wir prüfen lokal, ob wir springen dürfen (Client Prediction)
|
||||
if (player.grounded && !isCrouching) {
|
||||
|
||||
// A. Sofort lokal anwenden (damit es sich direkt anfühlt)
|
||||
player.vy = JUMP_POWER;
|
||||
player.grounded = false;
|
||||
|
||||
playSound('jump');
|
||||
spawnParticles(player.x + 15, player.y + 50, 'dust', 5); // Staubwolke an den Füßen
|
||||
|
||||
// B. An Server senden ("Ich habe JETZT gedrückt")
|
||||
// Die Funktion sendInput ist in network.js definiert
|
||||
if (typeof sendInput === "function") {
|
||||
sendInput("input", "JUMP");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. DUCK LOGIK
|
||||
if (action === "DUCK") {
|
||||
// Status merken, um unnötiges Senden zu vermeiden
|
||||
const wasCrouching = isCrouching;
|
||||
|
||||
// A. Lokal anwenden
|
||||
isCrouching = active;
|
||||
|
||||
// B. An Server senden (State Change: Start oder Ende)
|
||||
if (wasCrouching !== isCrouching) {
|
||||
if (typeof sendInput === "function") {
|
||||
sendInput("input", active ? "DUCK_START" : "DUCK_END");
|
||||
@@ -50,9 +40,9 @@ function handleInput(action, active) {
|
||||
// EVENT LISTENERS
|
||||
// ==========================================
|
||||
|
||||
// Tastatur
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Ignorieren, wenn User gerade Name in Highscore tippt
|
||||
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true);
|
||||
@@ -61,7 +51,6 @@ window.addEventListener('keydown', (e) => {
|
||||
e.preventDefault();
|
||||
console.log("🐞 Fordere Debug-Daten vom Server an...");
|
||||
if (typeof sendInput === "function") {
|
||||
// Wir senden ein manuelles Paket, da sendInput meist nur für Game-Inputs ist
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: "debug" }));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
function updateGameLogic() {
|
||||
// ===============================================
|
||||
// 1. GESCHWINDIGKEIT
|
||||
// ===============================================
|
||||
// Wir nutzen den lokalen Score für die Geschwindigkeit
|
||||
let currentSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
|
||||
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
||||
|
||||
@@ -14,33 +10,25 @@ function updateGameLogic() {
|
||||
obstacleBuffer.forEach(o => o.prevX = o.x);
|
||||
platformBuffer.forEach(p => p.prevX = p.x);
|
||||
|
||||
// ===============================================
|
||||
// 2. SPIELER PHYSIK (CLIENT PREDICTION)
|
||||
// ===============================================
|
||||
const originalHeight = 50;
|
||||
const crouchHeight = 25;
|
||||
|
||||
// Hitbox & Y-Pos anpassen
|
||||
player.h = isCrouching ? crouchHeight : originalHeight;
|
||||
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
|
||||
|
||||
// Alte Position (für One-Way Check)
|
||||
const oldY = player.y;
|
||||
|
||||
// Physik
|
||||
player.vy += GRAVITY;
|
||||
if (isCrouching && !player.grounded) player.vy += 2.0;
|
||||
|
||||
let newY = player.y + player.vy;
|
||||
let landed = false;
|
||||
|
||||
// --- PLATTFORMEN ---
|
||||
|
||||
if (player.vy > 0) {
|
||||
for (let plat of platformBuffer) {
|
||||
// Nur relevante Plattformen prüfen
|
||||
if (plat.x < GAME_WIDTH + 100 && plat.x > -100) {
|
||||
if (player.x + 30 > plat.x && player.x < plat.x + plat.w) {
|
||||
// "Passed Check": Vorher drüber, jetzt drauf/drunter
|
||||
const feetOld = oldY + originalHeight;
|
||||
const feetNew = newY + originalHeight;
|
||||
if (feetOld <= plat.y && feetNew >= plat.y) {
|
||||
@@ -55,7 +43,7 @@ function updateGameLogic() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- BODEN ---
|
||||
|
||||
if (!landed && newY + originalHeight >= GROUND_Y) {
|
||||
newY = GROUND_Y - originalHeight;
|
||||
player.vy = 0;
|
||||
@@ -69,34 +57,28 @@ function updateGameLogic() {
|
||||
player.y = newY;
|
||||
player.grounded = landed;
|
||||
|
||||
// ===============================================
|
||||
// 3. PUFFER BEWEGEN (STREAMING)
|
||||
// ===============================================
|
||||
|
||||
obstacleBuffer.forEach(o => o.x -= currentSpeed);
|
||||
platformBuffer.forEach(p => p.x -= currentSpeed);
|
||||
|
||||
// Aufräumen (Links raus)
|
||||
|
||||
obstacleBuffer = obstacleBuffer.filter(o => o.x + (o.w||30) > -200); // Muss -200 sein
|
||||
platformBuffer = platformBuffer.filter(p => p.x + (p.w||100) > -200); // Muss -200 sein
|
||||
|
||||
// ===============================================
|
||||
// 4. KOLLISION & TRANSFER (LOGIK + RENDER LISTE)
|
||||
// ===============================================
|
||||
|
||||
obstacles = [];
|
||||
platforms = [];
|
||||
const RENDER_LIMIT = 900;
|
||||
|
||||
// Hitbox definieren (für lokale Prüfung)
|
||||
|
||||
const pHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
|
||||
|
||||
// --- HINDERNISSE ---
|
||||
|
||||
obstacleBuffer.forEach(obs => {
|
||||
// Nur verarbeiten, wenn im Sichtbereich
|
||||
|
||||
if (obs.x < RENDER_LIMIT) {
|
||||
|
||||
// A. Metadaten laden (falls noch nicht da)
|
||||
|
||||
if (!obs.def) {
|
||||
let baseDef = null;
|
||||
if(gameConfig && gameConfig.obstacles) {
|
||||
@@ -115,8 +97,6 @@ function updateGameLogic() {
|
||||
};
|
||||
}
|
||||
|
||||
// B. Kollision prüfen (Nur wenn noch nicht eingesammelt)
|
||||
// Wir nutzen 'obs.collected' als Flag, damit wir Coins nicht doppelt zählen
|
||||
if (!obs.collected && !isGameOver) {
|
||||
if (checkCollision(pHitbox, obs)) {
|
||||
|
||||
@@ -125,8 +105,8 @@ function updateGameLogic() {
|
||||
|
||||
// 1. COIN
|
||||
if (type === "coin") {
|
||||
score += 2000; // Sofort addieren!
|
||||
obs.collected = true; // Markieren als "weg"
|
||||
score += 2000;
|
||||
obs.collected = true;
|
||||
playSound('coin');
|
||||
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 10);
|
||||
}
|
||||
@@ -134,27 +114,27 @@ function updateGameLogic() {
|
||||
else if (type === "powerup") {
|
||||
if (id === "p_god") godModeLives = 3;
|
||||
if (id === "p_bat") hasBat = true;
|
||||
if (id === "p_boot") bootTicks = 600; // ca. 10 Sekunden
|
||||
if (id === "p_boot") bootTicks = 600;
|
||||
playSound('powerup');
|
||||
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 20); // Mehr Partikel
|
||||
|
||||
obs.collected = true; // Markieren als "weg"
|
||||
obs.collected = true;
|
||||
}
|
||||
// 3. GEGNER (Teacher/Obstacle)
|
||||
|
||||
else {
|
||||
// Baseballschläger vs Lehrer
|
||||
|
||||
if (hasBat && type === "teacher") {
|
||||
hasBat = false;
|
||||
obs.collected = true; // Wegschlagen
|
||||
obs.collected = true;
|
||||
playSound('hit');
|
||||
spawnParticles(obs.x, obs.y, 'explosion', 5);
|
||||
// Effekt?
|
||||
|
||||
}
|
||||
// Godmode (Schild)
|
||||
else if (godModeLives > 0) {
|
||||
godModeLives--;
|
||||
// Optional: Gegner entfernen oder durchlaufen lassen?
|
||||
// Hier entfernen wir ihn, damit man nicht 2 Leben im selben Objekt verliert
|
||||
playSound('hit');
|
||||
spawnParticles(obs.x, obs.y, 'explosion', 5);
|
||||
obs.collected = true;
|
||||
}
|
||||
// TOT
|
||||
@@ -170,14 +150,13 @@ function updateGameLogic() {
|
||||
}
|
||||
}
|
||||
|
||||
// C. Zur Render-Liste hinzufügen (Nur wenn NICHT eingesammelt)
|
||||
if (!obs.collected) {
|
||||
obstacles.push(obs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- PLATTFORMEN ---
|
||||
|
||||
platformBuffer.forEach(plat => {
|
||||
if (plat.x < RENDER_LIMIT) {
|
||||
platforms.push(plat);
|
||||
@@ -185,17 +164,17 @@ function updateGameLogic() {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Robuste Kollisionsprüfung
|
||||
|
||||
function checkCollision(p, obs) {
|
||||
const def = obs.def || {};
|
||||
const w = def.width || obs.w || 30;
|
||||
const h = def.height || obs.h || 30;
|
||||
|
||||
// Kleines Padding, damit es fair ist
|
||||
|
||||
const padX = 8;
|
||||
const padY = (def.type === "teacher" || def.type === "principal") ? 20 : 5;
|
||||
|
||||
// Koordinaten
|
||||
|
||||
const pL = p.x + padX;
|
||||
const pR = p.x + p.w - padX;
|
||||
const pT = p.y + padY;
|
||||
|
||||
@@ -30,9 +30,7 @@ async function loadAssets() {
|
||||
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. SPIEL STARTEN
|
||||
// ==========================================
|
||||
|
||||
window.startGameClick = async function() {
|
||||
if (!isLoaded) return;
|
||||
|
||||
@@ -50,9 +48,7 @@ window.startGameClick = async function() {
|
||||
resize();
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// 3. GAME OVER & HIGHSCORE LOGIK
|
||||
// ==========================================
|
||||
|
||||
window.gameOver = function(reason) {
|
||||
if (isGameOver) return;
|
||||
isGameOver = true;
|
||||
@@ -69,16 +65,16 @@ window.gameOver = function(reason) {
|
||||
gameOverScreen.style.display = 'flex';
|
||||
document.getElementById('finalScore').innerText = finalScore;
|
||||
|
||||
// Input wieder anzeigen
|
||||
|
||||
document.getElementById('inputSection').style.display = 'flex';
|
||||
document.getElementById('submitBtn').disabled = false;
|
||||
|
||||
// Liste laden
|
||||
|
||||
loadLeaderboard();
|
||||
}
|
||||
};
|
||||
|
||||
// Name absenden (Button Click)
|
||||
|
||||
window.submitScore = async function() {
|
||||
const nameInput = document.getElementById('playerNameInput');
|
||||
const name = nameInput.value.trim();
|
||||
@@ -91,14 +87,14 @@ window.submitScore = async function() {
|
||||
const res = await fetch('/api/submit-name', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ sessionId: sessionID, name: name }) // sessionID aus state.js
|
||||
body: JSON.stringify({ sessionId: sessionID, name: name })
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Fehler beim Senden");
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Code lokal speichern (Claims)
|
||||
|
||||
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
myClaims.push({
|
||||
name: name,
|
||||
@@ -109,7 +105,7 @@ window.submitScore = async function() {
|
||||
});
|
||||
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
||||
|
||||
// UI Update
|
||||
|
||||
document.getElementById('inputSection').style.display = 'none';
|
||||
loadLeaderboard();
|
||||
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
||||
@@ -121,10 +117,10 @@ window.submitScore = async function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Bestenliste laden (Game Over Screen)
|
||||
|
||||
async function loadLeaderboard() {
|
||||
try {
|
||||
// sessionID wird mitgesendet, um den eigenen Eintrag zu markieren
|
||||
|
||||
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
||||
const entries = await res.json();
|
||||
|
||||
@@ -149,9 +145,7 @@ async function loadLeaderboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 4. GAME LOOP
|
||||
// ==========================================
|
||||
|
||||
function gameLoop(timestamp) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
@@ -183,9 +177,7 @@ function gameLoop(timestamp) {
|
||||
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 5. INIT
|
||||
// ==========================================
|
||||
|
||||
async function initGame() {
|
||||
try {
|
||||
const cRes = await fetch('/api/config');
|
||||
@@ -215,7 +207,7 @@ async function initGame() {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Mini-Leaderboard auf Startseite
|
||||
|
||||
async function loadStartScreenLeaderboard() {
|
||||
try {
|
||||
const listEl = document.getElementById('startLeaderboardList');
|
||||
@@ -235,15 +227,13 @@ async function loadStartScreenLeaderboard() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Audio Toggle Funktion für den Button
|
||||
|
||||
window.toggleAudioClick = function() {
|
||||
// 1. Audio umschalten (in audio.js)
|
||||
const muted = toggleMute();
|
||||
|
||||
// 2. Button Icon updaten
|
||||
updateMuteIcon(muted);
|
||||
|
||||
// 3. Fokus vom Button nehmen (damit Space nicht den Button drückt, sondern springt)
|
||||
|
||||
document.getElementById('mute-btn').blur();
|
||||
};
|
||||
|
||||
@@ -256,19 +246,15 @@ function updateMuteIcon(isMuted) {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MEINE CODES (LOCAL STORAGE)
|
||||
// ==========================================
|
||||
|
||||
// 1. Codes anzeigen (Wird vom Button im Startscreen aufgerufen)
|
||||
window.showMyCodes = function() {
|
||||
// Modal öffnen
|
||||
|
||||
openModal('codes');
|
||||
|
||||
const listEl = document.getElementById('codesList');
|
||||
if(!listEl) return;
|
||||
|
||||
// Daten aus dem Browser-Speicher holen
|
||||
|
||||
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
|
||||
if (rawClaims.length === 0) {
|
||||
@@ -276,7 +262,7 @@ window.showMyCodes = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sortieren nach Score (Höchster zuerst)
|
||||
|
||||
const sortedClaims = rawClaims
|
||||
.map((item, index) => ({ ...item, originalIndex: index }))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
@@ -284,7 +270,7 @@ window.showMyCodes = function() {
|
||||
let html = "";
|
||||
|
||||
sortedClaims.forEach(c => {
|
||||
// Icons basierend auf Score
|
||||
|
||||
let rankIcon = "📄";
|
||||
if (c.score >= 5000) rankIcon = "⭐";
|
||||
if (c.score >= 10000) rankIcon = "🔥";
|
||||
@@ -307,11 +293,11 @@ window.showMyCodes = function() {
|
||||
listEl.innerHTML = html;
|
||||
};
|
||||
|
||||
// 2. Code löschen (Lokal und auf Server)
|
||||
|
||||
window.deleteClaim = async function(sid, code) {
|
||||
if(!confirm("Eintrag wirklich löschen?")) return;
|
||||
|
||||
// Versuch, es auf dem Server zu löschen
|
||||
|
||||
try {
|
||||
await fetch('/api/claim/delete', {
|
||||
method: 'POST',
|
||||
@@ -322,25 +308,23 @@ window.deleteClaim = async function(sid, code) {
|
||||
console.warn("Server Delete fehlgeschlagen (vielleicht schon weg), lösche lokal...");
|
||||
}
|
||||
|
||||
// Lokal löschen
|
||||
|
||||
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
// Wir filtern den Eintrag raus, der die gleiche SessionID UND den gleichen Code hat
|
||||
|
||||
claims = claims.filter(c => c.code !== code);
|
||||
|
||||
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
||||
|
||||
// Liste aktualisieren
|
||||
|
||||
window.showMyCodes();
|
||||
|
||||
// Leaderboard aktualisieren (falls im Hintergrund sichtbar)
|
||||
|
||||
if(document.getElementById('startLeaderboardList')) {
|
||||
loadStartScreenLeaderboard();
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// MODAL LOGIK (Fenster auf/zu)
|
||||
// ==========================================
|
||||
|
||||
window.openModal = function(id) {
|
||||
const el = document.getElementById('modal-' + id);
|
||||
if(el) el.style.display = 'flex';
|
||||
@@ -351,7 +335,7 @@ window.closeModal = function() {
|
||||
modals.forEach(el => el.style.display = 'none');
|
||||
}
|
||||
|
||||
// Klick nebendran schließt Modal
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal-overlay')) {
|
||||
closeModal();
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
// ==========================================
|
||||
// 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.");
|
||||
|
||||
@@ -38,88 +21,72 @@ function connectGame() {
|
||||
platformBuffer = [];
|
||||
obstacles = [];
|
||||
platforms = [];
|
||||
currentLatencyMs = 0; // Reset Latenz
|
||||
currentLatencyMs = 0;
|
||||
|
||||
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
|
||||
const sentTime = msg.ts;
|
||||
|
||||
|
||||
// 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 msPerTick = 1000 / 20;
|
||||
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
|
||||
currentTick = targetTick;
|
||||
}
|
||||
|
||||
// 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
|
||||
const speedPerMs = currentSpeedPerTick / msPerTick;
|
||||
|
||||
|
||||
// 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);
|
||||
});
|
||||
@@ -135,7 +102,6 @@ function connectGame() {
|
||||
|
||||
if (msg.score !== undefined) score = msg.score;
|
||||
|
||||
// Powerups übernehmen (für Anzeige)
|
||||
if (msg.powerups) {
|
||||
godModeLives = msg.powerups.godLives;
|
||||
hasBat = msg.powerups.hasBat;
|
||||
@@ -154,7 +120,6 @@ function connectGame() {
|
||||
}
|
||||
}
|
||||
|
||||
// C. TOD (Server Authoritative)
|
||||
if (msg.type === "dead") {
|
||||
console.log("💀 Server sagt: Game Over");
|
||||
|
||||
@@ -171,15 +136,11 @@ function connectGame() {
|
||||
|
||||
|
||||
|
||||
// 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 ? "✅" : "❌";
|
||||
|
||||
@@ -194,12 +155,10 @@ function connectGame() {
|
||||
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();
|
||||
@@ -210,7 +169,7 @@ function connectGame() {
|
||||
}
|
||||
};
|
||||
|
||||
// --- 3. VERBINDUNG GETRENNT ---
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log("🔴 WS Verbindung getrennt.");
|
||||
if (pingInterval) clearInterval(pingInterval);
|
||||
@@ -221,13 +180,9 @@ function connectGame() {
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 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
|
||||
@@ -235,9 +190,6 @@ function sendPing() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INPUT SENDEN
|
||||
// ==========================================
|
||||
function sendInput(type, action) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({
|
||||
@@ -256,37 +208,33 @@ function generateSyncTable(label, clientList, serverList) {
|
||||
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)
|
||||
const msPerTick = 50;
|
||||
|
||||
|
||||
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);
|
||||
|
||||
@@ -310,12 +258,10 @@ function generateSyncTable(label, clientList, serverList) {
|
||||
matchedServerIndices.add(bestSIdx);
|
||||
|
||||
serverXRaw = bestMatch.x;
|
||||
serverXCorrected = bestMatch.x - latencyPx; // Hier rechnen wir den Ping raus
|
||||
serverXCorrected = bestMatch.x - latencyPx;
|
||||
|
||||
// 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";
|
||||
@@ -333,10 +279,8 @@ function generateSyncTable(label, clientList, serverList) {
|
||||
});
|
||||
});
|
||||
|
||||
// 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";
|
||||
|
||||
@@ -354,7 +298,6 @@ function generateSyncTable(label, clientList, serverList) {
|
||||
}
|
||||
});
|
||||
|
||||
// 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)"];
|
||||
|
||||
@@ -5,23 +5,20 @@ class Particle {
|
||||
constructor(x, y, type) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.life = 1.0; // 1.0 = 100% Leben
|
||||
this.type = type; // 'dust', 'sparkle', 'explosion'
|
||||
this.life = 1.0;
|
||||
this.type = type;
|
||||
|
||||
// Zufällige Geschwindigkeit
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
let speed = Math.random() * 2;
|
||||
|
||||
if (type === 'dust') {
|
||||
// Staub fliegt eher nach oben/hinten
|
||||
this.vx = -2 + Math.random();
|
||||
this.vy = -1 - Math.random();
|
||||
this.decay = 0.05; // Verschwindet schnell
|
||||
this.decay = 0.05;
|
||||
this.color = '#ddd';
|
||||
this.size = Math.random() * 4 + 2;
|
||||
}
|
||||
else if (type === 'sparkle') {
|
||||
// Münzen glitzern in alle Richtungen
|
||||
speed = Math.random() * 4 + 2;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
@@ -30,7 +27,6 @@ class Particle {
|
||||
this.size = Math.random() * 3 + 1;
|
||||
}
|
||||
else if (type === 'explosion') {
|
||||
// Tod
|
||||
speed = Math.random() * 6 + 2;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
@@ -65,7 +61,6 @@ class Particle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
|
||||
function spawnParticles(x, y, type, count = 5) {
|
||||
for(let i=0; i<count; i++) {
|
||||
@@ -74,7 +69,6 @@ function spawnParticles(x, y, type, count = 5) {
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
// Rückwärts loopen zum sicheren Löschen
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
particles[i].update();
|
||||
if (particles[i].life <= 0) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// RESIZE LOGIK (LETTERBOXING)
|
||||
// ==========================================
|
||||
function resize() {
|
||||
// 1. Interne Auflösung fixieren
|
||||
canvas.width = GAME_WIDTH; // 800
|
||||
canvas.height = GAME_HEIGHT; // 400
|
||||
|
||||
@@ -35,20 +34,13 @@ window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
|
||||
// ==========================================
|
||||
// DRAWING LOOP (MIT INTERPOLATION)
|
||||
// ==========================================
|
||||
// alpha (0.0 bis 1.0) gibt an, wie weit wir zeitlich zwischen zwei Physik-Ticks sind.
|
||||
|
||||
function drawGame(alpha = 1.0) {
|
||||
// 1. Canvas leeren
|
||||
|
||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
// ===============================================
|
||||
// HINTERGRUND
|
||||
// ===============================================
|
||||
let currentBg = null;
|
||||
if (bgSprites.length > 0) {
|
||||
// Wechselt alle 10.000 Punkte
|
||||
const changeInterval = 10000;
|
||||
const currentRawIndex = Math.floor(score / changeInterval);
|
||||
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
|
||||
@@ -63,63 +55,49 @@ function drawGame(alpha = 1.0) {
|
||||
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// BODEN
|
||||
// ===============================================
|
||||
|
||||
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
||||
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
||||
|
||||
// ===============================================
|
||||
// PLATTFORMEN (Interpoliert)
|
||||
// ===============================================
|
||||
|
||||
platforms.forEach(p => {
|
||||
// Interpolierte X-Position
|
||||
|
||||
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
||||
const rY = p.y;
|
||||
|
||||
// Holz-Optik
|
||||
|
||||
ctx.fillStyle = "#5D4037";
|
||||
ctx.fillRect(rX, rY, p.w, p.h);
|
||||
ctx.fillStyle = "#8D6E63";
|
||||
ctx.fillRect(rX, rY, p.w, 5); // Highlight oben
|
||||
ctx.fillRect(rX, rY, p.w, 5);
|
||||
});
|
||||
|
||||
// ===============================================
|
||||
// HINDERNISSE (Interpoliert)
|
||||
// ===============================================
|
||||
|
||||
obstacles.forEach(obs => {
|
||||
const def = obs.def || {};
|
||||
const img = sprites[def.id];
|
||||
|
||||
// Interpolation
|
||||
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
|
||||
const rY = obs.y;
|
||||
|
||||
// Hitbox Dimensionen
|
||||
const hbw = def.width || obs.w || 30;
|
||||
const hbh = def.height || obs.h || 30;
|
||||
|
||||
if (img && img.complete && img.naturalHeight !== 0) {
|
||||
// --- BILD VORHANDEN ---
|
||||
// Editor-Werte anwenden
|
||||
|
||||
const scale = def.imgScale || 1.0;
|
||||
const offX = def.imgOffsetX || 0.0;
|
||||
const offY = def.imgOffsetY || 0.0;
|
||||
|
||||
// 1. Skalierte Größe
|
||||
const drawW = hbw * scale;
|
||||
const drawH = hbh * scale;
|
||||
|
||||
// 2. Positionierung (Zentriert & Unten Bündig zur Hitbox)
|
||||
const baseX = rX + (hbw - drawW) / 2;
|
||||
const baseY = rY + (hbh - drawH);
|
||||
|
||||
// 3. Zeichnen
|
||||
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
|
||||
|
||||
} else {
|
||||
// --- FALLBACK (KEIN BILD) ---
|
||||
// Magenta als Warnung, Gold für Coins
|
||||
let color = "#FF00FF";
|
||||
if (def.type === "coin") color = "gold";
|
||||
else if (def.color) color = def.color;
|
||||
@@ -127,50 +105,36 @@ function drawGame(alpha = 1.0) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(rX, rY, hbw, hbh);
|
||||
|
||||
// Rahmen & Text
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.lineWidth = 2;
|
||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
||||
ctx.fillStyle = "white"; ctx.font = "bold 10px monospace";
|
||||
ctx.fillText(def.id || "?", rX, rY - 5);
|
||||
}
|
||||
|
||||
// --- DEBUG HITBOX (Client) ---
|
||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||
ctx.strokeStyle = "rgba(0,255,0,0.5)"; // Grün transparent
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
||||
}
|
||||
|
||||
// Sprechblase
|
||||
if(obs.speech) drawSpeechBubble(rX, rY, obs.speech);
|
||||
});
|
||||
|
||||
// ===============================================
|
||||
// DEBUG: SERVER STATE (Cyan)
|
||||
// ===============================================
|
||||
// Zeigt an, wo der Server die Objekte sieht (ohne Interpolation)
|
||||
|
||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||
if (serverObstacles) {
|
||||
ctx.strokeStyle = "cyan";
|
||||
ctx.lineWidth = 1;
|
||||
serverObstacles.forEach(sObj => {
|
||||
// Wir müssen hier die Latenz-Korrektur aus network.js abziehen,
|
||||
// um zu sehen, wo network.js sie hingeschoben hat?
|
||||
// Nein, serverObstacles enthält die Rohdaten.
|
||||
// Wenn wir wissen wollen, wo der Server "jetzt" ist, müssten wir schätzen.
|
||||
// Wir zeichnen einfach Raw, das hinkt optisch meist hinterher.
|
||||
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// SPIELER (Interpoliert)
|
||||
// ===============================================
|
||||
// Interpolierte Y-Position
|
||||
|
||||
let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha);
|
||||
|
||||
// Ducken Anpassung
|
||||
|
||||
const drawY = isCrouching ? rPlayerY + 25 : rPlayerY;
|
||||
const drawH = isCrouching ? 25 : 50;
|
||||
|
||||
@@ -181,16 +145,11 @@ function drawGame(alpha = 1.0) {
|
||||
ctx.fillRect(player.x, drawY, player.w, drawH);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// PARTIKEL (Visuelle Effekte)
|
||||
// ===============================================
|
||||
if (typeof drawParticles === 'function') {
|
||||
drawParticles();
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// HUD (Statusanzeige)
|
||||
// ===============================================
|
||||
|
||||
if (isGameRunning && !isGameOver) {
|
||||
ctx.fillStyle = "black";
|
||||
ctx.font = "bold 10px monospace";
|
||||
@@ -206,9 +165,6 @@ function drawGame(alpha = 1.0) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// GAME OVER OVERLAY
|
||||
// ===============================================
|
||||
if (isGameOver) {
|
||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
// ==========================================
|
||||
// GLOBALE STATUS VARIABLEN
|
||||
// ==========================================
|
||||
|
||||
// --- Konfiguration & Flags ---
|
||||
let gameConfig = null; // Wird von /api/config geladen
|
||||
let isLoaded = false; // Sind Assets geladen?
|
||||
let isGameRunning = false; // Läuft der Game Loop?
|
||||
let isGameOver = false; // Ist der Spieler tot?
|
||||
let sessionID = null; // UUID der aktuellen Session
|
||||
let gameConfig = null;
|
||||
let isLoaded = false;
|
||||
let isGameRunning = false;
|
||||
let isGameOver = false;
|
||||
let sessionID = null;
|
||||
|
||||
// --- NETZWERK & STREAMING (NEU) ---
|
||||
let socket = null; // Die WebSocket Verbindung
|
||||
let obstacleBuffer = []; // Warteschlange für kommende Hindernisse
|
||||
let platformBuffer = []; // Warteschlange für kommende Plattformen
|
||||
let socket = null;
|
||||
let obstacleBuffer = [];
|
||||
let platformBuffer = [];
|
||||
|
||||
let score = 0;
|
||||
let currentTick = 0;
|
||||
|
||||
// --- SPIELZUSTAND ---
|
||||
let score = 0; // Aktueller Punktestand (vom Server diktiert)
|
||||
let currentTick = 0; // Zeit-Einheit des Spiels
|
||||
|
||||
// --- POWERUPS (Client Visuals) ---
|
||||
let godModeLives = 0;
|
||||
let hasBat = false;
|
||||
let bootTicks = 0;
|
||||
|
||||
// --- HINTERGRUND ---
|
||||
let maxRawBgIndex = 0; // Welcher Hintergrund wird gezeigt?
|
||||
|
||||
// --- GAME LOOP TIMING ---
|
||||
let maxRawBgIndex = 0;
|
||||
|
||||
|
||||
let lastTime = 0;
|
||||
let accumulator = 0;
|
||||
|
||||
// --- GRAFIKEN ---
|
||||
let sprites = {}; // Cache für Hindernis-Bilder
|
||||
let playerSprite = new Image();
|
||||
let bgSprites = []; // Array der Hintergrund-Bilder
|
||||
|
||||
// --- ENTITIES (Render-Listen) ---
|
||||
let sprites = {};
|
||||
let playerSprite = new Image();
|
||||
let bgSprites = [];
|
||||
|
||||
|
||||
let player = {
|
||||
x: 50,
|
||||
y: 300,
|
||||
@@ -49,28 +43,25 @@ let player = {
|
||||
let particles = [];
|
||||
|
||||
|
||||
// Diese Listen werden von logic.js aus dem Buffer gefüllt und von render.js gezeichnet
|
||||
|
||||
let obstacles = [];
|
||||
let platforms = [];
|
||||
|
||||
// Debug-Daten (optional, falls der Server Debug-Infos schickt)
|
||||
|
||||
let serverObstacles = [];
|
||||
let serverPlatforms = [];
|
||||
|
||||
let currentLatencyMs = 0; // Aktuelle Latenz in Millisekunden
|
||||
let pingInterval = null; // Timer für den Ping
|
||||
let currentLatencyMs = 0;
|
||||
let pingInterval = null;
|
||||
|
||||
|
||||
// --- INPUT STATE ---
|
||||
let isCrouching = false;
|
||||
|
||||
// ==========================================
|
||||
// HTML ELEMENTE (Caching)
|
||||
// ==========================================
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const container = document.getElementById('game-container');
|
||||
|
||||
// UI Elemente
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const loadingText = document.getElementById('loadingText');
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
}
|
||||
|
||||
|
||||
/* =========================================
|
||||
1. GRUNDLAGEN & GLOBAL
|
||||
========================================= */
|
||||
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
Reference in New Issue
Block a user