UNPKG

ecspresso

Version:

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

200 lines (199 loc) 12.3 kB
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 {};