ecspresso 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +256 -148
  2. package/dist/asset-manager.d.ts +16 -16
  3. package/dist/asset-types.d.ts +18 -16
  4. package/dist/command-buffer.d.ts +30 -20
  5. package/dist/ecspresso-builder.d.ts +193 -0
  6. package/dist/ecspresso.d.ts +323 -209
  7. package/dist/entity-manager.d.ts +76 -30
  8. package/dist/event-bus.d.ts +6 -1
  9. package/dist/index.d.ts +6 -13
  10. package/dist/plugin.d.ts +61 -0
  11. package/dist/plugins/audio.d.ts +273 -0
  12. package/dist/{bundles/utils → plugins}/bounds.d.ts +20 -26
  13. package/dist/plugins/camera.d.ts +88 -0
  14. package/dist/plugins/collision.d.ts +285 -0
  15. package/dist/plugins/coroutine.d.ts +126 -0
  16. package/dist/plugins/diagnostics.d.ts +49 -0
  17. package/dist/{bundles/utils → plugins}/input.d.ts +22 -29
  18. package/dist/plugins/particles.d.ts +225 -0
  19. package/dist/plugins/physics2D.d.ts +163 -0
  20. package/dist/plugins/renderers/renderer2D.d.ts +262 -0
  21. package/dist/plugins/spatial-index.d.ts +58 -0
  22. package/dist/plugins/sprite-animation.d.ts +150 -0
  23. package/dist/plugins/state-machine.d.ts +244 -0
  24. package/dist/plugins/timers.d.ts +151 -0
  25. package/dist/{bundles/utils → plugins}/transform.d.ts +21 -22
  26. package/dist/plugins/tween.d.ts +162 -0
  27. package/dist/reactive-query-manager.d.ts +14 -3
  28. package/dist/resource-manager.d.ts +64 -23
  29. package/dist/screen-manager.d.ts +21 -15
  30. package/dist/screen-types.d.ts +15 -11
  31. package/dist/src/index.js +4 -0
  32. package/dist/src/index.js.map +25 -0
  33. package/dist/src/plugins/audio.js +4 -0
  34. package/dist/src/plugins/audio.js.map +10 -0
  35. package/dist/src/plugins/bounds.js +4 -0
  36. package/dist/src/plugins/bounds.js.map +10 -0
  37. package/dist/src/plugins/camera.js +4 -0
  38. package/dist/src/plugins/camera.js.map +10 -0
  39. package/dist/src/plugins/collision.js +4 -0
  40. package/dist/src/plugins/collision.js.map +11 -0
  41. package/dist/src/plugins/coroutine.js +4 -0
  42. package/dist/src/plugins/coroutine.js.map +10 -0
  43. package/dist/src/plugins/diagnostics.js +5 -0
  44. package/dist/src/plugins/diagnostics.js.map +10 -0
  45. package/dist/src/plugins/input.js +4 -0
  46. package/dist/src/plugins/input.js.map +10 -0
  47. package/dist/src/plugins/particles.js +4 -0
  48. package/dist/src/plugins/particles.js.map +10 -0
  49. package/dist/src/plugins/physics2D.js +4 -0
  50. package/dist/src/plugins/physics2D.js.map +11 -0
  51. package/dist/src/plugins/renderers/renderer2D.js +4 -0
  52. package/dist/src/plugins/renderers/renderer2D.js.map +10 -0
  53. package/dist/src/plugins/spatial-index.js +4 -0
  54. package/dist/src/plugins/spatial-index.js.map +11 -0
  55. package/dist/src/plugins/sprite-animation.js +4 -0
  56. package/dist/src/plugins/sprite-animation.js.map +10 -0
  57. package/dist/src/plugins/state-machine.js +4 -0
  58. package/dist/src/plugins/state-machine.js.map +10 -0
  59. package/dist/src/plugins/timers.js +4 -0
  60. package/dist/src/plugins/timers.js.map +10 -0
  61. package/dist/src/plugins/transform.js +4 -0
  62. package/dist/src/plugins/transform.js.map +10 -0
  63. package/dist/src/plugins/tween.js +4 -0
  64. package/dist/src/plugins/tween.js.map +11 -0
  65. package/dist/system-builder.d.ts +75 -112
  66. package/dist/type-utils.d.ts +247 -7
  67. package/dist/types.d.ts +58 -39
  68. package/dist/utils/check-required-cycle.d.ts +12 -0
  69. package/dist/utils/easing.d.ts +71 -0
  70. package/dist/utils/math.d.ts +67 -0
  71. package/dist/utils/narrowphase.d.ts +63 -0
  72. package/dist/utils/spatial-hash.d.ts +53 -0
  73. package/package.json +65 -27
  74. package/dist/bundle.d.ts +0 -123
  75. package/dist/bundles/renderers/renderer2D.d.ts +0 -220
  76. package/dist/bundles/renderers/renderer2D.js +0 -4
  77. package/dist/bundles/renderers/renderer2D.js.map +0 -10
  78. package/dist/bundles/utils/bounds.js +0 -4
  79. package/dist/bundles/utils/bounds.js.map +0 -10
  80. package/dist/bundles/utils/collision.d.ts +0 -204
  81. package/dist/bundles/utils/collision.js +0 -4
  82. package/dist/bundles/utils/collision.js.map +0 -10
  83. package/dist/bundles/utils/input.js +0 -4
  84. package/dist/bundles/utils/input.js.map +0 -10
  85. package/dist/bundles/utils/movement.d.ts +0 -86
  86. package/dist/bundles/utils/movement.js +0 -4
  87. package/dist/bundles/utils/movement.js.map +0 -10
  88. package/dist/bundles/utils/timers.d.ts +0 -172
  89. package/dist/bundles/utils/timers.js +0 -4
  90. package/dist/bundles/utils/timers.js.map +0 -10
  91. package/dist/bundles/utils/transform.js +0 -4
  92. package/dist/bundles/utils/transform.js.map +0 -10
  93. package/dist/index.js +0 -4
  94. package/dist/index.js.map +0 -22
@@ -1,29 +1,33 @@
1
1
  import EntityManager from "./entity-manager";
2
2
  import EventBus from "./event-bus";
3
+ import { type ResourceFactoryWithDeps, type ResourceDirectValue } from "./resource-manager";
3
4
  import AssetManager from "./asset-manager";
4
5
  import ScreenManager from "./screen-manager";
5
6
  import { type ReactiveQueryDefinition } from "./reactive-query-manager";
6
7
  import CommandBuffer from "./command-buffer";
7
8
  import type { System, SystemPhase, FilteredEntity, Entity, RemoveEntityOptions, HierarchyEntry, HierarchyIteratorOptions } from "./types";
8
- import type Bundle from "./bundle";
9
- import type { BundlesAreCompatible } from "./type-utils";
10
- import type { AssetHandle, AssetConfigurator } from "./asset-types";
11
- import type { ScreenDefinition, ScreenConfigurator } from "./screen-types";
9
+ import { type Plugin } from "./plugin";
10
+ import { SystemBuilder } from "./system-builder";
11
+ import type { AssetDefinition, AssetHandle } from "./asset-types";
12
+ import type { ScreenDefinition } from "./screen-types";
13
+ import { ECSpressoBuilder } from "./ecspresso-builder";
14
+ import type { WorldConfig, EmptyConfig } from "./type-utils";
12
15
  /**
13
16
  * Interface declaration for ECSpresso constructor to ensure type augmentation works properly.
14
17
  * This merges with the class declaration below.
15
18
  */
16
- export default interface ECSpresso<ComponentTypes extends Record<string, any> = {}, EventTypes extends Record<string, any> = {}, ResourceTypes extends Record<string, any> = {}, AssetTypes extends Record<string, unknown> = {}, ScreenStates extends Record<string, ScreenDefinition<any, any>> = {}> {
19
+ export default interface ECSpresso<Cfg extends WorldConfig = EmptyConfig, Labels extends string = string, Groups extends string = string, AssetGroupNames extends string = string, ReactiveQueryNames extends string = string> {
17
20
  /**
18
21
  * Default constructor
19
22
  */
20
- new (): ECSpresso<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates>;
23
+ new (): ECSpresso<Cfg, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
21
24
  }
22
25
  /**
23
26
  * ECSpresso is the central ECS framework class that connects all features.
24
27
  * It handles creation and management of entities, components, and systems, and provides lifecycle hooks.
25
28
  */
26
- export default class ECSpresso<ComponentTypes extends Record<string, any> = {}, EventTypes extends Record<string, any> = {}, ResourceTypes extends Record<string, any> = {}, AssetTypes extends Record<string, unknown> = {}, ScreenStates extends Record<string, ScreenDefinition<any, any>> = {}> {
29
+ export default class ECSpresso<Cfg extends WorldConfig = EmptyConfig, Labels extends string = string, Groups extends string = string, AssetGroupNames extends string = string, ReactiveQueryNames extends string = string> {
30
+ readonly _cfg: Cfg;
27
31
  /** Library version*/
28
32
  static readonly VERSION: string;
29
33
  /** Access/modify stored components and entities*/
@@ -38,8 +42,8 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
38
42
  private _systems;
39
43
  /** Systems grouped by execution phase, each sorted by priority */
40
44
  private _phaseSystems;
41
- /** Track installed bundles to prevent duplicates*/
42
- private _installedBundles;
45
+ /** Track installed plugins to prevent duplicates*/
46
+ private _installedPlugins;
43
47
  /** Disabled system groups */
44
48
  private _disabledGroups;
45
49
  /** Asset manager for loading and accessing assets */
@@ -64,36 +68,71 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
64
68
  private _interpolationAlpha;
65
69
  /** Maximum fixed update steps per frame (spiral-of-death protection) */
66
70
  private _maxFixedSteps;
71
+ /** Registry of required component relationships: trigger -> [{component, factory}] */
72
+ private _requiredComponents;
73
+ /** Pending plugin assets awaiting manager creation at build time */
74
+ private _pendingPluginAssets;
75
+ /** Pending plugin screens awaiting manager creation at build time */
76
+ private _pendingPluginScreens;
77
+ /** Whether diagnostics timing collection is enabled */
78
+ private _diagnosticsEnabled;
79
+ /** Per-system timing in ms, populated when diagnostics enabled */
80
+ private _systemTimings;
81
+ /** Per-phase timing in ms, populated when diagnostics enabled */
82
+ private _phaseTimings;
83
+ /** Per-system per-query seen entity IDs for onEntityEnter tracking */
84
+ private _entityEnterTracking;
85
+ /** Shared reusable set for per-tick entity enter comparison (avoids allocation) */
86
+ private _entityEnterFrameSet;
87
+ /** Pre-allocated process context per system (avoids per-frame allocation) */
88
+ private _systemContexts;
89
+ /** Pending system builder finalizers to run before next update/initialize */
90
+ private _pendingFinalizers;
91
+ private _batchingRegistrations;
67
92
  /**
68
93
  * Creates a new ECSpresso instance.
69
94
  */
70
95
  constructor();
71
96
  /**
72
- * Sets up component lifecycle hooks for reactive query tracking
97
+ * Subscribes to EntityManager lifecycle hooks for change detection,
98
+ * required component auto-addition, and reactive query tracking.
73
99
  * @private
74
100
  */
75
- private _setupReactiveQueryHooks;
101
+ private _subscribeLifecycleHooks;
76
102
  /**
77
- * Creates a new ECSpresso builder for type-safe bundle installation.
78
- * This is the preferred way to create an ECSpresso instance with bundles.
103
+ * Creates a new ECSpresso builder for type-safe plugin installation.
104
+ * Types are inferred from the builder chain use `.withPlugin()`,
105
+ * `.withComponentTypes<T>()`, `.withEventTypes<T>()`, and `.withResource()`
106
+ * to accumulate types without manual aggregate interfaces.
79
107
  *
80
108
  * @returns A builder instance for fluent method chaining
81
109
  *
82
110
  * @example
83
111
  * ```typescript
84
- * const ecs = ECSpresso.create<BaseComponents, BaseEvents, BaseResources>()
85
- * .withBundle(bundle1)
86
- * .withBundle(bundle2)
112
+ * const ecs = ECSpresso.create()
113
+ * .withPlugin(createRenderer2DPlugin({ ... }))
114
+ * .withPlugin(createPhysics2DPlugin())
115
+ * .withComponentTypes<{ player: true; enemy: { type: string } }>()
116
+ * .withEventTypes<{ gameStart: true }>()
117
+ * .withResource('score', { value: 0 })
87
118
  * .build();
119
+ *
120
+ * type ECS = typeof ecs;
88
121
  * ```
89
122
  */
90
- static create<C extends Record<string, any> = {}, E extends Record<string, any> = {}, R extends Record<string, any> = {}, A extends Record<string, unknown> = {}, S extends Record<string, ScreenDefinition<any, any>> = {}>(): ECSpressoBuilder<C, E, R, A, S>;
123
+ static create<Cfg2 extends WorldConfig = EmptyConfig>(): ECSpressoBuilder<Cfg2, never, never, never, never>;
91
124
  /**
92
- * Adds a system directly to this ECSpresso instance
125
+ * Adds a system directly to this ECSpresso instance.
126
+ * The system is registered when initialize() or update() is next called.
93
127
  * @param label Unique name to identify the system
94
128
  * @returns A SystemBuilder instance for method chaining
95
129
  */
96
- addSystem(label: string): import("./system-builder").SystemBuilderWithEcspresso<ComponentTypes, EventTypes, ResourceTypes, {}>;
130
+ addSystem(label: string): SystemBuilder<Cfg>;
131
+ /**
132
+ * Finalize and register all pending system builders.
133
+ * @private
134
+ */
135
+ private _finalizePendingBuilders;
97
136
  /**
98
137
  * Update all systems across execution phases.
99
138
  * Phases run in order: preUpdate -> fixedUpdate -> update -> postUpdate -> render.
@@ -106,6 +145,11 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
106
145
  * @private
107
146
  */
108
147
  private _executePhase;
148
+ /**
149
+ * Execute a non-fixed phase with optional timing, then play back the command buffer.
150
+ * @private
151
+ */
152
+ private _runPhase;
109
153
  /**
110
154
  * Initialize all resources and systems
111
155
  * This method:
@@ -127,7 +171,7 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
127
171
  * @param keys Optional array of resource keys to initialize. If not provided, all pending resources will be initialized.
128
172
  * @returns Promise that resolves when the specified resources are initialized
129
173
  */
130
- initializeResources<K extends keyof ResourceTypes>(...keys: K[]): Promise<void>;
174
+ initializeResources<K extends keyof Cfg['resources']>(...keys: K[]): Promise<void>;
131
175
  /**
132
176
  * Rebuild per-phase system arrays from the flat _systems list.
133
177
  * Each phase array is sorted by priority (higher first), with
@@ -141,14 +185,14 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
141
185
  * @param priority The new priority value (higher values execute first)
142
186
  * @returns true if the system was found and updated, false otherwise
143
187
  */
144
- updateSystemPriority(label: string, priority: number): boolean;
188
+ updateSystemPriority(label: Labels, priority: number): boolean;
145
189
  /**
146
190
  * Move a system to a different execution phase at runtime.
147
191
  * @param label The unique label of the system to move
148
192
  * @param phase The target phase
149
193
  * @returns true if the system was found and updated, false otherwise
150
194
  */
151
- updateSystemPhase(label: string, phase: SystemPhase): boolean;
195
+ updateSystemPhase(label: Labels, phase: SystemPhase): boolean;
152
196
  /**
153
197
  * The interpolation alpha between fixed update steps.
154
198
  * Ranges from 0 to <1, representing how far into the next
@@ -164,64 +208,88 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
164
208
  * Disable a system group. Systems in this group will be skipped during update().
165
209
  * @param groupName The name of the group to disable
166
210
  */
167
- disableSystemGroup(groupName: string): void;
211
+ disableSystemGroup(groupName: Groups): void;
168
212
  /**
169
213
  * Enable a system group. Systems in this group will run during update().
170
214
  * @param groupName The name of the group to enable
171
215
  */
172
- enableSystemGroup(groupName: string): void;
216
+ enableSystemGroup(groupName: Groups): void;
173
217
  /**
174
218
  * Check if a system group is enabled.
175
219
  * @param groupName The name of the group to check
176
220
  * @returns true if the group is enabled (or doesn't exist), false if disabled
177
221
  */
178
- isSystemGroupEnabled(groupName: string): boolean;
222
+ isSystemGroupEnabled(groupName: Groups): boolean;
179
223
  /**
180
224
  * Get all system labels that belong to a specific group.
181
225
  * @param groupName The name of the group
182
226
  * @returns Array of system labels in the group
183
227
  */
184
- getSystemsInGroup(groupName: string): string[];
228
+ getSystemsInGroup(groupName: Groups): string[];
185
229
  /**
186
230
  * Remove a system by its label
187
231
  * Calls the system's onDetach method with this ECSpresso instance if defined
188
232
  * @param label The unique label of the system to remove
189
233
  * @returns true if the system was found and removed, false otherwise
190
234
  */
191
- removeSystem(label: string): boolean;
235
+ removeSystem(label: Labels): boolean;
192
236
  /**
193
237
  * Internal method to register a system with this ECSpresso instance
194
238
  * @internal Used by SystemBuilder - replaces direct private property access
195
239
  */
196
- _registerSystem(system: System<ComponentTypes, any, any, EventTypes, ResourceTypes, AssetTypes, ScreenStates>): void;
240
+ _registerSystem(system: System<Cfg, any, any>): void;
197
241
  /**
198
242
  * Check if a resource exists
199
243
  */
200
- hasResource<K extends keyof ResourceTypes>(key: K): boolean;
244
+ hasResource<K extends keyof Cfg['resources']>(key: K): boolean;
201
245
  /**
202
- * Get a resource if it exists, or undefined if not
203
- */
204
- getResource<K extends keyof ResourceTypes>(key: K): ResourceTypes[K];
246
+ * Get a resource by key. Throws if the resource is not found.
247
+ * @param key The resource key
248
+ * @returns The resource value
249
+ * @throws Error if resource not found
250
+ * @see tryGetResource — the non-throwing alternative that returns undefined
251
+ */
252
+ getResource<K extends keyof Cfg['resources']>(key: K): Cfg['resources'][K];
205
253
  /**
206
- * Add a resource to the ECS instance
254
+ * Try to get a resource by key. Returns undefined if the resource is not found.
255
+ * Inspired by Bevy's `World::get_resource::<T>()` which returns `Option<&T>`.
256
+ *
257
+ * Two overloads:
258
+ * 1. Known key — full type safety from `ResourceTypes`
259
+ * 2. String key with explicit type param — for cross-plugin optional dependencies
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * // Known key (type inferred from ResourceTypes)
264
+ * const score = ecs.tryGetResource('score'); // ScoreResource | undefined
265
+ *
266
+ * // Cross-plugin optional dependency (caller specifies expected type)
267
+ * const si = ecs.tryGetResource<SpatialIndex>('spatialIndex') ?? null;
268
+ * ```
269
+ */
270
+ tryGetResource<K extends keyof Cfg['resources']>(key: K): Cfg['resources'][K] | undefined;
271
+ tryGetResource<T>(key: unknown extends T ? never : string): T | undefined;
272
+ /**
273
+ * Add a resource to the ECS instance.
274
+ *
275
+ * - Plain value → stored directly
276
+ * - Function → treated as a factory, called with this ECSpresso instance on first access
277
+ * - `{ factory, dependsOn?, onDispose? }` → factory with dependencies/disposal
278
+ * - `directValue(val)` → stores the value as-is (use to store functions/classes without invoking them)
207
279
  */
208
- addResource<K extends keyof ResourceTypes>(key: K, resource: ResourceTypes[K] | ((ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]>) | {
209
- dependsOn?: readonly string[];
210
- factory: (ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]>;
211
- onDispose?: (resource: ResourceTypes[K], ecs?: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => void | Promise<void>;
212
- }): this;
280
+ 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;
213
281
  /**
214
282
  * Remove a resource from the ECS instance (without calling onDispose)
215
283
  * @param key The resource key to remove
216
284
  * @returns True if the resource was removed, false if it didn't exist
217
285
  */
218
- removeResource<K extends keyof ResourceTypes>(key: K): boolean;
286
+ removeResource<K extends keyof Cfg['resources']>(key: K): boolean;
219
287
  /**
220
288
  * Dispose a single resource, calling its onDispose callback if defined
221
289
  * @param key The resource key to dispose
222
290
  * @returns True if the resource existed and was disposed, false if it didn't exist
223
291
  */
224
- disposeResource<K extends keyof ResourceTypes>(key: K): Promise<boolean>;
292
+ disposeResource<K extends keyof Cfg['resources']>(key: K): Promise<boolean>;
225
293
  /**
226
294
  * Dispose all initialized resources in reverse dependency order.
227
295
  * Resources that depend on others are disposed first.
@@ -235,34 +303,64 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
235
303
  * @returns This ECSpresso instance for chaining
236
304
  * @throws Error if the resource doesn't exist
237
305
  */
238
- updateResource<K extends keyof ResourceTypes>(key: K, updater: (current: ResourceTypes[K]) => ResourceTypes[K]): this;
306
+ updateResource<K extends keyof Cfg['resources']>(key: K, updater: (current: Cfg['resources'][K]) => Cfg['resources'][K]): this;
239
307
  /**
240
308
  * Get all resource keys that are currently registered
241
309
  * @returns Array of resource keys
242
310
  */
243
- getResourceKeys(): Array<keyof ResourceTypes>;
311
+ getResourceKeys(): Array<keyof Cfg['resources']>;
244
312
  /**
245
313
  * Check if a resource needs initialization (was added as a factory function)
246
314
  * @param key The resource key to check
247
315
  * @returns True if the resource needs initialization
248
316
  */
249
- resourceNeedsInitialization<K extends keyof ResourceTypes>(key: K): boolean;
317
+ resourceNeedsInitialization<K extends keyof Cfg['resources']>(key: K): boolean;
318
+ /**
319
+ * Get a component value from an entity.
320
+ * @param entityId The entity ID
321
+ * @param componentName The component to retrieve
322
+ * @returns The component value, or undefined if the entity doesn't have it
323
+ */
324
+ getComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K): Cfg['components'][K] | undefined;
325
+ /**
326
+ * Add or replace a component on an entity.
327
+ * Triggers component-added callbacks and marks the component as changed.
328
+ * @param entityId The entity ID
329
+ * @param componentName The component to add
330
+ * @param value The component value
331
+ */
332
+ addComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K, value: Cfg['components'][K]): void;
333
+ /**
334
+ * Add multiple components to an entity at once.
335
+ * @param entityId The entity ID
336
+ * @param components Object with component names as keys and component data as values
337
+ */
338
+ addComponents<T extends {
339
+ [K in keyof Cfg['components']]?: Cfg['components'][K];
340
+ }>(entityId: number, components: T & Record<Exclude<keyof T, keyof Cfg['components']>, never>): void;
341
+ /**
342
+ * Remove a component from an entity.
343
+ * Triggers component-removed and dispose callbacks.
344
+ * @param entityId The entity ID
345
+ * @param componentName The component to remove
346
+ */
347
+ removeComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K): void;
250
348
  /**
251
349
  * Check if an entity has a component
252
350
  */
253
- hasComponent<K extends keyof ComponentTypes>(entityId: number, componentName: K): boolean;
351
+ hasComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K): boolean;
254
352
  /**
255
353
  * Create an entity and add components to it in one call
256
354
  * @param components Object with component names as keys and component data as values
257
355
  * @returns The created entity with all components added
258
356
  */
259
357
  spawn<T extends {
260
- [K in keyof ComponentTypes]?: ComponentTypes[K];
261
- }>(components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): FilteredEntity<ComponentTypes, keyof T & keyof ComponentTypes, never>;
358
+ [K in keyof Cfg['components']]?: Cfg['components'][K];
359
+ }>(components: T & Record<Exclude<keyof T, keyof Cfg['components']>, never>): FilteredEntity<Cfg['components'], keyof T & keyof Cfg['components']>;
262
360
  /**
263
361
  * Get all entities with specific components
264
362
  */
265
- getEntitiesWithQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>, changedComponents?: ReadonlyArray<keyof ComponentTypes>, parentHas?: ReadonlyArray<keyof ComponentTypes>): Array<FilteredEntity<ComponentTypes, WithComponents, WithoutComponents>>;
363
+ 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>>;
266
364
  /**
267
365
  * Get the single entity matching a query. Throws if zero or more than one match.
268
366
  * @param withComponents Components the entity must have
@@ -270,7 +368,7 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
270
368
  * @returns The single matching entity
271
369
  * @throws If zero or more than one entity matches
272
370
  */
273
- getSingleton<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<ComponentTypes, WithComponents, WithoutComponents>;
371
+ getSingleton<WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<Cfg['components'], WithComponents, WithoutComponents>;
274
372
  /**
275
373
  * Get the single entity matching a query, or undefined if none match.
276
374
  * Throws if more than one entity matches.
@@ -279,14 +377,14 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
279
377
  * @returns The single matching entity, or undefined if none match
280
378
  * @throws If more than one entity matches
281
379
  */
282
- tryGetSingleton<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<ComponentTypes, WithComponents, WithoutComponents> | undefined;
380
+ tryGetSingleton<WithComponents extends keyof Cfg['components'], WithoutComponents extends keyof Cfg['components'] = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<Cfg['components'], WithComponents, WithoutComponents> | undefined;
283
381
  /**
284
382
  * Remove an entity (and optionally its descendants)
285
- * @param entityOrId Entity or entity ID to remove
383
+ * @param entityId Entity ID to remove
286
384
  * @param options Options for removal (cascade: true by default)
287
385
  * @returns true if entity was removed
288
386
  */
289
- removeEntity(entityOrId: number | Entity<ComponentTypes>, options?: RemoveEntityOptions): boolean;
387
+ removeEntity(entityId: number, options?: RemoveEntityOptions): boolean;
290
388
  /**
291
389
  * Create an entity as a child of another entity with initial components
292
390
  * @param parentId The parent entity ID
@@ -294,81 +392,81 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
294
392
  * @returns The created child entity
295
393
  */
296
394
  spawnChild<T extends {
297
- [K in keyof ComponentTypes]?: ComponentTypes[K];
298
- }>(parentId: number, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): FilteredEntity<ComponentTypes, keyof T & keyof ComponentTypes, never>;
395
+ [K in keyof Cfg['components']]?: Cfg['components'][K];
396
+ }>(parentId: number, components: T & Record<Exclude<keyof T, keyof Cfg['components']>, never>): FilteredEntity<Cfg['components'], keyof T & keyof Cfg['components']>;
299
397
  /**
300
398
  * Set the parent of an entity
301
- * @param childId The entity to set as a child
302
- * @param parentId The entity to set as the parent
399
+ * @param childId The entity ID to set as a child
400
+ * @param parentId The entity ID to set as the parent
303
401
  */
304
402
  setParent(childId: number, parentId: number): this;
305
403
  /**
306
404
  * Remove the parent relationship for an entity (orphan it)
307
- * @param childId The entity to orphan
405
+ * @param childId The entity ID to orphan
308
406
  * @returns true if a parent was removed, false if entity had no parent
309
407
  */
310
408
  removeParent(childId: number): boolean;
311
409
  /**
312
410
  * Get the parent of an entity
313
- * @param entityId The entity to get the parent of
411
+ * @param entityId The entity ID to get the parent of
314
412
  * @returns The parent entity ID, or null if no parent
315
413
  */
316
414
  getParent(entityId: number): number | null;
317
415
  /**
318
416
  * Get all children of an entity in insertion order
319
- * @param parentId The parent entity
417
+ * @param parentId The parent entity ID
320
418
  * @returns Readonly array of child entity IDs
321
419
  */
322
420
  getChildren(parentId: number): readonly number[];
323
421
  /**
324
422
  * Get a child at a specific index
325
- * @param parentId The parent entity
423
+ * @param parentId The parent entity ID
326
424
  * @param index The index of the child
327
425
  * @returns The child entity ID, or null if index is out of bounds
328
426
  */
329
427
  getChildAt(parentId: number, index: number): number | null;
330
428
  /**
331
429
  * Get the index of a child within its parent's children list
332
- * @param parentId The parent entity
333
- * @param childId The child entity to find
430
+ * @param parentId The parent entity ID
431
+ * @param childId The child entity ID to find
334
432
  * @returns The index of the child, or -1 if not found
335
433
  */
336
434
  getChildIndex(parentId: number, childId: number): number;
337
435
  /**
338
436
  * Get all ancestors of an entity in order [parent, grandparent, ...]
339
- * @param entityId The entity to get ancestors of
437
+ * @param entityId The entity ID to get ancestors of
340
438
  * @returns Readonly array of ancestor entity IDs
341
439
  */
342
440
  getAncestors(entityId: number): readonly number[];
343
441
  /**
344
442
  * Get all descendants of an entity in depth-first order
345
- * @param entityId The entity to get descendants of
443
+ * @param entityId The entity ID to get descendants of
346
444
  * @returns Readonly array of descendant entity IDs
347
445
  */
348
446
  getDescendants(entityId: number): readonly number[];
349
447
  /**
350
448
  * Get the root ancestor of an entity (topmost parent), or self if no parent
351
- * @param entityId The entity to get the root of
449
+ * @param entityId The entity ID to get the root of
352
450
  * @returns The root entity ID
353
451
  */
354
452
  getRoot(entityId: number): number;
355
453
  /**
356
454
  * Get siblings of an entity (other children of the same parent)
357
- * @param entityId The entity to get siblings of
455
+ * @param entityId The entity ID to get siblings of
358
456
  * @returns Readonly array of sibling entity IDs
359
457
  */
360
458
  getSiblings(entityId: number): readonly number[];
361
459
  /**
362
460
  * Check if an entity is a descendant of another entity
363
- * @param entityId The potential descendant
364
- * @param ancestorId The potential ancestor
461
+ * @param entityId The potential descendant ID
462
+ * @param ancestorId The potential ancestor ID
365
463
  * @returns true if entityId is a descendant of ancestorId
366
464
  */
367
465
  isDescendantOf(entityId: number, ancestorId: number): boolean;
368
466
  /**
369
467
  * Check if an entity is an ancestor of another entity
370
- * @param entityId The potential ancestor
371
- * @param descendantId The potential descendant
468
+ * @param entityId The potential ancestor ID
469
+ * @param descendantId The potential descendant ID
372
470
  * @returns true if entityId is an ancestor of descendantId
373
471
  */
374
472
  isAncestorOf(entityId: number, descendantId: number): boolean;
@@ -397,11 +495,11 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
397
495
  */
398
496
  private _emitHierarchyChanged;
399
497
  /**
400
- * Get all installed bundle IDs
498
+ * Get all installed plugin IDs
401
499
  */
402
- get installedBundles(): string[];
403
- get entityManager(): EntityManager<ComponentTypes>;
404
- get eventBus(): EventBus<EventTypes>;
500
+ get installedPlugins(): string[];
501
+ get entityManager(): EntityManager<Cfg["components"]>;
502
+ get eventBus(): EventBus<Cfg["events"]>;
405
503
  /**
406
504
  * Command buffer for queuing deferred structural changes.
407
505
  * Commands are executed automatically at the end of each update() cycle.
@@ -413,7 +511,7 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
413
511
  * ecs.commands.spawn({ position: { x: 0, y: 0 } });
414
512
  * ```
415
513
  */
416
- get commands(): CommandBuffer<ComponentTypes, EventTypes, ResourceTypes>;
514
+ get commands(): CommandBuffer<Cfg>;
417
515
  /**
418
516
  * The current tick number, incremented at the end of each update()
419
517
  */
@@ -425,6 +523,25 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
425
523
  * Manual change detection should compare: getChangeSeq(...) > changeThreshold
426
524
  */
427
525
  get changeThreshold(): number;
526
+ /**
527
+ * Toggle diagnostics timing collection. When enabled, system and phase
528
+ * timings are recorded each frame. When disabled, timing maps are cleared
529
+ * and no overhead is incurred.
530
+ */
531
+ enableDiagnostics(enabled: boolean): void;
532
+ get diagnosticsEnabled(): boolean;
533
+ get systemTimings(): ReadonlyMap<string, number>;
534
+ get phaseTimings(): Readonly<Record<SystemPhase, number>>;
535
+ get entityCount(): number;
536
+ /**
537
+ * Mutate a component in place and automatically mark it as changed.
538
+ * Throws if the entity does not exist or does not have the component.
539
+ * @param entityId The entity ID
540
+ * @param componentName The component to mutate
541
+ * @param mutator A function that receives the component value for in-place mutation
542
+ * @returns The mutated component value
543
+ */
544
+ mutateComponent<K extends keyof Cfg['components']>(entityId: number, componentName: K, mutator: (value: Cfg['components'][K]) => void): Cfg['components'][K];
428
545
  /**
429
546
  * Mark a component as changed on an entity.
430
547
  * Each call increments a global monotonic sequence; systems with changed
@@ -432,93 +549,130 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
432
549
  * @param entityId The entity ID
433
550
  * @param componentName The component that was changed
434
551
  */
435
- markChanged<K extends keyof ComponentTypes>(entityId: number, componentName: K): void;
552
+ markChanged<K extends keyof Cfg['components']>(entityId: number, componentName: K): void;
553
+ /**
554
+ * Register a dispose callback for a component type.
555
+ * Called when a component is removed (explicit removal, entity destruction, or replacement).
556
+ * Later registrations replace earlier ones for the same component type.
557
+ * @param componentName The component type to register disposal for
558
+ * @param callback Function receiving the component value being disposed and the entity ID
559
+ */
560
+ registerDispose<K extends keyof Cfg['components']>(componentName: K, callback: (ctx: {
561
+ value: Cfg['components'][K];
562
+ entityId: number;
563
+ }) => void): void;
564
+ /**
565
+ * Register a required component relationship.
566
+ * When an entity gains `trigger`, the `required` component is auto-added
567
+ * (using `factory` for the default value) if not already present.
568
+ * Enforced at insertion time (spawn/addComponent) only — removal is unrestricted.
569
+ * @param trigger The component whose presence triggers auto-addition
570
+ * @param required The component to auto-add
571
+ * @param factory Function that creates the default value for the required component
572
+ */
573
+ registerRequired<Trigger extends keyof Cfg['components'], Required extends keyof Cfg['components']>(trigger: Trigger, required: Required, factory: (triggerValue: Cfg['components'][Trigger]) => Cfg['components'][Required]): void;
574
+ /**
575
+ * Check for circular dependencies in the required components graph.
576
+ * @throws Error if adding trigger→newRequired would create a cycle
577
+ */
578
+ private _checkRequiredCycle;
436
579
  /**
437
580
  * Register a callback when a specific component is added to any entity
438
581
  * @param componentName The component key
439
582
  * @param handler Function receiving the new component value and the entity
440
583
  * @returns Unsubscribe function to remove the callback
441
584
  */
442
- onComponentAdded<K extends keyof ComponentTypes>(componentName: K, handler: (value: ComponentTypes[K], entity: Entity<ComponentTypes>) => void): () => void;
585
+ onComponentAdded<K extends keyof Cfg['components']>(componentName: K, handler: (ctx: {
586
+ value: Cfg['components'][K];
587
+ entity: Entity<Cfg['components']>;
588
+ }) => void): () => void;
443
589
  /**
444
590
  * Register a callback when a specific component is removed from any entity
445
591
  * @param componentName The component key
446
592
  * @param handler Function receiving the old component value and the entity
447
593
  * @returns Unsubscribe function to remove the callback
448
594
  */
449
- onComponentRemoved<K extends keyof ComponentTypes>(componentName: K, handler: (oldValue: ComponentTypes[K], entity: Entity<ComponentTypes>) => void): () => void;
595
+ onComponentRemoved<K extends keyof Cfg['components']>(componentName: K, handler: (ctx: {
596
+ value: Cfg['components'][K];
597
+ entity: Entity<Cfg['components']>;
598
+ }) => void): () => void;
450
599
  /**
451
600
  * Add a reactive query that triggers callbacks when entities enter/exit the query match.
452
601
  * @param name Unique name for the query
453
602
  * @param definition Query definition with with/without arrays and onEnter/onExit callbacks
454
603
  */
455
- addReactiveQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never>(name: string, definition: ReactiveQueryDefinition<ComponentTypes, WithComponents, WithoutComponents, OptionalComponents>): void;
604
+ 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;
456
605
  /**
457
606
  * Remove a reactive query by name.
458
607
  * @param name Name of the query to remove
459
608
  * @returns true if the query existed and was removed, false otherwise
460
609
  */
461
- removeReactiveQuery(name: string): boolean;
610
+ removeReactiveQuery(name: ReactiveQueryNames): boolean;
462
611
  /**
463
612
  * Subscribe to an event (convenience wrapper for eventBus.subscribe)
464
613
  * @param eventType The event type to subscribe to
465
614
  * @param callback The callback to invoke when the event is published
466
615
  * @returns An unsubscribe function
467
616
  */
468
- on<E extends keyof EventTypes>(eventType: E, callback: (data: EventTypes[E]) => void): () => void;
617
+ on<E extends keyof Cfg['events']>(eventType: E, callback: (data: Cfg['events'][E]) => void): () => void;
469
618
  /**
470
619
  * Unsubscribe from an event by callback reference (convenience wrapper for eventBus.unsubscribe)
471
620
  * @param eventType The event type to unsubscribe from
472
621
  * @param callback The callback to remove
473
622
  * @returns true if the callback was found and removed, false otherwise
474
623
  */
475
- off<E extends keyof EventTypes>(eventType: E, callback: (data: EventTypes[E]) => void): boolean;
624
+ off<E extends keyof Cfg['events']>(eventType: E, callback: (data: Cfg['events'][E]) => void): boolean;
476
625
  /**
477
626
  * Register a hook that runs after all systems in update()
478
627
  * @param callback The hook to call after all systems have processed
479
628
  * @returns An unsubscribe function to remove the hook
480
629
  */
481
- onPostUpdate(callback: (ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>, deltaTime: number) => void): () => void;
630
+ onPostUpdate(callback: (ctx: {
631
+ ecs: ECSpresso<Cfg>;
632
+ dt: number;
633
+ }) => void): () => void;
634
+ private requireAssetManager;
482
635
  /**
483
636
  * Get a loaded asset by key. Throws if not loaded.
484
637
  */
485
- getAsset<K extends keyof AssetTypes>(key: K): AssetTypes[K];
638
+ getAsset<K extends keyof Cfg['assets']>(key: K): Cfg['assets'][K];
486
639
  /**
487
640
  * Get a loaded asset or undefined if not loaded
488
641
  */
489
- getAssetOrUndefined<K extends keyof AssetTypes>(key: K): AssetTypes[K] | undefined;
642
+ tryGetAsset<K extends keyof Cfg['assets']>(key: K): Cfg['assets'][K] | undefined;
490
643
  /**
491
644
  * Get a handle to an asset with status information
492
645
  */
493
- getAssetHandle<K extends keyof AssetTypes>(key: K): AssetHandle<AssetTypes[K]>;
646
+ getAssetHandle<K extends keyof Cfg['assets']>(key: K): AssetHandle<Cfg['assets'][K]>;
494
647
  /**
495
648
  * Check if an asset is loaded
496
649
  */
497
- isAssetLoaded<K extends keyof AssetTypes>(key: K): boolean;
650
+ isAssetLoaded<K extends keyof Cfg['assets']>(key: K): boolean;
498
651
  /**
499
652
  * Load a single asset
500
653
  */
501
- loadAsset<K extends keyof AssetTypes>(key: K): Promise<AssetTypes[K]>;
654
+ loadAsset<K extends keyof Cfg['assets']>(key: K): Promise<Cfg['assets'][K]>;
502
655
  /**
503
656
  * Load all assets in a group
504
657
  */
505
- loadAssetGroup(groupName: string): Promise<void>;
658
+ loadAssetGroup(groupName: AssetGroupNames): Promise<void>;
506
659
  /**
507
660
  * Check if all assets in a group are loaded
508
661
  */
509
- isAssetGroupLoaded(groupName: string): boolean;
662
+ isAssetGroupLoaded(groupName: AssetGroupNames): boolean;
510
663
  /**
511
664
  * Get the loading progress of a group (0-1)
512
665
  */
513
- getAssetGroupProgress(groupName: string): number;
666
+ getAssetGroupProgress(groupName: AssetGroupNames): number;
667
+ private requireScreenManager;
514
668
  /**
515
669
  * Transition to a new screen, clearing the stack
516
670
  */
517
- setScreen<K extends keyof ScreenStates>(name: K, config: ScreenStates[K] extends ScreenDefinition<infer C, any> ? C : never): Promise<void>;
671
+ setScreen<K extends keyof Cfg['screens']>(name: K, config: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? C : never): Promise<void>;
518
672
  /**
519
673
  * Push a screen onto the stack (overlay)
520
674
  */
521
- pushScreen<K extends keyof ScreenStates>(name: K, config: ScreenStates[K] extends ScreenDefinition<infer C, any> ? C : never): Promise<void>;
675
+ pushScreen<K extends keyof Cfg['screens']>(name: K, config: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? C : never): Promise<void>;
522
676
  /**
523
677
  * Pop the current screen and return to the previous one
524
678
  */
@@ -526,164 +680,124 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
526
680
  /**
527
681
  * Get the current screen name
528
682
  */
529
- getCurrentScreen(): keyof ScreenStates | null;
683
+ getCurrentScreen(): keyof Cfg['screens'] | null;
684
+ /**
685
+ * Get the current screen config (immutable), narrowed to a specific screen.
686
+ * Throws if the current screen doesn't match.
687
+ */
688
+ getScreenConfig<K extends keyof Cfg['screens'] & string>(screen: K): Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
689
+ /**
690
+ * Get the current screen config (immutable).
691
+ * Returns a union of all possible config types.
692
+ */
693
+ getScreenConfig(): {
694
+ [K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
695
+ }[keyof Cfg['screens']];
696
+ /**
697
+ * Get the current screen config narrowed to a specific screen, or undefined if not on that screen.
698
+ */
699
+ tryGetScreenConfig<K extends keyof Cfg['screens'] & string>(screen: K): (Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never) | undefined;
700
+ /**
701
+ * Get the current screen config or undefined.
702
+ * Returns a union of all possible config types, or undefined.
703
+ */
704
+ tryGetScreenConfig(): {
705
+ [K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
706
+ }[keyof Cfg['screens']] | undefined;
530
707
  /**
531
- * Get the current screen config (immutable)
708
+ * Get the current screen state (mutable), narrowed to a specific screen.
709
+ * Throws if the current screen doesn't match.
532
710
  */
533
- getScreenConfig<K extends keyof ScreenStates>(): ScreenStates[K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never;
711
+ getScreenState<K extends keyof Cfg['screens'] & string>(screen: K): Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never;
534
712
  /**
535
- * Get the current screen config or null
713
+ * Get the current screen state (mutable).
714
+ * Returns a union of all possible state types.
536
715
  */
537
- getScreenConfigOrNull<K extends keyof ScreenStates>(): (ScreenStates[K] extends ScreenDefinition<infer C, any> ? Readonly<C> : never) | null;
716
+ getScreenState(): {
717
+ [K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never;
718
+ }[keyof Cfg['screens']];
538
719
  /**
539
- * Get the current screen state (mutable)
720
+ * Get the current screen state narrowed to a specific screen, or undefined if not on that screen.
540
721
  */
541
- getScreenState<K extends keyof ScreenStates>(): ScreenStates[K] extends ScreenDefinition<any, infer S> ? S : never;
722
+ tryGetScreenState<K extends keyof Cfg['screens'] & string>(screen: K): (Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never) | undefined;
542
723
  /**
543
- * Get the current screen state or null
724
+ * Get the current screen state or undefined.
725
+ * Returns a union of all possible state types, or undefined.
544
726
  */
545
- getScreenStateOrNull<K extends keyof ScreenStates>(): (ScreenStates[K] extends ScreenDefinition<any, infer S> ? S : never) | null;
727
+ tryGetScreenState(): {
728
+ [K in keyof Cfg['screens']]: Cfg['screens'][K] extends ScreenDefinition<any, infer S> ? S : never;
729
+ }[keyof Cfg['screens']] | undefined;
546
730
  /**
547
- * Update the current screen state
731
+ * Update the current screen state, narrowed to a specific screen.
732
+ * Throws if the current screen doesn't match.
548
733
  */
549
- updateScreenState<K extends keyof ScreenStates>(update: Partial<ScreenStates[K] extends ScreenDefinition<any, infer S> ? S : never> | ((current: ScreenStates[K] extends ScreenDefinition<any, infer S> ? S : never) => Partial<ScreenStates[K] extends ScreenDefinition<any, infer S> ? S : never>)): void;
734
+ 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;
735
+ /**
736
+ * Update the current screen state.
737
+ */
738
+ 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;
550
739
  /**
551
740
  * Check if a screen is the current screen
552
741
  */
553
- isCurrentScreen(screenName: keyof ScreenStates): boolean;
742
+ isCurrentScreen(screenName: keyof Cfg['screens']): boolean;
554
743
  /**
555
744
  * Check if a screen is active (current or in stack)
556
745
  */
557
- isScreenActive(screenName: keyof ScreenStates): boolean;
746
+ isScreenActive(screenName: keyof Cfg['screens']): boolean;
558
747
  /**
559
748
  * Get the screen stack depth
560
749
  */
561
750
  getScreenStackDepth(): number;
562
751
  /**
563
- * Internal method to set the asset manager
752
+ * Internal method to set the asset manager and drain pending plugin assets
564
753
  * @internal Used by ECSpressoBuilder
565
754
  */
566
- _setAssetManager(manager: AssetManager<AssetTypes>): void;
755
+ _setAssetManager(manager: AssetManager<Cfg['assets']>): void;
567
756
  /**
568
- * Internal method to set the screen manager
757
+ * Internal method to set the screen manager and drain pending plugin screens
569
758
  * @internal Used by ECSpressoBuilder
570
759
  */
571
- _setScreenManager(manager: ScreenManager<ScreenStates>): void;
760
+ _setScreenManager(manager: ScreenManager<Cfg['screens']>): void;
761
+ /** @internal */
762
+ _hasPendingPluginAssets(): boolean;
763
+ /** @internal */
764
+ _hasPendingPluginScreens(): boolean;
572
765
  /**
573
766
  * Internal method to set the fixed timestep interval
574
767
  * @internal Used by ECSpressoBuilder
575
768
  */
576
769
  _setFixedDt(dt: number): void;
577
770
  /**
578
- * Internal method to install a bundle into this ECSpresso instance.
579
- * Called by the ECSpressoBuilder during the build process.
580
- * The type safety is guaranteed by the builder's type system.
581
- */
582
- _installBundle<C extends Record<string, any>, E extends Record<string, any>, R extends Record<string, any>, A extends Record<string, unknown> = {}, S extends Record<string, ScreenDefinition<any, any>> = {}>(bundle: Bundle<C, E, R, A, S>): this;
583
- }
584
- /**
585
- * Resource factory with optional dependencies and disposal callback
586
- */
587
- type ResourceFactoryWithDeps<T> = {
588
- dependsOn?: readonly string[];
589
- factory: (context?: any) => T | Promise<T>;
590
- onDispose?: (resource: T, context?: any) => void | Promise<void>;
591
- };
592
- /**
593
- * Builder class for ECSpresso that provides fluent type-safe bundle installation.
594
- * Handles type checking during build process to ensure type safety.
595
- */
596
- export declare class ECSpressoBuilder<C extends Record<string, any> = {}, E extends Record<string, any> = {}, R extends Record<string, any> = {}, A extends Record<string, unknown> = {}, S extends Record<string, ScreenDefinition<any, any>> = {}> {
597
- /** The ECSpresso instance being built*/
598
- private ecspresso;
599
- /** Asset configurator for collecting asset definitions */
600
- private assetConfigurator;
601
- /** Screen configurator for collecting screen definitions */
602
- private screenConfigurator;
603
- /** Pending resources to add during build */
604
- private pendingResources;
605
- /** Fixed timestep interval (null means use default 1/60) */
606
- private _fixedDt;
607
- constructor();
608
- /**
609
- * Add the first bundle when starting with empty types.
610
- * This overload allows any bundle to be added to an empty ECSpresso instance.
611
- */
612
- withBundle<BC extends Record<string, any>, BE extends Record<string, any>, BR extends Record<string, any>>(this: ECSpressoBuilder<{}, {}, {}, A, S>, bundle: Bundle<BC, BE, BR>): ECSpressoBuilder<BC, BE, BR, A, S>;
771
+ * Register an asset definition for deferred registration.
772
+ * @internal Used by plugins that need to register assets
773
+ */
774
+ _registerAsset(key: string, definition: AssetDefinition<unknown>): void;
613
775
  /**
614
- * Add a subsequent bundle with type checking.
615
- * This overload enforces bundle type compatibility.
616
- */
617
- withBundle<BC extends Record<string, any>, BE extends Record<string, any>, BR extends Record<string, any>>(bundle: BundlesAreCompatible<C, BC, E, BE, R, BR> extends true ? Bundle<BC, BE, BR> : never): ECSpressoBuilder<C & BC, E & BE, R & BR, A, S>;
776
+ * Register a screen definition for deferred registration.
777
+ * @internal Used by plugins that need to register screens
778
+ */
779
+ _registerScreen(name: string, definition: ScreenDefinition<any, any>): void;
618
780
  /**
619
- * Add a resource during ECSpresso construction
620
- * @param key The resource key
621
- * @param resource The resource value, factory function, or factory with dependencies/disposal
622
- * @returns This builder with updated resource types
623
- *
624
- * @example
625
- * ```typescript
626
- * ECSpresso.create<Components, Events, Resources>()
627
- * .withResource('config', { debug: true })
628
- * .withResource('counter', () => 42)
629
- * .withResource('derived', {
630
- * dependsOn: ['base'],
631
- * factory: (ecs) => ecs.getResource('base') * 2,
632
- * onDispose: (value) => console.log('Disposed:', value)
633
- * })
634
- * .build();
635
- * ```
781
+ * Install a plugin into this ECSpresso instance.
782
+ * Deduplicates by plugin ID. Composite plugins call this in their install function.
636
783
  */
637
- withResource<K extends string, V>(key: K, resource: V | ((context?: any) => V | Promise<V>) | ResourceFactoryWithDeps<V>): ECSpressoBuilder<C, E, R & Record<K, V>, A, S>;
784
+ installPlugin(plugin: Plugin<any, any, any, any, any, any>): this;
638
785
  /**
639
- * Configure assets for this ECSpresso instance
640
- * @param configurator Function that receives an AssetConfigurator and returns it after adding assets
641
- * @returns This builder with updated asset types
642
- *
643
- * @example
644
- * ```typescript
645
- * ECSpresso.create<Components, Events, Resources>()
646
- * .withAssets(assets => assets
647
- * .add('playerSprite', () => loadTexture('player.png'))
648
- * .addGroup('level1', {
649
- * background: () => loadTexture('level1-bg.png'),
650
- * music: () => loadAudio('level1.mp3'),
651
- * })
652
- * )
653
- * .build();
654
- * ```
786
+ * Create a plugin factory from the built world's types.
787
+ * Returns a definePlugin equivalent with no manual type parameters.
655
788
  */
656
- withAssets<NewA extends Record<string, unknown>>(configurator: (assets: AssetConfigurator<{}>) => AssetConfigurator<NewA>): ECSpressoBuilder<C, E, R, A & NewA, S>;
789
+ pluginFactory(): <PL extends string = never, PG extends string = never, PAG extends string = never, PRQ extends string = never>(config: {
790
+ id: string;
791
+ install: (world: ECSpresso<Cfg>) => void;
792
+ }) => Plugin<Cfg, EmptyConfig, PL, PG, PAG, PRQ>;
657
793
  /**
658
- * Configure screens for this ECSpresso instance
659
- * @param configurator Function that receives a ScreenConfigurator and returns it after adding screens
660
- * @returns This builder with updated screen types
794
+ * Call a helper factory with this world instance, inferring the full world type.
795
+ * Eliminates the need for a separate `type ECS = typeof ecs` ceremony.
661
796
  *
662
797
  * @example
663
798
  * ```typescript
664
- * ECSpresso.create<Components, Events, Resources>()
665
- * .withScreens(screens => screens
666
- * .add('loading', {
667
- * initialState: () => ({ progress: 0 }),
668
- * })
669
- * .add('gameplay', {
670
- * initialState: ({ level }) => ({ score: 0, level }),
671
- * requiredAssetGroups: ['level1'],
672
- * })
673
- * )
674
- * .build();
799
+ * const helpers = ecs.getHelpers(createStateMachineHelpers);
675
800
  * ```
676
801
  */
677
- withScreens<NewS extends Record<string, ScreenDefinition<any, any>>>(configurator: (screens: ScreenConfigurator<{}>) => ScreenConfigurator<NewS>): ECSpressoBuilder<C, E, R, A, S & NewS>;
678
- /**
679
- * Configure the fixed timestep interval for the fixedUpdate phase.
680
- * @param dt The fixed timestep in seconds (e.g., 1/60 for 60Hz physics)
681
- * @returns This builder for method chaining
682
- */
683
- withFixedTimestep(dt: number): this;
684
- /**
685
- * Complete the build process and return the built ECSpresso instance
686
- */
687
- build(): ECSpresso<C, E, R, A, S>;
802
+ getHelpers<H>(factory: (world: this) => H): H;
688
803
  }
689
- export {};