import { isHandleEnabled } from '../../core/graph'; import { shapesOverlap } from '../../core/properties/collider'; import type { Entity } from '../Entity'; import type { World } from '../World'; import { bodyOf, type CollisionPair, type CollisionSource, isCollisionBody, type MoveResult, partsOf, Solids, } from './solids'; import type { System } from './System'; /** * Sorts collider overlaps into enter / stay / exit per tick, detection only; colliders of one body never * collide. Runs before the BehaviorSystem so graphs see this frame's events. This is the editor * run's collision; a played game has the physics engine instead (`PhysicsSystem`, #146). * * A placed tilemap is a solid body of its own: its blocking cells, merged into rectangles, stop a * move and report a collision with the tilemap as the other side. Two tilemaps never meet – they * are ground, not things that walk into each other. */ export class CollisionSystem implements System, CollisionSource { enters: CollisionPair[] = []; stays: CollisionPair[] = []; exits: CollisionPair[] = []; private previous = new Map(); private readonly solids = new Solids(); update(world: World): void { this.solids.reset(); // Switched-off bodies (or ones below something switched off) take no part at all. const bodies = world.entities.filter((e) => isCollisionBody(e) && isHandleEnabled(e)); const current = new Map(); for (let i = 0; i < bodies.length; i++) { for (let j = i + 1; j < bodies.length; j++) { const a = bodies[i]!; const b = bodies[j]!; if (bodyOf(a) === bodyOf(b)) continue; if (a.type === 'tilemap' && b.type === 'tilemap') continue; if (this.overlaps(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.solids.reset(); this.enters = []; this.stays = []; this.exits = []; } /** Live test of two bodies (world positions applied): any shape of one over any of the other. */ private overlaps(world: World, a: Entity, b: Entity): boolean { const others = this.solids.shapesOf(world, b); return this.solids.shapesOf(world, a).some((sa) => others.some((sb) => shapesOverlap(sa, sb))); } /** Whether any collider of `a` overlaps any collider of `b` (see `partsOf`). */ touching(world: World, a: Entity, b: Entity): boolean { for (const ca of partsOf(world, a)) { for (const cb of partsOf(world, b)) { if (ca !== cb && this.overlaps(world, ca, cb)) return true; } } return false; } moveAndSlide(world: World, entity: Entity, dx: number, dy: number): MoveResult { return this.solids.moveAndSlide(world, entity, dx, dy); } }