// Main game loop and state management import { GameState, KeyState, PlayerState, Enemy, Block } from './types'; import { Renderer } from './Renderer'; import { Physics } from './Physics'; import { Collision } from './Collision'; import { LevelManager, LevelTheme } from './LevelManager'; import { getAudioManager, SoundEffect } from './AudioManager'; import { GAME_CONFIG, KEYS } from '../constants'; const { CANVAS_WIDTH, CANVAS_HEIGHT, LEVEL_HEIGHT, PLAYER_WIDTH, PLAYER_HEIGHT } = GAME_CONFIG; export interface GameCallbacks { onGameOver?: (isWin: boolean, score: number) => void; onLevelComplete?: (levelNumber: number, nextLevel: number | null) => void; onGameComplete?: (totalScore: number) => void; } export class Game { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private renderer: Renderer; private physics: Physics; private collision: Collision; private levelManager: LevelManager; private state: GameState; private keys: KeyState; private animationId: number | null = null; private callbacks: GameCallbacks = {}; private totalScore: number = 0; private currentTheme: LevelTheme = 'overworld'; private isMobile: boolean = false; constructor(canvas: HTMLCanvasElement, startLevel: number = 0, isMobile: boolean = false) { this.canvas = canvas; this.ctx = canvas.getContext('2d')!; this.renderer = new Renderer(this.ctx); this.physics = new Physics(); this.collision = new Collision(); this.levelManager = new LevelManager(); this.keys = { left: false, right: false, jump: false }; this.isMobile = isMobile; // Set mobile mode for physics this.physics.setMobileMode(isMobile); // Set starting level if (startLevel > 0) { this.levelManager.setLevel(startLevel); } this.state = this.createInitialState(); this.currentTheme = this.levelManager.getCurrentLevelInfo().theme; this.setupEventListeners(); } private createInitialState(): GameState { const level = this.levelManager.getCurrentLevel(); return { player: { x: level.startPosition.x, y: level.startPosition.y, vx: 0, vy: 0, width: PLAYER_WIDTH, height: PLAYER_HEIGHT, isJumping: false, isFalling: true, facingRight: true, isAlive: true, score: this.totalScore, coins: 0, }, enemies: level.enemies.map(e => ({ ...e })), blocks: level.blocks.map(b => ({ ...b })), camera: { x: 0, y: 0 }, isRunning: false, isGameOver: false, isWin: false, level, }; } private setupEventListeners(): void { const handleKeyDown = (e: KeyboardEvent) => { if (KEYS.LEFT.includes(e.code)) { this.keys.left = true; e.preventDefault(); } if (KEYS.RIGHT.includes(e.code)) { this.keys.right = true; e.preventDefault(); } if (KEYS.JUMP.includes(e.code)) { this.keys.jump = true; e.preventDefault(); // Play jump sound if (this.state.isRunning && !this.state.player.isJumping && !this.state.player.isFalling) { getAudioManager().play('jump'); } } }; const handleKeyUp = (e: KeyboardEvent) => { if (KEYS.LEFT.includes(e.code)) { this.keys.left = false; } if (KEYS.RIGHT.includes(e.code)) { this.keys.right = false; } if (KEYS.JUMP.includes(e.code)) { this.keys.jump = false; } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); } private updateCamera(): void { // Follow player with smooth scrolling const targetX = this.state.player.x - CANVAS_WIDTH / 3; this.state.camera.x = Math.max(0, Math.min(targetX, this.state.level.width - CANVAS_WIDTH)); } private update(): void { if (!this.state.isRunning || this.state.isGameOver) return; const prevY = this.state.player.y; // Update player physics this.physics.updatePlayerMovement(this.state.player, this.keys); // Keep player in bounds if (this.state.player.x < 0) this.state.player.x = 0; if (this.state.player.x > this.state.level.width - this.state.player.width) { this.state.player.x = this.state.level.width - this.state.player.width; } // Check block collisions const { grounded, hitBlock } = this.collision.checkPlayerBlockCollision( this.state.player, this.state.blocks, prevY ); // Handle question block hit if (hitBlock && hitBlock.hasCoin) { hitBlock.isHit = true; hitBlock.hasCoin = false; this.state.player.coins++; this.state.player.score += 100; getAudioManager().play('coin'); } // Check coin collection const collectedCoins = this.collision.checkCoinCollision(this.state.player, this.state.blocks); collectedCoins.forEach(coin => { const index = this.state.blocks.indexOf(coin); if (index > -1) { this.state.blocks.splice(index, 1); this.state.player.coins++; this.state.player.score += 50; getAudioManager().play('coin'); } }); // Update enemies this.state.enemies.forEach(enemy => { if (enemy.isAlive) { this.physics.updateEnemyMovement(enemy, this.state.level.width); this.collision.checkEnemyBlockCollision(enemy, this.state.blocks); // Check player-enemy collision const { killed, playerHit } = this.collision.checkPlayerEnemyCollision( this.state.player, enemy ); if (killed) { enemy.isAlive = false; this.state.player.vy = -8; // Bounce off enemy this.state.player.score += 200; getAudioManager().play('stomp'); } else if (playerHit) { this.gameOver(false); } } }); // Check flag collision (win) if (this.collision.checkFlagCollision(this.state.player, this.state.level.flagPosition)) { this.levelComplete(); } // Check fall death if (this.collision.checkFallDeath(this.state.player, this.state.level.height)) { this.gameOver(false); } // Update camera this.updateCamera(); } private render(): void { this.renderer.render(this.state, this.currentTheme, this.levelManager.getCurrentLevelNumber()); } private gameLoop = (): void => { this.update(); this.render(); if (this.state.isRunning && !this.state.isGameOver) { this.animationId = requestAnimationFrame(this.gameLoop); } }; private levelComplete(): void { this.state.isGameOver = true; this.state.isWin = true; this.state.player.score += 1000; // Level completion bonus this.totalScore = this.state.player.score; getAudioManager().play('levelup'); // Render final frame this.render(); const currentLevelNum = this.levelManager.getCurrentLevelNumber(); // Check if there are more levels if (!this.levelManager.isLastLevel()) { // Proceed to next level after delay setTimeout(() => { const nextLevel = this.levelManager.nextLevel(); if (nextLevel) { this.currentTheme = this.levelManager.getCurrentLevelInfo().theme; this.state = this.createInitialState(); this.state.isRunning = true; this.gameLoop(); if (this.callbacks.onLevelComplete) { this.callbacks.onLevelComplete(currentLevelNum, this.levelManager.getCurrentLevelNumber()); } } }, 2000); } else { // Game complete! setTimeout(() => { if (this.callbacks.onGameComplete) { this.callbacks.onGameComplete(this.totalScore); } else if (this.callbacks.onGameOver) { this.callbacks.onGameOver(true, this.totalScore); } }, 2000); } } private gameOver(isWin: boolean): void { this.state.isGameOver = true; this.state.isWin = isWin; this.state.player.isAlive = false; getAudioManager().play('death'); // Render final frame this.render(); if (this.callbacks.onGameOver) { setTimeout(() => { this.callbacks.onGameOver!(false, this.state.player.score); }, 1500); } } start(): void { getAudioManager().resume(); this.state = this.createInitialState(); this.state.isRunning = true; this.gameLoop(); } stop(): void { this.state.isRunning = false; if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = null; } getAudioManager().stopBGM(); } reset(): void { this.stop(); this.totalScore = 0; this.levelManager.resetToFirstLevel(); this.currentTheme = this.levelManager.getCurrentLevelInfo().theme; this.state = this.createInitialState(); this.render(); } setCallbacks(callbacks: GameCallbacks): void { this.callbacks = callbacks; } // Legacy support setOnGameOver(callback: (isWin: boolean, score: number) => void): void { this.callbacks.onGameOver = callback; } getState(): GameState { return this.state; } getCurrentLevel(): number { return this.levelManager.getCurrentLevelNumber(); } getTotalLevels(): number { return this.levelManager.getLevelCount(); } // External touch/mobile control input setControls(controls: { left: boolean; right: boolean; jump: boolean }): void { const wasJumping = this.keys.jump; this.keys.left = controls.left; this.keys.right = controls.right; this.keys.jump = controls.jump; // Play jump sound when jump starts if (!wasJumping && controls.jump && this.state.isRunning && !this.state.player.isJumping && !this.state.player.isFalling) { getAudioManager().play('jump'); } } }