ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
287 lines (286 loc) • 9.72 kB
TypeScript
/**
* Collision Plugin for ECSpresso
*
* Provides layer-based collision detection with events.
* Uses worldTransform for position (world-space collision).
* Supports AABB and circle colliders.
*/
import { type BasePluginOptions } from 'ecspresso';
import type { TransformWorldConfig } from '../spatial/transform';
/**
* Axis-Aligned Bounding Box collider.
*/
export interface AABBCollider {
/** Width of the bounding box */
width: number;
/** Height of the bounding box */
height: number;
/** X offset from entity position (default: 0) */
offsetX?: number;
/** Y offset from entity position (default: 0) */
offsetY?: number;
}
/**
* Circle collider.
*/
export interface CircleCollider {
/** Radius of the circle */
radius: number;
/** X offset from entity position (default: 0) */
offsetX?: number;
/** Y offset from entity position (default: 0) */
offsetY?: number;
}
/**
* Collision layer configuration.
*/
export interface CollisionLayer<L extends string = never> {
/** The layer this entity belongs to */
layer: L;
/** Layers this entity can collide with */
collidesWith: readonly L[];
}
/**
* Component types provided by the collision plugin.
* Included automatically via `.withPlugin(createCollisionPlugin())`.
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createCollisionPlugin())
* .withComponentTypes<{ sprite: Sprite; enemy: boolean }>()
* .build();
* ```
*/
export interface CollisionComponentTypes<L extends string = never> {
aabbCollider: AABBCollider;
circleCollider: CircleCollider;
collisionLayer: CollisionLayer<L>;
}
/**
* Event fired when two entities collide.
*
* Normal components are flattened (`normalX`/`normalY`) rather than nested
* in a sub-object to avoid a per-event allocation in the collision hot path.
*/
export interface CollisionEvent<L extends string = never> {
/** First entity in the collision */
entityA: number;
/** Second entity in the collision */
entityB: number;
/** Layer of the first entity */
layerA: L;
/** Layer of the second entity */
layerB: L;
/** Contact normal X, pointing from entityA toward entityB */
normalX: number;
/** Contact normal Y, pointing from entityA toward entityB */
normalY: number;
/** Penetration depth (positive = overlapping) */
depth: number;
}
/**
* Event types provided by the collision plugin.
*/
export interface CollisionEventTypes<L extends string = never> {
collision: CollisionEvent<L>;
}
/**
* Configuration options for the collision plugin.
*/
export interface CollisionPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {
/** Name of the collision event (default: 'collision') */
collisionEventName?: string;
}
/**
* Create an AABB collider component.
*
* @param width Width of the bounding box
* @param height Height of the bounding box
* @param offsetX X offset from entity position
* @param offsetY Y offset from entity position
* @returns Component object suitable for spreading into spawn()
*
* @example
* ```typescript
* ecs.spawn({
* ...createTransform(100, 200),
* ...createAABBCollider(50, 30),
* });
* ```
*/
export declare function createAABBCollider(width: number, height: number, offsetX?: number, offsetY?: number): {
aabbCollider: AABBCollider;
};
/**
* Create a circle collider component.
*
* @param radius Radius of the circle
* @param offsetX X offset from entity position
* @param offsetY Y offset from entity position
* @returns Component object suitable for spreading into spawn()
*
* @example
* ```typescript
* ecs.spawn({
* ...createTransform(100, 200),
* ...createCircleCollider(25),
* });
* ```
*/
export declare function createCircleCollider(radius: number, offsetX?: number, offsetY?: number): {
circleCollider: CircleCollider;
};
/**
* Create a collision layer component.
*
* @param layer The layer this entity belongs to
* @param collidesWith Layers this entity can collide with
* @returns Component object suitable for spreading into spawn()
*
* @example
* ```typescript
* ecs.spawn({
* ...createTransform(100, 200),
* ...createAABBCollider(50, 30),
* ...createCollisionLayer('player', ['enemy', 'obstacle']),
* });
* ```
*/
export declare function createCollisionLayer<L extends string>(layer: L, collidesWith: readonly L[]): Pick<CollisionComponentTypes<L>, 'collisionLayer'>;
/**
* Layer factory result from defineCollisionLayers.
*/
export type LayerFactories<T extends Record<string, readonly string[]>> = {
[K in keyof T]: () => Pick<CollisionComponentTypes<Extract<keyof T, string>>, 'collisionLayer'>;
};
/**
* Extract layer names from a `defineCollisionLayers` result for use with
* `createCollisionPairHandler`'s `L` type parameter.
*
* @example
* ```typescript
* const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });
* type Layer = LayersOf<typeof layers>;
* const handler = createCollisionPairHandler<ECS, Layer>({
* 'player:enemy': (playerId, enemyId, ecs) => { ... },
* });
* ```
*/
export type LayersOf<T> = Extract<keyof T, string>;
/**
* Define collision layer relationships and get factory functions.
*
* @param rules Object mapping layer names to arrays of layers they collide with
* @returns Object with factory functions for each layer
*
* @example
* ```typescript
* const layers = defineCollisionLayers({
* player: ['enemy', 'enemyProjectile'],
* playerProjectile: ['enemy'],
* enemy: ['playerProjectile'],
* enemyProjectile: ['player'],
* });
*
* // Usage
* ecs.spawn({
* ...createTransform(100, 200),
* ...createAABBCollider(50, 30),
* ...layers.player(),
* });
* ```
*/
/**
* Validates that all `collidesWith` values reference actual layer keys.
* Catches typos at compile time.
*/
type ValidateCollidesWith<T> = {
[K in keyof T]: T[K] extends readonly (infer V)[] ? [V] extends [Extract<keyof T, string>] ? T[K] : readonly Extract<keyof T, string>[] : never;
};
export declare function defineCollisionLayers<const T extends Record<string, readonly string[]>>(rules: T & ValidateCollidesWith<T>): LayerFactories<T>;
/**
* Callback for a collision pair handler.
*
* @param firstEntityId Entity belonging to the first layer in the pair key
* @param secondEntityId Entity belonging to the second layer in the pair key
* @param ecs The ECS world instance (passed through from the subscriber)
*/
export type CollisionPairCallback<W = unknown> = (firstEntityId: number, secondEntityId: number, ecs: W) => void;
/**
* Create a collision pair handler that routes collision events to
* layer-pair-specific callbacks.
*
* Registering `"a:b"` automatically handles both `(layerA=a, layerB=b)` and
* `(layerA=b, layerB=a)`. Entity arguments are swapped to match the declared
* key order. If both `"a:b"` and `"b:a"` are explicitly registered, each gets
* its own handler with no implicit reverse.
*
* @typeParam W - The ECS world type (e.g. `ECSpresso<C, E, R>`). Defaults to `unknown`.
* @typeParam L - Union of valid layer names. Defaults to `string`.
* Provide specific layer names for compile-time key validation:
* `createCollisionPairHandler<ECS, keyof typeof layers>({...})`
*
* @param pairs Object mapping `"layerA:layerB"` keys to callbacks
* @returns A dispatch function to call with collision event data and ECS instance
*
* @example
* ```typescript
* // Basic usage:
* const handler = createCollisionPairHandler<ECS>({
* 'playerProjectile:enemy': (projectileId, enemyId, ecs) => {
* ecs.commands.removeEntity(projectileId);
* },
* });
*
* // With layer name validation:
* const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });
* type Layer = LayersOf<typeof layers>;
* const handler = createCollisionPairHandler<ECS, Layer>({
* 'player:enemy': (playerId, enemyId, ecs) => { ... },
* });
*
* ecs.eventBus.subscribe('collision', (data) => handler({ data, ecs }));
* ```
*/
export declare function createCollisionPairHandler<W = unknown, L extends string = string>(pairs: {
[K in `${L}:${L}`]?: CollisionPairCallback<W>;
}): (ctx: {
data: CollisionEvent<L>;
ecs: W;
}) => void;
/**
* Create a collision plugin for ECSpresso.
*
* This plugin provides:
* - Collision detection between entities with colliders
* - AABB-AABB, circle-circle, and AABB-circle collision
* - Layer-based filtering for collision pairs
* - Deduplication of A-B / B-A collisions
* - Automatic broadphase acceleration when spatialIndex resource is present
*
* Uses worldTransform for position (world-space collision detection).
* The `layers` parameter is required for type inference — at runtime the
* plugin does not consume it.
*
* @example
* ```typescript
* const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });
* const ecs = ECSpresso
* .create()
* .withPlugin(createTransformPlugin())
* .withPlugin(createCollisionPlugin({ layers }))
* .build();
*
* // Entity with collision
* ecs.spawn({
* ...createTransform(100, 200),
* ...createAABBCollider(50, 30),
* ...layers.player(),
* });
* ```
*/
export declare function createCollisionPlugin<L extends string, G extends string = 'physics'>(options: CollisionPluginOptions<G> & {
layers: LayerFactories<Record<L, readonly string[]>>;
}): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, CollisionComponentTypes<L>>, CollisionEventTypes<L>>, TransformWorldConfig, "collision-detection", G, never, never>;
export {};