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