ecspresso 0.11.0 → 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 (96) hide show
  1. package/README.md +200 -148
  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 -122
  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 +17 -11
  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 +53 -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
@@ -22,7 +22,7 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
22
22
  - **Component Lifecycle**: Callbacks for component add/remove with unsubscribe support
23
23
  - **Required Components**: Auto-add dependent components on spawn/addComponent (e.g. `localTransform` implies `worldTransform`)
24
24
  - **Command Buffer**: Deferred structural changes for safe entity/component operations during systems
25
- - **Timer Bundle**: ECS-native timers with event-based completion notifications
25
+ - **Timer Plugin**: ECS-native timers with event-based completion notifications
26
26
 
27
27
  ## Installation
28
28
 
@@ -48,7 +48,7 @@ npm install ecspresso
48
48
  - [Entity Hierarchy](#entity-hierarchy) -- [Traversal](#traversal), [Parent-First Traversal](#parent-first-traversal), [Cascade Deletion](#cascade-deletion)
49
49
  - [Change Detection](#change-detection) -- [Marking Changes](#marking-changes), [Changed Query Filter](#changed-query-filter), [Sequence Timing](#sequence-timing)
50
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)
51
+ - [Plugins](#plugins) -- [Plugin Factory](#plugin-factory), [Required Components](#required-components), [Built-in Plugins](#built-in-plugins), [Timer Plugin](#timer-plugin)
52
52
  - [Asset Management](#asset-management)
53
53
  - [Screen Management](#screen-management) -- [Screen-Scoped Systems](#screen-scoped-systems), [Screen Resource](#screen-resource)
54
54
  - [Type Safety](#type-safety)
@@ -67,8 +67,10 @@ interface Components {
67
67
  health: { value: number };
68
68
  }
69
69
 
70
- // 2. Create a world
71
- 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();
72
74
 
73
75
  // 3. Add a movement system
74
76
  world.addSystem('movement')
@@ -78,8 +80,7 @@ world.addSystem('movement')
78
80
  entity.components.position.x += entity.components.velocity.x * deltaTime;
79
81
  entity.components.position.y += entity.components.velocity.y * deltaTime;
80
82
  }
81
- })
82
- .build();
83
+ });
83
84
 
84
85
  // 4. Create entities
85
86
  const player = world.spawn({
@@ -187,8 +188,7 @@ world.addSystem('combat')
187
188
  // Combat logic here
188
189
  }
189
190
  }
190
- })
191
- .build();
191
+ });
192
192
  ```
193
193
 
194
194
  ### Resources
@@ -201,10 +201,11 @@ interface Resources {
201
201
  settings: { difficulty: 'easy' | 'hard' };
202
202
  }
203
203
 
204
- const world = new ECSpresso<Components, {}, Resources>();
205
-
206
- // Direct values
207
- world.addResource('score', { value: 0 });
204
+ const world = ECSpresso.create()
205
+ .withComponentTypes<Components>()
206
+ .withResourceTypes<Resources>()
207
+ .withResource('score', { value: 0 })
208
+ .build();
208
209
 
209
210
  // Sync or async factories (lazy initialization)
210
211
  world.addResource('config', () => ({ difficulty: 'normal', soundEnabled: true }));
@@ -224,16 +225,14 @@ world.addSystem('scoring')
224
225
  .setProcess((queries, deltaTime, ecs) => {
225
226
  const score = ecs.getResource('score');
226
227
  score.value += 10;
227
- })
228
- .build();
228
+ });
229
229
  ```
230
230
 
231
- **Builder pattern** -- resources chain naturally with other builder methods:
231
+ Resources also chain naturally with plugins in the builder:
232
232
 
233
233
  ```typescript
234
- const world = ECSpresso
235
- .create<Components, Events, Resources>()
236
- .withBundle(physicsBundle)
234
+ const world = ECSpresso.create()
235
+ .withPlugin(physicsPlugin)
237
236
  .withResource('config', { debug: true, maxEntities: 1000 })
238
237
  .withResource('score', () => ({ value: 0 }))
239
238
  .withResource('cache', {
@@ -267,21 +266,20 @@ await world.disposeResources(); // All, in reverse dependency order
267
266
 
268
267
  ### Method Chaining
269
268
 
270
- 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.
271
270
 
272
271
  ```typescript
273
272
  world.addSystem('physics')
274
273
  .addQuery('moving', { with: ['position', 'velocity'] })
275
274
  .setProcess((queries, deltaTime) => {
276
275
  // Physics logic
277
- })
278
- .and() // Returns ECSpresso for continued chaining
279
- .addSystem('rendering')
276
+ });
277
+
278
+ world.addSystem('rendering')
280
279
  .addQuery('visible', { with: ['position', 'sprite'] })
281
280
  .setProcess((queries) => {
282
281
  // Rendering logic
283
- })
284
- .build();
282
+ });
285
283
  ```
286
284
 
287
285
  ### Query Type Utilities
@@ -312,8 +310,7 @@ world.addSystem('movement')
312
310
  for (const entity of queries.entities) {
313
311
  updatePosition(entity, deltaTime);
314
312
  }
315
- })
316
- .build();
313
+ });
317
314
  ```
318
315
 
319
316
  ### System Phases
@@ -329,33 +326,35 @@ Each phase's command buffer is played back before the next phase begins, so enti
329
326
  ```typescript
330
327
  world.addSystem('input')
331
328
  .inPhase('preUpdate')
332
- .setProcess((queries, dt, ecs) => { /* Read input, update timers */ })
333
- .and()
334
- .addSystem('physics')
329
+ .setProcess((queries, dt, ecs) => { /* Read input, update timers */ });
330
+
331
+ world.addSystem('physics')
335
332
  .inPhase('fixedUpdate')
336
333
  .setProcess((queries, dt, ecs) => {
337
334
  // dt is always fixedDt here (e.g. 1/60)
338
335
  // Runs 0..N times per frame based on accumulated time
339
- })
340
- .and()
341
- .addSystem('gameplay')
336
+ });
337
+
338
+ world.addSystem('gameplay')
342
339
  .inPhase('update') // default phase
343
- .setProcess((queries, dt, ecs) => { /* Game logic, AI */ })
344
- .and()
345
- .addSystem('transform-sync')
340
+ .setProcess((queries, dt, ecs) => { /* Game logic, AI */ });
341
+
342
+ world.addSystem('transform-sync')
346
343
  .inPhase('postUpdate')
347
- .setProcess((queries, dt, ecs) => { /* Transform propagation */ })
348
- .and()
349
- .addSystem('renderer')
344
+ .setProcess((queries, dt, ecs) => { /* Transform propagation */ });
345
+
346
+ world.addSystem('renderer')
350
347
  .inPhase('render')
351
- .setProcess((queries, dt, ecs) => { /* Visual output */ })
352
- .build();
348
+ .setProcess((queries, dt, ecs) => { /* Visual output */ });
353
349
  ```
354
350
 
355
351
  **Fixed Timestep** -- The `fixedUpdate` phase uses a time accumulator for deterministic simulation. A spiral-of-death cap (8 steps) prevents runaway accumulation.
356
352
 
357
353
  ```typescript
358
- const world = ECSpresso.create<Components, Events, Resources>()
354
+ const world = ECSpresso.create()
355
+ .withComponentTypes<Components>()
356
+ .withEventTypes<Events>()
357
+ .withResourceTypes<Resources>()
359
358
  .withFixedTimestep(1 / 60) // 60Hz physics (default)
360
359
  .build();
361
360
  ```
@@ -372,13 +371,12 @@ Within each phase, systems execute in priority order (higher numbers first). Sys
372
371
  world.addSystem('physics')
373
372
  .inPhase('fixedUpdate')
374
373
  .setPriority(100) // Runs first within fixedUpdate
375
- .setProcess(() => { /* physics */ })
376
- .and()
377
- .addSystem('constraints')
374
+ .setProcess(() => { /* physics */ });
375
+
376
+ world.addSystem('constraints')
378
377
  .inPhase('fixedUpdate')
379
378
  .setPriority(50) // Runs second within fixedUpdate
380
- .setProcess(() => { /* constraints */ })
381
- .build();
379
+ .setProcess(() => { /* constraints */ });
382
380
  ```
383
381
 
384
382
  ### System Groups
@@ -389,13 +387,12 @@ Organize systems into groups that can be enabled/disabled at runtime:
389
387
  world.addSystem('renderSprites')
390
388
  .inGroup('rendering')
391
389
  .addQuery('sprites', { with: ['position', 'sprite'] })
392
- .setProcess((queries) => { /* ... */ })
393
- .and()
394
- .addSystem('renderParticles')
390
+ .setProcess((queries) => { /* ... */ });
391
+
392
+ world.addSystem('renderParticles')
395
393
  .inGroup('rendering')
396
394
  .inGroup('effects') // Systems can belong to multiple groups
397
- .setProcess(() => { /* ... */ })
398
- .build();
395
+ .setProcess(() => { /* ... */ });
399
396
 
400
397
  world.disableSystemGroup('rendering'); // All rendering systems skip
401
398
  world.enableSystemGroup('rendering'); // Resume rendering
@@ -416,8 +413,7 @@ world.addSystem('gameSystem')
416
413
  })
417
414
  .setOnDetach((ecs) => {
418
415
  console.log('System shutting down...');
419
- })
420
- .build();
416
+ });
421
417
 
422
418
  await world.initialize();
423
419
  ```
@@ -449,7 +445,10 @@ interface Events {
449
445
  };
450
446
  }
451
447
 
452
- const world = new ECSpresso<Components, Events>();
448
+ const world = ECSpresso.create()
449
+ .withComponentTypes<Components>()
450
+ .withEventTypes<Events>()
451
+ .build();
453
452
 
454
453
  // Subscribe - returns unsubscribe function
455
454
  const unsubscribe = world.on('playerDied', (data) => {
@@ -465,19 +464,16 @@ world.off('levelComplete', handler);
465
464
  // Handle events in systems
466
465
  world.addSystem('gameLogic')
467
466
  .setEventHandlers({
468
- playerDied: {
469
- handler: (data, ecs) => {
470
- // Respawn logic
471
- }
467
+ playerDied: (data, ecs) => {
468
+ // Respawn logic
472
469
  }
473
- })
474
- .build();
470
+ });
475
471
 
476
472
  // Publish events from anywhere
477
473
  world.eventBus.publish('playerDied', { playerId: 1 });
478
474
  ```
479
475
 
480
- **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)).
481
477
 
482
478
  ## Entity Hierarchy
483
479
 
@@ -546,7 +542,7 @@ world.removeEntity(parent.id, { cascade: false });
546
542
 
547
543
  Hierarchy changes emit the `hierarchyChanged` event (see [Events](#events)).
548
544
 
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.
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.
550
546
 
551
547
  ## Change Detection
552
548
 
@@ -578,8 +574,7 @@ world.addSystem('render-sync')
578
574
  for (const entity of queries.moved) {
579
575
  syncSpritePosition(entity);
580
576
  }
581
- })
582
- .build();
577
+ });
583
578
  ```
584
579
 
585
580
  When multiple components are listed in `changed`, entities matching **any** of them are included (OR semantics).
@@ -603,7 +598,7 @@ if (em.getChangeSeq(entity.id, 'localTransform') > ecs.changeThreshold) {
603
598
 
604
599
  **Deferred marking**: `ecs.commands.markChanged(entity.id, 'position')` queues a mark for command buffer playback.
605
600
 
606
- **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).
607
602
 
608
603
  ## Command Buffer
609
604
 
@@ -622,8 +617,7 @@ world.addSystem('combat')
622
617
  });
623
618
  }
624
619
  }
625
- })
626
- .build();
620
+ });
627
621
  ```
628
622
 
629
623
  ### Available Commands
@@ -654,52 +648,106 @@ ecs.commands.clear(); // Discard all queued commands
654
648
 
655
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.
656
650
 
657
- ## Bundles
651
+ ## Plugins
658
652
 
659
- Organize related systems and resources into reusable bundles:
653
+ Organize related systems and resources into reusable plugins:
660
654
 
661
655
  ```typescript
662
- import { Bundle } from 'ecspresso';
656
+ import ECSpresso, { definePlugin, type WorldConfigFrom } from 'ecspresso';
663
657
 
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 });
658
+ interface PhysicsComponents {
659
+ position: { x: number; y: number };
660
+ velocity: { x: number; y: number };
661
+ }
684
662
 
685
- // Register bundles with the world
686
- const game = ECSpresso.create<GameComponents, {}, GameResources>()
687
- .withBundle(physicsBundle)
663
+ interface PhysicsResources {
664
+ gravity: { value: number };
665
+ }
666
+
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)
688
695
  .build();
689
696
  ```
690
697
 
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
+
691
735
  ### Required Components
692
736
 
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:
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:
694
738
 
695
739
  ```typescript
696
- const transformBundle = new Bundle<TransformComponents>('transform')
697
- .registerRequired('localTransform', 'worldTransform', () => ({
698
- x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
699
- }));
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
+ });
700
748
 
701
749
  const world = ECSpresso.create()
702
- .withBundle(transformBundle)
750
+ .withPlugin(transformPlugin)
703
751
  .build();
704
752
 
705
753
  // worldTransform is auto-added with defaults
@@ -736,34 +784,34 @@ world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
736
784
  - Auto-added components are marked as changed and trigger reactive queries
737
785
  - Component names and factory return types are fully type-checked
738
786
 
739
- **Built-in requirements:** The Transform bundle registers `localTransform` → `worldTransform`. The Physics 2D bundle registers `rigidBody` → `velocity` and `rigidBody` → `force`.
787
+ **Built-in requirements:** The Transform plugin registers `localTransform` → `worldTransform`. The Physics 2D plugin registers `rigidBody` → `velocity` and `rigidBody` → `force`.
740
788
 
741
- ### Built-in Bundles
789
+ ### Built-in Plugins
742
790
 
743
- | Bundle | Import | Default Phase | Description |
791
+ | Plugin | Import | Default Phase | Description |
744
792
  |--------|--------|---------------|-------------|
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 |
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 |
752
800
 
753
- Each bundle accepts a `phase` option to override its default.
801
+ Each plugin accepts a `phase` option to override its default.
754
802
 
755
- ### Input Bundle
803
+ ### Input Plugin
756
804
 
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.
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.
758
806
 
759
807
  ```typescript
760
808
  import {
761
- createInputBundle,
809
+ createInputPlugin,
762
810
  type InputResourceTypes, type KeyCode
763
- } from 'ecspresso/bundles/input';
811
+ } from 'ecspresso/plugins/input';
764
812
 
765
813
  const world = ECSpresso.create()
766
- .withBundle(createInputBundle({
814
+ .withPlugin(createInputPlugin({
767
815
  actions: {
768
816
  jump: { keys: [' ', 'ArrowUp'] },
769
817
  shoot: { keys: ['z'], buttons: [0] },
@@ -788,19 +836,19 @@ input.setActionMap({
788
836
  });
789
837
  ```
790
838
 
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.
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.
792
840
 
793
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'`.
794
842
 
795
- ### Timer Bundle
843
+ ### Timer Plugin
796
844
 
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.
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.
798
846
 
799
847
  ```typescript
800
848
  import {
801
- createTimerBundle, createTimer, createRepeatingTimer,
849
+ createTimerPlugin, createTimer, createRepeatingTimer,
802
850
  type TimerComponentTypes, type TimerEventData
803
- } from 'ecspresso/bundles/timers';
851
+ } from 'ecspresso/plugins/timers';
804
852
 
805
853
  // Events used with onComplete must have TimerEventData payload
806
854
  interface Events {
@@ -808,21 +856,19 @@ interface Events {
808
856
  spawnWave: TimerEventData;
809
857
  }
810
858
 
811
- interface Components extends TimerComponentTypes<Events> {
812
- position: { x: number; y: number };
813
- }
814
-
815
859
  const world = ECSpresso
816
- .create<Components, Events>()
817
- .withBundle(createTimerBundle<Events>())
860
+ .create()
861
+ .withPlugin(createTimerPlugin())
862
+ .withComponentTypes<{ position: { x: number; y: number } }>()
863
+ .withEventTypes<Events>()
818
864
  .build();
819
865
 
820
866
  // 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' }) });
867
+ world.spawn({ ...createTimer(2.0), position: { x: 0, y: 0 } });
868
+ world.spawn({ ...createTimer(1.5, { onComplete: 'hideMessage' }) });
823
869
 
824
870
  // Repeating timer
825
- world.spawn({ ...createRepeatingTimer<Events>(5.0, { onComplete: 'spawnWave' }) });
871
+ world.spawn({ ...createRepeatingTimer(5.0, { onComplete: 'spawnWave' }) });
826
872
  ```
827
873
 
828
874
  Timer components expose `elapsed`, `duration`, `repeat`, `active`, `justFinished`, and optional `onComplete` for runtime control.
@@ -838,7 +884,10 @@ type Assets = {
838
884
  level1Background: { data: ImageBitmap };
839
885
  };
840
886
 
841
- const game = ECSpresso.create<Components, Events, Resources, Assets>()
887
+ const game = ECSpresso.create()
888
+ .withComponentTypes<Components>()
889
+ .withEventTypes<Events>()
890
+ .withResourceTypes<Resources>()
842
891
  .withAssets(assets => assets
843
892
  // Eager assets - loaded automatically during initialize()
844
893
  .add('playerTexture', async () => {
@@ -869,8 +918,7 @@ game.addSystem('gameplay')
869
918
  .requiresAssets(['playerTexture'])
870
919
  .setProcess((queries, dt, ecs) => {
871
920
  const player = ecs.getAsset('playerTexture');
872
- })
873
- .build();
921
+ });
874
922
  ```
875
923
 
876
924
  Asset events (`assetLoaded`, `assetFailed`, `assetGroupProgress`, `assetGroupLoaded`) are available through the event system -- see [Events](#events).
@@ -894,7 +942,10 @@ type Screens = {
894
942
  pause: ScreenDefinition<Record<string, never>, Record<string, never>>;
895
943
  };
896
944
 
897
- const game = ECSpresso.create<Components, Events, Resources, {}, Screens>()
945
+ const game = ECSpresso.create()
946
+ .withComponentTypes<Components>()
947
+ .withEventTypes<Events>()
948
+ .withResourceTypes<Resources>()
898
949
  .withScreens(screens => screens
899
950
  .add('menu', {
900
951
  initialState: () => ({ selectedOption: 0 }),
@@ -932,13 +983,11 @@ game.addSystem('menuUI')
932
983
  .inScreens(['menu']) // Only runs in 'menu'
933
984
  .setProcess((queries, dt, ecs) => {
934
985
  renderMenu(ecs.getScreenState().selectedOption);
935
- })
936
- .build();
986
+ });
937
987
 
938
988
  game.addSystem('animations')
939
989
  .excludeScreens(['pause']) // Runs in all screens except 'pause'
940
- .setProcess(() => { /* ... */ })
941
- .build();
990
+ .setProcess(() => { /* ... */ });
942
991
  ```
943
992
 
944
993
  ### Screen Resource
@@ -956,8 +1005,7 @@ game.addSystem('ui')
956
1005
  screen.stackDepth; // Number of screens in stack
957
1006
  screen.isCurrent('gameplay'); // Check current screen
958
1007
  screen.isActive('menu'); // true if in current or stack
959
- })
960
- .build();
1008
+ });
961
1009
  ```
962
1010
 
963
1011
  ## Type Safety
@@ -982,15 +1030,19 @@ world.addSystem('example')
982
1030
  entity.components.position.x; // ✅ guaranteed
983
1031
  entity.components.health.value; // ❌ not in query
984
1032
  }
985
- })
986
- .build();
1033
+ });
987
1034
 
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');
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
991
1043
  const world = ECSpresso.create()
992
- .withBundle(bundle1)
993
- .withBundle(bundle2) // Types merge successfully
1044
+ .withPlugin(plugin1)
1045
+ .withPlugin(plugin2)
994
1046
  .build();
995
1047
  ```
996
1048
 
@@ -1014,7 +1066,7 @@ world.entityManager.getComponent(123, 'position'); // undefined
1014
1066
  - Use `changed` query filters to skip unchanged entities in render sync, transform propagation, and similar systems
1015
1067
  - Call `markChanged` after in-place mutations so downstream systems can detect the change
1016
1068
  - Extract business logic into testable helper functions using query type utilities
1017
- - Bundle related systems for better organization and reusability
1069
+ - Group related systems into plugins for better organization and reusability
1018
1070
  - Use system phases to separate concerns (physics in `fixedUpdate`, rendering in `render`) and priorities for ordering within a phase
1019
1071
  - Use resource factories for expensive initialization (textures, audio, etc.)
1020
1072
  - Consider component callbacks for immediate reactions to state changes
@@ -38,7 +38,7 @@ export default class AssetManager<AssetTypes extends Record<string, unknown> = R
38
38
  /**
39
39
  * Get a loaded asset or undefined
40
40
  */
41
- getOrUndefined<K extends keyof AssetTypes>(key: K): AssetTypes[K] | undefined;
41
+ tryGet<K extends keyof AssetTypes>(key: K): AssetTypes[K] | undefined;
42
42
  /**
43
43
  * Get a handle to an asset with status information
44
44
  */
@@ -26,7 +26,7 @@ export interface AssetHandle<T> {
26
26
  /**
27
27
  * Get the asset value if loaded, undefined otherwise.
28
28
  */
29
- getOrUndefined(): T | undefined;
29
+ tryGet(): T | undefined;
30
30
  }
31
31
  /**
32
32
  * Resource interface for accessing assets in systems
@@ -56,7 +56,7 @@ export interface AssetsResource<A extends Record<string, unknown>, G extends str
56
56
  /**
57
57
  * Get a loaded asset or undefined if not loaded
58
58
  */
59
- getOrUndefined<K extends keyof A>(key: K): A[K] | undefined;
59
+ tryGet<K extends keyof A>(key: K): A[K] | undefined;
60
60
  /**
61
61
  * Get a handle to an asset with status information
62
62
  */