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">
|
<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>
|
<title>Escape the Teacher</title>
|
||||||
|
|
||||||
|
<script src="js/pixi.min.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<button id="mute-btn" onclick="toggleAudioClick()">🔊</button>
|
<button id="mute-btn" onclick="toggleAudioClick()">🔊</button>
|
||||||
|
|
||||||
<div id="rotate-overlay">
|
<div id="rotate-overlay">
|
||||||
<div class="icon">📱↻</div>
|
<div class="icon">📱↻</div>
|
||||||
<p>Bitte Gerät drehen!</p>
|
<p>Bitte Gerät drehen!</p>
|
||||||
<small>Querformat benötigt</small>
|
<small>Querformat benötigt</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="game-container">
|
|
||||||
<canvas id="gameCanvas"></canvas>
|
|
||||||
|
|
||||||
|
<div id="game-container">
|
||||||
<div id="ui-layer">
|
<div id="ui-layer">
|
||||||
SCORE: <span id="score">0</span>
|
SCORE: <span id="score">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="startScreen">
|
<div id="startScreen">
|
||||||
|
|
||||||
<div class="start-left">
|
<div class="start-left">
|
||||||
<h1>ESCAPE THE<br>TEACHER</h1>
|
<h1>ESCAPE THE<br>TEACHER</h1>
|
||||||
|
|
||||||
<p style="font-size: 12px; color: #aaa;">Dein Rekord: <span id="localHighscore" style="color:yellow">0</span></p>
|
<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>
|
<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-box">
|
||||||
<div class="info-title">SCHUL-NEWS</div>
|
<div class="info-title">SCHUL-NEWS</div>
|
||||||
@@ -78,13 +80,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modal-codes" class="modal-overlay">
|
<div id="modal-codes" class="modal-overlay" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="close-modal" onclick="closeModal()">X</button>
|
<button class="close-modal" onclick="closeModal()">X</button>
|
||||||
<h2 style="color:yellow">MEINE BEWEISE</h2>
|
<h2 style="color:yellow">MEINE BEWEISE</h2>
|
||||||
<div id="codesList" style="font-size: 10px; line-height: 1.8;">
|
<div id="codesList" style="font-size: 10px; line-height: 1.8;">
|
||||||
|
Lade Daten...
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,12 +100,12 @@
|
|||||||
Sebastian Unterschütz<br>
|
Sebastian Unterschütz<br>
|
||||||
Göltzschtalblick 16<br>
|
Göltzschtalblick 16<br>
|
||||||
08236 Ellefeld<br>
|
08236 Ellefeld<br>
|
||||||
<small>Kontakt: sebastian@unterschutz.de</small>
|
<small>Kontakt: sebastian@unterschuetz.de</small>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr style="border:1px solid #444; margin: 15px 0;">
|
<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>
|
<span style="color:#ffcc00; font-size:18px;">Max E.</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -129,50 +132,43 @@
|
|||||||
<h2>Datenschutzerklärung</h2>
|
<h2>Datenschutzerklärung</h2>
|
||||||
|
|
||||||
<p><strong>1. Datenschutz auf einen Blick</strong><br>
|
<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>
|
<p><strong>2. Verantwortlicher</strong><br>
|
||||||
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
||||||
Sebastian Unterschütz<br>
|
Sebastian Unterschütz<br>
|
||||||
Göltzschtalblick 16, 08236 Ellefeld<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>
|
<em>(Schulprojekt im Rahmen der IT232)</em></p>
|
||||||
|
|
||||||
<p><strong>3. Hosting (Hetzner)</strong><br>
|
<p><strong>3. Hosting (Hetzner)</strong><br>
|
||||||
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
||||||
<strong>Hetzner Online GmbH</strong><br>
|
<strong>Hetzner Online GmbH</strong><br>
|
||||||
Industriestr. 25<br>
|
Industriestr. 25, 91710 Gunzenhausen, Deutschland<br>
|
||||||
91710 Gunzenhausen<br>
|
|
||||||
Deutschland<br>
|
|
||||||
<br>
|
<br>
|
||||||
Serverstandort: <strong>Deutschland</strong> (ausschließlich).<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>4. Datenerfassung auf dieser Website</strong></p>
|
||||||
|
|
||||||
<p><strong>Server-Log-Dateien</strong><br>
|
<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:
|
Der Provider der Seiten (Hetzner) erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien (Browser, OS, Referrer, Hostname, Uhrzeit, IP-Adresse).<br>
|
||||||
<ul>
|
<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>
|
||||||
<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>
|
|
||||||
|
|
||||||
<p><strong>Spielstände & Highscores</strong><br>
|
<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>
|
<ul>
|
||||||
<li>Den von Ihnen gewählten Namen (Pseudonym empfohlen!)</li>
|
<li>Gewählter Name (Pseudonym empfohlen!)</li>
|
||||||
<li>Ihren Punktestand und Zeitstempel</li>
|
<li>Punktestand und Zeitstempel</li>
|
||||||
<li>Eine Session-ID und einen "Claim-Code" zur Verifizierung</li>
|
<li>Session-ID und "Claim-Code"</li>
|
||||||
</ul>
|
</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>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,5 +198,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -14,12 +14,20 @@ SOUNDS.hit.volume = 0.6;
|
|||||||
SOUNDS.music.loop = true;
|
SOUNDS.music.loop = true;
|
||||||
SOUNDS.music.volume = 0.2;
|
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';
|
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
||||||
|
|
||||||
function playSound(name) {
|
function playSound(name) {
|
||||||
if (isMuted || !SOUNDS[name]) return;
|
if (isMuted || !SOUNDS[name]) return;
|
||||||
|
|
||||||
const soundClone = SOUNDS[name].cloneNode();
|
const soundClone = SOUNDS[name].cloneNode();
|
||||||
soundClone.volume = SOUNDS[name].volume;
|
soundClone.volume = SOUNDS[name].volume;
|
||||||
soundClone.play().catch(() => {});
|
soundClone.play().catch(() => {});
|
||||||
@@ -27,28 +35,41 @@ function playSound(name) {
|
|||||||
|
|
||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
isMuted = !isMuted;
|
isMuted = !isMuted;
|
||||||
|
|
||||||
|
|
||||||
localStorage.setItem('escape_muted', isMuted);
|
localStorage.setItem('escape_muted', isMuted);
|
||||||
|
|
||||||
|
if(isMuted) SOUNDS.music.pause();
|
||||||
if(isMuted) {
|
else SOUNDS.music.play().catch(()=>{});
|
||||||
SOUNDS.music.pause();
|
|
||||||
} else {
|
|
||||||
SOUNDS.music.play().catch(()=>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
return isMuted;
|
return isMuted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMusic() {
|
function startMusic() {
|
||||||
|
if(!isMuted) SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked"));
|
||||||
if(!isMuted) {
|
|
||||||
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getMuteState() {
|
function getMuteState() {
|
||||||
return isMuted;
|
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;
|
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
||||||
|
|
||||||
updateParticles();
|
updateParticles();
|
||||||
|
if (typeof setMusicSpeed === "function") {
|
||||||
|
setMusicSpeed(currentSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
player.prevY = player.y;
|
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() {
|
async function loadAssets() {
|
||||||
const pPromise = new Promise(resolve => {
|
const keysToLoad = [];
|
||||||
playerSprite.src = "assets/player.png";
|
|
||||||
playerSprite.onload = resolve;
|
|
||||||
playerSprite.onerror = () => { resolve(); };
|
|
||||||
});
|
|
||||||
|
|
||||||
const bgPromises = gameConfig.backgrounds.map((bgFile, index) => {
|
// A. Player hinzufügen
|
||||||
return new Promise((resolve) => {
|
PIXI.Assets.add({ alias: 'player', src: 'assets/player.png' });
|
||||||
const img = new Image();
|
keysToLoad.push('player');
|
||||||
img.src = "assets/" + bgFile;
|
|
||||||
img.onload = () => { bgSprites[index] = img; resolve(); };
|
// B. Hintergründe aus Config
|
||||||
img.onerror = () => { resolve(); };
|
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);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const obsPromises = gameConfig.obstacles.map(def => {
|
// C. Hindernisse aus Config
|
||||||
return new Promise((resolve) => {
|
if (gameConfig.obstacles) {
|
||||||
if (!def.image) { resolve(); return; }
|
gameConfig.obstacles.forEach(def => {
|
||||||
const img = new Image();
|
if (def.image) {
|
||||||
img.src = "assets/" + def.image;
|
// Alias = ID (z.B. "teacher")
|
||||||
img.onload = () => { sprites[def.id] = img; resolve(); };
|
// Checken ob Alias schon existiert (vermeidet Warnungen)
|
||||||
img.onerror = () => { resolve(); };
|
if (!PIXI.Assets.cache.has(def.id)) {
|
||||||
|
PIXI.Assets.add({ alias: def.id, src: 'assets/' + def.image });
|
||||||
|
keysToLoad.push(def.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
|
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() {
|
window.startGameClick = async function() {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
|
|
||||||
startScreen.style.display = 'none';
|
startScreen.style.display = 'none';
|
||||||
document.body.classList.add('game-active');
|
document.body.classList.add('game-active');
|
||||||
|
|
||||||
// Score Reset visuell
|
|
||||||
score = 0;
|
score = 0;
|
||||||
const scoreEl = document.getElementById('score');
|
document.getElementById('score').innerText = "0";
|
||||||
if (scoreEl) scoreEl.innerText = "0";
|
|
||||||
|
if (typeof startMusic === 'function') startMusic();
|
||||||
|
|
||||||
// WebSocket Start
|
|
||||||
startMusic();
|
|
||||||
connectGame();
|
connectGame();
|
||||||
resize();
|
resize();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. GAME OVER & SCORE
|
||||||
|
// ==========================================
|
||||||
window.gameOver = function(reason) {
|
window.gameOver = function(reason) {
|
||||||
if (isGameOver) return;
|
if (isGameOver) return;
|
||||||
isGameOver = true;
|
isGameOver = true;
|
||||||
console.log("Game Over:", reason);
|
console.log("Game Over:", reason);
|
||||||
|
|
||||||
|
// Highscore Check (Lokal)
|
||||||
const finalScore = Math.floor(score / 10);
|
const finalScore = Math.floor(score / 10);
|
||||||
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
|
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||||
|
|
||||||
if (finalScore > currentHighscore) {
|
if (finalScore > parseInt(currentHighscore)) {
|
||||||
localStorage.setItem('escape_highscore', finalScore);
|
localStorage.setItem('escape_highscore', finalScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,22 +81,24 @@ window.gameOver = function(reason) {
|
|||||||
gameOverScreen.style.display = 'flex';
|
gameOverScreen.style.display = 'flex';
|
||||||
document.getElementById('finalScore').innerText = finalScore;
|
document.getElementById('finalScore').innerText = finalScore;
|
||||||
|
|
||||||
|
// Input Reset
|
||||||
document.getElementById('inputSection').style.display = 'flex';
|
document.getElementById('inputSection').style.display = 'flex';
|
||||||
document.getElementById('submitBtn').disabled = false;
|
document.getElementById('submitBtn').disabled = false;
|
||||||
|
document.getElementById('playerNameInput').value = "";
|
||||||
|
|
||||||
|
// Liste laden
|
||||||
loadLeaderboard();
|
loadLeaderboard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
window.submitScore = async function() {
|
window.submitScore = async function() {
|
||||||
const nameInput = document.getElementById('playerNameInput');
|
const nameInput = document.getElementById('playerNameInput');
|
||||||
const name = nameInput.value.trim();
|
const name = nameInput.value.trim();
|
||||||
const btn = document.getElementById('submitBtn');
|
const btn = document.getElementById('submitBtn');
|
||||||
|
|
||||||
if (!name) return alert("Bitte Namen eingeben!");
|
if (!name) return alert("Bitte Namen eingeben!");
|
||||||
|
if (!sessionID) return alert("Fehler: Keine Session ID vom Server erhalten.");
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -90,11 +108,10 @@ window.submitScore = async function() {
|
|||||||
body: JSON.stringify({ sessionId: sessionID, name: name })
|
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();
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Lokal speichern ("Meine Codes")
|
||||||
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
myClaims.push({
|
myClaims.push({
|
||||||
name: name,
|
name: name,
|
||||||
@@ -105,52 +122,104 @@ window.submitScore = async function() {
|
|||||||
});
|
});
|
||||||
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
||||||
|
|
||||||
|
// UI Update
|
||||||
document.getElementById('inputSection').style.display = 'none';
|
document.getElementById('inputSection').style.display = 'none';
|
||||||
loadLeaderboard();
|
loadLeaderboard();
|
||||||
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
alert("Fehler: " + e.message);
|
||||||
alert("Fehler beim Speichern: " + e.message);
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
async function loadLeaderboard() {
|
async function loadLeaderboard() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
||||||
const entries = await res.json();
|
const entries = await res.json();
|
||||||
|
|
||||||
let html = "<h3 style='margin-bottom:5px; color:#ffcc00;'>BESTENLISTE</h3>";
|
let html = "<h3 style='margin-bottom:5px; color:#ffcc00;'>BESTENLISTE</h3>";
|
||||||
|
|
||||||
if(entries.length === 0) html += "<div>Noch keine Einträge.</div>";
|
if(!entries || entries.length === 0) html += "<div>Leer.</div>";
|
||||||
|
else entries.forEach(e => {
|
||||||
entries.forEach(e => {
|
const color = e.isMe ? "cyan" : "white";
|
||||||
const color = e.isMe ? "cyan" : "white"; // Eigener Name in Cyan
|
const bg = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
|
||||||
const bgStyle = 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>`;
|
||||||
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>`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('leaderboard').innerHTML = html;
|
document.getElementById('leaderboard').innerHTML = html;
|
||||||
} catch(e) {
|
} catch(e) { console.error(e); }
|
||||||
console.error("Leaderboard 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) {
|
function gameLoop(timestamp) {
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
|
|
||||||
|
// Nur updaten, wenn Spiel läuft
|
||||||
if (isGameRunning && !isGameOver) {
|
if (isGameRunning && !isGameOver) {
|
||||||
if (!lastTime) lastTime = timestamp;
|
if (!lastTime) lastTime = timestamp;
|
||||||
const deltaTime = timestamp - lastTime;
|
const deltaTime = timestamp - lastTime;
|
||||||
@@ -160,46 +229,62 @@ function gameLoop(timestamp) {
|
|||||||
|
|
||||||
accumulator += deltaTime;
|
accumulator += deltaTime;
|
||||||
|
|
||||||
|
// --- FIXED TIME STEP (Physik: 20 TPS) ---
|
||||||
while (accumulator >= MS_PER_TICK) {
|
while (accumulator >= MS_PER_TICK) {
|
||||||
updateGameLogic();
|
updateGameLogic(); // logic.js (setzt prevX/prevY)
|
||||||
currentTick++;
|
currentTick++;
|
||||||
|
|
||||||
|
// Score lokal hochzählen (damit es flüssig aussieht)
|
||||||
|
// Server korrigiert, falls Abweichung zu groß
|
||||||
score++;
|
score++;
|
||||||
|
|
||||||
accumulator -= MS_PER_TICK;
|
accumulator -= MS_PER_TICK;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alpha = accumulator / MS_PER_TICK;
|
// HUD Update
|
||||||
|
|
||||||
// Score im HUD
|
|
||||||
const scoreEl = document.getElementById('score');
|
const scoreEl = document.getElementById('score');
|
||||||
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
|
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() {
|
async function initGame() {
|
||||||
try {
|
try {
|
||||||
const cRes = await fetch('/api/config');
|
const cRes = await fetch('/api/config');
|
||||||
gameConfig = await cRes.json();
|
gameConfig = await cRes.json();
|
||||||
|
|
||||||
|
// Pixi Assets laden
|
||||||
await loadAssets();
|
await loadAssets();
|
||||||
await loadStartScreenLeaderboard();
|
|
||||||
|
|
||||||
if (typeof getMuteState === 'function') {
|
// Startscreen Bestenliste
|
||||||
updateMuteIcon(getMuteState());
|
await loadStartScreenLeaderboard();
|
||||||
}
|
|
||||||
|
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
if(loadingText) loadingText.style.display = 'none';
|
if(loadingText) loadingText.style.display = 'none';
|
||||||
if(startBtn) startBtn.style.display = 'inline-block';
|
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 savedHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||||
const hsEl = document.getElementById('localHighscore');
|
const hsEl = document.getElementById('localHighscore');
|
||||||
if(hsEl) hsEl.innerText = savedHighscore;
|
if(hsEl) hsEl.innerText = savedHighscore;
|
||||||
|
|
||||||
|
// Loop starten (für Idle Rendering)
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
drawGame();
|
drawGame(1.0); // Initiale Zeichnung
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -207,16 +292,14 @@ async function initGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Startscreen Leaderboard
|
||||||
async function loadStartScreenLeaderboard() {
|
async function loadStartScreenLeaderboard() {
|
||||||
try {
|
try {
|
||||||
const listEl = document.getElementById('startLeaderboardList');
|
const listEl = document.getElementById('startLeaderboardList');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
const res = await fetch('/api/leaderboard');
|
const res = await fetch('/api/leaderboard');
|
||||||
const entries = await res.json();
|
const entries = await res.json();
|
||||||
|
if (!entries || entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
|
||||||
if (entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Keine Scores.</div>"; return; }
|
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
let icon = "#" + e.rank;
|
let icon = "#" + e.rank;
|
||||||
@@ -227,14 +310,13 @@ async function loadStartScreenLeaderboard() {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Audio Button Logik
|
||||||
window.toggleAudioClick = function() {
|
window.toggleAudioClick = function() {
|
||||||
const muted = toggleMute();
|
if (typeof toggleMute === 'function') {
|
||||||
|
const muted = toggleMute();
|
||||||
updateMuteIcon(muted);
|
updateMuteIcon(muted);
|
||||||
|
document.getElementById('mute-btn').blur();
|
||||||
|
}
|
||||||
document.getElementById('mute-btn').blur();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateMuteIcon(isMuted) {
|
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() {
|
// Start
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initGame();
|
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() {
|
let floorGraphic = null;
|
||||||
canvas.width = GAME_WIDTH; // 800
|
async function initPixi() {
|
||||||
canvas.height = GAME_HEIGHT; // 400
|
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 windowWidth = window.innerWidth - 20;
|
||||||
const windowHeight = window.innerHeight - 20;
|
const windowHeight = window.innerHeight - 20;
|
||||||
|
|
||||||
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
|
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
|
||||||
const windowRatio = windowWidth / windowHeight;
|
const windowRatio = windowWidth / windowHeight;
|
||||||
|
|
||||||
let finalWidth, finalHeight;
|
let finalWidth, finalHeight;
|
||||||
|
|
||||||
// 3. Skalierung berechnen (Aspect Ratio erhalten)
|
|
||||||
if (windowRatio < targetRatio) {
|
if (windowRatio < targetRatio) {
|
||||||
finalWidth = windowWidth;
|
finalWidth = windowWidth;
|
||||||
finalHeight = windowWidth / targetRatio;
|
finalHeight = windowWidth / targetRatio;
|
||||||
@@ -23,163 +58,210 @@ function resize() {
|
|||||||
finalWidth = finalHeight * targetRatio;
|
finalWidth = finalHeight * targetRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Container Größe setzen (Canvas füllt Container via CSS)
|
// CSS Skalierung
|
||||||
if (container) {
|
app.canvas.style.width = `${Math.floor(finalWidth)}px`;
|
||||||
container.style.width = `${Math.floor(finalWidth)}px`;
|
app.canvas.style.height = `${Math.floor(finalHeight)}px`;
|
||||||
container.style.height = `${Math.floor(finalHeight)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
resize();
|
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// RENDER LOOP (RETAINED MODE)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
function drawGame(alpha = 1.0) {
|
async function drawGame(alpha = 1.0) {
|
||||||
|
if (!app) {
|
||||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
if(!document.getElementById('game-container').querySelector('canvas')) await initPixi();
|
||||||
|
return;
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
|
// 1. HINTERGRUND
|
||||||
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
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 {
|
} else {
|
||||||
ctx.fillStyle = "#f0f0f0";
|
debugLayer.clear();
|
||||||
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// HELPER: SYNC SYSTEM
|
||||||
|
// ------------------------------------------------------
|
||||||
|
function syncSprites(dataList, cacheMap, type, alpha) {
|
||||||
|
const usedObjects = new Set();
|
||||||
|
|
||||||
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
dataList.forEach(obj => {
|
||||||
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
platforms.forEach(p => {
|
// B. Updaten (Interpolation)
|
||||||
|
const def = obj.def || {};
|
||||||
|
|
||||||
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
// Position interpolieren
|
||||||
const rY = p.y;
|
const rX = (obj.prevX !== undefined) ? lerp(obj.prevX, obj.x, alpha) : obj.x;
|
||||||
|
const rY = obj.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) {
|
|
||||||
|
|
||||||
|
if (type === 'platform') {
|
||||||
|
sprite.x = rX;
|
||||||
|
sprite.y = rY;
|
||||||
|
} else {
|
||||||
|
// Editor Werte
|
||||||
const scale = def.imgScale || 1.0;
|
const scale = def.imgScale || 1.0;
|
||||||
const offX = def.imgOffsetX || 0.0;
|
const offX = def.imgOffsetX || 0;
|
||||||
const offY = def.imgOffsetY || 0.0;
|
const offY = def.imgOffsetY || 0;
|
||||||
|
const hbw = def.width || 30;
|
||||||
|
const hbh = def.height || 30;
|
||||||
|
|
||||||
const drawW = hbw * scale;
|
const drawW = hbw * scale;
|
||||||
const drawH = hbh * scale;
|
|
||||||
|
|
||||||
const baseX = rX + (hbw - drawW) / 2;
|
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);
|
sprite.x = baseX + offX;
|
||||||
|
sprite.y = baseY + offY;
|
||||||
} else {
|
sprite.width = drawW;
|
||||||
let color = "#FF00FF";
|
sprite.height = hbh * scale;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
for (const [obj, sprite] of cacheMap.entries()) {
|
||||||
if (serverObstacles) {
|
if (!usedObjects.has(obj)) {
|
||||||
ctx.strokeStyle = "cyan";
|
gameLayer.removeChild(sprite);
|
||||||
ctx.lineWidth = 1;
|
sprite.destroy();
|
||||||
serverObstacles.forEach(sObj => {
|
cacheMap.delete(obj);
|
||||||
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 createPixiSprite(obj, type) {
|
||||||
function drawSpeechBubble(x, y, text) {
|
if (type === 'platform') {
|
||||||
const bX = x - 20;
|
const g = new PIXI.Graphics();
|
||||||
const bY = y - 40;
|
// Holz Plattform
|
||||||
const bW = 120;
|
g.rect(0, 0, obj.w, obj.h).fill(0x8B4513); // Braun
|
||||||
const bH = 30;
|
g.rect(0, 0, obj.w, 5).fill(0xA0522D); // Hellbraun Oben
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const def = obj.def || {};
|
||||||
|
|
||||||
ctx.fillStyle = "white"; ctx.fillRect(bX, bY, bW, bH);
|
// CHECK: Ist die Textur im Cache?
|
||||||
ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.strokeRect(bX, bY, bW, bH);
|
// Wir nutzen PIXI.Assets.get(), das ist sicherer als cache.has
|
||||||
ctx.fillStyle = "black"; ctx.font = "10px Arial"; ctx.textAlign = "center";
|
let texture = null;
|
||||||
ctx.fillText(text, bX + bW/2, bY + 20);
|
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) {
|
||||||
|
serverObstacles.forEach(o => {
|
||||||
|
g.rect(o.x, o.y, o.w, o.h).stroke({ width: 1, color: 0x00FFFF });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
// --- KONFIGURATION & FLAGS ---
|
||||||
let isLoaded = false;
|
let gameConfig = null; // Wird von /api/config geladen
|
||||||
let isGameRunning = false;
|
let isLoaded = false; // Sind Assets geladen?
|
||||||
let isGameOver = false;
|
let isGameRunning = false; // Läuft der Game Loop?
|
||||||
let sessionID = null;
|
let isGameOver = false; // Ist der Spieler tot?
|
||||||
|
let sessionID = null; // UUID der aktuellen Session (vom Server)
|
||||||
|
|
||||||
let socket = null;
|
// --- NETZWERK & STREAMING ---
|
||||||
let obstacleBuffer = [];
|
let socket = null; // WebSocket Verbindung
|
||||||
let platformBuffer = [];
|
let obstacleBuffer = []; // Warteschlange Hindernisse (vom Server)
|
||||||
|
let platformBuffer = []; // Warteschlange Plattformen (vom Server)
|
||||||
|
|
||||||
let score = 0;
|
// --- LATENZ & SYNC ---
|
||||||
let currentTick = 0;
|
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 godModeLives = 0;
|
||||||
let hasBat = false;
|
let hasBat = false;
|
||||||
let bootTicks = 0;
|
let bootTicks = 0;
|
||||||
|
|
||||||
|
// --- HINTERGRUND LOGIK ---
|
||||||
|
let maxRawBgIndex = 0; // Welches BG Bild ist dran?
|
||||||
|
|
||||||
let maxRawBgIndex = 0;
|
// --- GAME LOOP TIMING ---
|
||||||
|
|
||||||
|
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
let accumulator = 0;
|
let accumulator = 0;
|
||||||
|
|
||||||
|
// --- GRAFIKEN & EFFEKTE ---
|
||||||
|
let particles = []; // Array für Partikel-Effekte
|
||||||
|
|
||||||
let sprites = {};
|
// --- ENTITIES (Render-Listen) ---
|
||||||
let playerSprite = new Image();
|
|
||||||
let bgSprites = [];
|
|
||||||
|
|
||||||
|
|
||||||
let player = {
|
let player = {
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 300,
|
y: 300,
|
||||||
@@ -38,30 +62,30 @@ let player = {
|
|||||||
color: "red",
|
color: "red",
|
||||||
vy: 0,
|
vy: 0,
|
||||||
grounded: false,
|
grounded: false,
|
||||||
|
|
||||||
|
// WICHTIG für Interpolation:
|
||||||
prevY: 300
|
prevY: 300
|
||||||
};
|
};
|
||||||
let particles = [];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Diese Listen werden von logic.js gefüllt und von render.js gezeichnet
|
||||||
let obstacles = [];
|
let obstacles = [];
|
||||||
let platforms = [];
|
let platforms = [];
|
||||||
|
|
||||||
|
// Debug-Daten vom Server (für das Overlay)
|
||||||
let serverObstacles = [];
|
let serverObstacles = [];
|
||||||
let serverPlatforms = [];
|
let serverPlatforms = [];
|
||||||
|
|
||||||
let currentLatencyMs = 0;
|
// --- INPUT STATE ---
|
||||||
let pingInterval = null;
|
|
||||||
|
|
||||||
|
|
||||||
let isCrouching = false;
|
let isCrouching = false;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
const canvas = document.getElementById('gameCanvas');
|
// HTML ELEMENTE (DOM Caching)
|
||||||
const ctx = canvas.getContext('2d');
|
// ==========================================
|
||||||
const container = document.getElementById('game-container');
|
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 startScreen = document.getElementById('startScreen');
|
||||||
const startBtn = document.getElementById('startBtn');
|
const startBtn = document.getElementById('startBtn');
|
||||||
const loadingText = document.getElementById('loadingText');
|
const loadingText = document.getElementById('loadingText');
|
||||||
|
|||||||
Reference in New Issue
Block a user