add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
This commit is contained in:
@@ -1,247 +1,163 @@
|
||||
// ==========================================
|
||||
// INIT & ASSETS
|
||||
// 1. ASSETS LADEN
|
||||
// ==========================================
|
||||
async function loadAssets() {
|
||||
playerSprite.src = "assets/player.png";
|
||||
const pPromise = new Promise(resolve => {
|
||||
playerSprite.src = "assets/player.png";
|
||||
playerSprite.onload = resolve;
|
||||
playerSprite.onerror = () => { resolve(); };
|
||||
});
|
||||
|
||||
// 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;
|
||||
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(); };
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// START LOGIK
|
||||
// 2. SPIEL STARTEN
|
||||
// ==========================================
|
||||
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 Reset visuell
|
||||
score = 0;
|
||||
const scoreEl = document.getElementById('score');
|
||||
if (scoreEl) scoreEl.innerText = "0";
|
||||
|
||||
// WebSocket Start
|
||||
startMusic();
|
||||
connectGame();
|
||||
resize();
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// SCORE EINTRAGEN
|
||||
// 3. GAME OVER & HIGHSCORE LOGIK
|
||||
// ==========================================
|
||||
window.gameOver = function(reason) {
|
||||
if (isGameOver) return;
|
||||
isGameOver = true;
|
||||
console.log("Game Over:", reason);
|
||||
|
||||
const finalScore = Math.floor(score / 10);
|
||||
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||
|
||||
if (finalScore > currentHighscore) {
|
||||
localStorage.setItem('escape_highscore', finalScore);
|
||||
}
|
||||
|
||||
if (gameOverScreen) {
|
||||
gameOverScreen.style.display = 'flex';
|
||||
document.getElementById('finalScore').innerText = finalScore;
|
||||
|
||||
// Input wieder anzeigen
|
||||
document.getElementById('inputSection').style.display = 'flex';
|
||||
document.getElementById('submitBtn').disabled = false;
|
||||
|
||||
// Liste laden
|
||||
loadLeaderboard();
|
||||
}
|
||||
};
|
||||
|
||||
// Name absenden (Button Click)
|
||||
window.submitScore = async function() {
|
||||
const nameInput = document.getElementById('playerNameInput');
|
||||
const name = nameInput.value;
|
||||
const name = nameInput.value.trim();
|
||||
const btn = document.getElementById('submitBtn');
|
||||
|
||||
if (!name) return alert("Namen eingeben!");
|
||||
if (!name) return alert("Bitte 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 })
|
||||
body: JSON.stringify({ sessionId: sessionID, name: name }) // sessionID aus state.js
|
||||
});
|
||||
if (!res.ok) throw new Error("Server Error");
|
||||
|
||||
if (!res.ok) throw new Error("Fehler beim Senden");
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Code lokal speichern (Claims)
|
||||
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
|
||||
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! Code: ${data.claimCode}`);
|
||||
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
||||
|
||||
} catch (e) {
|
||||
alert("Fehler: " + e.message);
|
||||
console.error(e);
|
||||
alert("Fehler beim Speichern: " + 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 = "<div style='padding:10px; 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 => {
|
||||
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 += `
|
||||
<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 ${btnAttr}
|
||||
style="background:transparent; border:1px solid; padding:5px; font-size:9px; margin:0; ${btnStyle}">
|
||||
LÖSCHEN
|
||||
</button>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
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!"); }
|
||||
};
|
||||
|
||||
// Bestenliste laden (Game Over Screen)
|
||||
async function loadLeaderboard() {
|
||||
try {
|
||||
// sessionID wird mitgesendet, um den eigenen Eintrag zu markieren
|
||||
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
||||
const entries = await res.json();
|
||||
|
||||
let html = "<h3 style='margin-bottom:5px'>BESTENLISTE</h3>";
|
||||
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 ? "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 = `<div style='font-size:8px; color:#aaa;'>(${betterThanMe} waren besser)</div>`;
|
||||
} else if (e.isMe && betterThanMe === 0) {
|
||||
infoText = `<div style='font-size:8px; color:#ffcc00;'>👑 NIEMAND ist besser!</div>`;
|
||||
}
|
||||
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} margin-bottom:2px;">
|
||||
<div style="display:flex; justify-content:space-between; color:${color};">
|
||||
<span>#${e.rank} ${e.name.toUpperCase()}</span>
|
||||
<span>${Math.floor(e.score/10)}</span>
|
||||
</div>
|
||||
${infoText}
|
||||
<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(e.rank === 3 && entries.length > 3 && !entries[3].isMe) {
|
||||
html += "<div style='text-align:center; color:gray; font-size:8px;'>...</div>";
|
||||
}
|
||||
});
|
||||
|
||||
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 = "<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) {}
|
||||
}
|
||||
|
||||
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();
|
||||
} catch(e) {
|
||||
console.error("Leaderboard Error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// DER FIXIERTE GAME LOOP
|
||||
// 4. 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;
|
||||
@@ -254,28 +170,34 @@ function gameLoop(timestamp) {
|
||||
updateGameLogic();
|
||||
currentTick++;
|
||||
score++;
|
||||
if (currentTick - lastSentTick >= CHUNK_SIZE) sendChunk();
|
||||
accumulator -= MS_PER_TICK;
|
||||
}
|
||||
|
||||
const alpha = accumulator / MS_PER_TICK;
|
||||
|
||||
// Score im HUD
|
||||
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();
|
||||
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 5. INIT
|
||||
// ==========================================
|
||||
async function initGame() {
|
||||
try {
|
||||
const cRes = await fetch('/api/config'); gameConfig = await cRes.json();
|
||||
const cRes = await fetch('/api/config');
|
||||
gameConfig = await cRes.json();
|
||||
|
||||
// Erst alles laden
|
||||
await loadAssets();
|
||||
await loadStartScreenLeaderboard();
|
||||
|
||||
if (typeof getMuteState === 'function') {
|
||||
updateMuteIcon(getMuteState());
|
||||
}
|
||||
|
||||
isLoaded = true;
|
||||
if(loadingText) loadingText.style.display = 'none';
|
||||
if(startBtn) startBtn.style.display = 'inline-block';
|
||||
@@ -284,10 +206,7 @@ async function initGame() {
|
||||
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) {
|
||||
@@ -296,4 +215,147 @@ async function initGame() {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Mini-Leaderboard auf Startseite
|
||||
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; }
|
||||
|
||||
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) {}
|
||||
}
|
||||
|
||||
// Audio Toggle Funktion für den Button
|
||||
window.toggleAudioClick = function() {
|
||||
// 1. Audio umschalten (in audio.js)
|
||||
const muted = toggleMute();
|
||||
|
||||
// 2. Button Icon updaten
|
||||
updateMuteIcon(muted);
|
||||
|
||||
// 3. Fokus vom Button nehmen (damit Space nicht den Button drückt, sondern springt)
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MEINE CODES (LOCAL STORAGE)
|
||||
// ==========================================
|
||||
|
||||
// 1. Codes anzeigen (Wird vom Button im Startscreen aufgerufen)
|
||||
window.showMyCodes = function() {
|
||||
// Modal öffnen
|
||||
openModal('codes');
|
||||
|
||||
const listEl = document.getElementById('codesList');
|
||||
if(!listEl) return;
|
||||
|
||||
// Daten aus dem Browser-Speicher holen
|
||||
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;
|
||||
}
|
||||
|
||||
// Sortieren nach Score (Höchster zuerst)
|
||||
const sortedClaims = rawClaims
|
||||
.map((item, index) => ({ ...item, originalIndex: index }))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
let html = "";
|
||||
|
||||
sortedClaims.forEach(c => {
|
||||
// Icons basierend auf Score
|
||||
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;
|
||||
};
|
||||
|
||||
// 2. Code löschen (Lokal und auf Server)
|
||||
window.deleteClaim = async function(sid, code) {
|
||||
if(!confirm("Eintrag wirklich löschen?")) return;
|
||||
|
||||
// Versuch, es auf dem Server zu löschen
|
||||
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...");
|
||||
}
|
||||
|
||||
// Lokal löschen
|
||||
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
// Wir filtern den Eintrag raus, der die gleiche SessionID UND den gleichen Code hat
|
||||
claims = claims.filter(c => c.code !== code);
|
||||
|
||||
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
||||
|
||||
// Liste aktualisieren
|
||||
window.showMyCodes();
|
||||
|
||||
// Leaderboard aktualisieren (falls im Hintergrund sichtbar)
|
||||
if(document.getElementById('startLeaderboardList')) {
|
||||
loadStartScreenLeaderboard();
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// MODAL LOGIK (Fenster auf/zu)
|
||||
// ==========================================
|
||||
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');
|
||||
}
|
||||
|
||||
// Klick nebendran schließt Modal
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal-overlay')) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
initGame();
|
||||
Reference in New Issue
Block a user