ecspresso 0.3.6 → 0.4.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.
package/README.md CHANGED
@@ -8,12 +8,12 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
8
8
 
9
9
  ## Features
10
10
 
11
- - 🔒 **Type-Safe**: Full TypeScript support with type inference for components, events, and resources
12
- - 🧩 **Modular**: Bundle-based architecture for modular gameplay systems and features
13
- - 💡 **Flexible**: Easily create entities, add components, and build systems with a clean, fluent API
14
- - 🔄 **Event-Driven**: Integrated event bus for communication between systems
15
- - 🗄️ **Resource Management**: Global resources for sharing state across systems
16
- - ⏱️ **Priority Control**: Set execution priority for systems to ensure proper processing order
11
+ - 🔒 **Type-Safe**: Full TypeScript support with component, event, and resource type inference
12
+ - 🧩 **Modular**: Bundle-based architecture for organizing features
13
+ - 💡 **Developer-Friendly**: Clean, fluent API with method chaining
14
+ - 🔄 **Event-Driven**: Integrated event system for decoupled communication
15
+ - 🗄️ **Resource Management**: Global state management with lazy loading
16
+ - **Query System**: Powerful entity filtering with helper type utilities
17
17
 
18
18
  ## Installation
19
19
 
@@ -24,494 +24,347 @@ npm install ecspresso
24
24
  ## Quick Start
25
25
 
26
26
  ```typescript
27
- import { ECSpresso } from 'ecspresso';
27
+ import ECSpresso from 'ecspresso';
28
28
 
29
29
  // Define your component types
30
30
  interface Components {
31
31
  position: { x: number; y: number };
32
32
  velocity: { x: number; y: number };
33
- sprite: { url: string };
33
+ health: { value: number };
34
34
  }
35
35
 
36
- // Define your event types
37
- interface Events {
38
- collision: { entity1: number; entity2: number };
39
- scoreChange: { amount: number };
40
- }
41
-
42
- // Define your resource types
43
- interface Resources {
44
- score: { value: number };
45
- gameState: 'playing' | 'paused' | 'gameOver';
46
- assets: { textures: Record<string, any>; sounds: Record<string, any> };
47
- }
48
-
49
- // Create a world instance directly
50
- const world = new ECSpresso<Components, Events, Resources>();
51
-
52
- // Add resources - can be direct values or factory functions
53
- world.addResource('score', { value: 0 });
54
- world.addResource('gameState', 'paused');
55
- world.addResource('assets', async () => {
56
- // Simulate loading game assets
57
- const textures = await loadTextures();
58
- const sounds = await loadSounds();
59
- return { textures, sounds };
60
- });
36
+ // Create a world and add a system
37
+ const world = new ECSpresso<Components>();
61
38
 
62
- // Add a movement system directly to the world
63
39
  world.addSystem('movement')
64
- .addQuery('movingEntities', {
65
- with: ['position', 'velocity']
66
- })
40
+ .addQuery('moving', { with: ['position', 'velocity'] })
67
41
  .setProcess((queries, deltaTime) => {
68
- for (const entity of queries.movingEntities) {
42
+ for (const entity of queries.moving) {
69
43
  entity.components.position.x += entity.components.velocity.x * deltaTime;
70
44
  entity.components.position.y += entity.components.velocity.y * deltaTime;
71
45
  }
72
46
  })
73
- .setOnInitialize(async (ecs) => {
74
- // One-time initialization of the system
75
- console.log('Setting up movement system...');
76
- const gameState = ecs.getResource('gameState');
77
- gameState.lastMovementUpdate = Date.now();
78
- })
79
- .build(); // Don't forget to call build() to finalize the system
80
-
81
- // Create an entity with position and velocity components
82
- const entity = world.entityManager.createEntity();
83
- world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
84
- world.entityManager.addComponent(entity.id, 'velocity', { x: 10, y: 5 });
85
-
86
- // Initialize everything (systems and resources with factory functions) in one call
87
- await world.initialize();
88
-
89
- // Run a single update
90
- world.update(1/60);
91
-
92
- // Check new position
93
- const position = world.entityManager.getComponent(entity.id, 'position');
94
- console.log(position); // { x: 0.16666..., y: 0.08333... }
95
- ```
96
-
97
- ## Building Modular Systems with Bundles
98
-
99
- Bundles are a powerful way to organize game features:
100
-
101
- ```typescript
102
- // Create a player input bundle
103
- const inputBundle = new Bundle<Components, Events, Resources>('input-bundle')
104
- .addSystem('playerInput')
105
- .setProcess((_queries, _deltaTime, ecs) => {
106
- // Handle keyboard input and modify player velocity
107
- // ...
108
- });
109
-
110
- // Create a rendering bundle
111
- const renderBundle = new Bundle<Components, Events, Resources>('render-bundle')
112
- .addSystem('renderer')
113
- .addQuery('sprites', { with: ['position', 'sprite'] })
114
- .setProcess((queries) => {
115
- // Render all sprites
116
- for (const entity of queries.sprites) {
117
- // Draw entities at their positions
118
- // ...
119
- }
120
- });
121
-
122
- // Create a scoring bundle that adds a resource and listens for events
123
- const scoringBundle = new Bundle<Components, Events, Resources>('scoring-bundle')
124
- .addResource('score', { value: 0 })
125
- // Resources can also be added using factory functions
126
- .addResource('gameStats', () => ({
127
- highScore: 0,
128
- totalPlayTime: 0,
129
- sessionStartTime: Date.now()
130
- }))
131
- .addSystem('scoreKeeper')
132
- .setEventHandlers({
133
- scoreChange: {
134
- handler: (data, ecs) => {
135
- const score = ecs.getResource('score');
136
- score.value += data.amount;
137
- console.log(`Score: ${score.value}`);
138
- }
139
- }
140
- });
141
-
142
- // Create a game initialization bundle with event handlers
143
- const initBundle = new Bundle<Components, Events, Resources>('init-bundle')
144
- .addSystem('initialization')
145
- .setOnInitialize(async (ecs) => {
146
- console.log('Game systems initializing...');
147
- // Do one-time system setup here
148
- })
149
- .setEventHandlers({
150
- gameStart: {
151
- async handler(data, ecs) {
152
- console.log('Game starting...');
153
-
154
- // Initialize all resources and systems
155
- await ecs.initialize();
156
-
157
- // Resources and systems are now ready to use
158
- const assets = ecs.getResource('assets');
159
- console.log(`Loaded ${Object.keys(assets.textures).length} textures`);
160
-
161
- // Continue with game initialization
162
- // ...
163
- }
164
- }
165
- });
166
-
167
- // Create the game world with all features using the builder pattern
168
- const game = ECSpresso.create<Components, Events, Resources>()
169
- .withBundle(initBundle)
170
- .withBundle(inputBundle)
171
- .withBundle(renderBundle)
172
- .withBundle(scoringBundle)
173
- .build()
174
- .addResource('assets', async () => {
175
- // This won't execute until initializeResources is called
176
- return { textures: await loadTextures(), sounds: await loadSounds() };
177
- });
178
-
179
- // Start the game
180
- game.eventBus.publish('gameStart', {});
181
- ```
182
-
183
- ## Type Safety with the Builder Pattern
184
-
185
- ECSpresso uses a builder pattern to provide strong type checking for bundle compatibility:
186
-
187
- ```typescript
188
- // These bundles have compatible component types
189
- const bundle1 = new Bundle<{position: {x: number, y: number}}>('bundle1');
190
- const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('bundle2');
191
-
192
- // Create a world with both bundles - TypeScript will allow this
193
- const world = ECSpresso.create()
194
- .withBundle(bundle1)
195
- .withBundle(bundle2)
196
47
  .build();
197
48
 
198
- // These bundles have conflicting component types
199
- const bundle3 = new Bundle<{position: {x: number, y: number}}>('bundle3');
200
- const bundle4 = new Bundle<{position: string}>('bundle4');
49
+ // Create entities and run
50
+ const player = world.spawn({
51
+ position: { x: 0, y: 0 },
52
+ velocity: { x: 10, y: 5 },
53
+ health: { value: 100 }
54
+ });
201
55
 
202
- // TypeScript will show an error because bundles have conflicting types
203
- const world2 = ECSpresso.create()
204
- .withBundle(bundle3)
205
- // @ts-expect-error - TypeScript will flag this because the position types conflict
206
- .withBundle(bundle4)
207
- .build();
56
+ world.update(1/60); // Run one frame
208
57
  ```
209
58
 
210
- ## Working with Entities and Components
211
-
212
- ```typescript
213
- const world = ECSpresso.create<Components, Events, Resources>()
214
- .withBundle(/* your bundle */)
215
- .build();
59
+ ## Core Concepts
216
60
 
217
- // Create an entity
218
- const entity = world.entityManager.createEntity();
61
+ ### Entities and Components
219
62
 
220
- // Add components individually
221
- world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
222
- world.entityManager.addComponent(entity.id, 'velocity', { x: 0, y: 0 });
63
+ Entities are containers for components. Use `spawn()` to create entities with initial components:
223
64
 
224
- // Add multiple components at once
225
- world.entityManager.addComponents(entity, {
65
+ ```typescript
66
+ // Create entity with components
67
+ const entity = world.spawn({
226
68
  position: { x: 10, y: 20 },
227
- velocity: { x: 5, y: -2 }
69
+ health: { value: 100 }
228
70
  });
229
71
 
72
+ // Add components later
73
+ world.entityManager.addComponent(entity.id, 'velocity', { x: 5, y: 0 });
74
+
230
75
  // Get component data
231
76
  const position = world.entityManager.getComponent(entity.id, 'position');
232
77
 
233
- // Check if an entity has a component
234
- const hasPosition = world.entityManager.hasComponent(entity.id, 'position');
235
-
236
- // Remove a component
78
+ // Remove components or entities
237
79
  world.entityManager.removeComponent(entity.id, 'velocity');
238
-
239
- // Remove an entity (and all its components)
240
80
  world.entityManager.removeEntity(entity.id);
241
81
  ```
242
82
 
243
- ## Working with Systems and Queries
83
+ ### Systems and Queries
244
84
 
245
- Systems can be added directly to an ECSpresso instance:
85
+ Systems process entities that match specific component patterns:
246
86
 
247
87
  ```typescript
248
- const world = ECSpresso.create<Components, Events, Resources>()
249
- .build();
250
-
251
- world.addSystem('physicsSystem')
252
- // Set system execution priority (higher numbers execute first)
253
- .setPriority(50)
254
- // Query entities that have both position and velocity components
255
- .addQuery('movingEntities', {
256
- with: ['position', 'velocity']
88
+ world.addSystem('combat')
89
+ .addQuery('fighters', {
90
+ with: ['position', 'health'],
91
+ without: ['dead']
257
92
  })
258
- // Query entities that have position but not player component
259
- .addQuery('nonPlayerObjects', {
260
- with: ['position'],
261
- without: ['player']
262
- })
263
- // Query entities with different component combinations
264
- .addQuery('flyingNonPlayerEntities', {
265
- with: ['flying', 'position'],
266
- without: ['player', 'grounded']
93
+ .addQuery('projectiles', {
94
+ with: ['position', 'damage']
267
95
  })
268
96
  .setProcess((queries, deltaTime) => {
269
- // Process moving entities
270
- for (const entity of queries.movingEntities) {
271
- entity.components.position.x += entity.components.velocity.x * deltaTime;
272
- entity.components.position.y += entity.components.velocity.y * deltaTime;
273
- }
274
-
275
- // Process non-player objects
276
- for (const entity of queries.nonPlayerObjects) {
277
- // Do something with non-player objects
278
- }
279
-
280
- // Process flying non-player entities
281
- for (const entity of queries.flyingNonPlayerEntities) {
282
- // Apply flying behavior
97
+ // Process fighters and projectiles
98
+ for (const fighter of queries.fighters) {
99
+ for (const projectile of queries.projectiles) {
100
+ // Combat logic here
101
+ }
283
102
  }
284
103
  })
285
- .build(); // Finalizes and adds the system to the world
104
+ .build();
286
105
  ```
287
106
 
288
- ## System Lifecycle Hooks
107
+ ### Resources
289
108
 
290
- ECSpresso systems have two lifecycle hooks you can implement:
109
+ Resources provide global state accessible to all systems:
291
110
 
292
111
  ```typescript
293
- world.addSystem('gameSystem')
294
- // Called when `initialize()` is invoked on the ECSpresso instance
295
- // Good for one-time setup that depends on resources
296
- .setOnInitialize(async (ecs) => {
297
- console.log('System initializing');
298
- // Load assets, configure resources, etc.
299
- })
112
+ interface Resources {
113
+ score: { value: number };
114
+ settings: { difficulty: 'easy' | 'hard' };
115
+ }
300
116
 
301
- // Called when the system is removed or detached from the ECSpresso instance
302
- .setOnDetach((ecs) => {
303
- console.log('System detached');
304
- // Clean up resources, cancel subscriptions, etc.
305
- })
117
+ const world = new ECSpresso<Components, {}, Resources>();
306
118
 
119
+ // Add resources
120
+ world.addResource('score', { value: 0 });
121
+ world.addResource('settings', { difficulty: 'easy' });
122
+
123
+ // Use in systems
124
+ world.addSystem('scoring')
125
+ .setProcess((queries, deltaTime, ecs) => {
126
+ const score = ecs.getResource('score');
127
+ score.value += 10;
128
+ })
307
129
  .build();
308
130
  ```
309
131
 
310
- The `initialize()` method on the ECSpresso instance initializes pending resources first and then calls `onInitialize` on all systems:
132
+ ## Working with Systems
133
+
134
+ ### Method Chaining
135
+
136
+ ECSpresso supports fluent method chaining for creating multiple systems:
311
137
 
312
138
  ```typescript
313
- await ecs.initialize();
139
+ world.addSystem('physics')
140
+ .addQuery('moving', { with: ['position', 'velocity'] })
141
+ .setProcess((queries, deltaTime) => {
142
+ // Physics logic
143
+ })
144
+ .and() // Complete this system and continue chaining
145
+ .addSystem('rendering')
146
+ .addQuery('visible', { with: ['position', 'sprite'] })
147
+ .setProcess((queries) => {
148
+ // Rendering logic
149
+ })
150
+ .build(); // Only the final system needs .build()
314
151
  ```
315
152
 
316
- ## System Priority
153
+ ### Query Type Utilities
317
154
 
318
- ECSpresso allows you to control the execution order of systems using priorities:
155
+ Extract entity types from queries to create reusable helper functions:
319
156
 
320
157
  ```typescript
321
- // Systems with higher priority values execute before those with lower values
322
- // Default priority is 0 if not specified
158
+ import { createQueryDefinition, QueryResultEntity } from 'ecspresso';
323
159
 
324
- // Rendering system (runs first)
325
- world.addSystem('renderSystem')
326
- .setPriority(100)
327
- .setProcess(() => {
328
- // Rendering logic
329
- })
330
- .build();
160
+ // Create reusable query definitions
161
+ const movingQuery = createQueryDefinition({
162
+ with: ['position', 'velocity'] as const,
163
+ without: ['frozen'] as const
164
+ } as const);
331
165
 
332
- // Physics system (runs second)
333
- world.addSystem('physicsSystem')
334
- .setPriority(50)
335
- .setProcess(() => {
336
- // Physics update logic
337
- })
338
- .build();
166
+ // Extract entity type for helper functions
167
+ type MovingEntity = QueryResultEntity<Components, typeof movingQuery>;
339
168
 
340
- // Cleanup system (runs last)
341
- world.addSystem('cleanupSystem')
342
- .setPriority(0) // Default priority if not specified
343
- .setProcess(() => {
344
- // Cleanup logic
169
+ // Create type-safe helper functions
170
+ function updatePosition(entity: MovingEntity, deltaTime: number) {
171
+ entity.components.position.x += entity.components.velocity.x * deltaTime;
172
+ entity.components.position.y += entity.components.velocity.y * deltaTime;
173
+ }
174
+
175
+ // Use in systems
176
+ world.addSystem('movement')
177
+ .addQuery('entities', movingQuery)
178
+ .setProcess((queries, deltaTime) => {
179
+ for (const entity of queries.entities) {
180
+ updatePosition(entity, deltaTime); // Perfect type safety!
181
+ }
345
182
  })
346
183
  .build();
347
184
  ```
348
185
 
349
- Systems with the same priority value execute in the order they were registered, maintaining backward compatibility with existing code.
186
+ ### System Priority
350
187
 
351
- You can also update a system's priority dynamically at runtime:
188
+ Control execution order with priorities (higher numbers execute first):
352
189
 
353
190
  ```typescript
354
- // Change a system's priority (higher numbers execute first)
355
- world.updateSystemPriority('physicsSystem', 110); // Now physics will run before rendering
191
+ world.addSystem('physics')
192
+ .setPriority(100) // Runs first
193
+ .setProcess(() => { /* physics */ })
194
+ .and()
195
+ .addSystem('rendering')
196
+ .setPriority(50) // Runs second
197
+ .setProcess(() => { /* rendering */ })
198
+ .build();
356
199
  ```
357
200
 
358
- Priority also works with systems added through bundles:
201
+ ## Advanced Features
202
+
203
+ ### Bundles
204
+
205
+ Organize related systems and resources into reusable bundles:
359
206
 
360
207
  ```typescript
361
- const highPriorityBundle = new Bundle<Components>()
362
- .addSystem('importantSystem')
363
- .setPriority(100)
364
- .setProcess(() => {
365
- // This will run first
208
+ import { Bundle } from 'ecspresso';
209
+
210
+ const inputBundle = new Bundle<Components, Events, Resources>('input')
211
+ .addSystem('playerInput')
212
+ .addQuery('players', { with: ['position', 'velocity', 'player'] })
213
+ .setProcess((queries, deltaTime, ecs) => {
214
+ // Handle input
366
215
  });
367
216
 
368
- const lowPriorityBundle = new Bundle<Components>()
369
- .addSystem('lateSystem')
370
- .setPriority(0)
371
- .setProcess(() => {
372
- // This will run last
217
+ const renderBundle = new Bundle<Components, Events, Resources>('render')
218
+ .addSystem('renderer')
219
+ .addQuery('sprites', { with: ['position', 'sprite'] })
220
+ .setProcess((queries) => {
221
+ // Render sprites
373
222
  });
374
223
 
375
- const world = ECSpresso.create<Components>()
376
- .withBundle(lowPriorityBundle) // Added first but runs last due to priority
377
- .withBundle(highPriorityBundle) // Added second but runs first due to priority
224
+ // Create world with bundles
225
+ const game = ECSpresso.create<Components, Events, Resources>()
226
+ .withBundle(inputBundle)
227
+ .withBundle(renderBundle)
378
228
  .build();
379
229
  ```
380
230
 
381
- The system priority implementation is optimized with a cached sorting mechanism that only re-sorts systems when priorities change or when systems are added or removed, avoiding unnecessary sorting during each update cycle.
382
-
383
- ## Event System
231
+ ### Events
384
232
 
385
- The event system allows communication between systems:
233
+ Use events for decoupled system communication:
386
234
 
387
235
  ```typescript
388
- // Define an event handler in a system
389
- const collisionBundle = new Bundle<Components, Events, Resources>('collision-bundle')
390
- .addSystem('collisionResponse')
236
+ interface Events {
237
+ playerDied: { playerId: number };
238
+ levelComplete: { score: number };
239
+ }
240
+
241
+ const world = new ECSpresso<Components, Events, Resources>();
242
+
243
+ // Handle events in systems
244
+ world.addSystem('gameLogic')
391
245
  .setEventHandlers({
392
- collision: {
246
+ playerDied: {
393
247
  handler: (data, ecs) => {
394
- // Handle collision event
395
- // data contains entity1 and entity2 from the event
248
+ console.log(`Player ${data.playerId} died`);
249
+ // Respawn logic
396
250
  }
397
251
  }
398
- });
399
-
400
- const world = ECSpresso.create<Components, Events, Resources>()
401
- .withBundle(collisionBundle)
252
+ })
402
253
  .build();
403
254
 
404
- // Publish an event from anywhere
405
- world.eventBus.publish('collision', {
406
- entity1: 1,
407
- entity2: 2
408
- });
409
-
410
- // Subscribe to events manually (outside of systems)
411
- const unsubscribe = world.eventBus.subscribe('collision', (data) => {
412
- console.log(`Collision between entities ${data.entity1} and ${data.entity2}`);
413
- });
414
-
415
- // Stop listening
416
- unsubscribe();
255
+ // Publish events from anywhere
256
+ world.eventBus.publish('playerDied', { playerId: 1 });
417
257
  ```
418
258
 
419
- ## Resources
259
+ ### Resource Factories
420
260
 
421
- Resources provide global state accessible to all systems:
261
+ Create resources lazily with factory functions:
422
262
 
423
263
  ```typescript
424
- // Add a resource directly
425
- world.addResource('score', { value: 0 });
264
+ // Sync factory
265
+ world.addResource('config', () => ({
266
+ difficulty: 'normal',
267
+ soundEnabled: true
268
+ }));
426
269
 
427
- // Get a resource
428
- const score = world.getResource('score');
429
- score.value += 10;
270
+ // Async factory
271
+ world.addResource('assets', async () => {
272
+ const textures = await loadTextures();
273
+ return { textures };
274
+ });
430
275
 
431
- // Check if a resource exists
432
- const hasScore = world.hasResource('score');
276
+ // Initialize all resources
277
+ await world.initializeResources();
433
278
  ```
434
279
 
435
- ### Resource Factory Functions
280
+ ### System Lifecycle
436
281
 
437
- Resources can also be created using factory functions, which is useful for lazy initialization or async resources:
282
+ Systems can have initialization and cleanup hooks:
438
283
 
439
284
  ```typescript
440
- // Add a resource using a synchronous factory function
441
- world.addResource('controlMap', () => {
442
- console.log('Creating control map');
443
- return {
444
- up: false,
445
- down: false,
446
- left: false,
447
- right: false
448
- };
449
- });
285
+ world.addSystem('gameSystem')
286
+ .setOnInitialize(async (ecs) => {
287
+ // One-time setup
288
+ console.log('System starting...');
289
+ })
290
+ .setOnDetach((ecs) => {
291
+ // Cleanup when system is removed
292
+ console.log('System shutting down...');
293
+ })
294
+ .build();
450
295
 
451
- // Add a resource using a factory function that receives the ECSpresso instance
452
- world.addResource('playerConfig', (ecs) => {
453
- // Access other resources during initialization
454
- const gameConfig = ecs.getResource('gameConfig');
455
- return {
456
- speed: gameConfig.difficulty === 'hard' ? 200 : 100,
457
- startingHealth: gameConfig.difficulty === 'hard' ? 50 : 100
458
- };
459
- });
296
+ // Initialize all systems
297
+ await world.initialize();
298
+ ```
460
299
 
461
- // Add a resource using an asynchronous factory function
462
- world.addResource('gameAssets', async (ecs) => {
463
- console.log('Loading game assets...');
464
- // You can access other resources during async initialization
465
- const settings = ecs.getResource('settings');
466
- const assets = await loadAssets(settings.assetQuality);
467
- return assets;
468
- });
300
+ ## Type Safety
301
+
302
+ ECSpresso provides comprehensive TypeScript support:
469
303
 
470
- // Factory functions are executed when the resource is first accessed
471
- const controlMap = world.getResource('controlMap'); // Factory executes here
304
+ ### Component Type Safety
305
+ ```typescript
306
+ // ✅ Valid
307
+ world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
472
308
 
473
- // Or when explicitly initialized
474
- await world.initializeResources(); // Initializes all pending resources
309
+ // TypeScript error - invalid component
310
+ world.entityManager.addComponent(entity.id, 'invalid', { data: 'bad' });
475
311
 
476
- // You can also initialize specific resources
477
- await world.initializeResources('gameAssets', 'controlMap');
312
+ // TypeScript error - wrong component shape
313
+ world.entityManager.addComponent(entity.id, 'position', { x: 0 }); // missing y
478
314
  ```
479
315
 
480
- Factory functions are useful for:
481
- - Lazy loading of expensive resources
482
- - Async initialization of resources requiring network or file operations
483
- - Resources that depend on other systems being initialized first
484
- - Avoiding circular dependencies in your initialization code
316
+ ### Query Type Safety
317
+ ```typescript
318
+ world.addSystem('example')
319
+ .addQuery('moving', { with: ['position', 'velocity'] })
320
+ .setProcess((queries) => {
321
+ for (const entity of queries.moving) {
322
+ // ✅ TypeScript knows these exist
323
+ entity.components.position.x;
324
+ entity.components.velocity.y;
325
+
326
+ // ❌ TypeScript error - not guaranteed to exist
327
+ entity.components.health.value;
328
+ }
329
+ })
330
+ .build();
331
+ ```
485
332
 
486
- When using async factory functions, ensure you either:
487
- 1. Call `initializeResources()` explicitly before accessing the resource, or
488
- 2. Use `await` when getting a resource that might return a Promise
333
+ ### Bundle Type Compatibility
334
+ ```typescript
335
+ // Compatible bundles merge cleanly
336
+ const bundle1 = new Bundle<{position: {x: number, y: number}}>('bundle1');
337
+ const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('bundle2');
338
+
339
+ const world = ECSpresso.create()
340
+ .withBundle(bundle1)
341
+ .withBundle(bundle2) // ✅ Types merge successfully
342
+ .build();
343
+
344
+ // ❌ Conflicting types cause TypeScript errors
345
+ const conflictingBundle = new Bundle<{position: string}>('conflict');
346
+ // world.withBundle(conflictingBundle); // TypeScript error!
347
+ ```
489
348
 
490
349
  ## Component Callbacks
491
350
 
492
- You can listen for specific component types being added or removed on any entity using the `EntityManager` API:
351
+ React to component changes with callbacks:
493
352
 
494
353
  ```typescript
495
- // Create your entity manager
496
- const entityManager = new EntityManager<Components>();
497
-
498
- // Listen for when a "health" component is added
499
- entityManager.onComponentAdded('health', (value, entity) => {
354
+ // Listen for component additions/removals
355
+ world.entityManager.onComponentAdded('health', (value, entity) => {
500
356
  console.log(`Health added to entity ${entity.id}:`, value);
501
357
  });
502
358
 
503
- // Listen for when a "health" component is removed
504
- entityManager.onComponentRemoved('health', (oldValue, entity) => {
359
+ world.entityManager.onComponentRemoved('health', (oldValue, entity) => {
505
360
  console.log(`Health removed from entity ${entity.id}:`, oldValue);
506
361
  });
507
-
508
- // Create an entity and add/remove components to trigger callbacks
509
- const e = entityManager.createEntity();
510
- entityManager.addComponent(e.id, 'health', { value: 100 });
511
- // => logs: Health added to entity 1: { value: 100 }
512
- entityManager.removeComponent(e.id, 'health');
513
- // => logs: Health removed from entity 1: { value: 100 }
514
362
  ```
515
363
 
516
- This is useful for debugging, UI updates, or systems that need to react immediately to component changes.
364
+ ## Performance Tips
517
365
 
366
+ - Use query type utilities to extract business logic into testable helper functions
367
+ - Bundle related systems for better organization
368
+ - Use system priorities to ensure correct execution order
369
+ - Leverage resource factories for expensive initialization
370
+ - Consider component callbacks for immediate reactions to state changes