ecspresso 0.11.0 → 0.12.1

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 (96) hide show
  1. package/README.md +24 -959
  2. package/dist/asset-manager.d.ts +1 -1
  3. package/dist/asset-types.d.ts +2 -2
  4. package/dist/command-buffer.d.ts +34 -24
  5. package/dist/ecspresso-builder.d.ts +100 -72
  6. package/dist/ecspresso.d.ts +257 -123
  7. package/dist/entity-manager.d.ts +57 -47
  8. package/dist/index.d.ts +5 -4
  9. package/dist/plugin.d.ts +61 -0
  10. package/dist/{bundles → plugins}/audio.d.ts +27 -47
  11. package/dist/{bundles → plugins}/bounds.d.ts +17 -25
  12. package/dist/{bundles → plugins}/camera.d.ts +8 -9
  13. package/dist/{bundles → plugins}/collision.d.ts +22 -26
  14. package/dist/plugins/coroutine.d.ts +126 -0
  15. package/dist/{bundles → plugins}/diagnostics.d.ts +5 -4
  16. package/dist/{bundles → plugins}/input.d.ts +9 -15
  17. package/dist/plugins/particles.d.ts +225 -0
  18. package/dist/{bundles → plugins}/physics2D.d.ts +27 -23
  19. package/dist/{bundles → plugins}/renderers/renderer2D.d.ts +40 -39
  20. package/dist/{bundles → plugins}/spatial-index.d.ts +11 -10
  21. package/dist/plugins/sprite-animation.d.ts +150 -0
  22. package/dist/{bundles → plugins}/state-machine.d.ts +50 -104
  23. package/dist/plugins/timers.d.ts +151 -0
  24. package/dist/{bundles → plugins}/transform.d.ts +18 -19
  25. package/dist/{bundles → plugins}/tween.d.ts +36 -71
  26. package/dist/resource-manager.d.ts +32 -7
  27. package/dist/screen-manager.d.ts +26 -14
  28. package/dist/screen-types.d.ts +5 -2
  29. package/dist/src/index.js +2 -2
  30. package/dist/src/index.js.map +17 -17
  31. package/dist/src/plugins/audio.js +4 -0
  32. package/dist/src/plugins/audio.js.map +10 -0
  33. package/dist/src/plugins/bounds.js +4 -0
  34. package/dist/src/plugins/bounds.js.map +10 -0
  35. package/dist/src/plugins/camera.js +4 -0
  36. package/dist/src/plugins/camera.js.map +10 -0
  37. package/dist/src/plugins/collision.js +4 -0
  38. package/dist/src/plugins/collision.js.map +11 -0
  39. package/dist/src/plugins/coroutine.js +4 -0
  40. package/dist/src/plugins/coroutine.js.map +10 -0
  41. package/dist/src/plugins/diagnostics.js +5 -0
  42. package/dist/src/plugins/diagnostics.js.map +10 -0
  43. package/dist/src/plugins/input.js +4 -0
  44. package/dist/src/plugins/input.js.map +10 -0
  45. package/dist/src/plugins/particles.js +4 -0
  46. package/dist/src/plugins/particles.js.map +10 -0
  47. package/dist/src/plugins/physics2D.js +4 -0
  48. package/dist/src/plugins/physics2D.js.map +11 -0
  49. package/dist/src/plugins/renderers/renderer2D.js +4 -0
  50. package/dist/src/plugins/renderers/renderer2D.js.map +10 -0
  51. package/dist/src/plugins/spatial-index.js +4 -0
  52. package/dist/src/plugins/spatial-index.js.map +11 -0
  53. package/dist/src/plugins/sprite-animation.js +4 -0
  54. package/dist/src/plugins/sprite-animation.js.map +10 -0
  55. package/dist/src/plugins/state-machine.js +4 -0
  56. package/dist/src/plugins/state-machine.js.map +10 -0
  57. package/dist/src/plugins/timers.js +4 -0
  58. package/dist/src/plugins/timers.js.map +10 -0
  59. package/dist/src/plugins/transform.js +4 -0
  60. package/dist/src/plugins/transform.js.map +10 -0
  61. package/dist/src/plugins/tween.js +4 -0
  62. package/dist/src/plugins/tween.js.map +11 -0
  63. package/dist/system-builder.d.ts +66 -97
  64. package/dist/type-utils.d.ts +218 -27
  65. package/dist/types.d.ts +52 -24
  66. package/dist/utils/check-required-cycle.d.ts +1 -1
  67. package/dist/utils/narrowphase.d.ts +7 -7
  68. package/package.json +61 -45
  69. package/dist/bundle.d.ts +0 -173
  70. package/dist/bundles/timers.d.ts +0 -173
  71. package/dist/src/bundles/audio.js +0 -4
  72. package/dist/src/bundles/audio.js.map +0 -10
  73. package/dist/src/bundles/bounds.js +0 -4
  74. package/dist/src/bundles/bounds.js.map +0 -10
  75. package/dist/src/bundles/camera.js +0 -4
  76. package/dist/src/bundles/camera.js.map +0 -10
  77. package/dist/src/bundles/collision.js +0 -4
  78. package/dist/src/bundles/collision.js.map +0 -11
  79. package/dist/src/bundles/diagnostics.js +0 -5
  80. package/dist/src/bundles/diagnostics.js.map +0 -10
  81. package/dist/src/bundles/input.js +0 -4
  82. package/dist/src/bundles/input.js.map +0 -10
  83. package/dist/src/bundles/physics2D.js +0 -4
  84. package/dist/src/bundles/physics2D.js.map +0 -11
  85. package/dist/src/bundles/renderers/renderer2D.js +0 -4
  86. package/dist/src/bundles/renderers/renderer2D.js.map +0 -10
  87. package/dist/src/bundles/spatial-index.js +0 -4
  88. package/dist/src/bundles/spatial-index.js.map +0 -11
  89. package/dist/src/bundles/state-machine.js +0 -4
  90. package/dist/src/bundles/state-machine.js.map +0 -10
  91. package/dist/src/bundles/timers.js +0 -4
  92. package/dist/src/bundles/timers.js.map +0 -10
  93. package/dist/src/bundles/transform.js +0 -4
  94. package/dist/src/bundles/transform.js.map +0 -10
  95. package/dist/src/bundles/tween.js +0 -4
  96. package/dist/src/bundles/tween.js.map +0 -11
package/README.md CHANGED
@@ -7,7 +7,7 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
7
7
  ## Features
8
8
 
9
9
  - **Type-Safe**: Full TypeScript support with component, event, and resource type inference
10
- - **Modular**: Bundle-based architecture for organizing features
10
+ - **Modular**: Plugin-based architecture for organizing features
11
11
  - **Developer-Friendly**: Clean, fluent API with method chaining
12
12
  - **Event-Driven**: Integrated event system for decoupled communication
13
13
  - **Resource Management**: Global state management with lazy loading
@@ -15,14 +15,10 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
15
15
  - **Screen Management**: Game state/screen transitions with overlay support
16
16
  - **Entity Hierarchy**: Parent-child relationships with traversal and cascade deletion
17
17
  - **Query System**: Powerful entity filtering with helper type utilities
18
- - **System Phases**: Named execution phases (preUpdate → fixedUpdate → update → postUpdate → render) with fixed-timestep simulation
18
+ - **System Phases**: Named execution phases with fixed-timestep simulation
19
19
  - **Change Detection**: Per-system monotonic sequence change tracking with `changed` query filters
20
20
  - **Reactive Queries**: Enter/exit callbacks when entities match or unmatch queries
21
- - **System Groups**: Enable/disable groups of systems at runtime
22
- - **Component Lifecycle**: Callbacks for component add/remove with unsubscribe support
23
- - **Required Components**: Auto-add dependent components on spawn/addComponent (e.g. `localTransform` implies `worldTransform`)
24
21
  - **Command Buffer**: Deferred structural changes for safe entity/component operations during systems
25
- - **Timer Bundle**: ECS-native timers with event-based completion notifications
26
22
 
27
23
  ## Installation
28
24
 
@@ -30,31 +26,6 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
30
26
  npm install ecspresso
31
27
  ```
32
28
 
33
- ## Table of Contents
34
-
35
- - [Quick Start](#quick-start)
36
- - [Core Concepts](#core-concepts)
37
- - [Entities and Components](#entities-and-components) -- [Component Callbacks](#component-callbacks), [Reactive Queries](#reactive-queries)
38
- - [Systems and Queries](#systems-and-queries)
39
- - [Resources](#resources)
40
- - [Systems in Depth](#systems-in-depth)
41
- - [Method Chaining](#method-chaining)
42
- - [Query Type Utilities](#query-type-utilities)
43
- - [System Phases](#system-phases)
44
- - [System Priority](#system-priority)
45
- - [System Groups](#system-groups)
46
- - [System Lifecycle](#system-lifecycle)
47
- - [Events](#events)
48
- - [Entity Hierarchy](#entity-hierarchy) -- [Traversal](#traversal), [Parent-First Traversal](#parent-first-traversal), [Cascade Deletion](#cascade-deletion)
49
- - [Change Detection](#change-detection) -- [Marking Changes](#marking-changes), [Changed Query Filter](#changed-query-filter), [Sequence Timing](#sequence-timing)
50
- - [Command Buffer](#command-buffer) -- [Available Commands](#available-commands)
51
- - [Bundles](#bundles) -- [Required Components](#required-components), [Built-in Bundles](#built-in-bundles), [Timer Bundle](#timer-bundle)
52
- - [Asset Management](#asset-management)
53
- - [Screen Management](#screen-management) -- [Screen-Scoped Systems](#screen-scoped-systems), [Screen Resource](#screen-resource)
54
- - [Type Safety](#type-safety)
55
- - [Error Handling](#error-handling)
56
- - [Performance Tips](#performance-tips)
57
-
58
29
  ## Quick Start
59
30
 
60
31
  ```typescript
@@ -67,8 +38,10 @@ interface Components {
67
38
  health: { value: number };
68
39
  }
69
40
 
70
- // 2. Create a world
71
- const world = new ECSpresso<Components>();
41
+ // 2. Create a world using the builder — types are inferred automatically
42
+ const world = ECSpresso.create()
43
+ .withComponentTypes<Components>()
44
+ .build();
72
45
 
73
46
  // 3. Add a movement system
74
47
  world.addSystem('movement')
@@ -78,8 +51,7 @@ world.addSystem('movement')
78
51
  entity.components.position.x += entity.components.velocity.x * deltaTime;
79
52
  entity.components.position.y += entity.components.velocity.y * deltaTime;
80
53
  }
81
- })
82
- .build();
54
+ });
83
55
 
84
56
  // 4. Create entities
85
57
  const player = world.spawn({
@@ -92,930 +64,23 @@ const player = world.spawn({
92
64
  world.update(1/60);
93
65
  ```
94
66
 
95
- ## Core Concepts
96
-
97
- ### Entities and Components
98
-
99
- Entities are containers for components. Use `spawn()` to create entities with initial components:
100
-
101
- ```typescript
102
- // Create entity with components
103
- const entity = world.spawn({
104
- position: { x: 10, y: 20 },
105
- health: { value: 100 }
106
- });
107
-
108
- // Add components later
109
- world.entityManager.addComponent(entity.id, 'velocity', { x: 5, y: 0 });
110
-
111
- // Get component data (returns undefined if not found)
112
- const position = world.entityManager.getComponent(entity.id, 'position');
113
-
114
- // Remove components or entities
115
- world.entityManager.removeComponent(entity.id, 'velocity');
116
- world.entityManager.removeEntity(entity.id);
117
- ```
118
-
119
- #### Component Callbacks
120
-
121
- React to component additions and removals. Both methods return an unsubscribe function:
122
-
123
- ```typescript
124
- const unsubAdd = world.onComponentAdded('health', (value, entity) => {
125
- console.log(`Health added to entity ${entity.id}:`, value);
126
- });
127
-
128
- const unsubRemove = world.onComponentRemoved('health', (oldValue, entity) => {
129
- console.log(`Health removed from entity ${entity.id}:`, oldValue);
130
- });
131
-
132
- // Unsubscribe when done
133
- unsubAdd();
134
- unsubRemove();
135
- ```
136
-
137
- Also available on `world.entityManager.onComponentAdded()` / `onComponentRemoved()`.
138
-
139
- #### Reactive Queries
140
-
141
- Get callbacks when entities enter or exit a query match. Unlike regular queries that you poll during `update()`, reactive queries push notifications when the entity's components change:
142
-
143
- ```typescript
144
- world.addReactiveQuery('enemies', {
145
- with: ['position', 'enemy'],
146
- without: ['dead'],
147
- onEnter: (entity) => {
148
- console.log(`Enemy ${entity.id} appeared at`, entity.components.position);
149
- spawnHealthBar(entity.id);
150
- },
151
- onExit: (entityId) => {
152
- // Receives ID since entity may already be removed
153
- console.log(`Enemy ${entityId} gone`);
154
- removeHealthBar(entityId);
155
- },
156
- });
157
-
158
- // Triggers onEnter: spawning matching entity, adding required component, removing excluded component
159
- const enemy = world.spawn({ position: { x: 0, y: 0 }, enemy: true }); // onEnter fires
160
-
161
- // Triggers onExit: removing required component, adding excluded component, removing entity
162
- world.entityManager.addComponent(enemy.id, 'dead', true); // onExit fires
163
-
164
- // Existing matching entities trigger onEnter when query is added
165
- // Component replacement does NOT trigger enter/exit (match status unchanged)
166
-
167
- // Remove reactive query when no longer needed
168
- world.removeReactiveQuery('enemies'); // returns true if existed
169
- ```
170
-
171
- ### Systems and Queries
172
-
173
- Systems process entities that match specific component patterns:
174
-
175
- ```typescript
176
- world.addSystem('combat')
177
- .addQuery('fighters', {
178
- with: ['position', 'health'],
179
- without: ['dead']
180
- })
181
- .addQuery('projectiles', {
182
- with: ['position', 'damage']
183
- })
184
- .setProcess((queries, deltaTime) => {
185
- for (const fighter of queries.fighters) {
186
- for (const projectile of queries.projectiles) {
187
- // Combat logic here
188
- }
189
- }
190
- })
191
- .build();
192
- ```
193
-
194
- ### Resources
195
-
196
- Resources provide global state accessible to all systems.
197
-
198
- ```typescript
199
- interface Resources {
200
- score: { value: number };
201
- settings: { difficulty: 'easy' | 'hard' };
202
- }
203
-
204
- const world = new ECSpresso<Components, {}, Resources>();
205
-
206
- // Direct values
207
- world.addResource('score', { value: 0 });
208
-
209
- // Sync or async factories (lazy initialization)
210
- world.addResource('config', () => ({ difficulty: 'normal', soundEnabled: true }));
211
- world.addResource('database', async () => await connectToDatabase());
212
-
213
- // Factory with dependencies (initialized after dependencies are ready)
214
- world.addResource('cache', {
215
- dependsOn: ['database'],
216
- factory: (ecs) => ({ db: ecs.getResource('database') })
217
- });
218
-
219
- // Initialize all resources (respects dependency order, detects circular deps)
220
- await world.initializeResources();
221
-
222
- // Use in systems
223
- world.addSystem('scoring')
224
- .setProcess((queries, deltaTime, ecs) => {
225
- const score = ecs.getResource('score');
226
- score.value += 10;
227
- })
228
- .build();
229
- ```
230
-
231
- **Builder pattern** -- resources chain naturally with other builder methods:
232
-
233
- ```typescript
234
- const world = ECSpresso
235
- .create<Components, Events, Resources>()
236
- .withBundle(physicsBundle)
237
- .withResource('config', { debug: true, maxEntities: 1000 })
238
- .withResource('score', () => ({ value: 0 }))
239
- .withResource('cache', {
240
- dependsOn: ['database'],
241
- factory: (ecs) => createCache(ecs.getResource('database'))
242
- })
243
- .build();
244
- ```
245
-
246
- **Disposal** -- resources can define cleanup logic with `onDispose` callbacks:
247
-
248
- ```typescript
249
- world.addResource('keyboard', {
250
- factory: () => {
251
- const handler = (e: KeyboardEvent) => { /* ... */ };
252
- window.addEventListener('keydown', handler);
253
- return { handler };
254
- },
255
- onDispose: (resource) => {
256
- window.removeEventListener('keydown', resource.handler);
257
- }
258
- });
259
-
260
- await world.disposeResource('keyboard'); // Dispose a single resource
261
- await world.disposeResources(); // All, in reverse dependency order
262
- ```
263
-
264
- `onDispose` receives the resource value and the ECSpresso instance. Supports sync and async callbacks. Only initialized resources have their `onDispose` called. `removeResource()` still exists for removal without disposal.
265
-
266
- ## Systems in Depth
267
-
268
- ### Method Chaining
269
-
270
- Chain multiple systems using `.and()`. The `.and()` method returns the parent container (ECSpresso or Bundle), enabling fluent chaining:
271
-
272
- ```typescript
273
- world.addSystem('physics')
274
- .addQuery('moving', { with: ['position', 'velocity'] })
275
- .setProcess((queries, deltaTime) => {
276
- // Physics logic
277
- })
278
- .and() // Returns ECSpresso for continued chaining
279
- .addSystem('rendering')
280
- .addQuery('visible', { with: ['position', 'sprite'] })
281
- .setProcess((queries) => {
282
- // Rendering logic
283
- })
284
- .build();
285
- ```
286
-
287
- ### Query Type Utilities
288
-
289
- Extract entity types from queries to create reusable helper functions:
290
-
291
- ```typescript
292
- import { createQueryDefinition, QueryResultEntity } from 'ecspresso';
293
-
294
- // Create reusable query definitions
295
- const movingQuery = createQueryDefinition<Components>({
296
- with: ['position', 'velocity'],
297
- without: ['frozen']
298
- });
299
-
300
- // Extract entity type for helper functions
301
- type MovingEntity = QueryResultEntity<Components, typeof movingQuery>;
302
-
303
- function updatePosition(entity: MovingEntity, deltaTime: number) {
304
- entity.components.position.x += entity.components.velocity.x * deltaTime;
305
- entity.components.position.y += entity.components.velocity.y * deltaTime;
306
- }
307
-
308
- // Use in systems
309
- world.addSystem('movement')
310
- .addQuery('entities', movingQuery)
311
- .setProcess((queries, deltaTime) => {
312
- for (const entity of queries.entities) {
313
- updatePosition(entity, deltaTime);
314
- }
315
- })
316
- .build();
317
- ```
318
-
319
- ### System Phases
320
-
321
- Systems are organized into named execution phases that run in a fixed order:
322
-
323
- ```
324
- preUpdate → fixedUpdate → update → postUpdate → render
325
- ```
326
-
327
- Each phase's command buffer is played back before the next phase begins, so entities spawned in `preUpdate` are visible to `fixedUpdate`, and so on. Systems without `.inPhase()` default to `update`.
328
-
329
- ```typescript
330
- world.addSystem('input')
331
- .inPhase('preUpdate')
332
- .setProcess((queries, dt, ecs) => { /* Read input, update timers */ })
333
- .and()
334
- .addSystem('physics')
335
- .inPhase('fixedUpdate')
336
- .setProcess((queries, dt, ecs) => {
337
- // dt is always fixedDt here (e.g. 1/60)
338
- // Runs 0..N times per frame based on accumulated time
339
- })
340
- .and()
341
- .addSystem('gameplay')
342
- .inPhase('update') // default phase
343
- .setProcess((queries, dt, ecs) => { /* Game logic, AI */ })
344
- .and()
345
- .addSystem('transform-sync')
346
- .inPhase('postUpdate')
347
- .setProcess((queries, dt, ecs) => { /* Transform propagation */ })
348
- .and()
349
- .addSystem('renderer')
350
- .inPhase('render')
351
- .setProcess((queries, dt, ecs) => { /* Visual output */ })
352
- .build();
353
- ```
354
-
355
- **Fixed Timestep** -- The `fixedUpdate` phase uses a time accumulator for deterministic simulation. A spiral-of-death cap (8 steps) prevents runaway accumulation.
356
-
357
- ```typescript
358
- const world = ECSpresso.create<Components, Events, Resources>()
359
- .withFixedTimestep(1 / 60) // 60Hz physics (default)
360
- .build();
361
- ```
362
-
363
- **Interpolation** -- Use `ecs.interpolationAlpha` (0..1) in the render phase to smooth between fixed steps.
364
-
365
- **Runtime Phase Changes** -- Move systems between phases at runtime with `world.updateSystemPhase('debug-overlay', 'render')`.
366
-
367
- ### System Priority
368
-
369
- Within each phase, systems execute in priority order (higher numbers first). Systems with the same priority execute in registration order:
370
-
371
- ```typescript
372
- world.addSystem('physics')
373
- .inPhase('fixedUpdate')
374
- .setPriority(100) // Runs first within fixedUpdate
375
- .setProcess(() => { /* physics */ })
376
- .and()
377
- .addSystem('constraints')
378
- .inPhase('fixedUpdate')
379
- .setPriority(50) // Runs second within fixedUpdate
380
- .setProcess(() => { /* constraints */ })
381
- .build();
382
- ```
383
-
384
- ### System Groups
385
-
386
- Organize systems into groups that can be enabled/disabled at runtime:
387
-
388
- ```typescript
389
- world.addSystem('renderSprites')
390
- .inGroup('rendering')
391
- .addQuery('sprites', { with: ['position', 'sprite'] })
392
- .setProcess((queries) => { /* ... */ })
393
- .and()
394
- .addSystem('renderParticles')
395
- .inGroup('rendering')
396
- .inGroup('effects') // Systems can belong to multiple groups
397
- .setProcess(() => { /* ... */ })
398
- .build();
399
-
400
- world.disableSystemGroup('rendering'); // All rendering systems skip
401
- world.enableSystemGroup('rendering'); // Resume rendering
402
- world.isSystemGroupEnabled('rendering'); // true/false
403
- world.getSystemsInGroup('rendering'); // ['renderSprites', 'renderParticles']
404
-
405
- // If a system belongs to multiple groups, disabling ANY group skips the system
406
- ```
407
-
408
- ### System Lifecycle
409
-
410
- Systems can have initialization, cleanup, and post-update hooks:
411
-
412
- ```typescript
413
- world.addSystem('gameSystem')
414
- .setOnInitialize(async (ecs) => {
415
- console.log('System starting...');
416
- })
417
- .setOnDetach((ecs) => {
418
- console.log('System shutting down...');
419
- })
420
- .build();
421
-
422
- await world.initialize();
423
- ```
424
-
425
- **Post-Update Hooks** -- Register callbacks that run between the `postUpdate` and `render` phases:
426
-
427
- ```typescript
428
- // Returns unsubscribe function; multiple hooks run in registration order
429
- const unsubscribe = world.onPostUpdate((ecs, deltaTime) => {
430
- console.log(`Frame completed in ${deltaTime}s`);
431
- });
432
-
433
- unsubscribe();
434
- ```
435
-
436
- ## Events
437
-
438
- Use events for decoupled system communication. Events work across all features -- hierarchy changes, asset loading, timer completion, and custom game events all use the same system.
439
-
440
- ```typescript
441
- interface Events {
442
- playerDied: { playerId: number };
443
- levelComplete: { score: number };
444
- // Hierarchy events (if using entity hierarchy)
445
- hierarchyChanged: {
446
- entityId: number;
447
- oldParent: number | null;
448
- newParent: number | null;
449
- };
450
- }
451
-
452
- const world = new ECSpresso<Components, Events>();
453
-
454
- // Subscribe - returns unsubscribe function
455
- const unsubscribe = world.on('playerDied', (data) => {
456
- console.log(`Player ${data.playerId} died`);
457
- });
458
- unsubscribe();
459
-
460
- // Or unsubscribe by callback reference
461
- const handler = (data) => console.log(`Score: ${data.score}`);
462
- world.on('levelComplete', handler);
463
- world.off('levelComplete', handler);
464
-
465
- // Handle events in systems
466
- world.addSystem('gameLogic')
467
- .setEventHandlers({
468
- playerDied: {
469
- handler: (data, ecs) => {
470
- // Respawn logic
471
- }
472
- }
473
- })
474
- .build();
475
-
476
- // Publish events from anywhere
477
- world.eventBus.publish('playerDied', { playerId: 1 });
478
- ```
479
-
480
- **Built-in events**: `hierarchyChanged` (entity parent changes), `assetLoaded` / `assetFailed` / `assetGroupProgress` / `assetGroupLoaded` (asset loading), and timer `onComplete` events (see [Bundles](#bundles)).
481
-
482
- ## Entity Hierarchy
483
-
484
- Create parent-child relationships between entities for scene graphs, UI trees, or skeletal hierarchies:
485
-
486
- ```typescript
487
- const player = world.spawn({ position: { x: 0, y: 0 } });
488
-
489
- // Create child entity
490
- const weapon = world.spawnChild(player.id, { position: { x: 10, y: 0 } });
491
-
492
- // Or set parent on existing entity
493
- const shield = world.spawn({ position: { x: -10, y: 0 } });
494
- world.setParent(shield.id, player.id);
495
-
496
- // Orphan an entity
497
- world.removeParent(shield.id);
498
- ```
499
-
500
- ### Traversal
501
-
502
- | Method | Returns | Description |
503
- |--------|---------|-------------|
504
- | `getParent(id)` | `number \| null` | Parent entity ID |
505
- | `getChildren(id)` | `number[]` | Direct children |
506
- | `getAncestors(id)` | `number[]` | Entity up to root |
507
- | `getDescendants(id)` | `number[]` | Depth-first order |
508
- | `getRoot(id)` | `number` | Root of the hierarchy |
509
- | `getSiblings(id)` | `number[]` | Other children of same parent |
510
- | `getRootEntities()` | `number[]` | All root entities |
511
- | `getChildAt(id, index)` | `number` | Child at index |
512
- | `getChildIndex(parentId, childId)` | `number` | Index of child |
513
- | `isDescendantOf(id, ancestorId)` | `boolean` | Relationship check |
514
- | `isAncestorOf(id, descendantId)` | `boolean` | Relationship check |
515
-
516
- ### Parent-First Traversal
517
-
518
- Iterate the hierarchy with guaranteed parent-first order (useful for transform propagation):
519
-
520
- ```typescript
521
- // Callback-based traversal
522
- world.forEachInHierarchy((entityId, parentId, depth) => {
523
- // Parents are always visited before their children
524
- });
525
-
526
- // Filter to specific subtrees
527
- world.forEachInHierarchy(callback, { roots: [root.id] });
528
-
529
- // Generator-based (supports early termination)
530
- for (const { entityId, parentId, depth } of world.hierarchyIterator()) {
531
- if (depth > 2) break;
532
- }
533
- ```
534
-
535
- ### Cascade Deletion
536
-
537
- When removing entities, descendants are automatically removed by default:
538
-
539
- ```typescript
540
- world.removeEntity(parent.id);
541
- // All descendants are removed
542
-
543
- // To orphan children instead:
544
- world.removeEntity(parent.id, { cascade: false });
545
- ```
546
-
547
- Hierarchy changes emit the `hierarchyChanged` event (see [Events](#events)).
548
-
549
- **World position pattern**: `worldPos = localPos + parent.worldPos`. A parent's world position already includes all grandparents, so each entity only needs to combine its local position with its immediate parent's world position. The Transform bundle implements this automatically.
550
-
551
- ## Change Detection
552
-
553
- ECSpresso tracks component changes using a per-system monotonic sequence. Each `markChanged` call increments a global counter and stamps the component with a unique sequence number. Each system tracks the highest sequence it has seen; on its next execution, it only processes marks with a sequence greater than its last-seen value. This means each mark is processed exactly once per system, and marks expire after a single update cycle.
554
-
555
- ### Marking Changes
556
-
557
- Components are automatically marked as changed when added via `spawn()`, `addComponent()`, or `addComponents()`. For in-place mutations, call `markChanged` explicitly:
558
-
559
- ```typescript
560
- const position = world.entityManager.getComponent(entity.id, 'position');
561
- if (position) {
562
- position.x += 10;
563
- world.markChanged(entity.id, 'position');
564
- }
565
- ```
566
-
567
- ### Changed Query Filter
568
-
569
- Add `changed` to a query definition to filter entities to only those whose specified components changed since the system last ran:
570
-
571
- ```typescript
572
- world.addSystem('render-sync')
573
- .addQuery('moved', {
574
- with: ['position', 'sprite'],
575
- changed: ['position'], // Only entities whose position changed this tick
576
- })
577
- .setProcess((queries) => {
578
- for (const entity of queries.moved) {
579
- syncSpritePosition(entity);
580
- }
581
- })
582
- .build();
583
- ```
584
-
585
- When multiple components are listed in `changed`, entities matching **any** of them are included (OR semantics).
586
-
587
- ### Sequence Timing
588
-
589
- - Marks made between updates are visible to all systems on the next update
590
- - Spawn auto-marks are visible on the first update
591
- - Marks from earlier phases are visible to later phases within the same frame
592
- - Within a phase, a higher-priority system's marks are visible to lower-priority systems
593
- - Each mark is processed exactly once per system (single-update expiry)
594
-
595
- For manual change detection outside of system queries:
596
-
597
- ```typescript
598
- const em = ecs.entityManager;
599
- if (em.getChangeSeq(entity.id, 'localTransform') > ecs.changeThreshold) {
600
- // Component changed since last system execution (or since last update if between updates)
601
- }
602
- ```
603
-
604
- **Deferred marking**: `ecs.commands.markChanged(entity.id, 'position')` queues a mark for command buffer playback.
605
-
606
- **Built-in bundle usage**: Movement marks `localTransform` (fixedUpdate) → Transform propagation reads `localTransform` changed, writes+marks `worldTransform` (postUpdate) → Renderer reads `worldTransform` changed (render).
607
-
608
- ## Command Buffer
609
-
610
- Queue structural changes during system execution that execute between phases. This prevents issues when modifying entities during iteration.
611
-
612
- ```typescript
613
- world.addSystem('combat')
614
- .addQuery('enemies', { with: ['enemy', 'health'] })
615
- .setProcess((queries, dt, ecs) => {
616
- for (const entity of queries.enemies) {
617
- if (entity.components.health.value <= 0) {
618
- ecs.commands.removeEntity(entity.id);
619
- ecs.commands.spawn({
620
- position: entity.components.position,
621
- explosion: true,
622
- });
623
- }
624
- }
625
- })
626
- .build();
627
- ```
628
-
629
- ### Available Commands
630
-
631
- ```typescript
632
- // Entity operations
633
- ecs.commands.spawn({ position: { x: 0, y: 0 } });
634
- ecs.commands.spawnChild(parentId, { position: { x: 10, y: 0 } });
635
- ecs.commands.removeEntity(entityId);
636
- ecs.commands.removeEntity(entityId, { cascade: false });
637
-
638
- // Component operations
639
- ecs.commands.addComponent(entityId, 'velocity', { x: 5, y: 0 });
640
- ecs.commands.addComponents(entityId, { velocity: { x: 5, y: 0 }, health: { value: 100 } });
641
- ecs.commands.removeComponent(entityId, 'velocity');
642
-
643
- // Hierarchy operations
644
- ecs.commands.setParent(childId, parentId);
645
- ecs.commands.removeParent(childId);
646
-
647
- // Change detection
648
- ecs.commands.markChanged(entityId, 'position');
649
-
650
- // Utility
651
- ecs.commands.length; // Number of queued commands
652
- ecs.commands.clear(); // Discard all queued commands
653
- ```
654
-
655
- Commands execute in FIFO order. If a command fails (e.g., entity doesn't exist), it logs a warning and continues with remaining commands.
656
-
657
- ## Bundles
658
-
659
- Organize related systems and resources into reusable bundles:
660
-
661
- ```typescript
662
- import { Bundle } from 'ecspresso';
663
-
664
- const physicsBundle = new Bundle<GameComponents, {}, GameResources>('physics')
665
- .addSystem('applyVelocity')
666
- .addQuery('moving', { with: ['position', 'velocity'] })
667
- .setProcess((queries, deltaTime) => {
668
- for (const entity of queries.moving) {
669
- entity.components.position.x += entity.components.velocity.x * deltaTime;
670
- entity.components.position.y += entity.components.velocity.y * deltaTime;
671
- }
672
- })
673
- .and()
674
- .addSystem('applyGravity')
675
- .addQuery('falling', { with: ['velocity'] })
676
- .setProcess((queries, deltaTime, ecs) => {
677
- const gravity = ecs.getResource('gravity');
678
- for (const entity of queries.falling) {
679
- entity.components.velocity.y += gravity.value * deltaTime;
680
- }
681
- })
682
- .and()
683
- .addResource('gravity', { value: 9.8 });
684
-
685
- // Register bundles with the world
686
- const game = ECSpresso.create<GameComponents, {}, GameResources>()
687
- .withBundle(physicsBundle)
688
- .build();
689
- ```
690
-
691
- ### Required Components
692
-
693
- Bundles can declare that certain components depend on others. When an entity gains a trigger component, any required components that aren't already present are auto-added with default values:
694
-
695
- ```typescript
696
- const transformBundle = new Bundle<TransformComponents>('transform')
697
- .registerRequired('localTransform', 'worldTransform', () => ({
698
- x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
699
- }));
700
-
701
- const world = ECSpresso.create()
702
- .withBundle(transformBundle)
703
- .build();
704
-
705
- // worldTransform is auto-added with defaults
706
- const entity = world.spawn({
707
- localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
708
- });
709
-
710
- // Explicit values always win — no auto-add if already provided
711
- const entity2 = world.spawn({
712
- localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
713
- worldTransform: { x: 50, y: 50, rotation: 0, scaleX: 2, scaleY: 2 }, // used as-is
714
- });
715
- ```
716
-
717
- Requirements can also be registered via the builder or at runtime:
718
-
719
- ```typescript
720
- // Builder
721
- const world = ECSpresso.create()
722
- .withComponentTypes<Components>()
723
- .withRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }))
724
- .withRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }))
725
- .build();
726
-
727
- // Runtime
728
- world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
729
- ```
730
-
731
- **Behavior:**
732
- - Enforced at insertion time (`spawn`, `addComponent`, `addComponents`, `spawnChild`, command buffer)
733
- - Removal is unrestricted — removing a required component does not cascade
734
- - Transitive requirements resolve automatically (A requires B, B requires C → all three added)
735
- - Circular dependencies are detected and rejected at registration time
736
- - Auto-added components are marked as changed and trigger reactive queries
737
- - Component names and factory return types are fully type-checked
738
-
739
- **Built-in requirements:** The Transform bundle registers `localTransform` → `worldTransform`. The Physics 2D bundle registers `rigidBody` → `velocity` and `rigidBody` → `force`.
740
-
741
- ### Built-in Bundles
742
-
743
- | Bundle | Import | Default Phase | Description |
744
- |--------|--------|---------------|-------------|
745
- | **Input** | `ecspresso/bundles/input` | `preUpdate` | Frame-accurate keyboard/pointer input with action mapping |
746
- | **Timers** | `ecspresso/bundles/timers` | `preUpdate` | ECS-native timers with event-based completion |
747
- | **Movement** | `ecspresso/bundles/movement` | `fixedUpdate` | Velocity-based movement integration |
748
- | **Transform** | `ecspresso/bundles/transform` | `postUpdate` | Hierarchical transform propagation (local/world transforms) |
749
- | **Bounds** | `ecspresso/bundles/bounds` | `postUpdate` | Screen bounds enforcement (destroy, clamp, wrap) |
750
- | **Collision** | `ecspresso/bundles/collision` | `postUpdate` | Layer-based AABB/circle collision detection with events |
751
- | **2D Renderer** | `ecspresso/bundles/renderers/renderer2D` | `render` | Automated PixiJS scene graph wiring |
752
-
753
- Each bundle accepts a `phase` option to override its default.
754
-
755
- ### Input Bundle
756
-
757
- The input bundle provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent), and named action mapping. It's a resource-only bundle — input is polled via the `inputState` resource. DOM events are accumulated between frames and snapshotted once per frame, so all systems see consistent state.
758
-
759
- ```typescript
760
- import {
761
- createInputBundle,
762
- type InputResourceTypes, type KeyCode
763
- } from 'ecspresso/bundles/input';
764
-
765
- const world = ECSpresso.create()
766
- .withBundle(createInputBundle({
767
- actions: {
768
- jump: { keys: [' ', 'ArrowUp'] },
769
- shoot: { keys: ['z'], buttons: [0] },
770
- moveLeft: { keys: ['a', 'ArrowLeft'] },
771
- moveRight: { keys: ['d', 'ArrowRight'] },
772
- },
773
- }))
774
- .build();
775
-
776
- // In a system:
777
- const input = ecs.getResource('inputState');
778
- if (input.actions.justActivated('jump')) { /* ... */ }
779
- if (input.keyboard.isDown('ArrowRight')) { /* ... */ }
780
- if (input.pointer.justPressed(0)) { /* ... */ }
781
-
782
- // Runtime remapping — must include all configured actions
783
- input.setActionMap({
784
- jump: { keys: ['w'] },
785
- shoot: { keys: ['z'], buttons: [0] },
786
- moveLeft: { keys: ['a'] },
787
- moveRight: { keys: ['d'] },
788
- });
789
- ```
790
-
791
- Action names are type-safe — `isActive`, `justActivated`, `justDeactivated`, `setActionMap`, and `getActionMap` only accept action names from the config. The type parameter `A` is inferred from the `actions` object keys passed to `createInputBundle`. Defaults to `string` when no actions are configured.
792
-
793
- Key values use the `KeyCode` type — a union of all standard `KeyboardEvent.key` values — providing autocomplete and compile-time validation. Note that the space bar key is `' '` (a space character), not `'Space'`.
794
-
795
- ### Timer Bundle
796
-
797
- The timer bundle provides ECS-native timers that follow the "data, not callbacks" philosophy. Timers are components processed each frame, with optional event-based completion notifications.
798
-
799
- ```typescript
800
- import {
801
- createTimerBundle, createTimer, createRepeatingTimer,
802
- type TimerComponentTypes, type TimerEventData
803
- } from 'ecspresso/bundles/timers';
804
-
805
- // Events used with onComplete must have TimerEventData payload
806
- interface Events {
807
- hideMessage: TimerEventData; // { entityId, duration, elapsed }
808
- spawnWave: TimerEventData;
809
- }
810
-
811
- interface Components extends TimerComponentTypes<Events> {
812
- position: { x: number; y: number };
813
- }
814
-
815
- const world = ECSpresso
816
- .create<Components, Events>()
817
- .withBundle(createTimerBundle<Events>())
818
- .build();
819
-
820
- // One-shot timer (poll justFinished or use onComplete event)
821
- world.spawn({ ...createTimer<Events>(2.0), position: { x: 0, y: 0 } });
822
- world.spawn({ ...createTimer<Events>(1.5, { onComplete: 'hideMessage' }) });
823
-
824
- // Repeating timer
825
- world.spawn({ ...createRepeatingTimer<Events>(5.0, { onComplete: 'spawnWave' }) });
826
- ```
827
-
828
- Timer components expose `elapsed`, `duration`, `repeat`, `active`, `justFinished`, and optional `onComplete` for runtime control.
829
-
830
- ## Asset Management
831
-
832
- Manage game assets with eager/lazy loading, groups, and progress tracking:
833
-
834
- ```typescript
835
- type Assets = {
836
- playerTexture: { data: ImageBitmap };
837
- level1Music: { buffer: AudioBuffer };
838
- level1Background: { data: ImageBitmap };
839
- };
840
-
841
- const game = ECSpresso.create<Components, Events, Resources, Assets>()
842
- .withAssets(assets => assets
843
- // Eager assets - loaded automatically during initialize()
844
- .add('playerTexture', async () => {
845
- const img = await loadImage('player.png');
846
- return { data: img };
847
- })
848
- // Lazy asset group - loaded on demand
849
- .addGroup('level1', {
850
- level1Music: async () => ({ buffer: await loadAudio('level1.mp3') }),
851
- level1Background: async () => ({ data: await loadImage('level1-bg.png') }),
852
- })
853
- )
854
- .build();
855
-
856
- await game.initialize(); // Loads eager assets
857
- const player = game.getAsset('playerTexture'); // Access loaded asset
858
- game.isAssetLoaded('playerTexture'); // Check if loaded
859
-
860
- await game.loadAssetGroup('level1'); // Load group on demand
861
- game.getAssetGroupProgress('level1'); // 0-1 progress
862
- game.isAssetGroupLoaded('level1'); // Check if group is ready
863
- ```
864
-
865
- Systems can declare required assets and will only run when those assets are loaded:
866
-
867
- ```typescript
868
- game.addSystem('gameplay')
869
- .requiresAssets(['playerTexture'])
870
- .setProcess((queries, dt, ecs) => {
871
- const player = ecs.getAsset('playerTexture');
872
- })
873
- .build();
874
- ```
875
-
876
- Asset events (`assetLoaded`, `assetFailed`, `assetGroupProgress`, `assetGroupLoaded`) are available through the event system -- see [Events](#events).
877
-
878
- ## Screen Management
67
+ ## Documentation
879
68
 
880
- Manage game states/screens with transitions and overlay support:
881
-
882
- ```typescript
883
- import type { ScreenDefinition } from 'ecspresso';
884
-
885
- type Screens = {
886
- menu: ScreenDefinition<
887
- Record<string, never>, // Config (passed when entering)
888
- { selectedOption: number } // State (mutable during screen)
889
- >;
890
- gameplay: ScreenDefinition<
891
- { difficulty: string; level: number },
892
- { score: number; isPaused: boolean }
893
- >;
894
- pause: ScreenDefinition<Record<string, never>, Record<string, never>>;
895
- };
896
-
897
- const game = ECSpresso.create<Components, Events, Resources, {}, Screens>()
898
- .withScreens(screens => screens
899
- .add('menu', {
900
- initialState: () => ({ selectedOption: 0 }),
901
- onEnter: () => console.log('Entered menu'),
902
- onExit: () => console.log('Left menu'),
903
- })
904
- .add('gameplay', {
905
- initialState: () => ({ score: 0, isPaused: false }),
906
- onEnter: (config) => console.log(`Starting level ${config.level}`),
907
- onExit: () => console.log('Gameplay ended'),
908
- requiredAssetGroups: ['level1'],
909
- })
910
- .add('pause', {
911
- initialState: () => ({}),
912
- })
913
- )
914
- .build();
915
-
916
- await game.initialize();
917
- await game.setScreen('menu', {}); // Set initial screen
918
- await game.setScreen('gameplay', { difficulty: 'hard', level: 1 }); // Transition
919
- await game.pushScreen('pause', {}); // Push overlay
920
- await game.popScreen(); // Pop overlay
921
-
922
- const current = game.getCurrentScreen(); // 'gameplay'
923
- const config = game.getScreenConfig(); // { difficulty: 'hard', level: 1 }
924
- const state = game.getScreenState(); // { score: 0, isPaused: false }
925
- game.updateScreenState({ score: 100 });
926
- ```
927
-
928
- ### Screen-Scoped Systems
929
-
930
- ```typescript
931
- game.addSystem('menuUI')
932
- .inScreens(['menu']) // Only runs in 'menu'
933
- .setProcess((queries, dt, ecs) => {
934
- renderMenu(ecs.getScreenState().selectedOption);
935
- })
936
- .build();
937
-
938
- game.addSystem('animations')
939
- .excludeScreens(['pause']) // Runs in all screens except 'pause'
940
- .setProcess(() => { /* ... */ })
941
- .build();
942
- ```
943
-
944
- ### Screen Resource
945
-
946
- Access screen state through the `$screen` resource:
947
-
948
- ```typescript
949
- game.addSystem('ui')
950
- .setProcess((queries, dt, ecs) => {
951
- const screen = ecs.getResource('$screen');
952
- screen.current; // Current screen name
953
- screen.config; // Current screen config
954
- screen.state; // Current screen state (mutable)
955
- screen.isOverlay; // true if screen was pushed
956
- screen.stackDepth; // Number of screens in stack
957
- screen.isCurrent('gameplay'); // Check current screen
958
- screen.isActive('menu'); // true if in current or stack
959
- })
960
- .build();
961
- ```
962
-
963
- ## Type Safety
964
-
965
- ECSpresso provides comprehensive TypeScript support:
966
-
967
- ```typescript
968
- // ✅ Valid
969
- world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
970
-
971
- // ❌ TypeScript error - invalid component name
972
- world.entityManager.addComponent(entity.id, 'invalid', { data: 'bad' });
973
-
974
- // ❌ TypeScript error - wrong component shape
975
- world.entityManager.addComponent(entity.id, 'position', { x: 0 }); // missing y
976
-
977
- // Query type safety - TypeScript knows which components exist
978
- world.addSystem('example')
979
- .addQuery('moving', { with: ['position', 'velocity'] })
980
- .setProcess((queries) => {
981
- for (const entity of queries.moving) {
982
- entity.components.position.x; // ✅ guaranteed
983
- entity.components.health.value; // ❌ not in query
984
- }
985
- })
986
- .build();
987
-
988
- // Bundle type compatibility - conflicting types error at compile time
989
- const bundle1 = new Bundle<{position: {x: number, y: number}}>('b1');
990
- const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('b2');
991
- const world = ECSpresso.create()
992
- .withBundle(bundle1)
993
- .withBundle(bundle2) // Types merge successfully
994
- .build();
995
- ```
996
-
997
- ## Error Handling
998
-
999
- ECSpresso provides clear, contextual error messages:
1000
-
1001
- ```typescript
1002
- world.getResource('nonexistent');
1003
- // → "Resource 'nonexistent' not found. Available resources: [config, score, settings]"
1004
-
1005
- world.entityManager.addComponent(999, 'position', { x: 0, y: 0 });
1006
- // → "Cannot add component 'position': Entity with ID 999 does not exist"
1007
-
1008
- // Component not found returns undefined (no throw)
1009
- world.entityManager.getComponent(123, 'position'); // undefined
1010
- ```
69
+ - [Getting Started](./docs/getting-started.md)
70
+ - [Core Concepts](./docs/core-concepts.md) — entities, components, systems, resources
71
+ - [Systems](./docs/systems.md) — phases, priority, groups, lifecycle
72
+ - [Queries](./docs/queries.md) type utilities, reactive queries
73
+ - [Events](./docs/events.md) — pub/sub, built-in events
74
+ - [Entity Hierarchy](./docs/hierarchy.md) — parent-child, traversal, cascade deletion
75
+ - [Change Detection](./docs/change-detection.md) — marking, sequence timing
76
+ - [Command Buffer](./docs/command-buffer.md) deferred structural changes
77
+ - [Plugins](./docs/plugins.md) definePlugin, pluginFactory, required components
78
+ - [Asset Management](./docs/assets.md) — loading, groups, progress
79
+ - [Screen Management](./docs/screens.md) — transitions, scoped systems, overlays
80
+ - [Built-in Plugins](./docs/built-in-plugins.md) input, timers, physics, rendering
81
+ - [Type Safety](./docs/type-safety.md) type threading, error handling
82
+ - [Performance](./docs/performance.md) — optimization tips
1011
83
 
1012
- ## Performance Tips
84
+ ## License
1013
85
 
1014
- - Use `changed` query filters to skip unchanged entities in render sync, transform propagation, and similar systems
1015
- - Call `markChanged` after in-place mutations so downstream systems can detect the change
1016
- - Extract business logic into testable helper functions using query type utilities
1017
- - Bundle related systems for better organization and reusability
1018
- - Use system phases to separate concerns (physics in `fixedUpdate`, rendering in `render`) and priorities for ordering within a phase
1019
- - Use resource factories for expensive initialization (textures, audio, etc.)
1020
- - Consider component callbacks for immediate reactions to state changes
1021
- - Minimize the number of components in queries when possible to leverage indexing
86
+ MIT