UNPKG

ecspresso

Version:

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

275 lines (274 loc) 12.9 kB
import ECSpresso from "./ecspresso"; import type { WorldConfig, EmptyConfig, WorldConfigFrom } from "./type-utils"; /** * Execution phase for systems. Systems are grouped by phase and executed * in this fixed order: preUpdate -> fixedUpdate -> update -> postUpdate -> render. * Within each phase, systems are sorted by priority (higher first). */ export type SystemPhase = 'preUpdate' | 'fixedUpdate' | 'update' | 'postUpdate' | 'render'; export interface Entity<ComponentTypes> { id: number; components: Partial<ComponentTypes>; } /** * Options for removing an entity */ export interface RemoveEntityOptions { /** * Whether to also remove all descendants (default: true) */ cascade?: boolean; } /** * Options for hierarchy traversal methods */ export interface HierarchyIteratorOptions { /** Specific root entities to start traversal from. If not provided, all root entities are used. */ roots?: readonly number[]; } /** * Entry yielded during hierarchy traversal */ export interface HierarchyEntry { /** The entity being visited */ entityId: number; /** The parent entity ID, or null for root entities */ parentId: number | null; /** Depth in the hierarchy (0 for roots) */ depth: number; } export interface FilteredEntity<ComponentTypes, WithComponents extends keyof ComponentTypes = never, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never, MutatesComponents extends keyof ComponentTypes = keyof ComponentTypes> { id: number; components: Omit<Partial<ComponentTypes>, WithComponents | WithoutComponents | OptionalComponents> & { [K in WithComponents]: K extends MutatesComponents ? ComponentTypes[K] : Readonly<ComponentTypes[K]>; } & { [K in OptionalComponents]: ComponentTypes[K] | undefined; }; } export interface QueryConfig<ComponentTypes, WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes, OptionalComponents extends keyof ComponentTypes = WithComponents, MutatesComponents extends keyof ComponentTypes = keyof ComponentTypes> { with: ReadonlyArray<WithComponents>; without?: ReadonlyArray<WithoutComponents>; changed?: ReadonlyArray<WithComponents>; optional?: ReadonlyArray<OptionalComponents>; parentHas?: ReadonlyArray<keyof ComponentTypes>; /** * Components to auto-mark as changed on every iterated entity after * `process()` returns. Eliminates repeated `ecs.markChanged(id, name)` * boilerplate inside iteration loops. Components listed in `with` but * absent from `mutates` are narrowed to `Readonly<T>` on the iteration * entity, catching accidental writes at compile time. */ mutates?: ReadonlyArray<MutatesComponents>; /** @internal Pre-resolved component indices for `changed:`, populated at system registration. */ _changedIdx?: ReadonlyArray<number>; /** @internal Pre-resolved component indices for `mutates:`, populated at system registration. */ _mutatesIdx?: ReadonlyArray<number>; } /** * Utility type to derive the entity type that would result from a query definition. * This is useful for creating helper functions that operate on query results. * * @example * ```typescript * const queryDef = { * with: ['position', 'sprite'], * without: ['dead'] * }; * * type EntityType = QueryResultEntity<Components, typeof queryDef>; * * function updateSpritePosition(entity: EntityType) { * entity.components.sprite.position.set( * entity.components.position.x, * entity.components.position.y * ); * } * ``` */ export type QueryResultEntity<ComponentTypes extends Record<string, any>, QueryDef extends { with: ReadonlyArray<keyof ComponentTypes>; without?: ReadonlyArray<keyof ComponentTypes>; changed?: ReadonlyArray<keyof ComponentTypes>; optional?: ReadonlyArray<keyof ComponentTypes>; parentHas?: ReadonlyArray<keyof ComponentTypes>; mutates?: ReadonlyArray<keyof ComponentTypes>; }> = FilteredEntity<ComponentTypes, QueryDef['with'][number], QueryDef['without'] extends ReadonlyArray<any> ? QueryDef['without'][number] : never, QueryDef['optional'] extends ReadonlyArray<any> ? QueryDef['optional'][number] : never, QueryDef['mutates'] extends ReadonlyArray<any> ? QueryDef['mutates'][number] : QueryDef['with'][number]>; /** * Simplified query definition type for creating reusable queries */ export type QueryDefinition<ComponentTypes extends Record<string, any>, WithComponents extends keyof ComponentTypes = keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = keyof ComponentTypes, OptionalComponents extends keyof ComponentTypes = keyof ComponentTypes, MutatesComponents extends keyof ComponentTypes = keyof ComponentTypes> = { with: ReadonlyArray<WithComponents>; without?: ReadonlyArray<WithoutComponents>; changed?: ReadonlyArray<WithComponents>; optional?: ReadonlyArray<OptionalComponents>; parentHas?: ReadonlyArray<keyof ComponentTypes>; /** * Components to auto-mark as changed on every iterated entity after * `process()` returns. Components in `with` but absent from `mutates` * are narrowed to `Readonly<T>` on the iteration entity type. */ mutates?: ReadonlyArray<MutatesComponents>; /** @internal Pre-resolved component indices for `changed:`, populated at system registration. */ _changedIdx?: ReadonlyArray<number>; /** @internal Pre-resolved component indices for `mutates:`, populated at system registration. */ _mutatesIdx?: ReadonlyArray<number>; }; /** * Helper function to create a query definition with proper type inference. * This enables better TypeScript inference when creating reusable queries. * * @example * ```typescript * const movingEntitiesQuery = createQueryDefinition({ * with: ['position', 'velocity'], * without: ['dead'] * }); * * type MovingEntity = QueryResultEntity<Components, typeof movingEntitiesQuery>; * * function updatePosition(entity: MovingEntity) { * entity.components.position.x += entity.components.velocity.x; * entity.components.position.y += entity.components.velocity.y; * } * * world.addSystem('movement') * .addQuery('entities', movingEntitiesQuery) * .setProcess(({ queries }) => { * for (const entity of queries.entities) { * updatePosition(entity); * } * }); * ``` */ export declare function createQueryDefinition<ComponentTypes extends Record<string, any>, const QueryDef extends { with: ReadonlyArray<keyof ComponentTypes>; without?: ReadonlyArray<keyof ComponentTypes>; changed?: ReadonlyArray<keyof ComponentTypes>; optional?: ReadonlyArray<keyof ComponentTypes>; parentHas?: ReadonlyArray<keyof ComponentTypes>; mutates?: ReadonlyArray<keyof ComponentTypes>; }>(queryDef: QueryDef): QueryDef; export interface System<Cfg extends WorldConfig = EmptyConfig, WithComponents extends keyof Cfg['components'] = never, WithoutComponents extends keyof Cfg['components'] = never> { label: string; /** * System priority - higher values execute first (default: 0) * When systems have the same priority, they execute in registration order */ priority?: number; /** * Execution phase for this system (default: 'update') * Systems are grouped by phase and executed in order: * preUpdate -> fixedUpdate -> update -> postUpdate -> render */ phase?: SystemPhase; /** * Groups this system belongs to. If any group is disabled, the system will be skipped. */ groups?: string[]; /** * Screens where this system should run. If specified, system only runs * when current screen is in this list. */ inScreens?: ReadonlyArray<keyof Cfg['screens'] & string>; /** * Screens where this system should NOT run. If specified, system skips * when current screen is in this list. */ excludeScreens?: ReadonlyArray<keyof Cfg['screens'] & string>; /** * Assets that must be loaded for this system to run. * System will be skipped if any required asset is not loaded. */ requiredAssets?: ReadonlyArray<keyof Cfg['assets'] & string>; /** * When true, the system's process function runs even when all queries * return zero entities. Default is false (system is skipped when all * queries are empty). */ runWhenEmpty?: boolean; entityQueries?: { [queryName: string]: QueryConfig<Cfg['components'], WithComponents, WithoutComponents>; }; /** * Singleton queries that yield a single entity (or undefined) rather than * an array. Resolved into the process context's `queries` object under * the registered name. */ entitySingletons?: { [singletonName: string]: QueryConfig<Cfg['components'], WithComponents, WithoutComponents>; }; /** * Process method that runs during each update cycle. * Receives a single context object with queries, dt, and ecs. */ process?(ctx: { queries: { [queryName: string]: Array<FilteredEntity<Cfg['components'], WithComponents, WithoutComponents>>; }; dt: number; ecs: ECSpresso<Cfg>; }): void; /** * Lifecycle hook called when the system is initialized * This is called when ECSpresso.initialize() is invoked, after resources are initialized * Use this for one-time initialization that depends on resources * @param ecs The ECSpresso instance providing access to all ECS functionality */ onInitialize?(ecs: ECSpresso<Cfg>): void | Promise<void>; /** * Lifecycle hook called when the system is detached from the ECS * @param ecs The ECSpresso instance providing access to all ECS functionality */ onDetach?(ecs: import("./ecspresso").default<Cfg>): void; /** * Per-query callbacks that fire once per entity the first time it appears * in a query's results. Fires before process. Automatic cleanup when * entity leaves query (component removed, entity destroyed) so re-entry * fires the callback again. */ onEntityEnter?: Record<string, (ctx: { entity: FilteredEntity<Cfg['components'], WithComponents, WithoutComponents>; ecs: ECSpresso<Cfg>; }) => void>; /** * Event handlers for specific event types */ eventHandlers?: { [EventName in keyof Cfg['events']]?: (ctx: { data: Cfg['events'][EventName]; ecs: ECSpresso<Cfg>; }) => void; }; /** * @internal Precomputed pairs of (queryName, mutates, kind) derived at * system registration from queries/singletons declaring `mutates`. Null * when no query on the system declares `mutates`, so the post-process * auto-mark walk is a single pointer check away from zero cost for * non-users. */ _autoMarkPairs?: ReadonlyArray<{ queryName: string; mutatesIdx: ReadonlyArray<number>; kind: 'list' | 'singleton'; }> | null; } /** * Typed world interface for plugin helpers and structural typing. * * Generic over component types `C`: * - `BaseWorld` (no param): defaults to `{}`, meaning component-accessing methods * cannot be called (keys resolve to `never`). Use for functions that only need * `removeEntity`, `getResource`, etc. * - `BaseWorld<MyComponents>`: narrows `getComponent`, `hasComponent`, `markChanged`, * `spawn`, and command buffer methods to the declared component map. * * Structural typing ensures any `ECSpresso<Cfg>` where `Cfg['components']` is a * superset of `C` satisfies `BaseWorld<C>`. */ type _BaseWorldCfg<C extends Record<string, any>> = WorldConfigFrom<C, Record<string, any>, Record<string, any>, Record<string, unknown>, Record<string, any>>; type _EventBus = import("./event-bus").default<Record<string, any>>; export type BaseWorld<C extends Record<string, any> = {}> = Pick<ECSpresso<_BaseWorldCfg<C>>, 'getComponent' | 'hasComponent' | 'removeEntity' | 'spawn' | 'markChanged' | 'getResource' | 'hasResource'> & { eventBus: Pick<_EventBus, 'publish'>; commands: Pick<import("./command-buffer").default<_BaseWorldCfg<C>>, 'spawn' | 'removeEntity' | 'addComponent' | 'removeComponent'>; }; export type { Merge, MergeAll, TypesAreCompatible, ComponentsOf, EventsOf, ResourcesOf, LabelsOf, GroupsOf, AssetGroupNamesOf, ReactiveQueryNamesOf, AssetTypesOf, ScreenStatesOf, ComponentsOfWorld, EventsOfWorld, AssetsOfWorld, ScreenStatesOfWorld, AnyECSpresso, AnyPlugin, EventNameMatching, ChannelOfWorld, WorldConfig, EmptyConfig, WorldConfigFrom, ComponentsConfig, EventsConfig, ResourcesConfig, AssetsConfig, ScreensConfig, MergeConfigs, ConfigsAreCompatible, ConfigOf, WithComponents, WithEvents, WithResources, WithAssets, WithScreens } from './type-utils';