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
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
@@ -20,8 +20,9 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
20
20
  - **Reactive Queries**: Enter/exit callbacks when entities match or unmatch queries
21
21
  - **System Groups**: Enable/disable groups of systems at runtime
22
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`)
23
24
  - **Command Buffer**: Deferred structural changes for safe entity/component operations during systems
24
- - **Timer Bundle**: ECS-native timers with event-based completion notifications
25
+ - **Timer Plugin**: ECS-native timers with event-based completion notifications
25
26
 
26
27
  ## Installation
27
28
 
@@ -47,7 +48,7 @@ npm install ecspresso
47
48
  - [Entity Hierarchy](#entity-hierarchy) -- [Traversal](#traversal), [Parent-First Traversal](#parent-first-traversal), [Cascade Deletion](#cascade-deletion)
48
49
  - [Change Detection](#change-detection) -- [Marking Changes](#marking-changes), [Changed Query Filter](#changed-query-filter), [Sequence Timing](#sequence-timing)
49
50
  - [Command Buffer](#command-buffer) -- [Available Commands](#available-commands)
50
- - [Bundles](#bundles) -- [Built-in Bundles](#built-in-bundles), [Timer Bundle](#timer-bundle)
51
+ - [Plugins](#plugins) -- [Plugin Factory](#plugin-factory), [Required Components](#required-components), [Built-in Plugins](#built-in-plugins), [Timer Plugin](#timer-plugin)
51
52
  - [Asset Management](#asset-management)
52
53
  - [Screen Management](#screen-management) -- [Screen-Scoped Systems](#screen-scoped-systems), [Screen Resource](#screen-resource)
53
54
  - [Type Safety](#type-safety)
@@ -66,8 +67,10 @@ interface Components {
66
67
  health: { value: number };
67
68
  }
68
69
 
69
- // 2. Create a world
70
- const world = new ECSpresso<Components>();
70
+ // 2. Create a world using the builder — types are inferred automatically
71
+ const world = ECSpresso.create()
72
+ .withComponentTypes<Components>()
73
+ .build();
71
74
 
72
75
  // 3. Add a movement system
73
76
  world.addSystem('movement')
@@ -77,8 +80,7 @@ world.addSystem('movement')
77
80
  entity.components.position.x += entity.components.velocity.x * deltaTime;
78
81
  entity.components.position.y += entity.components.velocity.y * deltaTime;
79
82
  }
80
- })
81
- .build();
83
+ });
82
84
 
83
85
  // 4. Create entities
84
86
  const player = world.spawn({
@@ -107,7 +109,7 @@ const entity = world.spawn({
107
109
  // Add components later
108
110
  world.entityManager.addComponent(entity.id, 'velocity', { x: 5, y: 0 });
109
111
 
110
- // Get component data (returns null if not found)
112
+ // Get component data (returns undefined if not found)
111
113
  const position = world.entityManager.getComponent(entity.id, 'position');
112
114
 
113
115
  // Remove components or entities
@@ -186,8 +188,7 @@ world.addSystem('combat')
186
188
  // Combat logic here
187
189
  }
188
190
  }
189
- })
190
- .build();
191
+ });
191
192
  ```
192
193
 
193
194
  ### Resources
@@ -200,10 +201,11 @@ interface Resources {
200
201
  settings: { difficulty: 'easy' | 'hard' };
201
202
  }
202
203
 
203
- const world = new ECSpresso<Components, {}, Resources>();
204
-
205
- // Direct values
206
- world.addResource('score', { value: 0 });
204
+ const world = ECSpresso.create()
205
+ .withComponentTypes<Components>()
206
+ .withResourceTypes<Resources>()
207
+ .withResource('score', { value: 0 })
208
+ .build();
207
209
 
208
210
  // Sync or async factories (lazy initialization)
209
211
  world.addResource('config', () => ({ difficulty: 'normal', soundEnabled: true }));
@@ -223,16 +225,14 @@ world.addSystem('scoring')
223
225
  .setProcess((queries, deltaTime, ecs) => {
224
226
  const score = ecs.getResource('score');
225
227
  score.value += 10;
226
- })
227
- .build();
228
+ });
228
229
  ```
229
230
 
230
- **Builder pattern** -- resources chain naturally with other builder methods:
231
+ Resources also chain naturally with plugins in the builder:
231
232
 
232
233
  ```typescript
233
- const world = ECSpresso
234
- .create<Components, Events, Resources>()
235
- .withBundle(physicsBundle)
234
+ const world = ECSpresso.create()
235
+ .withPlugin(physicsPlugin)
236
236
  .withResource('config', { debug: true, maxEntities: 1000 })
237
237
  .withResource('score', () => ({ value: 0 }))
238
238
  .withResource('cache', {
@@ -266,21 +266,20 @@ await world.disposeResources(); // All, in reverse dependency order
266
266
 
267
267
  ### Method Chaining
268
268
 
269
- Chain multiple systems using `.and()`. The `.and()` method returns the parent container (ECSpresso or Bundle), enabling fluent chaining:
269
+ Systems use a fluent builder API: `world.addSystem().addQuery().setProcess()` -- systems are automatically registered via deferred finalization. No explicit termination call is needed.
270
270
 
271
271
  ```typescript
272
272
  world.addSystem('physics')
273
273
  .addQuery('moving', { with: ['position', 'velocity'] })
274
274
  .setProcess((queries, deltaTime) => {
275
275
  // Physics logic
276
- })
277
- .and() // Returns ECSpresso for continued chaining
278
- .addSystem('rendering')
276
+ });
277
+
278
+ world.addSystem('rendering')
279
279
  .addQuery('visible', { with: ['position', 'sprite'] })
280
280
  .setProcess((queries) => {
281
281
  // Rendering logic
282
- })
283
- .build();
282
+ });
284
283
  ```
285
284
 
286
285
  ### Query Type Utilities
@@ -311,8 +310,7 @@ world.addSystem('movement')
311
310
  for (const entity of queries.entities) {
312
311
  updatePosition(entity, deltaTime);
313
312
  }
314
- })
315
- .build();
313
+ });
316
314
  ```
317
315
 
318
316
  ### System Phases
@@ -328,33 +326,35 @@ Each phase's command buffer is played back before the next phase begins, so enti
328
326
  ```typescript
329
327
  world.addSystem('input')
330
328
  .inPhase('preUpdate')
331
- .setProcess((queries, dt, ecs) => { /* Read input, update timers */ })
332
- .and()
333
- .addSystem('physics')
329
+ .setProcess((queries, dt, ecs) => { /* Read input, update timers */ });
330
+
331
+ world.addSystem('physics')
334
332
  .inPhase('fixedUpdate')
335
333
  .setProcess((queries, dt, ecs) => {
336
334
  // dt is always fixedDt here (e.g. 1/60)
337
335
  // Runs 0..N times per frame based on accumulated time
338
- })
339
- .and()
340
- .addSystem('gameplay')
336
+ });
337
+
338
+ world.addSystem('gameplay')
341
339
  .inPhase('update') // default phase
342
- .setProcess((queries, dt, ecs) => { /* Game logic, AI */ })
343
- .and()
344
- .addSystem('transform-sync')
340
+ .setProcess((queries, dt, ecs) => { /* Game logic, AI */ });
341
+
342
+ world.addSystem('transform-sync')
345
343
  .inPhase('postUpdate')
346
- .setProcess((queries, dt, ecs) => { /* Transform propagation */ })
347
- .and()
348
- .addSystem('renderer')
344
+ .setProcess((queries, dt, ecs) => { /* Transform propagation */ });
345
+
346
+ world.addSystem('renderer')
349
347
  .inPhase('render')
350
- .setProcess((queries, dt, ecs) => { /* Visual output */ })
351
- .build();
348
+ .setProcess((queries, dt, ecs) => { /* Visual output */ });
352
349
  ```
353
350
 
354
351
  **Fixed Timestep** -- The `fixedUpdate` phase uses a time accumulator for deterministic simulation. A spiral-of-death cap (8 steps) prevents runaway accumulation.
355
352
 
356
353
  ```typescript
357
- const world = ECSpresso.create<Components, Events, Resources>()
354
+ const world = ECSpresso.create()
355
+ .withComponentTypes<Components>()
356
+ .withEventTypes<Events>()
357
+ .withResourceTypes<Resources>()
358
358
  .withFixedTimestep(1 / 60) // 60Hz physics (default)
359
359
  .build();
360
360
  ```
@@ -371,13 +371,12 @@ Within each phase, systems execute in priority order (higher numbers first). Sys
371
371
  world.addSystem('physics')
372
372
  .inPhase('fixedUpdate')
373
373
  .setPriority(100) // Runs first within fixedUpdate
374
- .setProcess(() => { /* physics */ })
375
- .and()
376
- .addSystem('constraints')
374
+ .setProcess(() => { /* physics */ });
375
+
376
+ world.addSystem('constraints')
377
377
  .inPhase('fixedUpdate')
378
378
  .setPriority(50) // Runs second within fixedUpdate
379
- .setProcess(() => { /* constraints */ })
380
- .build();
379
+ .setProcess(() => { /* constraints */ });
381
380
  ```
382
381
 
383
382
  ### System Groups
@@ -388,13 +387,12 @@ Organize systems into groups that can be enabled/disabled at runtime:
388
387
  world.addSystem('renderSprites')
389
388
  .inGroup('rendering')
390
389
  .addQuery('sprites', { with: ['position', 'sprite'] })
391
- .setProcess((queries) => { /* ... */ })
392
- .and()
393
- .addSystem('renderParticles')
390
+ .setProcess((queries) => { /* ... */ });
391
+
392
+ world.addSystem('renderParticles')
394
393
  .inGroup('rendering')
395
394
  .inGroup('effects') // Systems can belong to multiple groups
396
- .setProcess(() => { /* ... */ })
397
- .build();
395
+ .setProcess(() => { /* ... */ });
398
396
 
399
397
  world.disableSystemGroup('rendering'); // All rendering systems skip
400
398
  world.enableSystemGroup('rendering'); // Resume rendering
@@ -415,8 +413,7 @@ world.addSystem('gameSystem')
415
413
  })
416
414
  .setOnDetach((ecs) => {
417
415
  console.log('System shutting down...');
418
- })
419
- .build();
416
+ });
420
417
 
421
418
  await world.initialize();
422
419
  ```
@@ -448,7 +445,10 @@ interface Events {
448
445
  };
449
446
  }
450
447
 
451
- const world = new ECSpresso<Components, Events>();
448
+ const world = ECSpresso.create()
449
+ .withComponentTypes<Components>()
450
+ .withEventTypes<Events>()
451
+ .build();
452
452
 
453
453
  // Subscribe - returns unsubscribe function
454
454
  const unsubscribe = world.on('playerDied', (data) => {
@@ -464,19 +464,16 @@ world.off('levelComplete', handler);
464
464
  // Handle events in systems
465
465
  world.addSystem('gameLogic')
466
466
  .setEventHandlers({
467
- playerDied: {
468
- handler: (data, ecs) => {
469
- // Respawn logic
470
- }
467
+ playerDied: (data, ecs) => {
468
+ // Respawn logic
471
469
  }
472
- })
473
- .build();
470
+ });
474
471
 
475
472
  // Publish events from anywhere
476
473
  world.eventBus.publish('playerDied', { playerId: 1 });
477
474
  ```
478
475
 
479
- **Built-in events**: `hierarchyChanged` (entity parent changes), `assetLoaded` / `assetFailed` / `assetGroupProgress` / `assetGroupLoaded` (asset loading), and timer `onComplete` events (see [Bundles](#bundles)).
476
+ **Built-in events**: `hierarchyChanged` (entity parent changes), `assetLoaded` / `assetFailed` / `assetGroupProgress` / `assetGroupLoaded` (asset loading), and timer `onComplete` events (see [Plugins](#plugins)).
480
477
 
481
478
  ## Entity Hierarchy
482
479
 
@@ -545,7 +542,7 @@ world.removeEntity(parent.id, { cascade: false });
545
542
 
546
543
  Hierarchy changes emit the `hierarchyChanged` event (see [Events](#events)).
547
544
 
548
- **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.
545
+ **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 plugin implements this automatically.
549
546
 
550
547
  ## Change Detection
551
548
 
@@ -577,8 +574,7 @@ world.addSystem('render-sync')
577
574
  for (const entity of queries.moved) {
578
575
  syncSpritePosition(entity);
579
576
  }
580
- })
581
- .build();
577
+ });
582
578
  ```
583
579
 
584
580
  When multiple components are listed in `changed`, entities matching **any** of them are included (OR semantics).
@@ -602,7 +598,7 @@ if (em.getChangeSeq(entity.id, 'localTransform') > ecs.changeThreshold) {
602
598
 
603
599
  **Deferred marking**: `ecs.commands.markChanged(entity.id, 'position')` queues a mark for command buffer playback.
604
600
 
605
- **Built-in bundle usage**: Movement marks `localTransform` (fixedUpdate) → Transform propagation reads `localTransform` changed, writes+marks `worldTransform` (postUpdate) → Renderer reads `worldTransform` changed (render).
601
+ **Built-in plugin usage**: Movement marks `localTransform` (fixedUpdate) → Transform propagation reads `localTransform` changed, writes+marks `worldTransform` (postUpdate) → Renderer reads `worldTransform` changed (render).
606
602
 
607
603
  ## Command Buffer
608
604
 
@@ -621,8 +617,7 @@ world.addSystem('combat')
621
617
  });
622
618
  }
623
619
  }
624
- })
625
- .build();
620
+ });
626
621
  ```
627
622
 
628
623
  ### Available Commands
@@ -653,68 +648,170 @@ ecs.commands.clear(); // Discard all queued commands
653
648
 
654
649
  Commands execute in FIFO order. If a command fails (e.g., entity doesn't exist), it logs a warning and continues with remaining commands.
655
650
 
656
- ## Bundles
651
+ ## Plugins
657
652
 
658
- Organize related systems and resources into reusable bundles:
653
+ Organize related systems and resources into reusable plugins:
659
654
 
660
655
  ```typescript
661
- import { Bundle } from 'ecspresso';
656
+ import ECSpresso, { definePlugin, type WorldConfigFrom } from 'ecspresso';
662
657
 
663
- const physicsBundle = new Bundle<GameComponents, {}, GameResources>('physics')
664
- .addSystem('applyVelocity')
665
- .addQuery('moving', { with: ['position', 'velocity'] })
666
- .setProcess((queries, deltaTime) => {
667
- for (const entity of queries.moving) {
668
- entity.components.position.x += entity.components.velocity.x * deltaTime;
669
- entity.components.position.y += entity.components.velocity.y * deltaTime;
670
- }
671
- })
672
- .and()
673
- .addSystem('applyGravity')
674
- .addQuery('falling', { with: ['velocity'] })
675
- .setProcess((queries, deltaTime, ecs) => {
676
- const gravity = ecs.getResource('gravity');
677
- for (const entity of queries.falling) {
678
- entity.components.velocity.y += gravity.value * deltaTime;
679
- }
680
- })
681
- .and()
682
- .addResource('gravity', { value: 9.8 });
658
+ interface PhysicsComponents {
659
+ position: { x: number; y: number };
660
+ velocity: { x: number; y: number };
661
+ }
662
+
663
+ interface PhysicsResources {
664
+ gravity: { value: number };
665
+ }
683
666
 
684
- // Register bundles with the world
685
- const game = ECSpresso.create<GameComponents, {}, GameResources>()
686
- .withBundle(physicsBundle)
667
+ const physicsPlugin = definePlugin<WorldConfigFrom<PhysicsComponents, {}, PhysicsResources>>({
668
+ id: 'physics',
669
+ install(world) {
670
+ world.addSystem('applyVelocity')
671
+ .addQuery('moving', { with: ['position', 'velocity'] })
672
+ .setProcess((queries, deltaTime) => {
673
+ for (const entity of queries.moving) {
674
+ entity.components.position.x += entity.components.velocity.x * deltaTime;
675
+ entity.components.position.y += entity.components.velocity.y * deltaTime;
676
+ }
677
+ });
678
+
679
+ world.addSystem('applyGravity')
680
+ .addQuery('falling', { with: ['velocity'] })
681
+ .setProcess((queries, deltaTime, ecs) => {
682
+ const gravity = ecs.getResource('gravity');
683
+ for (const entity of queries.falling) {
684
+ entity.components.velocity.y += gravity.value * deltaTime;
685
+ }
686
+ });
687
+
688
+ world.addResource('gravity', { value: 9.8 });
689
+ },
690
+ });
691
+
692
+ // Register plugins with the world — types merge automatically
693
+ const game = ECSpresso.create()
694
+ .withPlugin(physicsPlugin)
687
695
  .build();
688
696
  ```
689
697
 
690
- ### Built-in Bundles
698
+ ### Plugin Factory
699
+
700
+ When multiple plugins share the same types (common in application code), use `pluginFactory()` on the builder or built world to capture types automatically:
701
+
702
+ ```typescript
703
+ // types.ts — builder accumulates all types
704
+ export const builder = ECSpresso.create()
705
+ .withPlugin(createPhysicsPlugin())
706
+ .withComponentTypes<{ player: boolean; enemy: EnemyData }>()
707
+ .withResourceTypes<{ score: number }>();
708
+
709
+ // Types flow from the builder — no manual imports or extends chains
710
+ export const definePlugin = builder.pluginFactory();
711
+
712
+ // movement-plugin.ts — no type params needed
713
+ import { definePlugin } from './types';
714
+
715
+ export const movementPlugin = definePlugin({
716
+ id: 'movement',
717
+ install(world) {
718
+ world.addSystem('movement')
719
+ .addQuery('moving', { with: ['position', 'velocity'] })
720
+ .setProcess((queries, dt) => { /* ... */ });
721
+ },
722
+ });
723
+ ```
724
+
725
+ You can also pass a world type directly to `definePlugin` as a one-off alternative:
726
+
727
+ ```typescript
728
+ type MyWorld = typeof ecs; // derive from a built world
729
+ const plugin = definePlugin<MyWorld>({
730
+ id: 'my-plugin',
731
+ install(world) { /* world is fully typed */ },
732
+ });
733
+ ```
734
+
735
+ ### Required Components
736
+
737
+ Plugins 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:
738
+
739
+ ```typescript
740
+ const transformPlugin = definePlugin<WorldConfigFrom<TransformComponents>>({
741
+ id: 'transform',
742
+ install(world) {
743
+ world.registerRequired('localTransform', 'worldTransform', () => ({
744
+ x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
745
+ }));
746
+ },
747
+ });
748
+
749
+ const world = ECSpresso.create()
750
+ .withPlugin(transformPlugin)
751
+ .build();
752
+
753
+ // worldTransform is auto-added with defaults
754
+ const entity = world.spawn({
755
+ localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
756
+ });
757
+
758
+ // Explicit values always win — no auto-add if already provided
759
+ const entity2 = world.spawn({
760
+ localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
761
+ worldTransform: { x: 50, y: 50, rotation: 0, scaleX: 2, scaleY: 2 }, // used as-is
762
+ });
763
+ ```
691
764
 
692
- | Bundle | Import | Default Phase | Description |
765
+ Requirements can also be registered via the builder or at runtime:
766
+
767
+ ```typescript
768
+ // Builder
769
+ const world = ECSpresso.create()
770
+ .withComponentTypes<Components>()
771
+ .withRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }))
772
+ .withRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }))
773
+ .build();
774
+
775
+ // Runtime
776
+ world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
777
+ ```
778
+
779
+ **Behavior:**
780
+ - Enforced at insertion time (`spawn`, `addComponent`, `addComponents`, `spawnChild`, command buffer)
781
+ - Removal is unrestricted — removing a required component does not cascade
782
+ - Transitive requirements resolve automatically (A requires B, B requires C → all three added)
783
+ - Circular dependencies are detected and rejected at registration time
784
+ - Auto-added components are marked as changed and trigger reactive queries
785
+ - Component names and factory return types are fully type-checked
786
+
787
+ **Built-in requirements:** The Transform plugin registers `localTransform` → `worldTransform`. The Physics 2D plugin registers `rigidBody` → `velocity` and `rigidBody` → `force`.
788
+
789
+ ### Built-in Plugins
790
+
791
+ | Plugin | Import | Default Phase | Description |
693
792
  |--------|--------|---------------|-------------|
694
- | **Input** | `ecspresso/bundles/utils/input` | `preUpdate` | Frame-accurate keyboard/pointer input with action mapping |
695
- | **Timers** | `ecspresso/bundles/utils/timers` | `preUpdate` | ECS-native timers with event-based completion |
696
- | **Movement** | `ecspresso/bundles/utils/movement` | `fixedUpdate` | Velocity-based movement integration |
697
- | **Transform** | `ecspresso/bundles/utils/transform` | `postUpdate` | Hierarchical transform propagation (local/world transforms) |
698
- | **Bounds** | `ecspresso/bundles/utils/bounds` | `postUpdate` | Screen bounds enforcement (destroy, clamp, wrap) |
699
- | **Collision** | `ecspresso/bundles/utils/collision` | `postUpdate` | Layer-based AABB/circle collision detection with events |
700
- | **2D Renderer** | `ecspresso/bundles/renderers/renderer2D` | `render` | Automated PixiJS scene graph wiring |
793
+ | **Input** | `ecspresso/plugins/input` | `preUpdate` | Frame-accurate keyboard/pointer input with action mapping |
794
+ | **Timers** | `ecspresso/plugins/timers` | `preUpdate` | ECS-native timers with event-based completion |
795
+ | **Movement** | `ecspresso/plugins/movement` | `fixedUpdate` | Velocity-based movement integration |
796
+ | **Transform** | `ecspresso/plugins/transform` | `postUpdate` | Hierarchical transform propagation (local/world transforms) |
797
+ | **Bounds** | `ecspresso/plugins/bounds` | `postUpdate` | Screen bounds enforcement (destroy, clamp, wrap) |
798
+ | **Collision** | `ecspresso/plugins/collision` | `postUpdate` | Layer-based AABB/circle collision detection with events |
799
+ | **2D Renderer** | `ecspresso/plugins/renderers/renderer2D` | `render` | Automated PixiJS scene graph wiring |
701
800
 
702
- Each bundle accepts a `phase` option to override its default.
801
+ Each plugin accepts a `phase` option to override its default.
703
802
 
704
- ### Input Bundle
803
+ ### Input Plugin
705
804
 
706
- 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.
805
+ The input plugin provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent), and named action mapping. It's a resource-only plugin — input is polled via the `inputState` resource. DOM events are accumulated between frames and snapshotted once per frame, so all systems see consistent state.
707
806
 
708
807
  ```typescript
709
808
  import {
710
- createInputBundle,
809
+ createInputPlugin,
711
810
  type InputResourceTypes, type KeyCode
712
- } from 'ecspresso/bundles/utils/input';
713
-
714
- interface Resources extends InputResourceTypes {}
811
+ } from 'ecspresso/plugins/input';
715
812
 
716
- const world = ECSpresso.create<Components, Events, Resources>()
717
- .withBundle(createInputBundle({
813
+ const world = ECSpresso.create()
814
+ .withPlugin(createInputPlugin({
718
815
  actions: {
719
816
  jump: { keys: [' ', 'ArrowUp'] },
720
817
  shoot: { keys: ['z'], buttons: [0] },
@@ -730,21 +827,28 @@ if (input.actions.justActivated('jump')) { /* ... */ }
730
827
  if (input.keyboard.isDown('ArrowRight')) { /* ... */ }
731
828
  if (input.pointer.justPressed(0)) { /* ... */ }
732
829
 
733
- // Runtime remapping
734
- input.setActionMap({ jump: { keys: ['w'] } });
830
+ // Runtime remapping — must include all configured actions
831
+ input.setActionMap({
832
+ jump: { keys: ['w'] },
833
+ shoot: { keys: ['z'], buttons: [0] },
834
+ moveLeft: { keys: ['a'] },
835
+ moveRight: { keys: ['d'] },
836
+ });
735
837
  ```
736
838
 
839
+ 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 `createInputPlugin`. Defaults to `string` when no actions are configured.
840
+
737
841
  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'`.
738
842
 
739
- ### Timer Bundle
843
+ ### Timer Plugin
740
844
 
741
- 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.
845
+ The timer plugin provides ECS-native timers that follow the "data, not callbacks" philosophy. Timers are components processed each frame, with optional event-based completion notifications.
742
846
 
743
847
  ```typescript
744
848
  import {
745
- createTimerBundle, createTimer, createRepeatingTimer,
849
+ createTimerPlugin, createTimer, createRepeatingTimer,
746
850
  type TimerComponentTypes, type TimerEventData
747
- } from 'ecspresso/bundles/utils/timers';
851
+ } from 'ecspresso/plugins/timers';
748
852
 
749
853
  // Events used with onComplete must have TimerEventData payload
750
854
  interface Events {
@@ -752,21 +856,19 @@ interface Events {
752
856
  spawnWave: TimerEventData;
753
857
  }
754
858
 
755
- interface Components extends TimerComponentTypes<Events> {
756
- position: { x: number; y: number };
757
- }
758
-
759
859
  const world = ECSpresso
760
- .create<Components, Events>()
761
- .withBundle(createTimerBundle<Events>())
860
+ .create()
861
+ .withPlugin(createTimerPlugin())
862
+ .withComponentTypes<{ position: { x: number; y: number } }>()
863
+ .withEventTypes<Events>()
762
864
  .build();
763
865
 
764
866
  // One-shot timer (poll justFinished or use onComplete event)
765
- world.spawn({ ...createTimer<Events>(2.0), position: { x: 0, y: 0 } });
766
- world.spawn({ ...createTimer<Events>(1.5, { onComplete: 'hideMessage' }) });
867
+ world.spawn({ ...createTimer(2.0), position: { x: 0, y: 0 } });
868
+ world.spawn({ ...createTimer(1.5, { onComplete: 'hideMessage' }) });
767
869
 
768
870
  // Repeating timer
769
- world.spawn({ ...createRepeatingTimer<Events>(5.0, { onComplete: 'spawnWave' }) });
871
+ world.spawn({ ...createRepeatingTimer(5.0, { onComplete: 'spawnWave' }) });
770
872
  ```
771
873
 
772
874
  Timer components expose `elapsed`, `duration`, `repeat`, `active`, `justFinished`, and optional `onComplete` for runtime control.
@@ -782,7 +884,10 @@ type Assets = {
782
884
  level1Background: { data: ImageBitmap };
783
885
  };
784
886
 
785
- const game = ECSpresso.create<Components, Events, Resources, Assets>()
887
+ const game = ECSpresso.create()
888
+ .withComponentTypes<Components>()
889
+ .withEventTypes<Events>()
890
+ .withResourceTypes<Resources>()
786
891
  .withAssets(assets => assets
787
892
  // Eager assets - loaded automatically during initialize()
788
893
  .add('playerTexture', async () => {
@@ -813,8 +918,7 @@ game.addSystem('gameplay')
813
918
  .requiresAssets(['playerTexture'])
814
919
  .setProcess((queries, dt, ecs) => {
815
920
  const player = ecs.getAsset('playerTexture');
816
- })
817
- .build();
921
+ });
818
922
  ```
819
923
 
820
924
  Asset events (`assetLoaded`, `assetFailed`, `assetGroupProgress`, `assetGroupLoaded`) are available through the event system -- see [Events](#events).
@@ -838,7 +942,10 @@ type Screens = {
838
942
  pause: ScreenDefinition<Record<string, never>, Record<string, never>>;
839
943
  };
840
944
 
841
- const game = ECSpresso.create<Components, Events, Resources, {}, Screens>()
945
+ const game = ECSpresso.create()
946
+ .withComponentTypes<Components>()
947
+ .withEventTypes<Events>()
948
+ .withResourceTypes<Resources>()
842
949
  .withScreens(screens => screens
843
950
  .add('menu', {
844
951
  initialState: () => ({ selectedOption: 0 }),
@@ -876,13 +983,11 @@ game.addSystem('menuUI')
876
983
  .inScreens(['menu']) // Only runs in 'menu'
877
984
  .setProcess((queries, dt, ecs) => {
878
985
  renderMenu(ecs.getScreenState().selectedOption);
879
- })
880
- .build();
986
+ });
881
987
 
882
988
  game.addSystem('animations')
883
989
  .excludeScreens(['pause']) // Runs in all screens except 'pause'
884
- .setProcess(() => { /* ... */ })
885
- .build();
990
+ .setProcess(() => { /* ... */ });
886
991
  ```
887
992
 
888
993
  ### Screen Resource
@@ -900,8 +1005,7 @@ game.addSystem('ui')
900
1005
  screen.stackDepth; // Number of screens in stack
901
1006
  screen.isCurrent('gameplay'); // Check current screen
902
1007
  screen.isActive('menu'); // true if in current or stack
903
- })
904
- .build();
1008
+ });
905
1009
  ```
906
1010
 
907
1011
  ## Type Safety
@@ -926,15 +1030,19 @@ world.addSystem('example')
926
1030
  entity.components.position.x; // ✅ guaranteed
927
1031
  entity.components.health.value; // ❌ not in query
928
1032
  }
929
- })
930
- .build();
1033
+ });
931
1034
 
932
- // Bundle type compatibility - conflicting types error at compile time
933
- const bundle1 = new Bundle<{position: {x: number, y: number}}>('b1');
934
- const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('b2');
1035
+ // Plugin type compatibility - conflicting types error at compile time
1036
+ const plugin1 = definePlugin<WorldConfigFrom<{ position: { x: number; y: number } }>>({
1037
+ id: 'p1', install() {},
1038
+ });
1039
+ const plugin2 = definePlugin<WorldConfigFrom<{ velocity: { x: number; y: number } }>>({
1040
+ id: 'p2', install() {},
1041
+ });
1042
+ // Builder merges plugin types automatically — no manual type params needed
935
1043
  const world = ECSpresso.create()
936
- .withBundle(bundle1)
937
- .withBundle(bundle2) // Types merge successfully
1044
+ .withPlugin(plugin1)
1045
+ .withPlugin(plugin2)
938
1046
  .build();
939
1047
  ```
940
1048
 
@@ -949,8 +1057,8 @@ world.getResource('nonexistent');
949
1057
  world.entityManager.addComponent(999, 'position', { x: 0, y: 0 });
950
1058
  // → "Cannot add component 'position': Entity with ID 999 does not exist"
951
1059
 
952
- // Component not found returns null (no throw)
953
- world.entityManager.getComponent(123, 'position'); // null
1060
+ // Component not found returns undefined (no throw)
1061
+ world.entityManager.getComponent(123, 'position'); // undefined
954
1062
  ```
955
1063
 
956
1064
  ## Performance Tips
@@ -958,7 +1066,7 @@ world.entityManager.getComponent(123, 'position'); // null
958
1066
  - Use `changed` query filters to skip unchanged entities in render sync, transform propagation, and similar systems
959
1067
  - Call `markChanged` after in-place mutations so downstream systems can detect the change
960
1068
  - Extract business logic into testable helper functions using query type utilities
961
- - Bundle related systems for better organization and reusability
1069
+ - Group related systems into plugins for better organization and reusability
962
1070
  - Use system phases to separate concerns (physics in `fixedUpdate`, rendering in `render`) and priorities for ordering within a phase
963
1071
  - Use resource factories for expensive initialization (textures, audio, etc.)
964
1072
  - Consider component callbacks for immediate reactions to state changes