big Performance fix
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m20s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m20s
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,6 +3,9 @@ function updateGameLogic() {
|
||||
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
||||
|
||||
updateParticles();
|
||||
if (typeof setMusicSpeed === "function") {
|
||||
setMusicSpeed(currentSpeed);
|
||||
}
|
||||
|
||||
|
||||
player.prevY = player.y;
|
||||
|
||||
@@ -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
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
2312
static/js/pixi.min.mjs
Normal file
File diff suppressed because one or more lines are too long
@@ -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 });
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user