super-mario / lib /engine /Collision.ts
asemxin
Initial commit: Super Mario Web game
2d54981
// 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;
}
}