ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
950 lines (949 loc) • 45.9 kB
TypeScript
import EntityManager from "./entity-manager";
import EventBus from "./event-bus";
import { type ResourceFactoryWithDeps, type ResourceDirectValue } from "./resource-manager";
import AssetManager from "./asset-manager";
import ScreenManager from "./screen-manager";
import { type ReactiveQueryDefinition } from "./reactive-query-manager";
import CommandBuffer from "./command-buffer";
import type { System, SystemPhase, FilteredEntity, Entity, RemoveEntityOptions, HierarchyEntry, HierarchyIteratorOptions } from "./types";
import { type Plugin } from "./plugin";
import { SystemBuilder } from "./system-builder";
import type { AssetDefinition, AssetHandle } from "./asset-types";
import type { ScreenDefinition } from "./screen-types";
import { ECSpressoBuilder } from "./ecspresso-builder";
import type { WorldConfig, EmptyConfig, ConflictingSlot, MissingRequirementSlot } from "./type-utils";
/**
* Interface declaration for ECSpresso constructor to ensure type augmentation works properly.
* This merges with the class declaration below.
*/
export default interface ECSpresso<Cfg extends WorldConfig = EmptyConfig, Labels extends string = string, Groups extends string = string, AssetGroupNames extends string = string, ReactiveQueryNames extends string = string> {
/**
* Default constructor
*/
new (): ECSpresso<Cfg, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
}
/**
* Branded sentinel used as the expected-parameter type when `installPlugin`
* detects an incompatible or unsatisfied plugin. The template-literal message
* surfaces in the TypeScript error, pointing at the failing WorldConfig slot.
* Uninhabitable at the value level (the symbol key is unreachable), so the
* argument is still rejected.
*/
declare const __pluginError: unique symbol;
export type PluginError<M extends string> = {
readonly [__pluginError]: M;
};
/**
* Resolves to the expected parameter type for `installPlugin` given a world
* config and a plugin's generic parameters:
* - `Plugin<PCfg, PReq, ...>` when compatible and all requirements are met.
* - `PluginError<"Plugin's X conflict with this world...">` when any slot conflicts.
* - `PluginError<"Plugin requires X not provided by this world">` when a requirement is missing.
*
* Exported so tests can assert the exact parameter type `installPlugin`
* produces without duplicating the conditional logic.
*/
export type InstallPluginParam<Cfg extends WorldConfig, PCfg extends WorldConfig, PReq extends WorldConfig, BL extends string, BG extends string, BAG extends string, BRQ extends string> = [
ConflictingSlot<Cfg, PCfg>
] extends [never] ? [MissingRequirementSlot<Cfg, PReq>] extends [never] ? Plugin<PCfg, PReq, BL, BG, BAG, BRQ> : PluginError<`Plugin requires ${MissingRequirementSlot<Cfg, PReq>} not provided by this world`> : PluginError<`Plugin's ${ConflictingSlot<Cfg, PCfg>} conflict with this world (same key, different type)`>;
/**
* ECSpresso is the central ECS framework class that connects all features.
* It handles creation and management of entities, components, and systems, and provides lifecycle hooks.
*/
export default class ECSpresso<Cfg extends WorldConfig = EmptyConfig, Labels extends string = string, Groups extends string = string, AssetGroupNames extends string = string, ReactiveQueryNames extends string = string> {
readonly _cfg: Cfg;
/** Library version*/
static readonly VERSION: string;
/** Access/modify stored components and entities*/
private _entityManager;
/** Publish/subscribe to events*/
private _eventBus;
/** Access/modify registered resources*/
private _resourceManager;
/** Command buffer for deferred structural changes */
private _commandBuffer;
/** Registered systems that will be updated in order*/
private _systems;
/** Systems grouped by execution phase, each sorted by priority */
private _phaseSystems;
/** Track installed plugins to prevent duplicates*/
private _installedPlugins;
/** Per-plugin disposers registered via the install function's second arg */
private _pluginCleanups;
/** Defaults applied to systems created via addSystem during a plugin's install */
private _currentSystemDefaults;
/** Entity IDs scoped to a specific screen — removed on that screen's exit */
private _screenScopedEntities;
/** Reverse index: entity ID -> scope screen name, for O(1) cleanup on remove */
private _entityScreenScope;
/**
* Active screen scope hint set during a screen-gated system's process tick.
* spawn / spawnChild / commands.spawn fall back to this when the caller did
* not pass an explicit `scope`. `null` means no hint (not in a gated tick,
* or system has no `inScreens` matching the current screen).
*/
private _activeScopeHint;
/** Disabled system groups */
private _disabledGroups;
/** Asset manager for loading and accessing assets */
private _assetManager;
/** Screen manager for state/screen transitions */
private _screenManager;
/** Reactive query manager for enter/exit callbacks */
private _reactiveQueryManager;
/** Post-update hooks to be called after all systems in update() */
private _postUpdateHooks;
/** Global tick counter, incremented at the end of each update() */
private _currentTick;
/** Per-system last-seen change sequence for change detection */
private _systemLastSeqs;
/** Change threshold used for public getEntitiesWithQuery and between-system resolution */
private _changeThreshold;
/** Fixed timestep interval in seconds (default: 1/60) */
private _fixedDt;
/** Accumulated time for fixed update steps */
private _fixedAccumulator;
/** Interpolation alpha between fixed steps (accumulator / fixedDt) */
private _interpolationAlpha;
/** Maximum fixed update steps per frame (spiral-of-death protection) */
private _maxFixedSteps;
/** Registry of required component relationships: trigger -> [{component, factory}] */
private _requiredComponents;
/** Pending plugin assets awaiting manager creation at build time */
private _pendingPluginAssets;
/** Pending plugin screens awaiting manager creation at build time */
private _pendingPluginScreens;
/** Whether diagnostics timing collection is enabled */
private _diagnosticsEnabled;
/** Per-system timing in ms, populated when diagnostics enabled */
private _systemTimings;
/** Per-phase timing in ms, populated when diagnostics enabled */
private _phaseTimings;
/** Per-system per-query seen entity IDs for onEntityEnter tracking */
private _entityEnterTracking;
/** Shared reusable set for per-tick entity enter comparison (avoids allocation) */
private _entityEnterFrameSet;
/** Pre-allocated process context per system (avoids per-frame allocation) */
private _systemContexts;
/** Shared scratch array for singleton query resolution (cleared and reused each resolution) */
private _singletonScratch;
/** Pending system builder finalizers to run before next update/initialize */
private _pendingFinalizers;
private _batchingRegistrations;
/** Whether `initialize()` has completed — flips the onInitialize firing path for late-added systems */
private _initializeFired;
private _systemsInitialized;
/**
* Creates a new ECSpresso instance.
*/
constructor();
/**
* Subscribes to EntityManager lifecycle hooks for change detection,
* required component auto-addition, and reactive query tracking.
* @private
*/
private _subscribeLifecycleHooks;
/**
* Creates a new ECSpresso builder for type-safe plugin installation.
* Types are inferred from the builder chain — use `.withPlugin()`,
* `.withComponentTypes<T>()`, `.withEventTypes<T>()`, and `.withResource()`
* to accumulate types without manual aggregate interfaces.
*
* @returns A builder instance for fluent method chaining
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createRenderer2DPlugin({ ... }))
* .withPlugin(createPhysics2DPlugin())
* .withComponentTypes<{ player: true; enemy: { type: string } }>()
* .withEventTypes<{ gameStart: true }>()
* .withResource('score', { value: 0 })
* .build();
*
* type ECS = typeof ecs;
* ```
*/
static create<Cfg2 extends WorldConfig = EmptyConfig>(): ECSpressoBuilder<Cfg2, never, never, never, never>;
/**
* Adds a system directly to this ECSpresso instance.
* The system is registered when initialize() or update() is next called.
* @param label Unique name to identify the system
* @returns A SystemBuilder instance for method chaining
*/
addSystem(label: string): SystemBuilder<Cfg>;
/**
* Finalize and register all pending system builders.
* @private
*/
private _finalizePendingBuilders;
/**
* Update all systems across execution phases.
* Phases run in order: preUpdate -> fixedUpdate -> update -> postUpdate -> render.
* The fixedUpdate phase uses a time accumulator for deterministic fixed-timestep simulation.
* @param deltaTime Time elapsed since the last update (in seconds)
*/
update(deltaTime: number): void;
/**
* Execute all systems in a single phase.
* @private
*/
private _executePhase;
/**
* Execute a non-fixed phase with optional timing, then play back the command buffer.
* @private
*/
private _runPhase;
/**
* Initialize all resources and systems
* This method:
* 1. Initializes all resources that were added as factory functions
* 2. Sets up asset manager and loads eager assets
* 3. Sets up screen manager
* 4. Calls the onInitialize lifecycle hook on all systems
*
* This is useful for game startup to ensure all resources are ready
* and systems are properly initialized before the game loop begins.
*
* @returns Promise that resolves when everything is initialized
*/
initialize(): Promise<void>;
/**
* Initialize specific resources or all resources that were added as factory functions but haven't been initialized yet.
* This is useful when you need to ensure resources are ready before proceeding.
* @param keys Optional array of resource keys to initialize. If not provided, all pending resources will be initialized.
* @returns Promise that resolves when the specified resources are initialized
*/
initializeResources<K extends keyof Cfg['resources']>(...keys: K[]): Promise<void>;
/**
* Rebuild per-phase system arrays from the flat _systems list.
* Each phase array is sorted by priority (higher first), with
* registration order as tiebreaker.
* @private
*/
private _rebuildPhaseSystems;
/**
* Update the priority of a system
* @param label The unique label of the system to update
* @param priority The new priority value (higher values execute first)
* @returns true if the system was found and updated, false otherwise
*/
updateSystemPriority(label: Labels, priority: number): boolean;
/**
* Move a system to a different execution phase at runtime.
* @param label The unique label of the system to move
* @param phase The target phase
* @returns true if the system was found and updated, false otherwise
*/
updateSystemPhase(label: Labels, phase: SystemPhase): boolean;
/**
* The interpolation alpha between fixed update steps.
* Ranges from 0 to <1, representing how far into the next
* fixed step the current frame is. Use in the render phase
* for smooth visual interpolation.
*/
get interpolationAlpha(): number;
/**
* The configured fixed timestep interval in seconds.
*/
get fixedDt(): number;
/**
* Disable a system group. Systems in this group will be skipped during update().
* @param groupName The name of the group to disable
*/
disableSystemGroup(groupName: Groups): void;
/**
* Enable a system group. Systems in this group will run during update().
* @param groupName The name of the group to enable
*/
enableSystemGroup(groupName: Groups): void;
/**
* Check if a system group is enabled.
* @param groupName The name of the group to check
* @returns true if the group is enabled (or doesn't exist), false if disabled
*/
isSystemGroupEnabled(groupName: Groups): boolean;
/**
* Get all system labels that belong to a specific group.
* @param groupName The name of the group
* @returns Array of system labels in the group
*/
getSystemsInGroup(groupName: Groups): string[];
/**
* Remove a system by its label
* Calls the system's onDetach method with this ECSpresso instance if defined
* @param label The unique label of the system to remove
* @returns true if the system was found and removed, false otherwise
*/
removeSystem(label: Labels): boolean;
/**
* Internal method to register a system with this ECSpresso instance
* @internal Used by SystemBuilder - replaces direct private property access
*/
_registerSystem(system: System<Cfg, any, any>): void;
/**
* Check if a resource exists
*/
hasResource<K extends keyof Cfg['resources']>(key: K): boolean;
/**
* Get a resource by key. Throws if the resource is not found.
* @param key The resource key
* @returns The resource value
* @throws Error if resource not found
* @see tryGetResource — the non-throwing alternative that returns undefined
*/
getResource<K extends keyof Cfg['resources']>(key: K): Cfg['resources'][K];
/**
* Try to get a resource by key. Returns undefined if the resource is not found.
* Inspired by Bevy's `World::get_resource::<T>()` which returns `Option<&T>`.
*
* Two overloads:
* 1. Known key — full type safety from `ResourceTypes`
* 2. String key with explicit type param — for cross-plugin optional dependencies
*
* @example
* ```typescript
* // Known key (type inferred from ResourceTypes)
* const score = ecs.tryGetResource('score'); // ScoreResource | undefined
*
* // Cross-plugin optional dependency (caller specifies expected type)
* const si = ecs.tryGetResource<SpatialIndex>('spatialIndex') ?? null;
* ```
*/
tryGetResource<K extends keyof Cfg['resources']>(key: K): Cfg['resources'][K] | undefined;
tryGetResource<T>(key: unknown extends T ? never : string): T | undefined;
/**
* Add a resource to the ECS instance.
*
* - Plain value → stored directly
* - Function → treated as a factory, called with this ECSpresso instance on first access
* - `{ factory, dependsOn?, onDispose? }` → factory with dependencies/disposal
* - `directValue(val)` → stores the value as-is (use to store functions/classes without invoking them)
*/
addResource<K extends keyof Cfg['resources']>(key: K, resource: Cfg['resources'][K] | ((ecs: ECSpresso<Cfg>) => Cfg['resources'][K] | Promise<Cfg['resources'][K]>) | ResourceFactoryWithDeps<Cfg['resources'][K], ECSpresso<Cfg>, keyof Cfg['resources'] & string> | ResourceDirectValue<Cfg['resources'][K]>): this;
/**
* Remove a resource from the ECS instance (without calling onDispose)
* @param key The resource key to remove
* @returns True if the resource was removed, false if it didn't exist
*/
removeResource<K extends keyof Cfg['resources']>(key: K): boolean;
/**
* Dispose a single resource, calling its onDispose callback if defined
* @param key The resource key to dispose
* @returns True if the resource existed and was disposed, false if it didn't exist
*/
disposeResource<K extends keyof Cfg['resources']>(key: K): Promise<boolean>;
/**
* Dispose all initialized resources in reverse dependency order.
* Resources that depend on others are disposed first.
* Calls each resource's onDispose callback if defined.
*/
disposeResources(): Promise<void>;
/**
* Update an existing resource using an updater function
* @param key The resource key to update
* @param updater Function that receives the current resource value and returns the new value
* @returns This ECSpresso instance for chaining
* @throws Error if the resource doesn't exist
*/
updateResource<K extends keyof Cfg['resources']>(key: K, updater: (current: Cfg['resources'][K]) => Cfg['resources'][K]): this;
/**
* Set a resource to a plain value.
* Unlike updateResource, does not require an updater function.
* @param key The resource key to set
* @param value The new resource value
* @returns This ECSpresso instance for chaining
*/
setResource<K extends keyof Cfg['resources']>(key: K, value: Cfg['resources'][K]): this;
/**
* Subscribe to changes for a specific resource key.
* @param key The resource key to watch
* @param callback Function called with (newValue, oldValue) when the resource changes
* @returns Unsubscribe function
*/
onResourceChange<K extends keyof Cfg['resources']>(key: K, callback: (newValue: Cfg['resources'][K], oldValue: Cfg['resources'][K]) => void): () => void;
/**
* Whether a resource has active change subscribers.
* Used by the system builder to skip caching for observed resources.
*/
isResourceObserved<K extends keyof Cfg['resources']>(key: K): boolean;
/**
* Get all resource keys that are currently registered
* @returns Array of resource keys
*/
getResourceKeys(): Array<keyof Cfg['resources']>;
/**
* Check if a resource needs initialization (was added as a factory function)
* @param key The resource key to check
* @returns True if the resource needs initialization
*/
resourceNeedsInitialization<K extends keyof Cfg['resources']>(key: K): boolean;
/**
* Get an entity by ID.
* @param entityId The entity ID
* @returns The entity, or undefined if it doesn't exist
*/
getEntity(entityId: number): Entity<Cfg['components']> | undefined;
/**
* Get a component value from an entity.
* @param entityId The entity ID
* @param componentName The component to retrieve
* @returns The component value, or undefined if the entity doesn't have it
*/
getComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K): Cfg['components'][K] | undefined;
/**
* Add or replace a component on an entity.
* Triggers component-added callbacks and marks the component as changed.
* @param entityId The entity ID
* @param componentName The component to add
* @param value The component value
*/
addComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K, value: Cfg['components'][K]): void;
/**
* Add multiple components to an entity at once.
* @param entityId The entity ID
* @param components Object with component names as keys and component data as values
*/
addComponents<T extends {
[K in keyof Cfg['components']]?: Cfg['components'][K];
}>(entityId: number, components: T & Record<Exclude<keyof T, keyof Cfg['components']>, never>): void;
/**
* Remove a component from an entity.
* Triggers component-removed and dispose callbacks.
* @param entityId The entity ID
* @param componentName The component to remove
*/
removeComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K): void;
/**
* Check if an entity has a component
*/
hasComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K): boolean;
/**
* Create an entity and add components to it in one call
* @param components Object with component names as keys and component data as values
* @returns The created entity with all components added
*/
spawn<T extends {
[K in keyof Cfg['components']]?: Cfg['components'][K];
}>(components: T & Record<Exclude<keyof T, keyof Cfg['components']>, never>, options?: {
scope?: (keyof Cfg['screens'] & string) | null;
}): FilteredEntity<Cfg['components'], keyof T & keyof Cfg['components']>;
/**
* Read the active scope hint set by the current screen-gated system tick.
* Returns `null` outside any system's process call, or when the active
* system is not gated to the current screen.
* @internal Used by CommandBuffer to capture intent at queue time.
*/
_getActiveScopeHint(): (keyof Cfg['screens'] & string) | null;
/**
* Tag an entity as scoped to a screen.
*
* Scope resolution order:
* 1. Explicit `options.scope === null` → unscoped (opt-out from hint).
* 2. Explicit `options.scope: string` → scope to that screen.
* 3. No `scope` provided (or `undefined`) → fall back to active hint set
* by the current screen-gated system tick. Otherwise unscoped.
*
* The entity is removed when its scoped screen exits.
*/
private _applyScreenScope;
/**
* Get all entities with specific components
*/
getEntitiesWithQuery<WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>, changedComponents?: ReadonlyArray<keyof Cfg['components']>, parentHas?: ReadonlyArray<keyof Cfg['components']>): Array<FilteredEntity<Cfg['components'], WithComponents, WithoutComponents>>;
/**
* Get the single entity matching a query. Throws if zero or more than one match.
* @param withComponents Components the entity must have
* @param withoutComponents Components the entity must not have
* @returns The single matching entity
* @throws If zero or more than one entity matches
*/
getSingleton<WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<Cfg['components'], WithComponents, WithoutComponents>;
/**
* Get the single entity matching a query, or undefined if none match.
* Throws if more than one entity matches.
* @param withComponents Components the entity must have
* @param withoutComponents Components the entity must not have
* @returns The single matching entity, or undefined if none match
* @throws If more than one entity matches
*/
tryGetSingleton<WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<Cfg['components'], WithComponents, WithoutComponents> | undefined;
/**
* Remove an entity (and optionally its descendants)
* @param entityId Entity ID to remove
* @param options Options for removal (cascade: true by default)
* @returns true if entity was removed
*/
removeEntity(entityId: number, options?: RemoveEntityOptions): boolean;
/**
* Create an entity as a child of another entity with initial components
* @param parentId The parent entity ID
* @param components Initial components to add
* @returns The created child entity
*/
spawnChild<T extends {
[K in keyof Cfg['components']]?: Cfg['components'][K];
}>(parentId: number, components: T & Record<Exclude<keyof T, keyof Cfg['components']>, never>, options?: {
scope?: (keyof Cfg['screens'] & string) | null;
}): FilteredEntity<Cfg['components'], keyof T & keyof Cfg['components']>;
/**
* Set the parent of an entity
* @param childId The entity ID to set as a child
* @param parentId The entity ID to set as the parent
*/
setParent(childId: number, parentId: number): this;
/**
* Remove the parent relationship for an entity (orphan it)
* @param childId The entity ID to orphan
* @returns true if a parent was removed, false if entity had no parent
*/
removeParent(childId: number): boolean;
/**
* Get the parent of an entity
* @param entityId The entity ID to get the parent of
* @returns The parent entity ID, or null if no parent
*/
getParent(entityId: number): number | null;
/**
* Get all children of an entity in insertion order
* @param parentId The parent entity ID
* @returns Readonly array of child entity IDs
*/
getChildren(parentId: number): readonly number[];
/**
* Get a child at a specific index
* @param parentId The parent entity ID
* @param index The index of the child
* @returns The child entity ID, or null if index is out of bounds
*/
getChildAt(parentId: number, index: number): number | null;
/**
* Get the index of a child within its parent's children list
* @param parentId The parent entity ID
* @param childId The child entity ID to find
* @returns The index of the child, or -1 if not found
*/
getChildIndex(parentId: number, childId: number): number;
/**
* Get all ancestors of an entity in order [parent, grandparent, ...]
* @param entityId The entity ID to get ancestors of
* @returns Readonly array of ancestor entity IDs
*/
getAncestors(entityId: number): readonly number[];
/**
* Get all descendants of an entity in depth-first order
* @param entityId The entity ID to get descendants of
* @returns Readonly array of descendant entity IDs
*/
getDescendants(entityId: number): readonly number[];
/**
* Get the root ancestor of an entity (topmost parent), or self if no parent
* @param entityId The entity ID to get the root of
* @returns The root entity ID
*/
getRoot(entityId: number): number;
/**
* Get siblings of an entity (other children of the same parent)
* @param entityId The entity ID to get siblings of
* @returns Readonly array of sibling entity IDs
*/
getSiblings(entityId: number): readonly number[];
/**
* Check if an entity is a descendant of another entity
* @param entityId The potential descendant ID
* @param ancestorId The potential ancestor ID
* @returns true if entityId is a descendant of ancestorId
*/
isDescendantOf(entityId: number, ancestorId: number): boolean;
/**
* Check if an entity is an ancestor of another entity
* @param entityId The potential ancestor ID
* @param descendantId The potential descendant ID
* @returns true if entityId is an ancestor of descendantId
*/
isAncestorOf(entityId: number, descendantId: number): boolean;
/**
* Get all root entities (entities that have children but no parent)
* @returns Readonly array of root entity IDs
*/
getRootEntities(): readonly number[];
/**
* Traverse the hierarchy in parent-first (breadth-first) order.
* Parents are guaranteed to be visited before their children.
* @param callback Function called for each entity with (entityId, parentId, depth)
* @param options Optional traversal options (roots to filter to specific subtrees)
*/
forEachInHierarchy(callback: (entityId: number, parentId: number | null, depth: number) => void, options?: HierarchyIteratorOptions): void;
/**
* Generator-based hierarchy traversal in parent-first (breadth-first) order.
* Supports early termination via break.
* @param options Optional traversal options (roots to filter to specific subtrees)
* @yields HierarchyEntry for each entity in parent-first order
*/
hierarchyIterator(options?: HierarchyIteratorOptions): Generator<HierarchyEntry, void, unknown>;
/**
* Emit a hierarchy changed event
* @internal
*/
private _emitHierarchyChanged;
/**
* Get all installed plugin IDs
*/
get installedPlugins(): string[];
get entityManager(): EntityManager<Cfg["components"]>;
get eventBus(): EventBus<Cfg["events"]>;
/**
* Command buffer for queuing deferred structural changes.
* Commands are executed automatically at the end of each update() cycle.
*
* @example
* ```typescript
* // In a system or event handler
* ecs.commands.removeEntity(entityId);
* ecs.commands.spawn({ position: { x: 0, y: 0 } });
* ```
*/
get commands(): CommandBuffer<Cfg>;
/**
* The current tick number, incremented at the end of each update()
*/
get currentTick(): number;
/**
* The current change detection threshold.
* During system execution, this is the system's last-seen sequence.
* Between updates, this is the global sequence after command buffer playback.
* Manual change detection should compare: getChangeSeq(...) > changeThreshold
*/
get changeThreshold(): number;
/**
* Toggle diagnostics timing collection. When enabled, system and phase
* timings are recorded each frame. When disabled, timing maps are cleared
* and no overhead is incurred.
*/
enableDiagnostics(enabled: boolean): void;
get diagnosticsEnabled(): boolean;
get systemTimings(): ReadonlyMap<string, number>;
get phaseTimings(): Readonly<Record<SystemPhase, number>>;
get entityCount(): number;
/**
* Mutate a component in place and automatically mark it as changed.
* Throws if the entity does not exist or does not have the component.
* @param entityId The entity ID
* @param componentName The component to mutate
* @param mutator A function that receives the component value for in-place mutation
* @returns The mutated component value
*/
mutateComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K, mutator: (value: Cfg['components'][K]) => void): Cfg['components'][K];
/**
* Mark a component as changed on an entity. Recorded iff the component is
* subscribed (auto-subscribed by any system's `changed:` filter, or implicit
* track-all when no subscription exists). Each recorded call increments a
* monotonic sequence; `changed:` queries see the mark once on their next run.
*/
markChanged<K extends keyof Cfg['components']>(entityId: number, componentName: K): void;
/**
* Register a dispose callback for a component type.
* Called when a component is removed (explicit removal, entity destruction, or replacement).
* Later registrations replace earlier ones for the same component type.
* @param componentName The component type to register disposal for
* @param callback Function receiving the component value being disposed and the entity ID
*/
registerDispose<K extends keyof Cfg['components']>(componentName: K, callback: (ctx: {
value: Cfg['components'][K];
entityId: number;
}) => void): void;
/**
* Register a required component relationship.
* When an entity gains `trigger`, the `required` component is auto-added
* (using `factory` for the default value) if not already present.
* Enforced at insertion time (spawn/addComponent) only — removal is unrestricted.
* @param trigger The component whose presence triggers auto-addition
* @param required The component to auto-add
* @param factory Function that creates the default value for the required component
*/
registerRequired<Trigger extends keyof Cfg['components'], Required extends keyof Cfg['components']>(trigger: Trigger, required: Required, factory: (triggerValue: Cfg['components'][Trigger]) => Cfg['components'][Required]): void;
/**
* Check for circular dependencies in the required components graph.
* @throws Error if adding trigger→newRequired would create a cycle
*/
private _checkRequiredCycle;
/**
* Register a callback when a specific component is added to any entity
* @param componentName The component key
* @param handler Function receiving the new component value and the entity
* @returns Unsubscribe function to remove the callback
*/
onComponentAdded<K extends keyof Cfg['components']>(componentName: K, handler: (ctx: {
value: Cfg['components'][K];
entity: Entity<Cfg['components']>;
}) => void): () => void;
/**
* Register a callback when a specific component is removed from any entity
* @param componentName The component key
* @param handler Function receiving the old component value and the entity
* @returns Unsubscribe function to remove the callback
*/
onComponentRemoved<K extends keyof Cfg['components']>(componentName: K, handler: (ctx: {
value: Cfg['components'][K];
entity: Entity<Cfg['components']>;
}) => void): () => void;
/**
* Add a reactive query that triggers callbacks when entities enter/exit the query match.
* @param name Unique name for the query
* @param definition Query definition with with/without arrays and onEnter/onExit callbacks
*/
addReactiveQuery<WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never, OptionalComponents extends keyof Cfg['components'] = never>(name: ReactiveQueryNames, definition: ReactiveQueryDefinition<Cfg['components'], WithComponents, WithoutComponents, OptionalComponents>): void;
/**
* Remove a reactive query by name.
* @param name Name of the query to remove
* @returns true if the query existed and was removed, false otherwise
*/
removeReactiveQuery(name: ReactiveQueryNames): boolean;
/**
* Subscribe to an event (convenience wrapper for eventBus.subscribe)
* @param eventType The event type to subscribe to
* @param callback The callback to invoke when the event is published
* @returns An unsubscribe function
*/
on<E extends keyof Cfg['events']>(eventType: E, callback: (data: Cfg['events'][E]) => void): () => void;
/**
* Unsubscribe from an event by callback reference (convenience wrapper for eventBus.unsubscribe)
* @param eventType The event type to unsubscribe from
* @param callback The callback to remove
* @returns true if the callback was found and removed, false otherwise
*/
off<E extends keyof Cfg['events']>(eventType: E, callback: (data: Cfg['events'][E]) => void): boolean;
/**
* Register a hook that runs after all systems in update()
* @param callback The hook to call after all systems have processed
* @returns An unsubscribe function to remove the hook
*/
onPostUpdate(callback: (ctx: {
ecs: ECSpresso<Cfg>;
dt: number;
}) => void): () => void;
private requireAssetManager;
/**
* Get a loaded asset by key. Throws if not loaded.
*/
getAsset<K extends keyof Cfg['assets']>(key: K): Cfg['assets'][K];
/**
* Get a loaded asset or undefined if not loaded
*/
tryGetAsset<K extends keyof Cfg['assets']>(key: K): Cfg['assets'][K] | undefined;
/**
* Get a handle to an asset with status information
*/
getAssetHandle<K extends keyof Cfg['assets']>(key: K): AssetHandle<Cfg['assets'][K]>;
/**
* Check if an asset is loaded
*/
isAssetLoaded<K extends keyof Cfg['assets']>(key: K): boolean;
/**
* Load a single asset
*/
loadAsset<K extends keyof Cfg['assets']>(key: K): Promise<Cfg['assets'][K]>;
/**
* Load all assets in a group
*/
loadAssetGroup(groupName: AssetGroupNames): Promise<void>;
/**
* Check if all assets in a group are loaded
*/
isAssetGroupLoaded(groupName: AssetGroupNames): boolean;
/**
* Get the loading progress of a group (0-1)
*/
getAssetGroupProgress(groupName: AssetGroupNames): number;
private requireScreenManager;
/**
* Transition to a new screen, clearing the stack
*/
setScreen<K extends keyof Cfg['screens']>(name: K, config: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? C : never): Promise<void>;
/**
* Push a screen onto the stack (overlay)
*/
pushScreen<K extends keyof Cfg['screens']>(name: K, config: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? C : never): Promise<void>;
/**
* Pop the current screen and return to the previous one
*/
popScreen(): Promise<void>;
/**
* Get the current screen name
*/
getCurrentScreen(): keyof Cfg['screens'] | null;
/**
* Get the current screen config (immutable), narrowed to a specific screen.
* Throws if the current screen doesn't match.
*/
getScreenConfig<K extends keyof Cfg['screens'] & string>(screen: K): Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
/**
* Get the current screen config (immutable).
* Returns a union of all possible config types.
*/
getScreenConfig(): {
[K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
}[keyof Cfg['screens']];
/**
* Get the current screen config narrowed to a specific screen, or undefined if not on that screen.
*/
tryGetScreenConfig<K extends keyof Cfg['screens'] & string>(screen: K): (Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never) | undefined;
/**
* Get the current screen config or undefined.
* Returns a union of all possible config types, or undefined.
*/
tryGetScreenConfig(): {
[K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
}[keyof Cfg['screens']] | undefined;
/**
* Get the current screen state (mutable), narrowed to a specific screen.
* Throws if the current screen doesn't match.
*/
getScreenState<K extends keyof Cfg['screens'] & string>(screen: K): Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never;
/**
* Get the current screen state (mutable).
* Returns a union of all possible state types.
*/
getScreenState(): {
[K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never;
}[keyof Cfg['screens']];
/**
* Get the current screen state narrowed to a specific screen, or undefined if not on that screen.
*/
tryGetScreenState<K extends keyof Cfg['screens'] & string>(screen: K): (Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never) | undefined;
/**
* Get the current screen state or undefined.
* Returns a union of all possible state types, or undefined.
*/
tryGetScreenState(): {
[K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never;
}[keyof Cfg['screens']] | undefined;
/**
* Update the current screen state, narrowed to a specific screen.
* Throws if the current screen doesn't match.
*/
updateScreenState<K extends keyof Cfg['screens'] & string>(screen: K, update: Partial<Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never> | ((current: Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never) => Partial<Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never>)): void;
/**
* Update the current screen state.
*/
updateScreenState<K extends keyof Cfg['screens']>(update: Partial<Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never> | ((current: Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never) => Partial<Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never>)): void;
/**
* Check if a screen is the current screen
*/
isCurrentScreen(screenName: keyof Cfg['screens']): boolean;
/**
* Check if a screen is active (current or in stack)
*/
isScreenActive(screenName: keyof Cfg['screens']): boolean;
/**
* Get the screen stack depth
*/
getScreenStackDepth(): number;
/**
* Subscribe to the `screenEnter` event for a specific screen. The handler
* fires only when that named screen becomes active (via `setScreen` or
* `pushScreen`). Multiple handlers can be registered for the same screen
* and fire in registration order.
*
* @returns A disposer function that unregisters the handler.
*/
onScreenEnter<K extends keyof Cfg['screens'] & string>(name: K, handler: (ctx: {
config: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? C : never;
ecs: ECSpresso<Cfg>;
}) => void): () => void;
/**
* Subscribe to the `screenExit` event for a specific screen. The handler
* fires only when that named screen exits (via `setScreen` away, or
* `popScreen` if it was on the stack). Multiple handlers can be registered
* for the same screen and fire in registration order.
*
* @returns A disposer function that unregisters the handler.
*/
onScreenExit<K extends keyof Cfg['screens'] & string>(name: K, handler: (ctx: {
ecs: ECSpresso<Cfg>;
}) => void): () => void;
/**
* Internal method to set the asset manager and drain pending plugin assets
* @internal Used by ECSpressoBuilder
*/
_setAssetManager(manager: AssetManager<Cfg['assets']>): void;
/**
* Internal method to set the screen manager and drain pending plugin screens
* @internal Used by ECSpressoBuilder
*/
_setScreenManager(manager: ScreenManager<Cfg['screens']>): void;
/** @internal */
_hasPendingPluginAssets(): boolean;
/** @internal */
_hasPendingPluginScreens(): boolean;
/**
* Internal method to set the fixed timestep interval
* @internal Used by ECSpressoBuilder
*/
_setFixedDt(dt: number): void;
/**
* Register an asset definition for deferred registration.
* @internal Used by plugins that need to register assets
*/
_registerAsset(key: string, definition: AssetDefinition<unknown>): void;
/**
* Register a screen definition for deferred registration.
* @internal Used by plugins that need to register screens
*/
_registerScreen(name: string, definition: ScreenDefinition<any, any>): void;
/**
* Install a plugin into this ECSpresso instance.
* Deduplicates by plugin ID. Composite plugins call this in their install function.
*
* The overload enforces that the plugin's provided config is compatible with
* this world's accumulated config, and that its required config is satisfied.
* When a check fails the parameter resolves to a `PluginError<...>` sentinel
* whose template-literal message names the failing WorldConfig slot, making
* the resulting "not assignable" error self-describing.
*/
installPlugin<PCfg extends WorldConfig, PReq extends WorldConfig = EmptyConfig, BL extends string = never, BG extends string = never, BAG extends string = never, BRQ extends string = never>(plugin: InstallPluginParam<Cfg, PCfg, PReq, BL, BG, BAG, BRQ>): this;
/**
* Install a plugin without enforcing compatibility constraints.
* @internal Used by the builder, which has already validated compatibility
* at `withPlugin` time via the builder's own constrained overload.
*/
_installPluginUnchecked(plugin: Plugin<WorldConfig, WorldConfig, string, string, string, string>): this;
/**
* Uninstall a previously installed plugin by id. Runs registered cleanup
* disposers in reverse order and removes the plugin from `installedPlugins`.
* Returns `true` if the plugin was installed (and has now been removed),
* `false` if no plugin with that id was installed.
*
* Disposers that throw are caught and logged via `console.warn` so a single
* failing disposer does not prevent later ones from running.
*/
uninstallPlugin(id: string): boolean;
/**
* Uninstall every installed plugin, running their cleanup disposers.
* Plugins are uninstalled in reverse install order so a plugin that depends
* on another is torn down before its dependency.
*
* Does not touch resource disposal — callers that need async resource
* teardown should `await world.disposeResources()` separately.
*/
dispose(): void;
/**
* Create a plugin factory from the built world's types.
* Returns a definePlugin equivalent with no manual type parameters.
*/
pluginFactory(): <PL extends string = never, PG extends string = never, PAG extends string = never, PRQ extends string = never>(config: {
id: string;
install: (world: ECSpresso<Cfg>) => void;
}) => Plugin<Cfg, EmptyConfig, PL, PG, PAG, PRQ>;
/**
* Call a helper factory with this world instance, inferring the full world type.
* Eliminates the need for a separate `type ECS = typeof ecs` ceremony.
*
* @example
* ```typescript
* const helpers = ecs.getHelpers(createStateMachineHelpers);
* ```
*/
getHelpers<H>(factory: (world: this) => H): H;
}
export {};