UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

287 lines (286 loc) 9.72 kB
/** * 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 {};