diff --git a/config.go b/config.go
index c210573..c309a2f 100644
--- a/config.go
+++ b/config.go
@@ -42,13 +42,14 @@ func initGameConfig() {
{ID: "desk", Type: "obstacle", Width: 40, Height: 30, Color: "#8B4513", Image: "desk1.png"},
{ID: "teacher", Type: "teacher", Width: 30, Height: 60, Color: "#000080", Image: "teacher1.png", CanTalk: true, SpeechLines: []string{"Halt!", "Handy weg!"}},
{ID: "trashcan", Type: "obstacle", Width: 25, Height: 35, Color: "#555", Image: "trash1.png"},
- {ID: "eraser", Type: "obstacle", Width: 30, Height: 20, Color: "#fff", Image: "eraser1.png", YOffset: 45.0},
+ {ID: "eraser", Type: "obstacle", Width: 30, Height: 20, Color: "#fff", Image: "eraser1.png", YOffset: 35.0},
- // --- BOSS OBJEKTE (Kommen häufiger im Bosskampf) ---
{ID: "principal", Type: "teacher", Width: 40, Height: 70, Color: "#000", Image: "principal1.png", CanTalk: true, SpeechLines: []string{"EXMATRIKULATION!"}},
// --- COINS ---
- {ID: "coin", Type: "coin", Width: 20, Height: 20, Color: "gold", Image: "coin1.png", YOffset: 20.0},
+ {ID: "coin0", Type: "coin", Width: 20, Height: 20, Color: "gold", Image: "coin1.png", YOffset: 40.0},
+ {ID: "coin1", Type: "coin", Width: 20, Height: 20, Color: "gold", Image: "coin1.png", YOffset: 50.0},
+ {ID: "coin2", Type: "coin", Width: 20, Height: 20, Color: "gold", Image: "coin1.png", YOffset: 60.0},
// --- POWERUPS ---
{ID: "p_god", Type: "powerup", Width: 30, Height: 30, Color: "cyan", Image: "powerup_god1.png", YOffset: 20.0}, // Godmode
@@ -56,7 +57,7 @@ func initGameConfig() {
{ID: "p_boot", Type: "powerup", Width: 30, Height: 30, Color: "lime", Image: "powerup_boot1.png", YOffset: 20.0}, // Boots
},
// Mehrere Hintergründe für Level-Wechsel
- Backgrounds: []string{"gym-background.jpg", "school-background.jpg", "school2-background.jpg"},
+ Backgrounds: []string{"school-background.jpg", "gym-background.jpg", "school2-background.jpg"},
}
log.Println("✅ Config mit Powerups geladen")
}
diff --git a/simulation.go b/simulation.go
index f170cb2..7b53589 100644
--- a/simulation.go
+++ b/simulation.go
@@ -47,8 +47,8 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
for i := 0; i < totalTicks; i++ {
- currentSpeed := BaseSpeed + (float64(score)/500.0)*0.5
- if currentSpeed > 12.0 {
+ currentSpeed := BaseSpeed + (float64(score)/750.0)*0.5
+ if currentSpeed > 14.0 {
currentSpeed = 12.0
}
diff --git a/static/index.html b/static/index.html
index d7615b4..081b6dd 100644
--- a/static/index.html
+++ b/static/index.html
@@ -145,12 +145,12 @@
function openModal(id) {
document.getElementById('modal-' + id).style.display = 'flex';
}
+
function closeModal() {
const modals = document.querySelectorAll('.modal-overlay');
modals.forEach(el => el.style.display = 'none');
}
-
- // Schließen wenn man daneben klickt
+
window.onclick = function(event) {
if (event.target.classList.contains('modal-overlay')) {
closeModal();
diff --git a/static/js/logic.js b/static/js/logic.js
index 53fe64b..04e14b3 100644
--- a/static/js/logic.js
+++ b/static/js/logic.js
@@ -1,7 +1,7 @@
function updateGameLogic() {
// 1. Speed Berechnung (Sync mit Server!)
- let currentSpeed = BASE_SPEED + (score / 500.0) * 0.5;
- if (currentSpeed > 12.0) currentSpeed = 12.0;
+ let currentSpeed = BASE_SPEED + (score / 750.0) * 0.5;
+ if (currentSpeed > 14.0) currentSpeed = 14.0;
// 2. Input & Sprung
if (isCrouching) inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" });
diff --git a/static/js/main.js b/static/js/main.js
index cbb2408..b71fe76 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -1,10 +1,25 @@
+// ==========================================
+// INIT & ASSETS
+// ==========================================
async function loadAssets() {
playerSprite.src = "assets/player.png";
- if (gameConfig.backgrounds && gameConfig.backgrounds.length > 0) {
- const bgName = gameConfig.backgrounds[0];
- if (!bgName.startsWith("#")) bgSprite.src = "assets/" + bgName;
- }
- const promises = gameConfig.obstacles.map(def => {
+
+ // 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;
@@ -12,26 +27,155 @@ async function loadAssets() {
img.onerror = () => { resolve(); };
});
});
- if (bgSprite.src) {
- promises.push(new Promise(r => { bgSprite.onload = r; bgSprite.onerror = r; }));
- }
- await Promise.all(promises);
+
+ // 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'); // Handy Rotate Check
+ 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;
+ // Wir resetten die Zeit, damit es keinen Sprung gibt
+ lastTime = performance.now();
resize();
- } catch(e) { location.reload(); }
+ } 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 claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
+
+ if (claims.length === 0) {
+ listEl.innerHTML = "
Keine Codes gespeichert.
";
+ return;
+ }
+
+ let html = "";
+ for (let i = claims.length - 1; i >= 0; i--) {
+ const c = claims[i];
+ 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(${i}, '${c.sessionId}', '${c.code}')"` : "disabled";
+
+ html += `
+
+
+ ${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";
+ html += `#${e.rank} ${e.name}${Math.floor(e.score/10)}
`;
+ });
+ document.getElementById('leaderboard').innerHTML = html;
+ } catch(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;
@@ -44,52 +188,71 @@ function gameOver(reason) {
drawGame();
}
+// ==========================================
+// DER FIXIERTE GAME LOOP
+// ==========================================
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
- if (!isLoaded || !isGameRunning || isGameOver) {
+ // 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;
- return;
+
+ 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);
}
- 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';
+ 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;
+ const hsEl = document.getElementById('localHighscore');
+ if(hsEl) hsEl.innerText = savedHighscore;
+
+ // Loop starten (mit dummy timestamp start)
requestAnimationFrame(gameLoop);
- } catch(e) { if(loadingText) loadingText.innerText = "Fehler!"; }
+
+ // Initiales Zeichnen erzwingen (damit Hintergrund sofort da ist)
+ drawGame();
+
+ } catch(e) {
+ console.error(e);
+ if(loadingText) loadingText.innerText = "Ladefehler (siehe Konsole)";
+ }
}
initGame();
\ No newline at end of file
diff --git a/static/js/render.js b/static/js/render.js
index 1b3421b..7b1f2aa 100644
--- a/static/js/render.js
+++ b/static/js/render.js
@@ -44,53 +44,72 @@ resize();
// --- DRAWING ---
function drawGame() {
+ // 1. Alles löschen
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
- // --- BACKGROUND ---
- // Hier war der Check schon drin, das ist gut
- if (bgSprite.complete && bgSprite.naturalHeight !== 0) {
- ctx.drawImage(bgSprite, 0, 0, GAME_WIDTH, GAME_HEIGHT);
+ // --- HINTERGRUND (Level-Wechsel) ---
+ let currentBg = null;
+
+ // Haben wir Hintergründe geladen?
+ if (bgSprites.length > 0) {
+ // Wechsel alle 2000 Punkte (Server-Score) = 200 Punkte (Anzeige)
+ const changeInterval = 2000;
+
+ // Berechne Index: 0-1999 -> 0, 2000-3999 -> 1, etc.
+ // Das % (Modulo) sorgt dafür, dass es wieder von vorne anfängt, wenn die Bilder ausgehen
+ const bgIndex = Math.floor(score / changeInterval) % bgSprites.length;
+
+ currentBg = bgSprites[bgIndex];
+ }
+
+ // Zeichnen (wenn Bild geladen und nicht kaputt)
+ if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
+ // Streckt das Bild exakt auf die Spielgröße (800x400)
+ ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
} else {
+ // Fallback: Hellgrau, falls Bild fehlt
ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
}
+ // --- BODEN ---
+ // Halb-transparent, damit er über dem Hintergrund liegt
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
- // --- HINDERNISSE (HIER WAR DER FEHLER) ---
+ // --- HINDERNISSE ---
obstacles.forEach(obs => {
const img = sprites[obs.def.id];
- // FIX: Wir prüfen jetzt strikt, ob das Bild wirklich bereit ist
+ // Prüfen ob Bild geladen ist
if (img && img.complete && img.naturalHeight !== 0) {
ctx.drawImage(img, obs.x, obs.y, obs.def.width, obs.def.height);
} else {
- // Fallback: Wenn Bild fehlt/kaputt -> Farbiges Rechteck
- // Wir prüfen auf Typ Coin, damit Coins gold sind, auch wenn Bild fehlt
+ // Fallback Farbe (Münzen Gold, Rest aus Config)
if (obs.def.type === "coin") ctx.fillStyle = "gold";
- else ctx.fillStyle = obs.def.color;
+ else ctx.fillStyle = obs.def.color || "red";
ctx.fillRect(obs.x, obs.y, obs.def.width, obs.def.height);
}
if(obs.speech) drawSpeechBubble(obs.x, obs.y, obs.speech);
});
-
/*
- // --- DEBUG ---
+ // --- DEBUG RAHMEN (Server Hitboxen) ---
+ // Grün im Spiel, Rot bei Tod
ctx.strokeStyle = isGameOver ? "red" : "lime";
ctx.lineWidth = 2;
serverObstacles.forEach(srvObs => {
ctx.strokeRect(srvObs.x, srvObs.y, srvObs.w, srvObs.h);
});
- */
+ */
- // --- PLAYER ---
+
+ // --- SPIELER ---
+ // Y-Position und Höhe anpassen für Ducken
const drawY = isCrouching ? player.y + 25 : player.y;
const drawH = isCrouching ? 25 : 50;
- // Hier war der Check auch schon korrekt
if (playerSprite.complete && playerSprite.naturalHeight !== 0) {
ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
} else {
@@ -98,27 +117,31 @@ function drawGame() {
ctx.fillRect(player.x, drawY, player.w, drawH);
}
- // --- POWERUP UI (Oben Links) ---
+ // --- HUD (Powerup Status oben links) ---
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`;
- // Drift Anzeige
- /*
+ // Drift Info (nur wenn Objekte da sind)
if (obstacles.length > 0 && serverObstacles.length > 0) {
const drift = Math.abs(obstacles[0].x - serverObstacles[0].x).toFixed(1);
- statusText += ` | Drift: ${drift}px`;
+ // statusText += ` | Drift: ${drift}px`; // Einkommentieren für Debugging
+ }
+
+ if(statusText !== "") {
+ ctx.fillText(statusText, 10, 40);
}
- */
- ctx.fillText(statusText, 10, 40);
}
+ // --- GAME OVER OVERLAY ---
if (isGameOver) {
+ // Dunkler Schleier über alles
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
}
diff --git a/static/js/state.js b/static/js/state.js
index 4326cbd..799b659 100644
--- a/static/js/state.js
+++ b/static/js/state.js
@@ -28,7 +28,7 @@ let accumulator = 0;
let sprites = {};
let playerSprite = new Image();
let bgSprite = new Image();
-
+let bgSprites = [];
// Spiel-Objekte
let player = {
x: 50, y: 300, w: 30, h: 50, color: "red",
diff --git a/static/style.css b/static/style.css
index c5926e3..7bc6e3e 100644
--- a/static/style.css
+++ b/static/style.css
@@ -367,12 +367,21 @@ input {
/* 1. Haupt-Container: Alles zentrieren! */
#startScreen {
- flex-direction: row; /* Nebeneinander lassen, da Bildschirm breit ist */
- align-items: center; /* Vertikal mittig */
- justify-content: center; /* Horizontal mittig */
- gap: 20px; /* Abstand zwischen Menü und Leaderboard */
- padding: 10px;
- overflow-y: auto; /* Scrollen erlauben zur Sicherheit */
+ position: absolute;
+ top: 0; left: 0;
+ width: 100%; height: 100%;
+ background:
+ linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
+ url('assets/school-background.jpg');
+
+ background-size: cover;
+ background-position: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 10;
+ box-sizing: border-box;
+ padding: 20px;
}
/* 2. Linke Seite (Menü) zentrieren */
@@ -419,9 +428,16 @@ input {
/* 8. Game Over Screen auch zentrieren */
#gameOverScreen {
+ position: absolute;
+ top: 0; left: 0;
+ width: 100%; height: 100%;
+ background: rgba(0, 0, 0, 0.9);
+ display: flex;
justify-content: center;
align-items: center;
- padding-top: 0;
+ z-index: 10;
+ box-sizing: border-box;
+ padding: 20px;
}
#inputSection { margin: 5px 0; }
input { padding: 5px; font-size: 12px; width: 150px; margin-bottom: 5px; }