import { colliderShape, shapesOverlap } from '../../core/properties/collider'; import type { Entity } from '../Entity'; import type { World } from '../World'; import type { System } from './System'; /** Two collider entities that overlap (or overlapped) – always the same pair order per key. */ export interface CollisionPair { a: Entity; b: Entity; } /** * Detects overlaps between collider entities once per tick and sorts them into enter / * stay / exit lists for the frame. Detection only – nothing is pushed apart. Colliders * of the same body (same parent) never collide with each other. Runs before the * BehaviorSystem so graphs see this frame's events. */ export class CollisionSystem implements System { enters: CollisionPair[] = []; stays: CollisionPair[] = []; exits: CollisionPair[] = []; private previous = new Map(); update(world: World): void { const colliders = world.entities.filter((e) => e.type === 'collider'); const current = new Map(); for (let i = 0; i < colliders.length; i++) { for (let j = i + 1; j < colliders.length; j++) { const a = colliders[i]!; const b = colliders[j]!; if (bodyOf(a) === bodyOf(b)) continue; if (this.overlapping(world, a, b)) current.set(`${a.id}|${b.id}`, { a, b }); } } this.enters = []; this.stays = []; this.exits = []; for (const [key, pair] of current) (this.previous.has(key) ? this.stays : this.enters).push(pair); for (const [key, pair] of this.previous) if (!current.has(key)) this.exits.push(pair); this.previous = current; } stop(): void { this.previous = new Map(); this.enters = []; this.stays = []; this.exits = []; } /** Live test of two collider entities (world positions applied). */ overlapping(world: World, a: Entity, b: Entity): boolean { return shapesOverlap(colliderShape(a.props, world.worldPosition(a)), colliderShape(b.props, world.worldPosition(b))); } /** Whether any collider of `a` overlaps any collider of `b` (see `collidersOf`). */ touching(world: World, a: Entity, b: Entity): boolean { for (const ca of collidersOf(world, a)) { for (const cb of collidersOf(world, b)) { if (ca !== cb && this.overlapping(world, ca, cb)) return true; } } return false; } } /** The object a collider belongs to: its parent, or the collider itself when top-level. */ export function bodyOf(collider: Entity): Entity { return collider.parent ?? collider; } /** A collider stands for itself; any other entity stands for its direct collider children. */ export function collidersOf(world: World, entity: Entity): Entity[] { if (entity.type === 'collider') return [entity]; return world.childrenOf(entity).filter((e) => e.type === 'collider'); }