ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
200 lines (199 loc) • 12.3 kB
TypeScript
import ECSpresso, { type InstallPluginParam } from "./ecspresso";
import type { ResourceFactoryWithDeps, ResourceDirectValue } from "./resource-manager";
import { type Plugin } from "./plugin";
import type { WorldConfig, EmptyConfig, MergeConfigs, TypesAreCompatible, WithComponents, WithEvents, WithResources } from "./type-utils";
import type { AssetConfiguratorFn, AssetsResource } from "./asset-types";
import type { ScreenDefinition, ScreenConfigurator, ScreenResource } from "./screen-types";
/**
* Helper type: finalize built-in resources ($assets, $screen) in the resource map.
* Auto-injects $assets/$screen when plugins contribute asset/screen types even without
* explicit withAssets()/withScreens(). Also narrows the AssetGroupNames on $assets.
*/
type FinalizeBuiltinResources<Cfg extends WorldConfig, AG extends string> = {
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Omit<Cfg['resources'], '$assets' | '$screen'> & ([keyof Cfg['assets']] extends [never] ? {} : {
$assets: AssetsResource<Cfg['assets'], AG>;
}) & ([keyof Cfg['screens']] extends [never] ? {} : {
$screen: ScreenResource<Cfg['screens']>;
});
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'];
};
/**
* Builder class for ECSpresso that provides fluent type-safe plugin installation.
* Handles type checking during build process to ensure type safety.
*/
export declare class ECSpressoBuilder<Cfg extends WorldConfig = EmptyConfig, Labels extends string = never, Groups extends string = never, AssetGroupNames extends string = never, ReactiveQueryNames extends string = never> {
/** Asset configurator for collecting asset definitions */
private assetConfigurator;
/** Screen configurator for collecting screen definitions */
private screenConfigurator;
/** Pending resources to add during build */
private pendingResources;
/** Pending dispose callbacks to register during build */
private pendingDisposeCallbacks;
/** Pending required component registrations to apply during build */
private pendingRequiredComponents;
/** Pending plugins to install during build */
private pendingPlugins;
/** Fixed timestep interval (null means use default 1/60) */
private _fixedDt;
/** True when `.disableChangeTracking()` was called; flips the runtime into "track nothing" at build. */
private _disableChangeTracking;
constructor();
/**
* Add the first plugin when starting with empty types.
* This overload allows any plugin to be added to an empty ECSpresso instance.
* Only merges the plugin's Provides (PCfg) into accumulated config, not its Requires (PReq).
*/
withPlugin<PCfg extends WorldConfig, PReq extends WorldConfig = EmptyConfig, BL extends string = never, BG extends string = never, BAG extends string = never, BRQ extends string = never>(this: ECSpressoBuilder<{
readonly components: {};
readonly events: {};
readonly resources: {};
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'];
}, Labels, Groups, AssetGroupNames, ReactiveQueryNames>, plugin: Plugin<PCfg, PReq, BL, BG, BAG, BRQ>): ECSpressoBuilder<MergeConfigs<Cfg, PCfg>, Labels | BL, Groups | BG, AssetGroupNames | BAG, ReactiveQueryNames | BRQ>;
/**
* Add a subsequent plugin with type checking.
* This overload enforces plugin type compatibility and requirement satisfaction.
* Only merges the plugin's Provides (PCfg) into accumulated config, not its Requires (PReq).
*/
withPlugin<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>): ECSpressoBuilder<MergeConfigs<Cfg, PCfg>, Labels | BL, Groups | BG, AssetGroupNames | BAG, ReactiveQueryNames | BRQ>;
/**
* Add application-specific component types to the builder chain.
* This is a pure type-level operation with no runtime cost.
* Conflicts with existing component types (same key, different type) produce a `never` return.
*/
withComponentTypes<T extends Record<string, any>>(): TypesAreCompatible<Cfg['components'], T> extends true ? ECSpressoBuilder<WithComponents<Cfg, T>, Labels, Groups, AssetGroupNames, ReactiveQueryNames> : never;
/**
* Add application-specific event types to the builder chain.
* This is a pure type-level operation with no runtime cost.
* Conflicts with existing event types (same key, different type) produce a `never` return.
*/
withEventTypes<T extends Record<string, any>>(): TypesAreCompatible<Cfg['events'], T> extends true ? ECSpressoBuilder<WithEvents<Cfg, T>, Labels, Groups, AssetGroupNames, ReactiveQueryNames> : never;
/**
* Add application-specific resource types to the builder chain.
* This is a pure type-level operation with no runtime cost.
* Conflicts with existing resource types (same key, different type) produce a `never` return.
*/
withResourceTypes<T extends Record<string, any>>(): TypesAreCompatible<Cfg['resources'], T> extends true ? ECSpressoBuilder<WithResources<Cfg, T>, Labels, Groups, AssetGroupNames, ReactiveQueryNames> : never;
/**
* Opt out of change tracking entirely. After this call every `markChanged`
* (and `commands.markChanged`) is a runtime no-op, and `changed:` filters
* yield nothing. Useful for worlds with no reactive consumers (e.g. the
* physics bench) to skip the per-mark sequence stamp and array allocation.
*
* By default, change tracking is auto-derived: any system that declares a
* `changed:` filter auto-subscribes those components, and all other marks
* become no-ops. If no system declares `changed:`, every mark is recorded
* (track-all default). Call this to force the no-op-everything mode.
*/
disableChangeTracking(): this;
/**
* Add a resource during ECSpresso construction.
*
* When the key matches a pre-declared resource type (via `withResourceTypes`, `create<C,E,R>()`,
* or plugin resources), the value is validated against that type.
* For new keys, the value type is inferred as before.
*
* @param key The resource key
* @param resource The resource value, factory function, or factory with dependencies/disposal
* @returns This builder with updated resource types
*/
withResource<K extends keyof Cfg['resources'] & string>(key: K, resource: Cfg['resources'][K] | ((context: ECSpresso<Cfg>) => Cfg['resources'][K] | Promise<Cfg['resources'][K]>) | ResourceFactoryWithDeps<Cfg['resources'][K], ECSpresso<Cfg>, keyof Cfg['resources'] & string> | ResourceDirectValue<Cfg['resources'][K]>): ECSpressoBuilder<Cfg, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
withResource<K extends string, V>(key: K & ([K] extends [keyof Cfg['resources']] ? [V] extends [Cfg['resources'][K & keyof Cfg['resources']]] ? string : never : string), resource: V | ((context: ECSpresso<WithResources<Cfg, Record<K, V>>>) => V | Promise<V>) | ResourceFactoryWithDeps<V, ECSpresso<WithResources<Cfg, Record<K, V>>>, keyof (Cfg['resources'] & Record<K, V>) & string> | ResourceDirectValue<V>): ECSpressoBuilder<WithResources<Cfg, Record<K, V>>, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
/**
* Register a dispose callback for a component type during build.
* Called when a component is removed (explicit removal, entity destruction, or replacement).
* @param componentName The component type to register disposal for
* @param callback Function receiving the component value being disposed
* @returns This builder for method chaining
*/
withDispose<K extends keyof Cfg['components'] & string>(componentName: K, callback: (ctx: {
value: Cfg['components'][K];
entityId: number;
}) => void): this;
/**
* Register a required component relationship during build.
* When an entity gains `trigger`, the `required` component is auto-added
* (using `factory` for the default value) if not already present.
* @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
* @returns This builder for method chaining
*/
withRequired<Trigger extends keyof Cfg['components'] & string, Required extends keyof Cfg['components'] & string>(trigger: Trigger, required: Required, factory: (triggerValue: Cfg['components'][Trigger]) => Cfg['components'][Required]): this;
/**
* Configure assets for this ECSpresso instance
* @param configurator Function that receives an AssetConfigurator and returns it after adding assets
* @returns This builder with updated asset types
*/
withAssets<NewAssets extends Record<string, unknown>, NewAssetGroups extends string = never>(configurator: AssetConfiguratorFn<NewAssets, NewAssetGroups>): ECSpressoBuilder<{
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'] & {
$assets: AssetsResource<Cfg['assets'] & NewAssets, string>;
};
readonly assets: Cfg['assets'] & NewAssets;
readonly screens: Cfg['screens'];
}, Labels, Groups, AssetGroupNames | NewAssetGroups, ReactiveQueryNames>;
/**
* Configure screens for this ECSpresso instance
* @param configurator Function that receives a ScreenConfigurator and returns it after adding screens
* @returns This builder with updated screen types
*/
withScreens<NewS extends Record<string, ScreenDefinition<any, any>>>(configurator: (screens: ScreenConfigurator<{}, ECSpresso<{
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'];
readonly assets: Cfg['assets'];
readonly screens: Record<string, ScreenDefinition>;
}>>) => ScreenConfigurator<NewS, ECSpresso<{
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'];
readonly assets: Cfg['assets'];
readonly screens: Record<string, ScreenDefinition>;
}>>): ECSpressoBuilder<{
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'] & {
$screen: ScreenResource<Cfg['screens'] & NewS>;
};
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'] & NewS;
}, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
/**
* Configure the fixed timestep interval for the fixedUpdate phase.
* @param dt The fixed timestep in seconds (e.g., 1/60 for 60Hz physics)
* @returns This builder for method chaining
*/
withFixedTimestep(dt: number): this;
/**
* Declare reactive query names that will be registered at runtime.
* This is a pure type-level operation with no runtime cost.
*/
withReactiveQueryNames<N extends string>(): ECSpressoBuilder<Cfg, Labels, Groups, AssetGroupNames, ReactiveQueryNames | N>;
/**
* Create a plugin factory from the builder's accumulated 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>;
/**
* Complete the build process and return the built ECSpresso instance
*/
build(): ECSpresso<FinalizeBuiltinResources<Cfg, [AssetGroupNames] extends [never] ? string : AssetGroupNames>, [
Labels
] extends [never] ? string : Labels, [
Groups
] extends [never] ? string : Groups, [
AssetGroupNames
] extends [never] ? string : AssetGroupNames, [
ReactiveQueryNames
] extends [never] ? string : ReactiveQueryNames>;
}
export {};