// ========================================== // INIT & ASSETS // ========================================== async function loadAssets() { playerSprite.src = "assets/player.png"; // Hintergründe laden 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 = () => { console.warn("BG fehlt:", bgFile); bgSprites[index] = null; resolve(); }; }); }); // Hindernisse laden 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(); }; }); }); // Player laden (kleiner Promise Wrapper) const pPromise = new Promise(r => { playerSprite.onload = r; playerSprite.onerror = r; }); await Promise.all([pPromise, ...bgPromises, ...obsPromises]); } // ========================================== // START LOGIK // ========================================== window.startGameClick = async function() { if (!isLoaded) return; startScreen.style.display = 'none'; document.body.classList.add('game-active'); try { const sRes = await fetch('/api/start', {method:'POST'}); const sData = await sRes.json(); sessionID = sData.sessionId; rng = new PseudoRNG(sData.seed); isGameRunning = true; maxRawBgIndex = 0; lastTime = performance.now(); resize(); } catch(e) { alert("Start Fehler: " + e.message); location.reload(); } }; // ========================================== // SCORE EINTRAGEN // ========================================== window.submitScore = async function() { const nameInput = document.getElementById('playerNameInput'); const name = nameInput.value; const btn = document.getElementById('submitBtn'); if (!name) return alert("Namen eingeben!"); 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 Error"); const data = await res.json(); 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)); document.getElementById('inputSection').style.display = 'none'; loadLeaderboard(); alert(`Gespeichert! Code: ${data.claimCode}`); } catch (e) { alert("Fehler: " + e.message); btn.disabled = false; } }; // ========================================== // MEINE CODES & LÖSCHEN // ========================================== window.showMyCodes = function() { if(window.openModal) window.openModal('codes'); const listEl = document.getElementById('codesList'); if(!listEl) return; const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]'); if (rawClaims.length === 0) { listEl.innerHTML = "
Keine Codes gespeichert.
"; return; } const sortedClaims = rawClaims .map((item, index) => ({ ...item, originalIndex: index })) .sort((a, b) => b.score - a.score); let html = ""; sortedClaims.forEach(c => { const canDelete = c.sessionId ? true : false; const btnStyle = canDelete ? "cursor:pointer; color:#ff4444; border-color:#ff4444;" : "cursor:not-allowed; color:gray; border-color:gray;"; const btnAttr = canDelete ? `onclick="deleteClaim(${c.originalIndex}, '${c.sessionId}', '${c.code}')"` : "disabled"; let rankIcon = "📄"; if (c.score >= 10000) rankIcon = "🔥"; if (c.score >= 5000) rankIcon = "⭐"; html += `
${rankIcon} ${c.code} (${c.score} Pkt)
${c.name} • ${c.date}
`; }); listEl.innerHTML = html; }; window.deleteClaim = async function(index, sid, code) { if(!confirm("Wirklich löschen?")) return; try { const res = await fetch('/api/claim/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ sessionId: sid, claimCode: code }) }); if (!res.ok) { if(!confirm("Server Fehler (evtl. schon weg). Lokal löschen?")) return; } let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]'); claims.splice(index, 1); localStorage.setItem('escape_claims', JSON.stringify(claims)); window.showMyCodes(); loadLeaderboard(); } catch(e) { alert("Verbindungsfehler!"); } }; async function loadLeaderboard() { try { const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`); const entries = await res.json(); let html = "

BESTENLISTE

"; entries.forEach(e => { const color = e.isMe ? "yellow" : "white"; const bgStyle = e.isMe ? "background:rgba(255,255,0,0.1);" : ""; const betterThanMe = e.rank - 1; let infoText = ""; if (e.isMe && betterThanMe > 0) { infoText = `
(${betterThanMe} waren besser)
`; } else if (e.isMe && betterThanMe === 0) { infoText = `
👑 NIEMAND ist besser!
`; } html += `
#${e.rank} ${e.name.toUpperCase()} ${Math.floor(e.score/10)}
${infoText}
`; if(e.rank === 3 && entries.length > 3 && !entries[3].isMe) { html += "
...
"; } }); document.getElementById('leaderboard').innerHTML = html; } catch(e) { console.error(e); } } 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 = "
Noch keine Scores.
"; 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 += `
${icon} ${e.name}${Math.floor(e.score / 10)}
`; }); listEl.innerHTML = html; } catch (e) {} } function gameOver(reason) { if (isGameOver) return; isGameOver = true; const finalScoreVal = Math.floor(score / 10); const currentHighscore = localStorage.getItem('escape_highscore') || 0; if (finalScoreVal > currentHighscore) localStorage.setItem('escape_highscore', finalScoreVal); gameOverScreen.style.display = 'flex'; document.getElementById('finalScore').innerText = finalScoreVal; loadLeaderboard(); drawGame(); } // ========================================== // DER FIXIERTE GAME LOOP // ========================================== function gameLoop(timestamp) { requestAnimationFrame(gameLoop); // 1. Wenn Assets noch nicht da sind, machen wir gar nichts if (!isLoaded) return; // 2. PHYSIK-LOGIK (Nur wenn Spiel läuft und nicht Game Over) // Das hier sorgt dafür, dass der Dino stehen bleibt, wenn wir im Menü sind if (isGameRunning && !isGameOver) { if (!lastTime) lastTime = timestamp; const deltaTime = timestamp - lastTime; lastTime = timestamp; if (deltaTime > 1000) { accumulator = 0; return; } accumulator += deltaTime; while (accumulator >= MS_PER_TICK) { updateGameLogic(); currentTick++; score++; if (currentTick - lastSentTick >= CHUNK_SIZE) sendChunk(); accumulator -= MS_PER_TICK; } const scoreEl = document.getElementById('score'); if (scoreEl) scoreEl.innerText = Math.floor(score / 10); } // 3. RENDERING (IMMER!) // Das hier war das Problem. Früher stand hier "return" wenn !isGameRunning. // Jetzt malen wir immer. Wenn isGameRunning false ist, malt er einfach den Start-Zustand. drawGame(); } async function initGame() { try { const cRes = await fetch('/api/config'); gameConfig = await cRes.json(); // Erst alles laden await loadAssets(); await loadStartScreenLeaderboard(); isLoaded = true; if(loadingText) loadingText.style.display = 'none'; if(startBtn) startBtn.style.display = 'inline-block'; const savedHighscore = localStorage.getItem('escape_highscore') || 0; const hsEl = document.getElementById('localHighscore'); if(hsEl) hsEl.innerText = savedHighscore; // Loop starten (mit dummy timestamp start) requestAnimationFrame(gameLoop); // Initiales Zeichnen erzwingen (damit Hintergrund sofort da ist) drawGame(); } catch(e) { console.error(e); if(loadingText) loadingText.innerText = "Ladefehler (siehe Konsole)"; } } initGame();