Private
Public Access
1
0

big Performance fix
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m20s

This commit is contained in:
Sebastian Unterschütz
2025-12-04 22:43:36 +01:00
parent 13c4e7318c
commit 626493177f
8 changed files with 5136 additions and 393 deletions

View File

@@ -5,31 +5,33 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Escape the Teacher</title>
<script src="js/pixi.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<button id="mute-btn" onclick="toggleAudioClick()">🔊</button>
<div id="rotate-overlay">
<div class="icon">📱↻</div>
<p>Bitte Gerät drehen!</p>
<small>Querformat benötigt</small>
</div>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="game-container">
<div id="ui-layer">
SCORE: <span id="score">0</span>
</div>
<div id="startScreen">
<div class="start-left">
<h1>ESCAPE THE<br>TEACHER</h1>
<p style="font-size: 12px; color: #aaa;">Dein Rekord: <span id="localHighscore" style="color:yellow">0</span></p>
<button id="startBtn" onclick="startGameClick()">STARTEN</button>
<div id="loadingText">Lade Grafiken...</div>
<div id="loadingText">Lade Assets...</div>
<div class="info-box">
<div class="info-title">SCHUL-NEWS</div>
@@ -78,13 +80,14 @@
</div>
</div>
<div id="modal-codes" class="modal-overlay">
<div id="modal-codes" class="modal-overlay" style="display:none;">
<div class="modal-content">
<button class="close-modal" onclick="closeModal()">X</button>
<h2 style="color:yellow">MEINE BEWEISE</h2>
<div id="codesList" style="font-size: 10px; line-height: 1.8;">
Lade Daten...
</div>
<p style="margin-top:20px; font-size:9px; color:#888;">Zeige diesen Code für deinen Preis oder lösche den Eintrag.</p>
<p style="margin-top:20px; font-size:9px; color:#888;">Zeige diesen Code dem Lehrer für deinen Preis oder lösche den Eintrag.</p>
</div>
</div>
@@ -97,12 +100,12 @@
Sebastian Unterschütz<br>
Göltzschtalblick 16<br>
08236 Ellefeld<br>
<small>Kontakt: sebastian@unterschutz.de</small>
<small>Kontakt: sebastian@unterschuetz.de</small>
</p>
<hr style="border:1px solid #444; margin: 15px 0;">
<p><strong>🎵 Musik Design:</strong><br>
<p><strong>🎵 Musik & Sound Design:</strong><br>
<span style="color:#ffcc00; font-size:18px;">Max E.</span>
</p>
@@ -129,50 +132,43 @@
<h2>Datenschutzerklärung</h2>
<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>
Allgemeine Hinweise: Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.</p>
<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>
E-Mail: sebastian@unterschuetz.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>
Industriestr. 25, 91710 Gunzenhausen, 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>
Wir haben mit dem Anbieter einen Vertrag zur Auftragsverarbeitung (AVV) geschlossen, der die Einhaltung der DSGVO gewährleistet.</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>Uhrzeit der Serveranfrage</li>
<li>IP-Adresse</li>
</ul>
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>
Der Provider der Seiten (Hetzner) erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien (Browser, OS, Referrer, Hostname, Uhrzeit, IP-Adresse).<br>
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse an technischer Fehlerfreiheit und Sicherheit). Die Daten werden nach spätestens 14 Tagen gelöscht.</p>
<p><strong>Spielstände & Highscores</strong><br>
Wenn Sie einen Highscore eintragen, speichern wir in unserer Datenbank:
Wenn Sie einen Highscore eintragen, speichern wir:
<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>
<li>Gewählter Name (Pseudonym empfohlen!)</li>
<li>Punktestand und Zeitstempel</li>
<li>Session-ID und "Claim-Code"</li>
</ul>
Diese Daten dienen ausschließlich der Darstellung der Bestenliste und der Spielmechanik.</p>
Diese Daten dienen der Darstellung der Bestenliste.</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>
Das Spiel speichert Einstellungen (Audio) und Codes lokal in Ihrem Browser. Wir setzen <strong>keine Tracking-Cookies</strong> oder Analyse-Tools ein.</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>
Sie haben jederzeit das Recht auf Auskunft, Berichtigung und Löschung Ihrer Daten. Wenden Sie sich dazu an den Verantwortlichen im Impressum.</p>
</div>
</div>
@@ -202,5 +198,6 @@
}
}
</script>
</body>
</html>

View File

@@ -14,12 +14,20 @@ SOUNDS.hit.volume = 0.6;
SOUNDS.music.loop = true;
SOUNDS.music.volume = 0.2;
// Standard: Pitch beibehalten (WICHTIG für euch!)
if (SOUNDS.music.preservesPitch !== undefined) {
SOUNDS.music.preservesPitch = true;
} else if (SOUNDS.music.mozPreservesPitch !== undefined) {
SOUNDS.music.mozPreservesPitch = true; // Firefox Fallback
} else if (SOUNDS.music.webkitPreservesPitch !== undefined) {
SOUNDS.music.webkitPreservesPitch = true; // Safari Fallback
}
// Mute Status laden
let isMuted = localStorage.getItem('escape_muted') === 'true';
function playSound(name) {
if (isMuted || !SOUNDS[name]) return;
const soundClone = SOUNDS[name].cloneNode();
soundClone.volume = SOUNDS[name].volume;
soundClone.play().catch(() => {});
@@ -27,28 +35,41 @@ function playSound(name) {
function toggleMute() {
isMuted = !isMuted;
localStorage.setItem('escape_muted', isMuted);
if(isMuted) {
SOUNDS.music.pause();
} else {
SOUNDS.music.play().catch(()=>{});
}
if(isMuted) SOUNDS.music.pause();
else SOUNDS.music.play().catch(()=>{});
return isMuted;
}
function startMusic() {
if(!isMuted) {
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
}
if(!isMuted) SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked"));
}
function getMuteState() {
return isMuted;
}
// --- GESCHWINDIGKEIT ANPASSEN ---
function setMusicSpeed(gameSpeed) {
if (isMuted || !SOUNDS.music) return;
const baseGameSpeed = 15.0; // Muss zu BASE_SPEED in config.js passen
// Faktor berechnen: Speed 30 = Musik 1.3x
let rate = 1.0 + (gameSpeed - baseGameSpeed) * 0.02;
// Limits
if (rate < 1.0) rate = 1.0;
if (rate > 2.0) rate = 2.0;
// Nur bei spürbarer Änderung anwenden
if (Math.abs(SOUNDS.music.playbackRate - rate) > 0.05) {
SOUNDS.music.playbackRate = rate;
}
}
function resetMusicSpeed() {
if (SOUNDS.music) SOUNDS.music.playbackRate = 1.0;
}

View File

@@ -3,6 +3,9 @@ function updateGameLogic() {
if (currentSpeed > 36.0) currentSpeed = 36.0;
updateParticles();
if (typeof setMusicSpeed === "function") {
setMusicSpeed(currentSpeed);
}
player.prevY = player.y;

View File

@@ -1,63 +1,79 @@
// ==========================================
// 1. ASSETS LADEN
// 1. ASSETS LADEN (PIXI V8)
// ==========================================
// ==========================================
// 1. ASSETS LADEN (PIXI V8 KORREKT)
// ==========================================
async function loadAssets() {
const pPromise = new Promise(resolve => {
playerSprite.src = "assets/player.png";
playerSprite.onload = resolve;
playerSprite.onerror = () => { resolve(); };
});
const keysToLoad = [];
const bgPromises = gameConfig.backgrounds.map((bgFile, index) => {
return new Promise((resolve) => {
const img = new Image();
img.src = "assets/" + bgFile;
img.onload = () => { bgSprites[index] = img; resolve(); };
img.onerror = () => { resolve(); };
});
});
// A. Player hinzufügen
PIXI.Assets.add({ alias: 'player', src: 'assets/player.png' });
keysToLoad.push('player');
const obsPromises = gameConfig.obstacles.map(def => {
return new Promise((resolve) => {
if (!def.image) { resolve(); return; }
const img = new Image();
img.src = "assets/" + def.image;
img.onload = () => { sprites[def.id] = img; resolve(); };
img.onerror = () => { resolve(); };
});
// B. Hintergründe aus Config
if (gameConfig.backgrounds) {
gameConfig.backgrounds.forEach(bg => {
// Alias = Dateiname (z.B. "school-background.jpg")
PIXI.Assets.add({ alias: bg, src: 'assets/' + bg });
keysToLoad.push(bg);
});
}
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
// C. Hindernisse aus Config
if (gameConfig.obstacles) {
gameConfig.obstacles.forEach(def => {
if (def.image) {
// Alias = ID (z.B. "teacher")
// Checken ob Alias schon existiert (vermeidet Warnungen)
if (!PIXI.Assets.cache.has(def.id)) {
PIXI.Assets.add({ alias: def.id, src: 'assets/' + def.image });
keysToLoad.push(def.id);
}
}
});
}
try {
console.log("Lade Assets...", keysToLoad);
// Alles auf einmal laden
await PIXI.Assets.load(keysToLoad);
console.log("✅ Alle Texturen geladen!");
} catch (e) {
console.error("❌ Asset Fehler:", e);
}
}
// ... (Rest der Datei: startGameClick, gameLoop etc. BLEIBT GLEICH)
// ...
window.startGameClick = async function() {
if (!isLoaded) return;
startScreen.style.display = 'none';
document.body.classList.add('game-active');
// Score Reset visuell
score = 0;
const scoreEl = document.getElementById('score');
if (scoreEl) scoreEl.innerText = "0";
document.getElementById('score').innerText = "0";
if (typeof startMusic === 'function') startMusic();
// WebSocket Start
startMusic();
connectGame();
resize();
};
// ==========================================
// 3. GAME OVER & SCORE
// ==========================================
window.gameOver = function(reason) {
if (isGameOver) return;
isGameOver = true;
console.log("Game Over:", reason);
// Highscore Check (Lokal)
const finalScore = Math.floor(score / 10);
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
if (finalScore > currentHighscore) {
if (finalScore > parseInt(currentHighscore)) {
localStorage.setItem('escape_highscore', finalScore);
}
@@ -65,22 +81,24 @@ window.gameOver = function(reason) {
gameOverScreen.style.display = 'flex';
document.getElementById('finalScore').innerText = finalScore;
// Input Reset
document.getElementById('inputSection').style.display = 'flex';
document.getElementById('submitBtn').disabled = false;
document.getElementById('playerNameInput').value = "";
// Liste laden
loadLeaderboard();
}
};
window.submitScore = async function() {
const nameInput = document.getElementById('playerNameInput');
const name = nameInput.value.trim();
const btn = document.getElementById('submitBtn');
if (!name) return alert("Bitte Namen eingeben!");
if (!sessionID) return alert("Fehler: Keine Session ID vom Server erhalten.");
btn.disabled = true;
try {
@@ -90,11 +108,10 @@ window.submitScore = async function() {
body: JSON.stringify({ sessionId: sessionID, name: name })
});
if (!res.ok) throw new Error("Fehler beim Senden");
if (!res.ok) throw new Error("Server antwortet nicht");
const data = await res.json();
// Lokal speichern ("Meine Codes")
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
myClaims.push({
name: name,
@@ -105,52 +122,104 @@ 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}`);
} catch (e) {
console.error(e);
alert("Fehler beim Speichern: " + e.message);
alert("Fehler: " + e.message);
btn.disabled = false;
}
};
async function loadLeaderboard() {
try {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json();
let html = "<h3 style='margin-bottom:5px; color:#ffcc00;'>BESTENLISTE</h3>";
if(entries.length === 0) html += "<div>Noch keine Einträge.</div>";
entries.forEach(e => {
const color = e.isMe ? "cyan" : "white"; // Eigener Name in Cyan
const bgStyle = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
html += `
<div style="border-bottom:1px dotted #444; padding:5px; ${bgStyle} display:flex; justify-content:space-between; color:${color}; font-size:12px;">
<span>#${e.rank} ${e.name}</span>
<span>${Math.floor(e.score/10)}</span>
</div>`;
if(!entries || entries.length === 0) html += "<div>Leer.</div>";
else entries.forEach(e => {
const color = e.isMe ? "cyan" : "white";
const bg = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
html += `<div style="display:flex; justify-content:space-between; color:${color}; ${bg} padding:4px; border-bottom:1px dotted #444; font-size:12px;">
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
});
document.getElementById('leaderboard').innerHTML = html;
} catch(e) {
console.error("Leaderboard Error:", e);
}
} catch(e) { console.error(e); }
}
// ==========================================
// 4. MEINE CODES (LOGIK)
// ==========================================
window.showMyCodes = function() {
openModal('codes');
const listEl = document.getElementById('codesList');
if(!listEl) return;
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
if (rawClaims.length === 0) {
listEl.innerHTML = "<div style='padding:20px; text-align:center; color:#666;'>Keine Codes.</div>";
return;
}
const sortedClaims = rawClaims.sort((a, b) => b.score - a.score);
let html = "";
sortedClaims.forEach(c => {
let rankIcon = "📄";
if (c.score >= 5000) rankIcon = "⭐";
if (c.score >= 10000) rankIcon = "🔥";
html += `
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
<div style="text-align:left;">
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${rankIcon} ${c.code}</span>
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
<span style="color:#aaa; font-size:9px;">${c.name}${c.date}</span>
</div>
<button onclick="deleteClaim('${c.code}')"
style="background:transparent; border:1px solid #ff4444; color:#ff4444; padding:4px 8px; font-size:9px; cursor:pointer;">
LÖSCHEN
</button>
</div>`;
});
listEl.innerHTML = html;
};
window.deleteClaim = async function(code) {
if(!confirm("Eintrag wirklich löschen?")) return;
// Suchen der SessionID für den Server-Call
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
const item = claims.find(c => c.code === code);
if (item && item.sessionId) {
try {
await fetch('/api/claim/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: item.sessionId, claimCode: code })
});
} catch(e) { console.warn("Server delete failed"); }
}
// Lokal löschen
claims = claims.filter(c => c.code !== code);
localStorage.setItem('escape_claims', JSON.stringify(claims));
window.showMyCodes(); // Refresh
};
// ==========================================
// 5. GAME LOOP (PHYSICS + RENDER)
// ==========================================
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
if (!isLoaded) return;
// Nur updaten, wenn Spiel läuft
if (isGameRunning && !isGameOver) {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime;
@@ -160,46 +229,62 @@ function gameLoop(timestamp) {
accumulator += deltaTime;
// --- FIXED TIME STEP (Physik: 20 TPS) ---
while (accumulator >= MS_PER_TICK) {
updateGameLogic();
updateGameLogic(); // logic.js (setzt prevX/prevY)
currentTick++;
// Score lokal hochzählen (damit es flüssig aussieht)
// Server korrigiert, falls Abweichung zu groß
score++;
accumulator -= MS_PER_TICK;
}
const alpha = accumulator / MS_PER_TICK;
// Score im HUD
// HUD Update
const scoreEl = document.getElementById('score');
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
}
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
// --- INTERPOLATION (Rendering: 60+ FPS) ---
// alpha berechnen: Wie viel % ist seit dem letzten Tick vergangen?
const alpha = (isGameRunning && !isGameOver) ? (accumulator / MS_PER_TICK) : 1.0;
// drawGame ist jetzt in render.js und nutzt Pixi
drawGame(alpha);
}
// ==========================================
// 6. INITIALISIERUNG
// ==========================================
async function initGame() {
try {
const cRes = await fetch('/api/config');
gameConfig = await cRes.json();
// Pixi Assets laden
await loadAssets();
await loadStartScreenLeaderboard();
if (typeof getMuteState === 'function') {
updateMuteIcon(getMuteState());
}
// Startscreen Bestenliste
await loadStartScreenLeaderboard();
isLoaded = true;
if(loadingText) loadingText.style.display = 'none';
if(startBtn) startBtn.style.display = 'inline-block';
// Mute Icon setzen (audio.js State)
if (typeof getMuteState === 'function') {
updateMuteIcon(getMuteState());
}
// Lokaler Highscore
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
const hsEl = document.getElementById('localHighscore');
if(hsEl) hsEl.innerText = savedHighscore;
// Loop starten (für Idle Rendering)
requestAnimationFrame(gameLoop);
drawGame();
drawGame(1.0); // Initiale Zeichnung
} catch(e) {
console.error(e);
@@ -207,16 +292,14 @@ async function initGame() {
}
}
// Helper: Startscreen Leaderboard
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'>Keine Scores.</div>"; return; }
if (!entries || entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
let html = "";
entries.forEach(e => {
let icon = "#" + e.rank;
@@ -227,14 +310,13 @@ async function loadStartScreenLeaderboard() {
} catch (e) {}
}
// Helper: Audio Button Logik
window.toggleAudioClick = function() {
if (typeof toggleMute === 'function') {
const muted = toggleMute();
updateMuteIcon(muted);
document.getElementById('mute-btn').blur();
}
};
function updateMuteIcon(isMuted) {
@@ -246,100 +328,10 @@ function updateMuteIcon(isMuted) {
}
}
// Modal Helpers
window.openModal = function(id) { document.getElementById('modal-' + id).style.display = 'flex'; }
window.closeModal = function() { document.querySelectorAll('.modal-overlay').forEach(el => el.style.display = 'none'); }
window.onclick = function(event) { if (event.target.classList.contains('modal-overlay')) closeModal(); }
window.showMyCodes = function() {
openModal('codes');
const listEl = document.getElementById('codesList');
if(!listEl) return;
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
if (rawClaims.length === 0) {
listEl.innerHTML = "<div style='padding:20px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>";
return;
}
const sortedClaims = rawClaims
.map((item, index) => ({ ...item, originalIndex: index }))
.sort((a, b) => b.score - a.score);
let html = "";
sortedClaims.forEach(c => {
let rankIcon = "📄";
if (c.score >= 5000) rankIcon = "⭐";
if (c.score >= 10000) rankIcon = "🔥";
if (c.score >= 20000) rankIcon = "👑";
html += `
<div style="border-bottom:1px solid #444; padding:10px 0; display:flex; justify-content:space-between; align-items:center;">
<div style="text-align:left;">
<span style="color:#00e5ff; font-weight:bold; font-size:14px;">${rankIcon} ${c.code}</span>
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
<span style="color:#aaa; font-size:10px;">${c.name}${c.date}</span>
</div>
<button onclick="deleteClaim('${c.sessionId}', '${c.code}')"
style="background:transparent; border:1px solid #ff4444; color:#ff4444; padding:5px 10px; font-size:10px; cursor:pointer;">
LÖSCHEN
</button>
</div>`;
});
listEl.innerHTML = html;
};
window.deleteClaim = async function(sid, code) {
if(!confirm("Eintrag wirklich löschen?")) return;
try {
await fetch('/api/claim/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sid, claimCode: code })
});
} catch(e) {
console.warn("Server Delete fehlgeschlagen (vielleicht schon weg), lösche lokal...");
}
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
claims = claims.filter(c => c.code !== code);
localStorage.setItem('escape_claims', JSON.stringify(claims));
window.showMyCodes();
if(document.getElementById('startLeaderboardList')) {
loadStartScreenLeaderboard();
}
};
window.openModal = function(id) {
const el = document.getElementById('modal-' + id);
if(el) el.style.display = 'flex';
}
window.closeModal = function() {
const modals = document.querySelectorAll('.modal-overlay');
modals.forEach(el => el.style.display = 'none');
}
window.onclick = function(event) {
if (event.target.classList.contains('modal-overlay')) {
closeModal();
}
}
// Start
initGame();

2312
static/js/pixi.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2312
static/js/pixi.min.mjs Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +1,55 @@
// ==========================================
// RESIZE LOGIK (LETTERBOXING)
// PIXI INITIALISIERUNG (V8)
// ==========================================
function resize() {
canvas.width = GAME_WIDTH; // 800
canvas.height = GAME_HEIGHT; // 400
let floorGraphic = null;
async function initPixi() {
if (app) return;
app = new PIXI.Application();
// 1. Asynchrones Init (v8 Standard)
await app.init({
width: GAME_WIDTH,
height: GAME_HEIGHT,
backgroundColor: 0x1a1a1a, // Dunkelgrau
preference: 'webgpu', // Versuch WebGPU (schneller auf Handy!)
resolution: Math.min(window.devicePixelRatio || 1, 2), // Retina Limit
autoDensity: true,
antialias: false,
roundPixels: true // Wichtig für Pixelart
});
// 2. Canvas einhängen
document.getElementById('game-container').appendChild(app.canvas);
// 3. Layer erstellen
bgLayer = new PIXI.Container();
gameLayer = new PIXI.Container();
debugLayer = new PIXI.Graphics(); // Für Hitboxen
// Sortierung aktivieren (damit Player vor Obstacles ist)
gameLayer.sortableChildren = true;
app.stage.addChild(bgLayer);
app.stage.addChild(gameLayer);
app.stage.addChild(debugLayer);
// Einmalig Resizen
resize();
console.log(`🚀 Renderer: ${app.renderer.name}`);
}
function resize() {
if (!app || !app.canvas) return;
// 2. Verfügbaren Platz berechnen
const windowWidth = window.innerWidth - 20;
const windowHeight = window.innerHeight - 20;
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
const windowRatio = windowWidth / windowHeight;
let finalWidth, finalHeight;
// 3. Skalierung berechnen (Aspect Ratio erhalten)
if (windowRatio < targetRatio) {
finalWidth = windowWidth;
finalHeight = windowWidth / targetRatio;
@@ -23,163 +58,210 @@ function resize() {
finalWidth = finalHeight * targetRatio;
}
// 4. Container Größe setzen (Canvas füllt Container via CSS)
if (container) {
container.style.width = `${Math.floor(finalWidth)}px`;
container.style.height = `${Math.floor(finalHeight)}px`;
}
// CSS Skalierung
app.canvas.style.width = `${Math.floor(finalWidth)}px`;
app.canvas.style.height = `${Math.floor(finalHeight)}px`;
}
window.addEventListener('resize', resize);
resize();
// ==========================================
// RENDER LOOP (RETAINED MODE)
// ==========================================
function drawGame(alpha = 1.0) {
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
let currentBg = null;
if (bgSprites.length > 0) {
const changeInterval = 10000;
const currentRawIndex = Math.floor(score / changeInterval);
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
const bgIndex = maxRawBgIndex % bgSprites.length;
currentBg = bgSprites[bgIndex];
async function drawGame(alpha = 1.0) {
if (!app) {
if(!document.getElementById('game-container').querySelector('canvas')) await initPixi();
return;
}
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
// 1. HINTERGRUND
updateBackground();
// 2. BODEN (FIX: Einmalig erstellen oder updaten)
if (!floorGraphic) {
floorGraphic = new PIXI.Graphics();
bgLayer.addChild(floorGraphic); // Zum Background Layer hinzufügen
}
floorGraphic.clear();
// Boden: Dunkelgrau
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 50).fill(0x333333);
// Grüne Linie oben drauf (Gras/Teppich)
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 4).fill(0x4CAF50);
// 3. OBJEKTE SYNCEN
syncSprites(obstacles, spriteCache, 'obstacle', alpha);
syncSprites(platforms, platformCache, 'platform', alpha);
// 4. SPIELER
updatePlayer(alpha);
// 5. DEBUG
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
drawDebugOverlay(alpha);
} else {
ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
debugLayer.clear();
}
}
// ------------------------------------------------------
// HELPER: SYNC SYSTEM
// ------------------------------------------------------
function syncSprites(dataList, cacheMap, type, alpha) {
const usedObjects = new Set();
dataList.forEach(obj => {
usedObjects.add(obj);
let sprite = cacheMap.get(obj);
// A. Erstellen (wenn neu)
if (!sprite) {
sprite = createPixiSprite(obj, type);
gameLayer.addChild(sprite);
cacheMap.set(obj, sprite);
}
// B. Updaten (Interpolation)
const def = obj.def || {};
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
platforms.forEach(p => {
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
const rY = p.y;
ctx.fillStyle = "#5D4037";
ctx.fillRect(rX, rY, p.w, p.h);
ctx.fillStyle = "#8D6E63";
ctx.fillRect(rX, rY, p.w, 5);
});
obstacles.forEach(obs => {
const def = obs.def || {};
const img = sprites[def.id];
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
const rY = obs.y;
const hbw = def.width || obs.w || 30;
const hbh = def.height || obs.h || 30;
if (img && img.complete && img.naturalHeight !== 0) {
// Position interpolieren
const rX = (obj.prevX !== undefined) ? lerp(obj.prevX, obj.x, alpha) : obj.x;
const rY = obj.y;
if (type === 'platform') {
sprite.x = rX;
sprite.y = rY;
} else {
// Editor Werte
const scale = def.imgScale || 1.0;
const offX = def.imgOffsetX || 0.0;
const offY = def.imgOffsetY || 0.0;
const offX = def.imgOffsetX || 0;
const offY = def.imgOffsetY || 0;
const hbw = def.width || 30;
const hbh = def.height || 30;
const drawW = hbw * scale;
const drawH = hbh * scale;
const baseX = rX + (hbw - drawW) / 2;
const baseY = rY + (hbh - drawH);
const baseY = rY + (hbh - (hbh * scale));
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
} else {
let color = "#FF00FF";
if (def.type === "coin") color = "gold";
else if (def.color) color = def.color;
ctx.fillStyle = color;
ctx.fillRect(rX, rY, hbw, hbh);
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);
sprite.x = baseX + offX;
sprite.y = baseY + offY;
sprite.width = drawW;
sprite.height = hbh * scale;
}
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);
}
if(obs.speech) drawSpeechBubble(rX, rY, obs.speech);
});
// C. Aufräumen (Garbage Collection)
for (const [obj, sprite] of cacheMap.entries()) {
if (!usedObjects.has(obj)) {
gameLayer.removeChild(sprite);
sprite.destroy();
cacheMap.delete(obj);
}
}
}
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
function createPixiSprite(obj, type) {
if (type === 'platform') {
const g = new PIXI.Graphics();
// Holz Plattform
g.rect(0, 0, obj.w, obj.h).fill(0x8B4513); // Braun
g.rect(0, 0, obj.w, 5).fill(0xA0522D); // Hellbraun Oben
return g;
}
else {
const def = obj.def || {};
// CHECK: Ist die Textur im Cache?
// Wir nutzen PIXI.Assets.get(), das ist sicherer als cache.has
let texture = null;
try {
if (def.id) texture = PIXI.Assets.get(def.id);
} catch(e) {}
if (texture) {
const s = new PIXI.Sprite(texture);
return s;
} else {
// FALLBACK (Wenn Bild fehlt -> Magenta Box)
const g = new PIXI.Graphics();
let color = 0xFF00FF;
if (def.type === 'coin') color = 0xFFD700;
g.rect(0, 0, def.width||30, def.height||30).fill(color);
return g;
}
}
}
// ------------------------------------------------------
// PLAYER & BG
// ------------------------------------------------------
function updatePlayer(alpha) {
if (!pixiPlayer) {
if (PIXI.Assets.cache.has('player')) {
pixiPlayer = PIXI.Sprite.from('player');
} else {
pixiPlayer = new PIXI.Graphics().rect(0,0,30,50).fill(0xFF0000);
}
gameLayer.addChild(pixiPlayer);
pixiPlayer.zIndex = 100; // Immer im Vordergrund
}
let rY = lerp(player.prevY || player.y, player.y, alpha);
const drawY = isCrouching ? rY + 25 : rY;
pixiPlayer.x = player.x;
pixiPlayer.y = drawY;
}
function updateBackground() {
// FEHLERBEHEBUNG:
// Wir prüfen 'gameConfig.backgrounds' statt 'bgSprites'
if (!gameConfig || !gameConfig.backgrounds || gameConfig.backgrounds.length === 0) return;
const changeInterval = 10000;
const idx = Math.floor(score / changeInterval) % gameConfig.backgrounds.length;
// Der Key ist der Dateiname (so haben wir es in main.js geladen)
const bgKey = gameConfig.backgrounds[idx];
// Sicherstellen, dass Asset geladen ist
if (!PIXI.Assets.cache.has(bgKey)) return;
if (!bgSprite) {
bgSprite = new PIXI.Sprite();
bgSprite.width = GAME_WIDTH;
bgSprite.height = GAME_HEIGHT;
bgLayer.addChild(bgSprite);
}
// Textur wechseln wenn nötig
const tex = PIXI.Assets.get(bgKey);
if (tex && bgSprite.texture !== tex) {
bgSprite.texture = tex;
}
}
// ------------------------------------------------------
// DEBUG
// ------------------------------------------------------
function drawDebugOverlay(alpha) {
const g = debugLayer;
g.clear();
// Server (Cyan)
if (serverObstacles) {
ctx.strokeStyle = "cyan";
ctx.lineWidth = 1;
serverObstacles.forEach(sObj => {
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
serverObstacles.forEach(o => {
g.rect(o.x, o.y, o.w, o.h).stroke({ width: 1, color: 0x00FFFF });
});
}
}
let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha);
const drawY = isCrouching ? rPlayerY + 25 : rPlayerY;
const drawH = isCrouching ? 25 : 50;
if (playerSprite.complete && playerSprite.naturalHeight !== 0) {
ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
} else {
ctx.fillStyle = player.color;
ctx.fillRect(player.x, drawY, player.w, drawH);
}
if (typeof drawParticles === 'function') {
drawParticles();
}
if (isGameRunning && !isGameOver) {
ctx.fillStyle = "black";
ctx.font = "bold 10px monospace";
ctx.textAlign = "left";
let statusText = "";
if(godModeLives > 0) statusText += `🛡️ x${godModeLives} `;
if(hasBat) statusText += `⚾ BAT `;
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
if(statusText !== "") {
ctx.fillText(statusText, 10, 40);
}
}
if (isGameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
}
}
// Helper: Sprechblase zeichnen
function drawSpeechBubble(x, y, text) {
const bX = x - 20;
const bY = y - 40;
const bW = 120;
const bH = 30;
ctx.fillStyle = "white"; ctx.fillRect(bX, bY, bW, bH);
ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.strokeRect(bX, bY, bW, bH);
ctx.fillStyle = "black"; ctx.font = "10px Arial"; ctx.textAlign = "center";
ctx.fillText(text, bX + bW/2, bY + 20);
// Client (Grün)
obstacles.forEach(o => {
const def = o.def || {};
const rX = (o.prevX !== undefined) ? lerp(o.prevX, o.x, alpha) : o.x;
g.rect(rX, o.y, def.width||30, def.height||30).stroke({ width: 1, color: 0x00FF00 });
});
}

View File

@@ -1,35 +1,59 @@
// ==========================================
// GLOBALE STATUS VARIABLEN
// ==========================================
let gameConfig = null;
let isLoaded = false;
let isGameRunning = false;
let isGameOver = false;
let sessionID = null;
// --- 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 (vom Server)
let socket = null;
let obstacleBuffer = [];
let platformBuffer = [];
// --- NETZWERK & STREAMING ---
let socket = null; // WebSocket Verbindung
let obstacleBuffer = []; // Warteschlange Hindernisse (vom Server)
let platformBuffer = []; // Warteschlange Plattformen (vom Server)
let score = 0;
let currentTick = 0;
// --- LATENZ & SYNC ---
let currentLatencyMs = 0; // Gemessene One-Way Latenz
let pingInterval = null; // Interval ID für den Ping-Loop
// --- PIXI JS (RENDERING) ---
let app = null; // Die Pixi Application
let bgLayer = null; // Container: Hintergrund
let gameLayer = null; // Container: Spielwelt (Player, Items)
let debugLayer = null; // Graphics: Hitboxen
// --- CACHING (PIXI SPRITES) ---
// Map<LogikObjekt, PIXI.Sprite>
// Wir ordnen jedem Logik-Objekt ein festes Sprite zu, statt neu zu erstellen
const spriteCache = new Map();
const platformCache = new Map();
// Referenzen für statische Sprites
let pixiPlayer = null;
let bgSprite = null;
// --- SPIELZUSTAND ---
let score = 0; // Aktueller Score
let currentTick = 0; // Zeitbasis (Synchronisiert mit Server)
// --- POWERUPS (Client Visuals) ---
let godModeLives = 0;
let hasBat = false;
let bootTicks = 0;
// --- HINTERGRUND LOGIK ---
let maxRawBgIndex = 0; // Welches BG Bild ist dran?
let maxRawBgIndex = 0;
// --- GAME LOOP TIMING ---
let lastTime = 0;
let accumulator = 0;
// --- GRAFIKEN & EFFEKTE ---
let particles = []; // Array für Partikel-Effekte
let sprites = {};
let playerSprite = new Image();
let bgSprites = [];
// --- ENTITIES (Render-Listen) ---
let player = {
x: 50,
y: 300,
@@ -38,30 +62,30 @@ let player = {
color: "red",
vy: 0,
grounded: false,
// WICHTIG für Interpolation:
prevY: 300
};
let particles = [];
// Diese Listen werden von logic.js gefüllt und von render.js gezeichnet
let obstacles = [];
let platforms = [];
// Debug-Daten vom Server (für das Overlay)
let serverObstacles = [];
let serverPlatforms = [];
let currentLatencyMs = 0;
let pingInterval = null;
// --- INPUT STATE ---
let isCrouching = false;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ==========================================
// HTML ELEMENTE (DOM Caching)
// ==========================================
const container = document.getElementById('game-container');
// Hinweis: 'canvas' und 'ctx' gibt es nicht mehr, da PixiJS das verwaltet!
// UI Elemente
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const loadingText = document.getElementById('loadingText');