All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m23s
353 lines
12 KiB
JavaScript
353 lines
12 KiB
JavaScript
// ==========================================
|
|
// 1. ASSETS LADEN (PIXI V8)
|
|
// ==========================================
|
|
// ==========================================
|
|
// 1. ASSETS LADEN (PIXI V8 KORREKT)
|
|
// ==========================================
|
|
// ==========================================
|
|
// 1. ASSETS LADEN (FEHLERTOLERANT)
|
|
// ==========================================
|
|
async function loadAssets() {
|
|
const assetsToLoad = [];
|
|
|
|
// --- 1. REGISTRIEREN (Namen zu Pfaden zuordnen) ---
|
|
|
|
// A. Player
|
|
// Prüfen ob schon da, um Warnungen bei Hot-Reload zu vermeiden
|
|
if (!PIXI.Assets.cache.has('player')) {
|
|
PIXI.Assets.add({ alias: 'player', src: 'assets/player.png' });
|
|
assetsToLoad.push('player');
|
|
}
|
|
|
|
// B. Hintergründe
|
|
if (gameConfig.backgrounds) {
|
|
gameConfig.backgrounds.forEach(bg => {
|
|
if (!PIXI.Assets.cache.has(bg)) {
|
|
PIXI.Assets.add({ alias: bg, src: 'assets/' + bg });
|
|
assetsToLoad.push(bg);
|
|
}
|
|
});
|
|
}
|
|
|
|
// C. Hindernisse
|
|
if (gameConfig.obstacles) {
|
|
gameConfig.obstacles.forEach(def => {
|
|
if (def.image && !PIXI.Assets.cache.has(def.id)) {
|
|
PIXI.Assets.add({ alias: def.id, src: 'assets/' + def.image });
|
|
assetsToLoad.push(def.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- 2. LADEN (Mit Fehler-Abfangung) ---
|
|
console.log(`Lade ${assetsToLoad.length} Assets...`);
|
|
|
|
// Wir erstellen für jedes Asset einen eigenen Lade-Versuch
|
|
const loadPromises = assetsToLoad.map(key => {
|
|
return PIXI.Assets.load(key)
|
|
.catch(err => {
|
|
// HIER IST DER TRICK:
|
|
// Wenn ein Fehler passiert, loggen wir ihn, aber werfen ihn NICHT weiter.
|
|
// Wir geben einfach null zurück. Damit gilt dieser Task als "erledigt".
|
|
console.warn(`⚠️ Asset Fehler bei '${key}': Datei fehlt oder defekt.`);
|
|
return null;
|
|
});
|
|
});
|
|
|
|
// Wir warten, bis ALLE Versuche durch sind (egal ob Erfolg oder Fehler)
|
|
await Promise.all(loadPromises);
|
|
|
|
console.log("✅ Ladevorgang abgeschlossen (vorhandene Assets sind bereit).");
|
|
}
|
|
|
|
// ... (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 = 0;
|
|
document.getElementById('score').innerText = "0";
|
|
|
|
if (typeof startMusic === 'function') 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 > parseInt(currentHighscore)) {
|
|
localStorage.setItem('escape_highscore', finalScore);
|
|
}
|
|
|
|
if (gameOverScreen) {
|
|
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 {
|
|
const res = await fetch('/api/submit-name', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ sessionId: sessionID, name: name })
|
|
});
|
|
|
|
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,
|
|
score: Math.floor(score / 10),
|
|
code: data.claimCode,
|
|
date: new Date().toLocaleString('de-DE'),
|
|
sessionId: sessionID
|
|
});
|
|
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
|
|
|
// UI Update
|
|
document.getElementById('inputSection').style.display = 'none';
|
|
loadLeaderboard();
|
|
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
|
|
|
} catch (e) {
|
|
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 || 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(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;
|
|
lastTime = timestamp;
|
|
|
|
if (deltaTime > 1000) { accumulator = 0; return; }
|
|
|
|
accumulator += deltaTime;
|
|
|
|
// --- FIXED TIME STEP (Physik: 20 TPS) ---
|
|
while (accumulator >= MS_PER_TICK) {
|
|
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;
|
|
}
|
|
|
|
// HUD Update
|
|
const scoreEl = document.getElementById('score');
|
|
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
|
|
}
|
|
|
|
// --- 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();
|
|
|
|
// 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(1.0); // Initiale Zeichnung
|
|
|
|
} catch(e) {
|
|
console.error(e);
|
|
if(loadingText) loadingText.innerText = "Ladefehler (siehe Konsole)";
|
|
}
|
|
}
|
|
|
|
// 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 || entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
|
|
let html = "";
|
|
entries.forEach(e => {
|
|
let icon = "#" + e.rank;
|
|
if (e.rank === 1) icon = "🥇"; if (e.rank === 2) icon = "🥈"; if (e.rank === 3) icon = "🥉";
|
|
html += `<div class="hof-entry"><span><span class="hof-rank">${icon}</span> ${e.name}</span><span class="hof-score">${Math.floor(e.score / 10)}</span></div>`;
|
|
});
|
|
listEl.innerHTML = html;
|
|
} 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) {
|
|
const btn = document.getElementById('mute-btn');
|
|
if (btn) {
|
|
btn.innerText = isMuted ? "🔇" : "🔊";
|
|
btn.style.color = isMuted ? "#ff4444" : "white";
|
|
btn.style.borderColor = isMuted ? "#ff4444" : "#555";
|
|
}
|
|
}
|
|
|
|
// 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(); }
|
|
|
|
// Start
|
|
initGame(); |