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..5c3766a 100644 --- a/static/js/render.js +++ b/static/js/render.js @@ -44,53 +44,71 @@ 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 = 10000; + + // 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]; + } + + + if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) { + 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 +116,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; }