const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const container = document.getElementById('game-container'); const startScreen = document.getElementById('startScreen'); const startBtn = document.getElementById('startBtn'); const loadingText = document.getElementById('loadingText'); const gameOverScreen = document.getElementById('gameOverScreen'); class PseudoRNG { constructor(seed) { this.state = BigInt(seed); } nextFloat() { const a = 1664525n; const c = 1013904223n; const m = 4294967296n; this.state = (this.state * a + c) % m; return Number(this.state) / Number(m); } nextRange(min, max) { return min + (this.nextFloat() * (max - min)); } pick(array) { if (!array || array.length === 0) return null; const idx = Math.floor(this.nextRange(0, array.length)); return array[idx]; } } const GAME_WIDTH = 800; const GAME_HEIGHT = 400; canvas.width = GAME_WIDTH; canvas.height = GAME_HEIGHT; const GRAVITY = 0.6; const JUMP_POWER = -12; const GROUND_Y = 350; const GAME_SPEED = 5; const CHUNK_SIZE = 60; let gameConfig = null; let isLoaded = false; let isGameRunning = false; let isGameOver = false; let sessionID = null; let rng = null; let score = 0; let currentTick = 0; let lastSentTick = 0; let inputLog = []; let isCrouching = false; let sprites = {}; let playerSprite = new Image(); let bgSprite = new Image(); let player = { x: 50, y: 300, w: 30, h: 50, color: "red", vy: 0, grounded: false }; let obstacles = []; function resize() { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const targetRatio = GAME_WIDTH / GAME_HEIGHT; let finalWidth, finalHeight; if (windowWidth / windowHeight < targetRatio) { finalWidth = windowWidth; finalHeight = windowWidth / targetRatio; } else { finalHeight = windowHeight; finalWidth = finalHeight * targetRatio; } canvas.style.width = `${finalWidth}px`; canvas.style.height = `${finalHeight}px`; if(container) { container.style.width = `${finalWidth}px`; container.style.height = `${finalHeight}px`; } } window.addEventListener('resize', resize); resize(); 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 => { 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(); }; }); }); if (bgSprite.src) { promises.push(new Promise(r => { bgSprite.onload = r; bgSprite.onerror = r; })); } await Promise.all(promises); } window.startGameClick = async function() { if (!isLoaded) return; startScreen.style.display = 'none'; try { const sRes = await fetch('/api/start', {method:'POST'}); const sData = await sRes.json(); sessionID = sData.sessionId; rng = new PseudoRNG(sData.seed); isGameRunning = true; } catch(e) { location.reload(); } }; function handleInput(action, active) { if (isGameOver) { if(active) location.reload(); return; } const relativeTick = currentTick - lastSentTick; if (action === "JUMP" && active) { if (player.grounded && !isCrouching) { player.vy = JUMP_POWER; player.grounded = false; inputLog.push({ t: relativeTick, act: "JUMP" }); } } if (action === "DUCK") { isCrouching = active; } } window.addEventListener('keydown', (e) => { if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true); if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", true); }); window.addEventListener('keyup', (e) => { if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", false); }); window.addEventListener('mousedown', (e) => { if (e.target === canvas && e.button === 0) { handleInput("JUMP", true); } }); let touchStartY = 0; window.addEventListener('touchstart', (e) => { if(e.target === canvas) { e.preventDefault(); touchStartY = e.touches[0].clientY; } }, { passive: false }); window.addEventListener('touchend', (e) => { if(e.target === canvas) { e.preventDefault(); const touchEndY = e.changedTouches[0].clientY; const diff = touchEndY - touchStartY; if (diff < -30) { handleInput("JUMP", true); } else if (diff > 30) { handleInput("DUCK", true); setTimeout(() => handleInput("DUCK", false), 800); } else if (Math.abs(diff) < 10) { handleInput("JUMP", true); } } }); async function sendChunk() { const ticksToSend = currentTick - lastSentTick; if (ticksToSend <= 0) return; const payload = { sessionId: sessionID, inputs: [...inputLog], totalTicks: ticksToSend }; inputLog = []; lastSentTick = currentTick; try { const res = await fetch('/api/validate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await res.json(); if (data.status === "dead") { gameOver("Vom Server gestoppt"); } else { const sScore = data.verifiedScore; if (Math.abs(score - sScore) > 200) { score = sScore; } } } catch (e) { console.error(e); } } function updateGameLogic() { if (isCrouching) { const relativeTick = currentTick - lastSentTick; inputLog.push({ t: relativeTick, act: "DUCK" }); } const originalHeight = 50; const crouchHeight = 25; player.h = isCrouching ? crouchHeight : originalHeight; let drawY = player.y; if (isCrouching) { drawY = player.y + (originalHeight - crouchHeight); } player.vy += GRAVITY; if (isCrouching && !player.grounded) player.vy += 2.0; player.y += player.vy; if (player.y + originalHeight >= GROUND_Y) { player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true; } else { player.grounded = false; } let nextObstacles = []; let rightmostX = 0; for (let obs of obstacles) { obs.x -= GAME_SPEED; const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h }; if (checkCollision(playerHitbox, obs)) { player.color = "darkred"; if (!isGameOver) { sendChunk(); gameOver("Kollision (Client)"); } } if (obs.x + obs.def.width > -100) { nextObstacles.push(obs); if (obs.x + obs.def.width > rightmostX) { rightmostX = obs.x + obs.def.width; } } } obstacles = nextObstacles; if (rightmostX < GAME_WIDTH - 10 && gameConfig && gameConfig.obstacles) { const gap = Math.floor(400 + rng.nextRange(0, 500)); let spawnX = rightmostX + gap; if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH; let possibleObs = []; gameConfig.obstacles.forEach(def => { if (def.id === "eraser") { if (score >= 500) possibleObs.push(def); } else { possibleObs.push(def); } }); const def = rng.pick(possibleObs); let speech = null; if (def && def.canTalk) { if (rng.nextFloat() > 0.7) { speech = rng.pick(def.speechLines); } } if (def) { const yOffset = def.yOffset || 0; obstacles.push({ x: spawnX, y: GROUND_Y - def.height - yOffset, def: def, speech: speech }); } } } function checkCollision(p, obs) { const paddingX = 10; const paddingY_Top = 25; const paddingY_Bottom = 5; return ( p.x + p.w - paddingX > obs.x + paddingX && p.x + paddingX < obs.x + obs.def.width - paddingX && p.y + p.h - paddingY_Bottom > obs.y + paddingY_Top && p.y + paddingY_Top < obs.y + obs.def.height - paddingY_Bottom ); } 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 }) }); const data = await res.json(); const claimCode = data.claimCode; let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]'); myClaims.push({ name: name, score: Math.floor(score / 10), code: claimCode, date: new Date().toLocaleString('de-DE'), sessionId: sessionID }); localStorage.setItem('escape_claims', JSON.stringify(myClaims)); alert(`Gespeichert!\nDein Beweis-Code: ${claimCode}\n(Findest du unter "Meine Codes")`); document.getElementById('inputSection').style.display = 'none'; loadLeaderboard(); } catch (e) { console.error(e); alert("Fehler beim Speichern!"); btn.disabled = false; } }; async function loadLeaderboard() { const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`); const entries = await res.json(); let html = "