Contains game-jumpnrun and game-labyrinth projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
238 lines
7.5 KiB
JavaScript
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();
|