// Game Loop + State Management const Game = { state: 'menu', // menu, playing, levelComplete, gameOver, win currentLevel: 0, elapsedTime: 0, totalTime: 0, lastTime: 0, grid: null, isMobile: false, init() { this.isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); Renderer.init(); this.setupInput(); this.fitCanvas(); window.addEventListener('resize', () => this.fitCanvas()); this.state = 'menu'; this.lastTime = performance.now() / 1000; this.loop(); }, fitCanvas() { const canvas = Renderer.canvas; const dpadHeight = this.isMobile ? 190 : 0; const maxW = window.innerWidth - 8; const maxH = window.innerHeight - 8 - dpadHeight; const scaleX = maxW / canvas.width; const scaleY = maxH / canvas.height; const scale = Math.min(scaleX, scaleY, 3); // max 3x um nicht zu groß auf Desktop canvas.style.width = Math.floor(canvas.width * scale) + 'px'; canvas.style.height = Math.floor(canvas.height * scale) + 'px'; }, setupInput() { document.addEventListener('keydown', (e) => { if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) { e.preventDefault(); } this.handleKey(e.key); }); // Touch-Steuerung (Swipe auf Canvas) let touchStartX = 0, touchStartY = 0; const canvas = Renderer.canvas; canvas.addEventListener('touchstart', (e) => { e.preventDefault(); touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }); canvas.addEventListener('touchend', (e) => { e.preventDefault(); const dx = e.changedTouches[0].clientX - touchStartX; const dy = e.changedTouches[0].clientY - touchStartY; const absDx = Math.abs(dx); const absDy = Math.abs(dy); if (Math.max(absDx, absDy) < 20) { // Tap = Enter this.handleKey('Enter'); return; } if (absDx > absDy) { this.handleKey(dx > 0 ? 'ArrowRight' : 'ArrowLeft'); } else { this.handleKey(dy > 0 ? 'ArrowDown' : 'ArrowUp'); } }); // D-Pad Buttons const dpadMap = { 'dpad-up': 'ArrowUp', 'dpad-down': 'ArrowDown', 'dpad-left': 'ArrowLeft', 'dpad-right': 'ArrowRight' }; for (const [id, key] of Object.entries(dpadMap)) { const btn = document.getElementById(id); if (!btn) continue; btn.addEventListener('touchstart', (e) => { e.preventDefault(); this.handleKey(key); }); // Auch wiederholtes Drücken ermöglichen let interval = null; btn.addEventListener('touchstart', () => { interval = setInterval(() => this.handleKey(key), 150); }); btn.addEventListener('touchend', () => clearInterval(interval)); btn.addEventListener('touchcancel', () => clearInterval(interval)); } }, handleKey(key) { switch (this.state) { case 'menu': if (key === 'Enter' || key === ' ') this.startGame(); break; case 'playing': Player.handleInput(key); break; case 'gameOver': if (key === 'Enter' || key === ' ') this.startGame(); break; case 'levelComplete': if (key === 'Enter' || key === ' ') this.nextLevel(); break; case 'win': if (key === 'Enter' || key === ' ') { this.currentLevel = 0; this.totalTime = 0; this.startGame(); } break; } }, startGame() { this.currentLevel = 0; this.totalTime = 0; this.loadLevel(0); this.state = 'playing'; this.fitCanvas(); }, loadLevel(index) { const level = LEVELS[index]; // Deep copy des Grids this.grid = level.grid.map(row => [...row]); const cols = this.grid[0].length; const rows = this.grid.length; Renderer.resize(cols, rows); this.fitCanvas(); // Spieler-Start finden const playerStart = Utils.findTile(this.grid, 2); Player.init(playerStart.col, playerStart.row); // Monster-Start finden const monsterStart = Utils.findTile(this.grid, 4); Monster.init(monsterStart.col, monsterStart.row, level.monsterSpeed, level.speedIncrease, level.maxMonsterSpeed); // Start-Tiles zu Wegen machen (damit man drüber laufen kann) this.grid[playerStart.row][playerStart.col] = 0; this.grid[monsterStart.row][monsterStart.col] = 0; this.elapsedTime = 0; }, nextLevel() { this.currentLevel++; if (this.currentLevel >= LEVELS.length) { this.state = 'win'; } else { this.loadLevel(this.currentLevel); this.state = 'playing'; } }, checkWin() { // Spieler hat den Ausgang erreicht const exit = Utils.findTile(this.grid, 3); if (exit && Player.col === exit.col && Player.row === exit.row) { this.totalTime += this.elapsedTime; this.state = 'levelComplete'; } }, checkGameOver() { if (Monster.checkCollision(Player.visualX, Player.visualY)) { this.state = 'gameOver'; } }, update(dt) { if (this.state !== 'playing') return; this.elapsedTime += dt; Player.update(dt, this.grid); Monster.update(dt, this.grid, Player.col, Player.row, this.elapsedTime); this.checkWin(); this.checkGameOver(); }, draw(time) { switch (this.state) { case 'menu': Renderer.drawMenuScreen(); break; case 'playing': Renderer.clear(); Renderer.drawGrid(this.grid, time); Player.draw(time); Monster.draw(time); Renderer.drawHUD(this.currentLevel + 1, this.elapsedTime, Monster.speed); break; case 'gameOver': Renderer.clear(); Renderer.drawGrid(this.grid, time); Player.draw(time); Monster.draw(time); Renderer.drawHUD(this.currentLevel + 1, this.elapsedTime, Monster.speed); Renderer.drawGameOverScreen(this.currentLevel + 1, this.elapsedTime); break; case 'levelComplete': Renderer.clear(); Renderer.drawGrid(this.grid, time); Player.draw(time); Renderer.drawHUD(this.currentLevel + 1, this.elapsedTime, Monster.speed); Renderer.drawLevelCompleteScreen(this.currentLevel + 1); break; case 'win': Renderer.drawWinScreen(this.totalTime); break; } }, loop() { const now = performance.now() / 1000; const dt = Math.min(now - this.lastTime, 0.05); // Cap delta time this.lastTime = now; this.update(dt); this.draw(now); requestAnimationFrame(() => this.loop()); } }; // Start! Game.init();