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:
@@ -14,12 +14,20 @@ SOUNDS.hit.volume = 0.6;
|
||||
SOUNDS.music.loop = true;
|
||||
SOUNDS.music.volume = 0.2;
|
||||
|
||||
// Standard: Pitch beibehalten (WICHTIG für euch!)
|
||||
if (SOUNDS.music.preservesPitch !== undefined) {
|
||||
SOUNDS.music.preservesPitch = true;
|
||||
} else if (SOUNDS.music.mozPreservesPitch !== undefined) {
|
||||
SOUNDS.music.mozPreservesPitch = true; // Firefox Fallback
|
||||
} else if (SOUNDS.music.webkitPreservesPitch !== undefined) {
|
||||
SOUNDS.music.webkitPreservesPitch = true; // Safari Fallback
|
||||
}
|
||||
|
||||
// Mute Status laden
|
||||
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
||||
|
||||
function playSound(name) {
|
||||
if (isMuted || !SOUNDS[name]) return;
|
||||
|
||||
const soundClone = SOUNDS[name].cloneNode();
|
||||
soundClone.volume = SOUNDS[name].volume;
|
||||
soundClone.play().catch(() => {});
|
||||
@@ -27,28 +35,41 @@ function playSound(name) {
|
||||
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
|
||||
|
||||
localStorage.setItem('escape_muted', isMuted);
|
||||
|
||||
|
||||
if(isMuted) {
|
||||
SOUNDS.music.pause();
|
||||
} else {
|
||||
SOUNDS.music.play().catch(()=>{});
|
||||
}
|
||||
if(isMuted) SOUNDS.music.pause();
|
||||
else SOUNDS.music.play().catch(()=>{});
|
||||
|
||||
return isMuted;
|
||||
}
|
||||
|
||||
function startMusic() {
|
||||
|
||||
if(!isMuted) {
|
||||
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
|
||||
}
|
||||
if(!isMuted) SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked"));
|
||||
}
|
||||
|
||||
|
||||
function getMuteState() {
|
||||
return isMuted;
|
||||
}
|
||||
|
||||
// --- GESCHWINDIGKEIT ANPASSEN ---
|
||||
function setMusicSpeed(gameSpeed) {
|
||||
if (isMuted || !SOUNDS.music) return;
|
||||
|
||||
const baseGameSpeed = 15.0; // Muss zu BASE_SPEED in config.js passen
|
||||
|
||||
// Faktor berechnen: Speed 30 = Musik 1.3x
|
||||
let rate = 1.0 + (gameSpeed - baseGameSpeed) * 0.02;
|
||||
|
||||
// Limits
|
||||
if (rate < 1.0) rate = 1.0;
|
||||
if (rate > 2.0) rate = 2.0;
|
||||
|
||||
// Nur bei spürbarer Änderung anwenden
|
||||
if (Math.abs(SOUNDS.music.playbackRate - rate) > 0.05) {
|
||||
SOUNDS.music.playbackRate = rate;
|
||||
}
|
||||
}
|
||||
|
||||
function resetMusicSpeed() {
|
||||
if (SOUNDS.music) SOUNDS.music.playbackRate = 1.0;
|
||||
}
|
||||
@@ -3,6 +3,9 @@ function updateGameLogic() {
|
||||
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
||||
|
||||
updateParticles();
|
||||
if (typeof setMusicSpeed === "function") {
|
||||
setMusicSpeed(currentSpeed);
|
||||
}
|
||||
|
||||
|
||||
player.prevY = player.y;
|
||||
|
||||
@@ -1,63 +1,79 @@
|
||||
// ==========================================
|
||||
// 1. ASSETS LADEN
|
||||
// 1. ASSETS LADEN (PIXI V8)
|
||||
// ==========================================
|
||||
// ==========================================
|
||||
// 1. ASSETS LADEN (PIXI V8 KORREKT)
|
||||
// ==========================================
|
||||
async function loadAssets() {
|
||||
const pPromise = new Promise(resolve => {
|
||||
playerSprite.src = "assets/player.png";
|
||||
playerSprite.onload = resolve;
|
||||
playerSprite.onerror = () => { resolve(); };
|
||||
});
|
||||
const keysToLoad = [];
|
||||
|
||||
const bgPromises = gameConfig.backgrounds.map((bgFile, index) => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = "assets/" + bgFile;
|
||||
img.onload = () => { bgSprites[index] = img; resolve(); };
|
||||
img.onerror = () => { resolve(); };
|
||||
// A. Player hinzufügen
|
||||
PIXI.Assets.add({ alias: 'player', src: 'assets/player.png' });
|
||||
keysToLoad.push('player');
|
||||
|
||||
// B. Hintergründe aus Config
|
||||
if (gameConfig.backgrounds) {
|
||||
gameConfig.backgrounds.forEach(bg => {
|
||||
// Alias = Dateiname (z.B. "school-background.jpg")
|
||||
PIXI.Assets.add({ alias: bg, src: 'assets/' + bg });
|
||||
keysToLoad.push(bg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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(); };
|
||||
// C. Hindernisse aus Config
|
||||
if (gameConfig.obstacles) {
|
||||
gameConfig.obstacles.forEach(def => {
|
||||
if (def.image) {
|
||||
// Alias = ID (z.B. "teacher")
|
||||
// Checken ob Alias schon existiert (vermeidet Warnungen)
|
||||
if (!PIXI.Assets.cache.has(def.id)) {
|
||||
PIXI.Assets.add({ alias: def.id, src: 'assets/' + def.image });
|
||||
keysToLoad.push(def.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!isLoaded) return;
|
||||
|
||||
startScreen.style.display = 'none';
|
||||
document.body.classList.add('game-active');
|
||||
|
||||
// Score Reset visuell
|
||||
score = 0;
|
||||
const scoreEl = document.getElementById('score');
|
||||
if (scoreEl) scoreEl.innerText = "0";
|
||||
document.getElementById('score').innerText = "0";
|
||||
|
||||
if (typeof startMusic === 'function') startMusic();
|
||||
|
||||
// WebSocket Start
|
||||
startMusic();
|
||||
connectGame();
|
||||
resize();
|
||||
};
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 3. GAME OVER & SCORE
|
||||
// ==========================================
|
||||
window.gameOver = function(reason) {
|
||||
if (isGameOver) return;
|
||||
isGameOver = true;
|
||||
console.log("Game Over:", reason);
|
||||
|
||||
// Highscore Check (Lokal)
|
||||
const finalScore = Math.floor(score / 10);
|
||||
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||
|
||||
if (finalScore > currentHighscore) {
|
||||
if (finalScore > parseInt(currentHighscore)) {
|
||||
localStorage.setItem('escape_highscore', finalScore);
|
||||
}
|
||||
|
||||
@@ -65,22 +81,24 @@ window.gameOver = function(reason) {
|
||||
gameOverScreen.style.display = 'flex';
|
||||
document.getElementById('finalScore').innerText = finalScore;
|
||||
|
||||
|
||||
// Input Reset
|
||||
document.getElementById('inputSection').style.display = 'flex';
|
||||
document.getElementById('submitBtn').disabled = false;
|
||||
document.getElementById('playerNameInput').value = "";
|
||||
|
||||
|
||||
// Liste laden
|
||||
loadLeaderboard();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
window.submitScore = async function() {
|
||||
const nameInput = document.getElementById('playerNameInput');
|
||||
const name = nameInput.value.trim();
|
||||
const btn = document.getElementById('submitBtn');
|
||||
|
||||
if (!name) return alert("Bitte Namen eingeben!");
|
||||
if (!sessionID) return alert("Fehler: Keine Session ID vom Server erhalten.");
|
||||
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
@@ -90,11 +108,10 @@ window.submitScore = async function() {
|
||||
body: JSON.stringify({ sessionId: sessionID, name: name })
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Fehler beim Senden");
|
||||
|
||||
if (!res.ok) throw new Error("Server antwortet nicht");
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
// Lokal speichern ("Meine Codes")
|
||||
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
myClaims.push({
|
||||
name: name,
|
||||
@@ -105,52 +122,104 @@ window.submitScore = async function() {
|
||||
});
|
||||
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
||||
|
||||
|
||||
// UI Update
|
||||
document.getElementById('inputSection').style.display = 'none';
|
||||
loadLeaderboard();
|
||||
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Fehler beim Speichern: " + e.message);
|
||||
alert("Fehler: " + e.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadLeaderboard() {
|
||||
try {
|
||||
|
||||
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
||||
const entries = await res.json();
|
||||
|
||||
let html = "<h3 style='margin-bottom:5px; color:#ffcc00;'>BESTENLISTE</h3>";
|
||||
|
||||
if(entries.length === 0) html += "<div>Noch keine Einträge.</div>";
|
||||
|
||||
entries.forEach(e => {
|
||||
const color = e.isMe ? "cyan" : "white"; // Eigener Name in Cyan
|
||||
const bgStyle = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
|
||||
|
||||
html += `
|
||||
<div style="border-bottom:1px dotted #444; padding:5px; ${bgStyle} display:flex; justify-content:space-between; color:${color}; font-size:12px;">
|
||||
<span>#${e.rank} ${e.name}</span>
|
||||
<span>${Math.floor(e.score/10)}</span>
|
||||
</div>`;
|
||||
if(!entries || entries.length === 0) html += "<div>Leer.</div>";
|
||||
else entries.forEach(e => {
|
||||
const color = e.isMe ? "cyan" : "white";
|
||||
const bg = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
|
||||
html += `<div style="display:flex; justify-content:space-between; color:${color}; ${bg} padding:4px; border-bottom:1px dotted #444; font-size:12px;">
|
||||
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
|
||||
});
|
||||
|
||||
document.getElementById('leaderboard').innerHTML = html;
|
||||
} catch(e) {
|
||||
console.error("Leaderboard Error:", e);
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 4. MEINE CODES (LOGIK)
|
||||
// ==========================================
|
||||
window.showMyCodes = function() {
|
||||
openModal('codes');
|
||||
const listEl = document.getElementById('codesList');
|
||||
if(!listEl) return;
|
||||
|
||||
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
if (rawClaims.length === 0) {
|
||||
listEl.innerHTML = "<div style='padding:20px; text-align:center; color:#666;'>Keine Codes.</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedClaims = rawClaims.sort((a, b) => b.score - a.score);
|
||||
let html = "";
|
||||
|
||||
sortedClaims.forEach(c => {
|
||||
let rankIcon = "📄";
|
||||
if (c.score >= 5000) rankIcon = "⭐";
|
||||
if (c.score >= 10000) rankIcon = "🔥";
|
||||
|
||||
html += `
|
||||
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div style="text-align:left;">
|
||||
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${rankIcon} ${c.code}</span>
|
||||
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
|
||||
<span style="color:#aaa; font-size:9px;">${c.name} • ${c.date}</span>
|
||||
</div>
|
||||
<button onclick="deleteClaim('${c.code}')"
|
||||
style="background:transparent; border:1px solid #ff4444; color:#ff4444; padding:4px 8px; font-size:9px; cursor:pointer;">
|
||||
LÖSCHEN
|
||||
</button>
|
||||
</div>`;
|
||||
});
|
||||
listEl.innerHTML = html;
|
||||
};
|
||||
|
||||
window.deleteClaim = async function(code) {
|
||||
if(!confirm("Eintrag wirklich löschen?")) return;
|
||||
|
||||
// Suchen der SessionID für den Server-Call
|
||||
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
const item = claims.find(c => c.code === code);
|
||||
|
||||
if (item && item.sessionId) {
|
||||
try {
|
||||
await fetch('/api/claim/delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ sessionId: item.sessionId, claimCode: code })
|
||||
});
|
||||
} catch(e) { console.warn("Server delete failed"); }
|
||||
}
|
||||
|
||||
// Lokal löschen
|
||||
claims = claims.filter(c => c.code !== code);
|
||||
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
||||
window.showMyCodes(); // Refresh
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// 5. GAME LOOP (PHYSICS + RENDER)
|
||||
// ==========================================
|
||||
function gameLoop(timestamp) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
if (!isLoaded) return;
|
||||
|
||||
// Nur updaten, wenn Spiel läuft
|
||||
if (isGameRunning && !isGameOver) {
|
||||
if (!lastTime) lastTime = timestamp;
|
||||
const deltaTime = timestamp - lastTime;
|
||||
@@ -160,46 +229,62 @@ function gameLoop(timestamp) {
|
||||
|
||||
accumulator += deltaTime;
|
||||
|
||||
// --- FIXED TIME STEP (Physik: 20 TPS) ---
|
||||
while (accumulator >= MS_PER_TICK) {
|
||||
updateGameLogic();
|
||||
updateGameLogic(); // logic.js (setzt prevX/prevY)
|
||||
currentTick++;
|
||||
|
||||
// Score lokal hochzählen (damit es flüssig aussieht)
|
||||
// Server korrigiert, falls Abweichung zu groß
|
||||
score++;
|
||||
|
||||
accumulator -= MS_PER_TICK;
|
||||
}
|
||||
|
||||
const alpha = accumulator / MS_PER_TICK;
|
||||
|
||||
// Score im HUD
|
||||
// HUD Update
|
||||
const scoreEl = document.getElementById('score');
|
||||
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
|
||||
}
|
||||
|
||||
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
|
||||
// --- INTERPOLATION (Rendering: 60+ FPS) ---
|
||||
// alpha berechnen: Wie viel % ist seit dem letzten Tick vergangen?
|
||||
const alpha = (isGameRunning && !isGameOver) ? (accumulator / MS_PER_TICK) : 1.0;
|
||||
|
||||
// drawGame ist jetzt in render.js und nutzt Pixi
|
||||
drawGame(alpha);
|
||||
}
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 6. INITIALISIERUNG
|
||||
// ==========================================
|
||||
async function initGame() {
|
||||
try {
|
||||
const cRes = await fetch('/api/config');
|
||||
gameConfig = await cRes.json();
|
||||
|
||||
// Pixi Assets laden
|
||||
await loadAssets();
|
||||
await loadStartScreenLeaderboard();
|
||||
|
||||
if (typeof getMuteState === 'function') {
|
||||
updateMuteIcon(getMuteState());
|
||||
}
|
||||
// Startscreen Bestenliste
|
||||
await loadStartScreenLeaderboard();
|
||||
|
||||
isLoaded = true;
|
||||
if(loadingText) loadingText.style.display = 'none';
|
||||
if(startBtn) startBtn.style.display = 'inline-block';
|
||||
|
||||
// Mute Icon setzen (audio.js State)
|
||||
if (typeof getMuteState === 'function') {
|
||||
updateMuteIcon(getMuteState());
|
||||
}
|
||||
|
||||
// Lokaler Highscore
|
||||
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||
const hsEl = document.getElementById('localHighscore');
|
||||
if(hsEl) hsEl.innerText = savedHighscore;
|
||||
|
||||
// Loop starten (für Idle Rendering)
|
||||
requestAnimationFrame(gameLoop);
|
||||
drawGame();
|
||||
drawGame(1.0); // Initiale Zeichnung
|
||||
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
@@ -207,16 +292,14 @@ async function initGame() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper: Startscreen Leaderboard
|
||||
async function loadStartScreenLeaderboard() {
|
||||
try {
|
||||
const listEl = document.getElementById('startLeaderboardList');
|
||||
if (!listEl) return;
|
||||
const res = await fetch('/api/leaderboard');
|
||||
const entries = await res.json();
|
||||
|
||||
if (entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Keine Scores.</div>"; return; }
|
||||
|
||||
if (!entries || entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
|
||||
let html = "";
|
||||
entries.forEach(e => {
|
||||
let icon = "#" + e.rank;
|
||||
@@ -227,14 +310,13 @@ async function loadStartScreenLeaderboard() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
// Helper: Audio Button Logik
|
||||
window.toggleAudioClick = function() {
|
||||
const muted = toggleMute();
|
||||
|
||||
updateMuteIcon(muted);
|
||||
|
||||
|
||||
document.getElementById('mute-btn').blur();
|
||||
if (typeof toggleMute === 'function') {
|
||||
const muted = toggleMute();
|
||||
updateMuteIcon(muted);
|
||||
document.getElementById('mute-btn').blur();
|
||||
}
|
||||
};
|
||||
|
||||
function updateMuteIcon(isMuted) {
|
||||
@@ -246,100 +328,10 @@ function updateMuteIcon(isMuted) {
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Helpers
|
||||
window.openModal = function(id) { document.getElementById('modal-' + id).style.display = 'flex'; }
|
||||
window.closeModal = function() { document.querySelectorAll('.modal-overlay').forEach(el => el.style.display = 'none'); }
|
||||
window.onclick = function(event) { if (event.target.classList.contains('modal-overlay')) closeModal(); }
|
||||
|
||||
window.showMyCodes = function() {
|
||||
|
||||
openModal('codes');
|
||||
|
||||
const listEl = document.getElementById('codesList');
|
||||
if(!listEl) return;
|
||||
|
||||
|
||||
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
|
||||
if (rawClaims.length === 0) {
|
||||
listEl.innerHTML = "<div style='padding:20px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const sortedClaims = rawClaims
|
||||
.map((item, index) => ({ ...item, originalIndex: index }))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
let html = "";
|
||||
|
||||
sortedClaims.forEach(c => {
|
||||
|
||||
let rankIcon = "📄";
|
||||
if (c.score >= 5000) rankIcon = "⭐";
|
||||
if (c.score >= 10000) rankIcon = "🔥";
|
||||
if (c.score >= 20000) rankIcon = "👑";
|
||||
|
||||
html += `
|
||||
<div style="border-bottom:1px solid #444; padding:10px 0; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div style="text-align:left;">
|
||||
<span style="color:#00e5ff; font-weight:bold; font-size:14px;">${rankIcon} ${c.code}</span>
|
||||
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
|
||||
<span style="color:#aaa; font-size:10px;">${c.name} • ${c.date}</span>
|
||||
</div>
|
||||
<button onclick="deleteClaim('${c.sessionId}', '${c.code}')"
|
||||
style="background:transparent; border:1px solid #ff4444; color:#ff4444; padding:5px 10px; font-size:10px; cursor:pointer;">
|
||||
LÖSCHEN
|
||||
</button>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
listEl.innerHTML = html;
|
||||
};
|
||||
|
||||
|
||||
window.deleteClaim = async function(sid, code) {
|
||||
if(!confirm("Eintrag wirklich löschen?")) return;
|
||||
|
||||
|
||||
try {
|
||||
await fetch('/api/claim/delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ sessionId: sid, claimCode: code })
|
||||
});
|
||||
} catch(e) {
|
||||
console.warn("Server Delete fehlgeschlagen (vielleicht schon weg), lösche lokal...");
|
||||
}
|
||||
|
||||
|
||||
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||
|
||||
claims = claims.filter(c => c.code !== code);
|
||||
|
||||
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
||||
|
||||
|
||||
window.showMyCodes();
|
||||
|
||||
|
||||
if(document.getElementById('startLeaderboardList')) {
|
||||
loadStartScreenLeaderboard();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
window.openModal = function(id) {
|
||||
const el = document.getElementById('modal-' + id);
|
||||
if(el) el.style.display = 'flex';
|
||||
}
|
||||
|
||||
window.closeModal = function() {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(el => el.style.display = 'none');
|
||||
}
|
||||
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal-overlay')) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Start
|
||||
initGame();
|
||||
2312
static/js/pixi.min.js
vendored
Normal file
2312
static/js/pixi.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2312
static/js/pixi.min.mjs
Normal file
2312
static/js/pixi.min.mjs
Normal file
File diff suppressed because one or more lines are too long
@@ -1,20 +1,55 @@
|
||||
// ==========================================
|
||||
// RESIZE LOGIK (LETTERBOXING)
|
||||
// PIXI INITIALISIERUNG (V8)
|
||||
// ==========================================
|
||||
function resize() {
|
||||
canvas.width = GAME_WIDTH; // 800
|
||||
canvas.height = GAME_HEIGHT; // 400
|
||||
let floorGraphic = null;
|
||||
async function initPixi() {
|
||||
if (app) return;
|
||||
|
||||
app = new PIXI.Application();
|
||||
|
||||
// 1. Asynchrones Init (v8 Standard)
|
||||
await app.init({
|
||||
width: GAME_WIDTH,
|
||||
height: GAME_HEIGHT,
|
||||
backgroundColor: 0x1a1a1a, // Dunkelgrau
|
||||
preference: 'webgpu', // Versuch WebGPU (schneller auf Handy!)
|
||||
resolution: Math.min(window.devicePixelRatio || 1, 2), // Retina Limit
|
||||
autoDensity: true,
|
||||
antialias: false,
|
||||
roundPixels: true // Wichtig für Pixelart
|
||||
});
|
||||
|
||||
// 2. Canvas einhängen
|
||||
document.getElementById('game-container').appendChild(app.canvas);
|
||||
|
||||
// 3. Layer erstellen
|
||||
bgLayer = new PIXI.Container();
|
||||
gameLayer = new PIXI.Container();
|
||||
debugLayer = new PIXI.Graphics(); // Für Hitboxen
|
||||
|
||||
// Sortierung aktivieren (damit Player vor Obstacles ist)
|
||||
gameLayer.sortableChildren = true;
|
||||
|
||||
app.stage.addChild(bgLayer);
|
||||
app.stage.addChild(gameLayer);
|
||||
app.stage.addChild(debugLayer);
|
||||
|
||||
// Einmalig Resizen
|
||||
resize();
|
||||
|
||||
console.log(`🚀 Renderer: ${app.renderer.name}`);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
if (!app || !app.canvas) return;
|
||||
|
||||
// 2. Verfügbaren Platz berechnen
|
||||
const windowWidth = window.innerWidth - 20;
|
||||
const windowHeight = window.innerHeight - 20;
|
||||
|
||||
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
|
||||
const windowRatio = windowWidth / windowHeight;
|
||||
|
||||
let finalWidth, finalHeight;
|
||||
|
||||
// 3. Skalierung berechnen (Aspect Ratio erhalten)
|
||||
if (windowRatio < targetRatio) {
|
||||
finalWidth = windowWidth;
|
||||
finalHeight = windowWidth / targetRatio;
|
||||
@@ -23,163 +58,210 @@ function resize() {
|
||||
finalWidth = finalHeight * targetRatio;
|
||||
}
|
||||
|
||||
// 4. Container Größe setzen (Canvas füllt Container via CSS)
|
||||
if (container) {
|
||||
container.style.width = `${Math.floor(finalWidth)}px`;
|
||||
container.style.height = `${Math.floor(finalHeight)}px`;
|
||||
}
|
||||
// CSS Skalierung
|
||||
app.canvas.style.width = `${Math.floor(finalWidth)}px`;
|
||||
app.canvas.style.height = `${Math.floor(finalHeight)}px`;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
|
||||
// ==========================================
|
||||
// RENDER LOOP (RETAINED MODE)
|
||||
// ==========================================
|
||||
|
||||
function drawGame(alpha = 1.0) {
|
||||
|
||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
let currentBg = null;
|
||||
if (bgSprites.length > 0) {
|
||||
const changeInterval = 10000;
|
||||
const currentRawIndex = Math.floor(score / changeInterval);
|
||||
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
|
||||
const bgIndex = maxRawBgIndex % bgSprites.length;
|
||||
currentBg = bgSprites[bgIndex];
|
||||
async function drawGame(alpha = 1.0) {
|
||||
if (!app) {
|
||||
if(!document.getElementById('game-container').querySelector('canvas')) await initPixi();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
|
||||
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
} else {
|
||||
ctx.fillStyle = "#f0f0f0";
|
||||
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
// 1. HINTERGRUND
|
||||
updateBackground();
|
||||
|
||||
// 2. BODEN (FIX: Einmalig erstellen oder updaten)
|
||||
if (!floorGraphic) {
|
||||
floorGraphic = new PIXI.Graphics();
|
||||
bgLayer.addChild(floorGraphic); // Zum Background Layer hinzufügen
|
||||
}
|
||||
floorGraphic.clear();
|
||||
// Boden: Dunkelgrau
|
||||
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 50).fill(0x333333);
|
||||
// Grüne Linie oben drauf (Gras/Teppich)
|
||||
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 4).fill(0x4CAF50);
|
||||
|
||||
|
||||
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
||||
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
||||
|
||||
|
||||
platforms.forEach(p => {
|
||||
|
||||
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
||||
const rY = p.y;
|
||||
|
||||
|
||||
ctx.fillStyle = "#5D4037";
|
||||
ctx.fillRect(rX, rY, p.w, p.h);
|
||||
ctx.fillStyle = "#8D6E63";
|
||||
ctx.fillRect(rX, rY, p.w, 5);
|
||||
});
|
||||
|
||||
|
||||
obstacles.forEach(obs => {
|
||||
const def = obs.def || {};
|
||||
const img = sprites[def.id];
|
||||
|
||||
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
|
||||
const rY = obs.y;
|
||||
|
||||
const hbw = def.width || obs.w || 30;
|
||||
const hbh = def.height || obs.h || 30;
|
||||
|
||||
if (img && img.complete && img.naturalHeight !== 0) {
|
||||
|
||||
const scale = def.imgScale || 1.0;
|
||||
const offX = def.imgOffsetX || 0.0;
|
||||
const offY = def.imgOffsetY || 0.0;
|
||||
|
||||
const drawW = hbw * scale;
|
||||
const drawH = hbh * scale;
|
||||
|
||||
const baseX = rX + (hbw - drawW) / 2;
|
||||
const baseY = rY + (hbh - drawH);
|
||||
|
||||
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
|
||||
|
||||
} else {
|
||||
let color = "#FF00FF";
|
||||
if (def.type === "coin") color = "gold";
|
||||
else if (def.color) color = def.color;
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(rX, rY, hbw, hbh);
|
||||
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.lineWidth = 2;
|
||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
||||
ctx.fillStyle = "white"; ctx.font = "bold 10px monospace";
|
||||
ctx.fillText(def.id || "?", rX, rY - 5);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
// 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) {
|
||||
if (serverObstacles) {
|
||||
ctx.strokeStyle = "cyan";
|
||||
ctx.lineWidth = 1;
|
||||
serverObstacles.forEach(sObj => {
|
||||
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);
|
||||
drawDebugOverlay(alpha);
|
||||
} 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);
|
||||
debugLayer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Sprechblase zeichnen
|
||||
function drawSpeechBubble(x, y, text) {
|
||||
const bX = x - 20;
|
||||
const bY = y - 40;
|
||||
const bW = 120;
|
||||
const bH = 30;
|
||||
// ------------------------------------------------------
|
||||
// HELPER: SYNC SYSTEM
|
||||
// ------------------------------------------------------
|
||||
function syncSprites(dataList, cacheMap, type, alpha) {
|
||||
const usedObjects = new Set();
|
||||
|
||||
ctx.fillStyle = "white"; ctx.fillRect(bX, bY, bW, bH);
|
||||
ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.strokeRect(bX, bY, bW, bH);
|
||||
ctx.fillStyle = "black"; ctx.font = "10px Arial"; ctx.textAlign = "center";
|
||||
ctx.fillText(text, bX + bW/2, bY + 20);
|
||||
dataList.forEach(obj => {
|
||||
usedObjects.add(obj);
|
||||
let sprite = cacheMap.get(obj);
|
||||
|
||||
// A. Erstellen (wenn neu)
|
||||
if (!sprite) {
|
||||
sprite = createPixiSprite(obj, type);
|
||||
gameLayer.addChild(sprite);
|
||||
cacheMap.set(obj, sprite);
|
||||
}
|
||||
|
||||
// B. Updaten (Interpolation)
|
||||
const def = obj.def || {};
|
||||
|
||||
// Position interpolieren
|
||||
const rX = (obj.prevX !== undefined) ? lerp(obj.prevX, obj.x, alpha) : obj.x;
|
||||
const rY = obj.y;
|
||||
|
||||
if (type === 'platform') {
|
||||
sprite.x = rX;
|
||||
sprite.y = rY;
|
||||
} else {
|
||||
// Editor Werte
|
||||
const scale = def.imgScale || 1.0;
|
||||
const offX = def.imgOffsetX || 0;
|
||||
const offY = def.imgOffsetY || 0;
|
||||
const hbw = def.width || 30;
|
||||
const hbh = def.height || 30;
|
||||
|
||||
const drawW = hbw * scale;
|
||||
const baseX = rX + (hbw - drawW) / 2;
|
||||
const baseY = rY + (hbh - (hbh * scale));
|
||||
|
||||
sprite.x = baseX + offX;
|
||||
sprite.y = baseY + offY;
|
||||
sprite.width = drawW;
|
||||
sprite.height = hbh * scale;
|
||||
}
|
||||
});
|
||||
|
||||
// C. Aufräumen (Garbage Collection)
|
||||
for (const [obj, sprite] of cacheMap.entries()) {
|
||||
if (!usedObjects.has(obj)) {
|
||||
gameLayer.removeChild(sprite);
|
||||
sprite.destroy();
|
||||
cacheMap.delete(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createPixiSprite(obj, type) {
|
||||
if (type === 'platform') {
|
||||
const g = new PIXI.Graphics();
|
||||
// Holz Plattform
|
||||
g.rect(0, 0, obj.w, obj.h).fill(0x8B4513); // Braun
|
||||
g.rect(0, 0, obj.w, 5).fill(0xA0522D); // Hellbraun Oben
|
||||
return g;
|
||||
}
|
||||
else {
|
||||
const def = obj.def || {};
|
||||
|
||||
// CHECK: Ist die Textur im Cache?
|
||||
// Wir nutzen PIXI.Assets.get(), das ist sicherer als cache.has
|
||||
let texture = null;
|
||||
try {
|
||||
if (def.id) texture = PIXI.Assets.get(def.id);
|
||||
} catch(e) {}
|
||||
|
||||
if (texture) {
|
||||
const s = new PIXI.Sprite(texture);
|
||||
return s;
|
||||
} else {
|
||||
// FALLBACK (Wenn Bild fehlt -> Magenta Box)
|
||||
const g = new PIXI.Graphics();
|
||||
let color = 0xFF00FF;
|
||||
if (def.type === 'coin') color = 0xFFD700;
|
||||
|
||||
g.rect(0, 0, def.width||30, def.height||30).fill(color);
|
||||
return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// PLAYER & BG
|
||||
// ------------------------------------------------------
|
||||
function updatePlayer(alpha) {
|
||||
if (!pixiPlayer) {
|
||||
if (PIXI.Assets.cache.has('player')) {
|
||||
pixiPlayer = PIXI.Sprite.from('player');
|
||||
} else {
|
||||
pixiPlayer = new PIXI.Graphics().rect(0,0,30,50).fill(0xFF0000);
|
||||
}
|
||||
gameLayer.addChild(pixiPlayer);
|
||||
pixiPlayer.zIndex = 100; // Immer im Vordergrund
|
||||
}
|
||||
|
||||
let rY = lerp(player.prevY || player.y, player.y, alpha);
|
||||
const drawY = isCrouching ? rY + 25 : rY;
|
||||
|
||||
pixiPlayer.x = player.x;
|
||||
pixiPlayer.y = drawY;
|
||||
}
|
||||
|
||||
function updateBackground() {
|
||||
// FEHLERBEHEBUNG:
|
||||
// Wir prüfen 'gameConfig.backgrounds' statt 'bgSprites'
|
||||
if (!gameConfig || !gameConfig.backgrounds || gameConfig.backgrounds.length === 0) return;
|
||||
|
||||
const changeInterval = 10000;
|
||||
const idx = Math.floor(score / changeInterval) % gameConfig.backgrounds.length;
|
||||
|
||||
// Der Key ist der Dateiname (so haben wir es in main.js geladen)
|
||||
const bgKey = gameConfig.backgrounds[idx];
|
||||
|
||||
// Sicherstellen, dass Asset geladen ist
|
||||
if (!PIXI.Assets.cache.has(bgKey)) return;
|
||||
|
||||
if (!bgSprite) {
|
||||
bgSprite = new PIXI.Sprite();
|
||||
bgSprite.width = GAME_WIDTH;
|
||||
bgSprite.height = GAME_HEIGHT;
|
||||
bgLayer.addChild(bgSprite);
|
||||
}
|
||||
|
||||
// Textur wechseln wenn nötig
|
||||
const tex = PIXI.Assets.get(bgKey);
|
||||
if (tex && bgSprite.texture !== tex) {
|
||||
bgSprite.texture = tex;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// DEBUG
|
||||
// ------------------------------------------------------
|
||||
function drawDebugOverlay(alpha) {
|
||||
const g = debugLayer;
|
||||
g.clear();
|
||||
|
||||
// Server (Cyan)
|
||||
if (serverObstacles) {
|
||||
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;
|
||||
let isLoaded = false;
|
||||
let isGameRunning = false;
|
||||
let isGameOver = false;
|
||||
let sessionID = null;
|
||||
// --- KONFIGURATION & FLAGS ---
|
||||
let gameConfig = null; // Wird von /api/config geladen
|
||||
let isLoaded = false; // Sind Assets geladen?
|
||||
let isGameRunning = false; // Läuft der Game Loop?
|
||||
let isGameOver = false; // Ist der Spieler tot?
|
||||
let sessionID = null; // UUID der aktuellen Session (vom Server)
|
||||
|
||||
let socket = null;
|
||||
let obstacleBuffer = [];
|
||||
let platformBuffer = [];
|
||||
// --- NETZWERK & STREAMING ---
|
||||
let socket = null; // WebSocket Verbindung
|
||||
let obstacleBuffer = []; // Warteschlange Hindernisse (vom Server)
|
||||
let platformBuffer = []; // Warteschlange Plattformen (vom Server)
|
||||
|
||||
let score = 0;
|
||||
let currentTick = 0;
|
||||
// --- LATENZ & SYNC ---
|
||||
let currentLatencyMs = 0; // Gemessene One-Way Latenz
|
||||
let pingInterval = null; // Interval ID für den Ping-Loop
|
||||
|
||||
// --- PIXI JS (RENDERING) ---
|
||||
let app = null; // Die Pixi Application
|
||||
let bgLayer = null; // Container: Hintergrund
|
||||
let gameLayer = null; // Container: Spielwelt (Player, Items)
|
||||
let debugLayer = null; // Graphics: Hitboxen
|
||||
|
||||
// --- CACHING (PIXI SPRITES) ---
|
||||
// Map<LogikObjekt, PIXI.Sprite>
|
||||
// Wir ordnen jedem Logik-Objekt ein festes Sprite zu, statt neu zu erstellen
|
||||
const spriteCache = new Map();
|
||||
const platformCache = new Map();
|
||||
|
||||
// Referenzen für statische Sprites
|
||||
let pixiPlayer = null;
|
||||
let bgSprite = null;
|
||||
|
||||
// --- SPIELZUSTAND ---
|
||||
let score = 0; // Aktueller Score
|
||||
let currentTick = 0; // Zeitbasis (Synchronisiert mit Server)
|
||||
|
||||
// --- POWERUPS (Client Visuals) ---
|
||||
let godModeLives = 0;
|
||||
let hasBat = false;
|
||||
let bootTicks = 0;
|
||||
|
||||
// --- HINTERGRUND LOGIK ---
|
||||
let maxRawBgIndex = 0; // Welches BG Bild ist dran?
|
||||
|
||||
let maxRawBgIndex = 0;
|
||||
|
||||
|
||||
// --- GAME LOOP TIMING ---
|
||||
let lastTime = 0;
|
||||
let accumulator = 0;
|
||||
|
||||
// --- GRAFIKEN & EFFEKTE ---
|
||||
let particles = []; // Array für Partikel-Effekte
|
||||
|
||||
let sprites = {};
|
||||
let playerSprite = new Image();
|
||||
let bgSprites = [];
|
||||
|
||||
|
||||
// --- ENTITIES (Render-Listen) ---
|
||||
let player = {
|
||||
x: 50,
|
||||
y: 300,
|
||||
@@ -38,30 +62,30 @@ let player = {
|
||||
color: "red",
|
||||
vy: 0,
|
||||
grounded: false,
|
||||
|
||||
// WICHTIG für Interpolation:
|
||||
prevY: 300
|
||||
};
|
||||
let particles = [];
|
||||
|
||||
|
||||
|
||||
// Diese Listen werden von logic.js gefüllt und von render.js gezeichnet
|
||||
let obstacles = [];
|
||||
let platforms = [];
|
||||
|
||||
|
||||
// Debug-Daten vom Server (für das Overlay)
|
||||
let serverObstacles = [];
|
||||
let serverPlatforms = [];
|
||||
|
||||
let currentLatencyMs = 0;
|
||||
let pingInterval = null;
|
||||
|
||||
|
||||
// --- INPUT STATE ---
|
||||
let isCrouching = false;
|
||||
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
// ==========================================
|
||||
// HTML ELEMENTE (DOM Caching)
|
||||
// ==========================================
|
||||
const container = document.getElementById('game-container');
|
||||
|
||||
// Hinweis: 'canvas' und 'ctx' gibt es nicht mehr, da PixiJS das verwaltet!
|
||||
|
||||
// UI Elemente
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const loadingText = document.getElementById('loadingText');
|
||||
|
||||
Reference in New Issue
Block a user