UNPKG

ecspresso

Version:

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

254 lines (253 loc) 15.3 kB
import type ECSpresso from "./ecspresso"; import type { SystemDefaults } from "./plugin"; import type { FilteredEntity, QueryDefinition, System, SystemPhase } from "./types"; import type { WorldConfig, EmptyConfig } from "./type-utils"; export declare const PROCESS_EACH_QUERY: "__each"; type ProcessEachKey = typeof PROCESS_EACH_QUERY; /** * Builder class for creating type-safe ECS Systems with proper query inference. * Systems are automatically registered with their ECSpresso instance when * finalized (at the start of initialize() or update()). */ export declare class SystemBuilder<Cfg extends WorldConfig = EmptyConfig, Queries extends Record<string, QueryDefinition<Cfg['components']>> = {}, Label extends string = string, SysGroups extends string = never, ResourceKeys extends keyof Cfg['resources'] = never, Singletons extends Record<string, QueryDefinition<Cfg['components']>> = {}> { private _label; private queries; private singletons; private processFunction?; private detachFunction?; private initializeFunction?; private eventHandlers?; private _priority; private _phase; private _groups; private _inScreens?; private _excludeScreens?; private _requiredAssets?; private _runWhenEmpty; private _entityEnterHandlers; private _resourceKeys?; constructor(_label: string, defaults?: SystemDefaults<Cfg>); get label(): string; /** * Create a system object with all configured properties. * @internal Used by ECSpresso to finalize and register the system */ _createSystemObject(): System<Cfg, any, any>; /** * Set the priority of this system. Systems with higher priority values * execute before those with lower values. Systems with the same priority * execute in the order they were registered. * @param priority The priority value (default: 0) * @returns This SystemBuilder instance for method chaining */ setPriority(priority: number): this; /** * Set the execution phase for this system. * Systems are grouped by phase and executed in order: * preUpdate -> fixedUpdate -> update -> postUpdate -> render * @param phase The phase to assign this system to (default: 'update') * @returns This SystemBuilder instance for method chaining */ inPhase(phase: SystemPhase): this; /** * Add this system to a group. Systems can belong to multiple groups. * When any group a system belongs to is disabled, the system will be skipped. * @param groupName The name of the group to add the system to * @returns This SystemBuilder instance for method chaining */ inGroup<G extends string>(groupName: G): SystemBuilder<Cfg, Queries, Label, SysGroups | G, ResourceKeys, Singletons>; /** * Restrict this system to only run in specified screens. * System will be skipped during update() when the current screen * is not in this list. * @param screens Array of screen names where this system should run * @returns This SystemBuilder instance for method chaining */ inScreens(screens: ReadonlyArray<keyof Cfg['screens'] & string>): this; /** * Exclude this system from running in specified screens. * System will be skipped during update() when the current screen * is in this list. * @param screens Array of screen names where this system should NOT run * @returns This SystemBuilder instance for method chaining */ excludeScreens(screens: ReadonlyArray<keyof Cfg['screens'] & string>): this; /** * Require specific assets to be loaded for this system to run. * System will be skipped during update() if any required asset * is not loaded. * @param assets Array of asset keys that must be loaded * @returns This SystemBuilder instance for method chaining */ requiresAssets(assets: ReadonlyArray<keyof Cfg['assets'] & string>): this; /** * Allow this system to run even when all queries return zero entities. * By default, systems with queries are skipped when no entities match. */ runWhenEmpty(): this; /** * Declare resource dependencies for this system. Resources are resolved * once (on first process call) and the same object is reused every frame. * The resolved resources are available as ctx.resources in setProcess. * @param keys Array of resource keys to resolve * @returns This SystemBuilder instance for method chaining */ withResources<RK extends keyof Cfg['resources'] & string>(keys: readonly RK[]): SystemBuilder<Cfg, Queries, Label, SysGroups, RK, Singletons>; /** * Add a query definition to the system. * * When `mutates` is declared, every iterated entity is automatically * `markChanged`'d for each listed component after the system's * `process()` returns. Components in `with` but absent from `mutates` * are narrowed to `Readonly<T>` in the iteration entity type. */ addQuery<QueryName extends string, WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never, OptionalComponents extends keyof Cfg['components'] = never, MutatesComponents extends WithComponents = WithComponents, NewQueries extends Queries & Record<QueryName, QueryDefinition<Cfg['components'], WithComponents, WithoutComponents, OptionalComponents, MutatesComponents>> = Queries & Record<QueryName, QueryDefinition<Cfg['components'], WithComponents, WithoutComponents, OptionalComponents, MutatesComponents>>>(name: QueryName, definition: { with: ReadonlyArray<WithComponents>; without?: ReadonlyArray<WithoutComponents>; changed?: ReadonlyArray<WithComponents>; optional?: ReadonlyArray<OptionalComponents>; parentHas?: ReadonlyArray<keyof Cfg['components']>; mutates?: ReadonlyArray<MutatesComponents>; }): SystemBuilder<Cfg, NewQueries, Label, SysGroups, ResourceKeys, Singletons>; /** * Add a singleton query — a named query that yields a single * `FilteredEntity | undefined` instead of an array. Surfaces on the * process context's `queries` object alongside regular queries. * * When multiple entities match, the first is returned (no error). Use * the instance-level `getSingleton` / `tryGetSingleton` helpers on * `ECSpresso` if you need strictness guarantees. * * When `mutates` is declared, the resolved entity is automatically * `markChanged`'d for each listed component after the system's * `process()` returns. Components in `with` but absent from `mutates` * are narrowed to `Readonly<T>` in the iteration entity type. */ addSingleton<SingletonName extends string, WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never, OptionalComponents extends keyof Cfg['components'] = never, MutatesComponents extends WithComponents = WithComponents, NewSingletons extends Singletons & Record<SingletonName, QueryDefinition<Cfg['components'], WithComponents, WithoutComponents, OptionalComponents, MutatesComponents>> = Singletons & Record<SingletonName, QueryDefinition<Cfg['components'], WithComponents, WithoutComponents, OptionalComponents, MutatesComponents>>>(name: SingletonName, definition: { with: ReadonlyArray<WithComponents>; without?: ReadonlyArray<WithoutComponents>; changed?: ReadonlyArray<WithComponents>; optional?: ReadonlyArray<OptionalComponents>; parentHas?: ReadonlyArray<keyof Cfg['components']>; mutates?: ReadonlyArray<MutatesComponents>; }): SystemBuilder<Cfg, Queries, Label, SysGroups, ResourceKeys, NewSingletons>; /** * Set the system's process function that runs each update. * The callback receives a single context object { queries, dt, ecs, resources? }. * The context is pre-allocated per system and reused every frame. * @param process Function to process entities matching the system's queries each update * @returns This SystemBuilder instance for method chaining */ setProcess(process: SystemProcessFn<Cfg, Queries, ResourceKeys, Singletons>): this; private _wrapWithResources; /** * Inline-query terminator: define a single query and a per-entity callback * in one call. Collapses the common `addQuery` + `setProcess` + for-loop * pattern into a single chain step. * * Only valid on a builder with no prior queries or process function — * TypeScript narrows `this` to `never` otherwise, and a runtime guard * throws for untyped callers. For multi-query systems use * `addQuery` + `setProcess`. * * When `mutates` is declared, the callback may `return false` to skip the * auto-mark for that specific entity. Returning `true`, `undefined`, or * any other value stamps all components listed in `mutates`. Components * in `with` but absent from `mutates` are narrowed to `Readonly<T>` on * the per-entity iteration type. * * @param definition Inline query definition (with / without / optional / changed / parentHas / mutates) * @param process Callback invoked once per matching entity each frame */ setProcessEach<W extends keyof Cfg['components'], WO extends keyof Cfg['components'] = never, O extends keyof Cfg['components'] = never, M extends W = W>(this: [keyof Queries] extends [never] ? [keyof Singletons] extends [never] ? SystemBuilder<Cfg, Queries, Label, SysGroups, ResourceKeys, Singletons> : never : never, definition: { with: ReadonlyArray<W>; without?: ReadonlyArray<WO>; optional?: ReadonlyArray<O>; changed?: ReadonlyArray<W>; parentHas?: ReadonlyArray<keyof Cfg['components']>; mutates?: ReadonlyArray<M>; }, process: (ctx: { entity: FilteredEntity<Cfg['components'], W, WO, O, M>; dt: number; ecs: ECSpresso<Cfg>; } & ([ResourceKeys] extends [never] ? {} : { resources: { readonly [K in ResourceKeys]: Cfg['resources'][K]; }; })) => boolean | void): SystemBuilder<Cfg, Queries & Record<ProcessEachKey, QueryDefinition<Cfg['components'], W, WO, O, M>>, Label, SysGroups, ResourceKeys, Singletons>; /** * Register a callback that fires once per entity the first time it appears * in a query's results. Fires before process. Automatic cleanup when entity * leaves the query so re-entry fires the callback again. * @param queryName Name of a query previously added via addQuery * @param callback Function called with the entity and ecs instance * @returns This SystemBuilder instance for method chaining */ setOnEntityEnter<QN extends keyof Queries & string>(queryName: QN, callback: (ctx: { entity: FilteredEntity<Cfg['components'], Queries[QN] extends QueryDefinition<Cfg['components'], infer W> ? W : never, Queries[QN] extends QueryDefinition<Cfg['components'], any, infer WO> ? WO : never, Queries[QN] extends QueryDefinition<Cfg['components'], any, any, infer O> ? O : never>; ecs: ECSpresso<Cfg>; }) => void): this; /** * Set the onDetach lifecycle hook * Called when the system is removed from the ECS * @param onDetach Function to run when this system is detached from the ECS * @returns This SystemBuilder instance for method chaining */ setOnDetach(onDetach: SystemLifecycleFn<Cfg>): this; /** * Set the onInitialize lifecycle hook. * * Fires exactly once per system. For systems added before `initialize()`, * the hook is awaited inside `initialize()` itself. For systems added * after `initialize()` has returned, the hook fires on registration (at * the next `update()`'s finalize step) — async hooks run fire-and-forget, * so don't rely on completion ordering against the first `process` call. * * @param onInitialize Function to run when this system is initialized * @returns This SystemBuilder instance for method chaining */ setOnInitialize(onInitialize: SystemLifecycleFn<Cfg>): this; /** * Set event handlers for the system * These handlers will be automatically subscribed when the system is attached * @param handlers Object mapping event names to handler functions * @returns This SystemBuilder instance for method chaining */ setEventHandlers(handlers: { [EventName in keyof Cfg['events']]?: (ctx: { data: Cfg['events'][EventName]; ecs: ECSpresso<Cfg>; }) => void; }): this; } type QueryResults<ComponentTypes extends Record<string, any>, Queries extends Record<string, QueryDefinition<ComponentTypes>>, Singletons extends Record<string, QueryDefinition<ComponentTypes>> = {}> = { [QueryName in keyof Queries]: QueryName extends string ? FilteredEntity<ComponentTypes, Queries[QueryName] extends QueryDefinition<ComponentTypes, infer W> ? W : never, Queries[QueryName] extends QueryDefinition<ComponentTypes, any, infer WO> ? WO : never, Queries[QueryName] extends QueryDefinition<ComponentTypes, any, any, infer O> ? O : never, Queries[QueryName] extends QueryDefinition<ComponentTypes, infer W2, any, any, infer M> ? (unknown extends M ? W2 : M) : never>[] : never; } & { [SingletonName in keyof Singletons]: SingletonName extends string ? FilteredEntity<ComponentTypes, Singletons[SingletonName] extends QueryDefinition<ComponentTypes, infer W> ? W : never, Singletons[SingletonName] extends QueryDefinition<ComponentTypes, any, infer WO> ? WO : never, Singletons[SingletonName] extends QueryDefinition<ComponentTypes, any, any, infer O> ? O : never, Singletons[SingletonName] extends QueryDefinition<ComponentTypes, infer W2, any, any, infer M> ? (unknown extends M ? W2 : M) : never> | undefined : never; }; /** * Context object passed to system process functions. * Pre-allocated per system and reused every frame (zero per-frame allocation). * When resources are declared via withResources(), the context includes a * `resources` field with the resolved values (cached once on first call). */ export type ProcessContext<Cfg extends WorldConfig, Queries extends Record<string, QueryDefinition<Cfg['components']>>, ResourceKeys extends keyof Cfg['resources'] = never, Singletons extends Record<string, QueryDefinition<Cfg['components']>> = {}> = { queries: QueryResults<Cfg['components'], Queries, Singletons>; dt: number; ecs: ECSpresso<Cfg>; } & ([ResourceKeys] extends [never] ? {} : { resources: { readonly [K in ResourceKeys]: Cfg['resources'][K]; }; }); /** * Function signature for system process methods. * Receives a single context object with queries, dt, ecs, and optionally resources. */ export type SystemProcessFn<Cfg extends WorldConfig, Queries extends Record<string, QueryDefinition<Cfg['components']>>, ResourceKeys extends keyof Cfg['resources'] = never, Singletons extends Record<string, QueryDefinition<Cfg['components']>> = {}> = (ctx: ProcessContext<Cfg, Queries, ResourceKeys, Singletons>) => void; /** * Type for system lifecycle functions * These can be asynchronous */ export type SystemLifecycleFn<Cfg extends WorldConfig> = (ecs: ECSpresso<Cfg>) => void | Promise<void>; export {};