Files
kids/game-labyrinth/js/main.js
Chris Nennemann d0cdf2cc2c Initial commit: kids games
Contains game-jumpnrun and game-labyrinth projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:41:51 +01:00

238 lines
7.5 KiB
JavaScript

// 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();