UNPKG

ecspresso

Version:

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

950 lines (949 loc) 45.9 kB
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 {};