ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
254 lines (253 loc) • 15.3 kB
TypeScript
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 {};