asemxin
feat: reduce mobile control sensitivity for better touch experience
0c4f8d5
// 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');
}
}
}