// Collision detection system import { PlayerState, Block, Enemy, Position } from './types'; import { GAME_CONFIG } from '../constants'; const { TILE_SIZE } = GAME_CONFIG; export interface CollisionResult { collided: boolean; side?: 'top' | 'bottom' | 'left' | 'right'; block?: Block; } export class Collision { // AABB collision check between two rectangles checkAABB( a: { x: number; y: number; width: number; height: number }, b: { x: number; y: number; width: number; height: number } ): boolean { return ( a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y ); } // Determine collision side getCollisionSide( player: PlayerState, block: Block, prevY: number ): 'top' | 'bottom' | 'left' | 'right' { const playerBottom = player.y + player.height; const playerTop = player.y; const blockBottom = block.y + block.height; const blockTop = block.y; // Check if landing on top if (prevY + player.height <= block.y + 2 && player.vy >= 0) { return 'top'; } // Check if hitting from below if (prevY >= blockBottom - 2 && player.vy < 0) { return 'bottom'; } // Determine left/right based on position const playerCenterX = player.x + player.width / 2; const blockCenterX = block.x + block.width / 2; return playerCenterX < blockCenterX ? 'right' : 'left'; } // Check player collision with blocks checkPlayerBlockCollision( player: PlayerState, blocks: Block[], prevY: number ): { grounded: boolean; hitBlock?: Block } { let grounded = false; let hitBlock: Block | undefined; for (const block of blocks) { // Skip coins - they have separate handling if (block.type === 'coin') continue; if (this.checkAABB(player, block)) { const side = this.getCollisionSide(player, block, prevY); switch (side) { case 'top': player.y = block.y - player.height; player.vy = 0; player.isJumping = false; player.isFalling = false; grounded = true; break; case 'bottom': player.y = block.y + block.height; player.vy = 1; // Small downward velocity if (block.type === 'question' && !block.isHit) { hitBlock = block; } break; case 'left': player.x = block.x + block.width; player.vx = 0; break; case 'right': player.x = block.x - player.width; player.vx = 0; break; } } } return { grounded, hitBlock }; } // Check enemy collision with blocks (ground only) checkEnemyBlockCollision(enemy: Enemy, blocks: Block[]): void { for (const block of blocks) { if (block.type === 'coin') continue; if (this.checkAABB(enemy, block)) { // Land on top if (enemy.vy > 0) { enemy.y = block.y - enemy.height; enemy.vy = 0; } // Side collision - reverse direction if ( enemy.x + enemy.width > block.x && enemy.x < block.x + block.width ) { if (enemy.y + enemy.height > block.y + 4) { enemy.direction *= -1; } } } } } // Check player collision with enemy checkPlayerEnemyCollision( player: PlayerState, enemy: Enemy ): { killed: boolean; playerHit: boolean } { if (!enemy.isAlive || !player.isAlive) { return { killed: false, playerHit: false }; } if (this.checkAABB(player, enemy)) { // Check if player is landing on enemy (stomping) if (player.vy > 0 && player.y + player.height < enemy.y + enemy.height / 2) { return { killed: true, playerHit: false }; } else { return { killed: false, playerHit: true }; } } return { killed: false, playerHit: false }; } // Check player collision with coin checkCoinCollision(player: PlayerState, blocks: Block[]): Block[] { const collectedCoins: Block[] = []; blocks.forEach((block, index) => { if (block.type === 'coin' && this.checkAABB(player, block)) { collectedCoins.push(block); } }); return collectedCoins; } // Check if player reached the flag checkFlagCollision(player: PlayerState, flagPosition: Position): boolean { return ( player.x + player.width >= flagPosition.x && player.x <= flagPosition.x + 50 && player.y <= flagPosition.y + 100 ); } // Check if player fell off the level checkFallDeath(player: PlayerState, levelHeight: number): boolean { return player.y > levelHeight + 100; } }