82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
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<string, CollisionPair>();
|
||
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<string, CollisionPair>();
|
||
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);
|
||
}
|
||
}
|