ecspresso 0.4.2 → 0.5.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 +435 -23
- package/dist/asset-manager.d.ts +111 -0
- package/dist/asset-types.d.ts +104 -0
- package/dist/bundle.d.ts +60 -4
- package/dist/ecspresso.d.ts +281 -13
- package/dist/entity-manager.d.ts +100 -2
- package/dist/event-bus.d.ts +5 -0
- package/dist/hierarchy-manager.d.ts +107 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +13 -10
- package/dist/screen-manager.d.ts +116 -0
- package/dist/screen-types.d.ts +119 -0
- package/dist/system-builder.d.ts +39 -6
- package/dist/types.d.ts +40 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
*(pronounced "ex-presso")*
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A type-safe, modular, and extensible Entity Component System (ECS) framework for TypeScript.
|
|
5
|
+
A type-safe, modular, and extensible Entity Component System (ECS) framework for TypeScript and JavaScript.
|
|
8
6
|
|
|
9
7
|
## Features
|
|
10
8
|
|
|
@@ -13,6 +11,9 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
13
11
|
- **Developer-Friendly**: Clean, fluent API with method chaining
|
|
14
12
|
- **Event-Driven**: Integrated event system for decoupled communication
|
|
15
13
|
- **Resource Management**: Global state management with lazy loading
|
|
14
|
+
- **Asset Management**: Eager/lazy asset loading with groups and progress tracking
|
|
15
|
+
- **Screen Management**: Game state/screen transitions with overlay support
|
|
16
|
+
- **Entity Hierarchy**: Parent-child relationships with traversal and cascade deletion
|
|
16
17
|
- **Query System**: Powerful entity filtering with helper type utilities
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
@@ -135,21 +136,32 @@ world.addSystem('scoring')
|
|
|
135
136
|
|
|
136
137
|
### Method Chaining
|
|
137
138
|
|
|
138
|
-
Chain multiple systems for cleaner code:
|
|
139
|
+
Chain multiple systems using `.and()` for cleaner code. The `.and()` method returns the parent container (ECSpresso or Bundle), enabling fluent chaining:
|
|
139
140
|
|
|
140
141
|
```typescript
|
|
142
|
+
// Chaining systems on ECSpresso
|
|
141
143
|
world.addSystem('physics')
|
|
142
144
|
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
143
145
|
.setProcess((queries, deltaTime) => {
|
|
144
146
|
// Physics logic
|
|
145
147
|
})
|
|
146
|
-
.and() //
|
|
148
|
+
.and() // Returns ECSpresso for continued chaining
|
|
147
149
|
.addSystem('rendering')
|
|
148
150
|
.addQuery('visible', { with: ['position', 'sprite'] })
|
|
149
151
|
.setProcess((queries) => {
|
|
150
152
|
// Rendering logic
|
|
151
153
|
})
|
|
152
154
|
.build();
|
|
155
|
+
|
|
156
|
+
// Chaining systems in a Bundle
|
|
157
|
+
const bundle = new Bundle<Components>()
|
|
158
|
+
.addSystem('movement')
|
|
159
|
+
.setProcess(() => { /* ... */ })
|
|
160
|
+
.and() // Returns Bundle for continued chaining
|
|
161
|
+
.addSystem('collision')
|
|
162
|
+
.setProcess(() => { /* ... */ })
|
|
163
|
+
.and()
|
|
164
|
+
.addResource('config', { speed: 10 });
|
|
153
165
|
```
|
|
154
166
|
|
|
155
167
|
### Query Type Utilities
|
|
@@ -209,35 +221,49 @@ Organize related systems and resources into reusable bundles:
|
|
|
209
221
|
```typescript
|
|
210
222
|
import { Bundle } from 'ecspresso';
|
|
211
223
|
|
|
212
|
-
|
|
213
|
-
interface InputComponents {
|
|
224
|
+
interface GameComponents {
|
|
214
225
|
position: { x: number; y: number };
|
|
215
226
|
velocity: { x: number; y: number };
|
|
216
|
-
|
|
227
|
+
sprite: { texture: string };
|
|
217
228
|
}
|
|
218
229
|
|
|
219
|
-
interface
|
|
220
|
-
|
|
221
|
-
sprite: any; // Your sprite type
|
|
230
|
+
interface GameResources {
|
|
231
|
+
gravity: { value: number };
|
|
222
232
|
}
|
|
223
233
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
234
|
+
// Create a bundle with multiple systems using .and() for chaining
|
|
235
|
+
const physicsBundle = new Bundle<GameComponents, {}, GameResources>('physics')
|
|
236
|
+
.addSystem('applyVelocity')
|
|
237
|
+
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
238
|
+
.setProcess((queries, deltaTime) => {
|
|
239
|
+
for (const entity of queries.moving) {
|
|
240
|
+
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
241
|
+
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
.and() // Returns the bundle for continued chaining
|
|
245
|
+
.addSystem('applyGravity')
|
|
246
|
+
.addQuery('falling', { with: ['velocity'] })
|
|
227
247
|
.setProcess((queries, deltaTime, ecs) => {
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
const gravity = ecs.getResource('gravity');
|
|
249
|
+
for (const entity of queries.falling) {
|
|
250
|
+
entity.components.velocity.y += gravity.value * deltaTime;
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
.and()
|
|
254
|
+
.addResource('gravity', { value: 9.8 });
|
|
230
255
|
|
|
231
|
-
const renderBundle = new Bundle<
|
|
256
|
+
const renderBundle = new Bundle<GameComponents>('render')
|
|
232
257
|
.addSystem('renderer')
|
|
233
258
|
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
234
259
|
.setProcess((queries) => {
|
|
235
260
|
// Render sprites
|
|
236
|
-
})
|
|
261
|
+
})
|
|
262
|
+
.and();
|
|
237
263
|
|
|
238
264
|
// Create world with bundles
|
|
239
|
-
const game = ECSpresso.create()
|
|
240
|
-
.withBundle(
|
|
265
|
+
const game = ECSpresso.create<GameComponents, {}, GameResources>()
|
|
266
|
+
.withBundle(physicsBundle)
|
|
241
267
|
.withBundle(renderBundle)
|
|
242
268
|
.build();
|
|
243
269
|
```
|
|
@@ -254,6 +280,19 @@ interface Events {
|
|
|
254
280
|
|
|
255
281
|
const world = new ECSpresso<Components, Events>();
|
|
256
282
|
|
|
283
|
+
// Subscribe to events with on() - returns unsubscribe function
|
|
284
|
+
const unsubscribe = world.on('playerDied', (data) => {
|
|
285
|
+
console.log(`Player ${data.playerId} died`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Unsubscribe when done
|
|
289
|
+
unsubscribe();
|
|
290
|
+
|
|
291
|
+
// Or unsubscribe by callback reference with off()
|
|
292
|
+
const handler = (data) => console.log(`Level complete! Score: ${data.score}`);
|
|
293
|
+
world.on('levelComplete', handler);
|
|
294
|
+
world.off('levelComplete', handler);
|
|
295
|
+
|
|
257
296
|
// Handle events in systems
|
|
258
297
|
world.addSystem('gameLogic')
|
|
259
298
|
.setEventHandlers({
|
|
@@ -318,6 +357,369 @@ world.addSystem('gameSystem')
|
|
|
318
357
|
await world.initialize();
|
|
319
358
|
```
|
|
320
359
|
|
|
360
|
+
### Post-Update Hooks
|
|
361
|
+
|
|
362
|
+
Register callbacks that run after all systems have processed during `update()`:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Register a post-update hook - returns unsubscribe function
|
|
366
|
+
const unsubscribe = world.onPostUpdate((ecs, deltaTime) => {
|
|
367
|
+
// Runs after all systems in update()
|
|
368
|
+
// Useful for cleanup, state sync, or debug logging
|
|
369
|
+
console.log(`Frame completed in ${deltaTime}s`);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Multiple hooks run in registration order
|
|
373
|
+
world.onPostUpdate((ecs) => {
|
|
374
|
+
// First hook
|
|
375
|
+
});
|
|
376
|
+
world.onPostUpdate((ecs) => {
|
|
377
|
+
// Second hook
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Unsubscribe when no longer needed
|
|
381
|
+
unsubscribe();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Entity Hierarchy
|
|
385
|
+
|
|
386
|
+
Create parent-child relationships between entities for scene graphs, UI trees, or skeletal hierarchies:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
const world = new ECSpresso<Components>();
|
|
390
|
+
|
|
391
|
+
// Create a parent entity
|
|
392
|
+
const player = world.spawn({
|
|
393
|
+
position: { x: 0, y: 0 }
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Create a child entity using spawnChild
|
|
397
|
+
const weapon = world.spawnChild(player.id, {
|
|
398
|
+
position: { x: 10, y: 0 } // Relative to parent
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Or set parent on existing entity
|
|
402
|
+
const shield = world.spawn({ position: { x: -10, y: 0 } });
|
|
403
|
+
world.setParent(shield.id, player.id);
|
|
404
|
+
|
|
405
|
+
// Query relationships
|
|
406
|
+
world.getParent(weapon.id); // player.id
|
|
407
|
+
world.getChildren(player.id); // [weapon.id, shield.id]
|
|
408
|
+
|
|
409
|
+
// Orphan an entity (remove from parent)
|
|
410
|
+
world.removeParent(shield.id);
|
|
411
|
+
world.getParent(shield.id); // null
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### Traversal Methods
|
|
415
|
+
|
|
416
|
+
Navigate the hierarchy tree with traversal utilities:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// Build a hierarchy: root -> child -> grandchild
|
|
420
|
+
const root = world.spawn({ position: { x: 0, y: 0 } });
|
|
421
|
+
const child = world.spawnChild(root.id, { position: { x: 10, y: 0 } });
|
|
422
|
+
const grandchild = world.spawnChild(child.id, { position: { x: 20, y: 0 } });
|
|
423
|
+
|
|
424
|
+
// Ancestors (from entity up to root)
|
|
425
|
+
world.getAncestors(grandchild.id); // [child.id, root.id]
|
|
426
|
+
|
|
427
|
+
// Descendants (depth-first order)
|
|
428
|
+
world.getDescendants(root.id); // [child.id, grandchild.id]
|
|
429
|
+
|
|
430
|
+
// Get root of any entity
|
|
431
|
+
world.getRoot(grandchild.id); // root.id
|
|
432
|
+
|
|
433
|
+
// Siblings (other children of same parent)
|
|
434
|
+
const child2 = world.spawnChild(root.id, { position: { x: -10, y: 0 } });
|
|
435
|
+
world.getSiblings(child.id); // [child2.id]
|
|
436
|
+
|
|
437
|
+
// Relationship checks
|
|
438
|
+
world.isDescendantOf(grandchild.id, root.id); // true
|
|
439
|
+
world.isAncestorOf(root.id, grandchild.id); // true
|
|
440
|
+
|
|
441
|
+
// All root entities (entities with children but no parent)
|
|
442
|
+
world.getRootEntities(); // [root.id]
|
|
443
|
+
|
|
444
|
+
// Child ordering
|
|
445
|
+
world.getChildAt(root.id, 0); // child.id
|
|
446
|
+
world.getChildIndex(root.id, child2.id); // 1
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Cascade Deletion
|
|
450
|
+
|
|
451
|
+
When removing entities, descendants are automatically removed by default:
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
const parent = world.spawn({ position: { x: 0, y: 0 } });
|
|
455
|
+
const child = world.spawnChild(parent.id, { position: { x: 10, y: 0 } });
|
|
456
|
+
const grandchild = world.spawnChild(child.id, { position: { x: 20, y: 0 } });
|
|
457
|
+
|
|
458
|
+
// Remove parent - cascades to all descendants
|
|
459
|
+
world.removeEntity(parent.id);
|
|
460
|
+
world.entityManager.getEntity(child.id); // undefined
|
|
461
|
+
world.entityManager.getEntity(grandchild.id); // undefined
|
|
462
|
+
|
|
463
|
+
// To orphan children instead of deleting them:
|
|
464
|
+
world.removeEntity(parent.id, { cascade: false });
|
|
465
|
+
// Children still exist but have no parent
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### Hierarchy Events
|
|
469
|
+
|
|
470
|
+
React to hierarchy changes with the `hierarchyChanged` event:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
interface Events {
|
|
474
|
+
hierarchyChanged: {
|
|
475
|
+
entityId: number;
|
|
476
|
+
oldParent: number | null;
|
|
477
|
+
newParent: number | null;
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const world = new ECSpresso<Components, Events>();
|
|
482
|
+
|
|
483
|
+
world.on('hierarchyChanged', (data) => {
|
|
484
|
+
if (data.newParent !== null) {
|
|
485
|
+
console.log(`Entity ${data.entityId} attached to ${data.newParent}`);
|
|
486
|
+
} else {
|
|
487
|
+
console.log(`Entity ${data.entityId} detached from ${data.oldParent}`);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Events fire on setParent, removeParent, and spawnChild
|
|
492
|
+
world.setParent(child.id, parent.id); // Emits hierarchyChanged
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Asset Management
|
|
496
|
+
|
|
497
|
+
Manage game assets with eager/lazy loading, groups, and progress tracking:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// Define asset types
|
|
501
|
+
type Assets = {
|
|
502
|
+
playerTexture: { data: ImageBitmap };
|
|
503
|
+
enemyTexture: { data: ImageBitmap };
|
|
504
|
+
level1Music: { buffer: AudioBuffer };
|
|
505
|
+
level1Background: { data: ImageBitmap };
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Create world with assets using the builder pattern
|
|
509
|
+
const game = ECSpresso.create<Components, Events, Resources, Assets>()
|
|
510
|
+
.withAssets(assets => assets
|
|
511
|
+
// Eager assets - loaded automatically during initialize()
|
|
512
|
+
.add('playerTexture', async () => {
|
|
513
|
+
const img = await loadImage('player.png');
|
|
514
|
+
return { data: img };
|
|
515
|
+
})
|
|
516
|
+
.add('enemyTexture', async () => {
|
|
517
|
+
const img = await loadImage('enemy.png');
|
|
518
|
+
return { data: img };
|
|
519
|
+
})
|
|
520
|
+
// Lazy asset group - loaded on demand
|
|
521
|
+
.addGroup('level1', {
|
|
522
|
+
level1Music: async () => {
|
|
523
|
+
const buffer = await loadAudio('level1.mp3');
|
|
524
|
+
return { buffer };
|
|
525
|
+
},
|
|
526
|
+
level1Background: async () => {
|
|
527
|
+
const img = await loadImage('level1-bg.png');
|
|
528
|
+
return { data: img };
|
|
529
|
+
},
|
|
530
|
+
})
|
|
531
|
+
)
|
|
532
|
+
.build();
|
|
533
|
+
|
|
534
|
+
// Initialize loads eager assets automatically
|
|
535
|
+
await game.initialize();
|
|
536
|
+
|
|
537
|
+
// Access loaded assets
|
|
538
|
+
const player = game.getAsset('playerTexture');
|
|
539
|
+
|
|
540
|
+
// Check if asset is loaded
|
|
541
|
+
if (game.isAssetLoaded('enemyTexture')) {
|
|
542
|
+
const enemy = game.getAsset('enemyTexture');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Load asset groups on demand (e.g., when entering a level)
|
|
546
|
+
await game.loadAssetGroup('level1');
|
|
547
|
+
|
|
548
|
+
// Track loading progress
|
|
549
|
+
const progress = game.getAssetGroupProgress('level1'); // 0-1
|
|
550
|
+
|
|
551
|
+
// Check if group is fully loaded
|
|
552
|
+
if (game.isAssetGroupLoaded('level1')) {
|
|
553
|
+
const music = game.getAsset('level1Music');
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
#### Asset Events
|
|
558
|
+
|
|
559
|
+
React to asset loading with built-in events:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
game.addSystem('loadingUI')
|
|
563
|
+
.setEventHandlers({
|
|
564
|
+
assetLoaded: {
|
|
565
|
+
handler: (data) => console.log(`Loaded: ${data.key}`)
|
|
566
|
+
},
|
|
567
|
+
assetFailed: {
|
|
568
|
+
handler: (data) => console.error(`Failed: ${data.key}`, data.error)
|
|
569
|
+
},
|
|
570
|
+
assetGroupProgress: {
|
|
571
|
+
handler: (data) => {
|
|
572
|
+
console.log(`${data.group}: ${data.loaded}/${data.total}`);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
assetGroupLoaded: {
|
|
576
|
+
handler: (data) => console.log(`Group ready: ${data.group}`)
|
|
577
|
+
}
|
|
578
|
+
})
|
|
579
|
+
.build();
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### Systems with Asset Requirements
|
|
583
|
+
|
|
584
|
+
Systems can declare required assets and will only run when those assets are loaded:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
game.addSystem('gameplay')
|
|
588
|
+
.requiresAssets(['playerTexture', 'enemyTexture'])
|
|
589
|
+
.setProcess((queries, dt, ecs) => {
|
|
590
|
+
// This only runs when both assets are loaded
|
|
591
|
+
const player = ecs.getAsset('playerTexture');
|
|
592
|
+
})
|
|
593
|
+
.build();
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Screen Management
|
|
597
|
+
|
|
598
|
+
Manage game states/screens with transitions and overlay support:
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
import type { ScreenDefinition } from 'ecspresso';
|
|
602
|
+
|
|
603
|
+
// Define screen types with config and state
|
|
604
|
+
type Screens = {
|
|
605
|
+
menu: ScreenDefinition<
|
|
606
|
+
Record<string, never>, // Config (passed when entering)
|
|
607
|
+
{ selectedOption: number } // State (mutable during screen)
|
|
608
|
+
>;
|
|
609
|
+
gameplay: ScreenDefinition<
|
|
610
|
+
{ difficulty: string; level: number }, // Config
|
|
611
|
+
{ score: number; isPaused: boolean } // State
|
|
612
|
+
>;
|
|
613
|
+
pause: ScreenDefinition<
|
|
614
|
+
Record<string, never>,
|
|
615
|
+
Record<string, never>
|
|
616
|
+
>;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Create world with screens
|
|
620
|
+
const game = ECSpresso.create<Components, Events, Resources, {}, Screens>()
|
|
621
|
+
.withScreens(screens => screens
|
|
622
|
+
.add('menu', {
|
|
623
|
+
initialState: () => ({ selectedOption: 0 }),
|
|
624
|
+
onEnter: () => console.log('Entered menu'),
|
|
625
|
+
onExit: () => console.log('Left menu'),
|
|
626
|
+
})
|
|
627
|
+
.add('gameplay', {
|
|
628
|
+
initialState: () => ({ score: 0, isPaused: false }),
|
|
629
|
+
onEnter: (config) => console.log(`Starting level ${config.level}`),
|
|
630
|
+
onExit: () => console.log('Gameplay ended'),
|
|
631
|
+
// Require assets before screen can be entered
|
|
632
|
+
requiredAssetGroups: ['level1'],
|
|
633
|
+
})
|
|
634
|
+
.add('pause', {
|
|
635
|
+
initialState: () => ({}),
|
|
636
|
+
onEnter: () => console.log('Paused'),
|
|
637
|
+
onExit: () => console.log('Resumed'),
|
|
638
|
+
})
|
|
639
|
+
)
|
|
640
|
+
.build();
|
|
641
|
+
|
|
642
|
+
await game.initialize();
|
|
643
|
+
|
|
644
|
+
// Set initial screen
|
|
645
|
+
await game.setScreen('menu', {});
|
|
646
|
+
|
|
647
|
+
// Transition to gameplay (clears screen stack)
|
|
648
|
+
await game.setScreen('gameplay', { difficulty: 'hard', level: 1 });
|
|
649
|
+
|
|
650
|
+
// Push overlay screen (adds to stack, previous screen stays active)
|
|
651
|
+
await game.pushScreen('pause', {});
|
|
652
|
+
|
|
653
|
+
// Pop overlay (returns to previous screen)
|
|
654
|
+
await game.popScreen();
|
|
655
|
+
|
|
656
|
+
// Access current screen info
|
|
657
|
+
const current = game.getCurrentScreen(); // 'gameplay'
|
|
658
|
+
const config = game.getScreenConfig(); // { difficulty: 'hard', level: 1 }
|
|
659
|
+
const state = game.getScreenState(); // { score: 0, isPaused: false }
|
|
660
|
+
|
|
661
|
+
// Update screen state
|
|
662
|
+
game.updateScreenState({ score: 100 });
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
#### Screen-Scoped Systems
|
|
666
|
+
|
|
667
|
+
Systems can be restricted to run only in specific screens:
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// Only runs when 'menu' is the current screen
|
|
671
|
+
game.addSystem('menuUI')
|
|
672
|
+
.inScreens(['menu'])
|
|
673
|
+
.setProcess((queries, dt, ecs) => {
|
|
674
|
+
const state = ecs.getScreenState();
|
|
675
|
+
renderMenu(state.selectedOption);
|
|
676
|
+
})
|
|
677
|
+
.build();
|
|
678
|
+
|
|
679
|
+
// Only runs in 'gameplay' screen
|
|
680
|
+
game.addSystem('scoring')
|
|
681
|
+
.inScreens(['gameplay'])
|
|
682
|
+
.setProcess((queries, dt, ecs) => {
|
|
683
|
+
const state = ecs.getScreenState();
|
|
684
|
+
ecs.updateScreenState({ score: state.score + 1 });
|
|
685
|
+
})
|
|
686
|
+
.build();
|
|
687
|
+
|
|
688
|
+
// Runs in all screens EXCEPT 'pause'
|
|
689
|
+
game.addSystem('animations')
|
|
690
|
+
.excludeScreens(['pause'])
|
|
691
|
+
.setProcess(() => {
|
|
692
|
+
// Animations continue except when paused
|
|
693
|
+
})
|
|
694
|
+
.build();
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
#### Screen Resource
|
|
698
|
+
|
|
699
|
+
Access screen state through the `$screen` resource:
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
game.addSystem('ui')
|
|
703
|
+
.setProcess((queries, dt, ecs) => {
|
|
704
|
+
const screen = ecs.getResource('$screen');
|
|
705
|
+
|
|
706
|
+
console.log(screen.current); // Current screen name
|
|
707
|
+
console.log(screen.config); // Current screen config
|
|
708
|
+
console.log(screen.state); // Current screen state (mutable)
|
|
709
|
+
console.log(screen.isOverlay); // true if screen was pushed
|
|
710
|
+
console.log(screen.stackDepth); // Number of screens in stack
|
|
711
|
+
|
|
712
|
+
// Check screen status
|
|
713
|
+
if (screen.isCurrent('gameplay')) {
|
|
714
|
+
// ...
|
|
715
|
+
}
|
|
716
|
+
if (screen.isActive('menu')) {
|
|
717
|
+
// true if menu is current OR in the stack
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
.build();
|
|
721
|
+
```
|
|
722
|
+
|
|
321
723
|
## Type Safety
|
|
322
724
|
|
|
323
725
|
ECSpresso provides comprehensive TypeScript support:
|
|
@@ -384,14 +786,23 @@ world.entityManager.onComponentRemoved('health', (oldValue, entity) => {
|
|
|
384
786
|
|
|
385
787
|
## Error Handling
|
|
386
788
|
|
|
387
|
-
ECSpresso provides clear error messages for common issues:
|
|
789
|
+
ECSpresso provides clear, contextual error messages for common issues:
|
|
388
790
|
|
|
389
791
|
```typescript
|
|
390
|
-
// Resource not found
|
|
792
|
+
// Resource not found with helpful context
|
|
391
793
|
try {
|
|
392
794
|
const missing = world.getResource('nonexistent');
|
|
393
795
|
} catch (error) {
|
|
394
|
-
console.error(error.message);
|
|
796
|
+
console.error(error.message);
|
|
797
|
+
// "Resource 'nonexistent' not found. Available resources: [config, score, settings]"
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Entity operations with detailed context
|
|
801
|
+
try {
|
|
802
|
+
world.entityManager.addComponent(999, 'position', { x: 0, y: 0 });
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.error(error.message);
|
|
805
|
+
// "Cannot add component 'position': Entity with ID 999 does not exist"
|
|
395
806
|
}
|
|
396
807
|
|
|
397
808
|
// Component not found returns null
|
|
@@ -408,3 +819,4 @@ if (component === null) {
|
|
|
408
819
|
- Use system priorities to control execution order
|
|
409
820
|
- Use resource factories for expensive initialization (textures, audio, etc.)
|
|
410
821
|
- Consider component callbacks for immediate reactions to state changes
|
|
822
|
+
- Minimize the number of components in queries when possible to leverage indexing
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset management for ECSpresso ECS framework
|
|
3
|
+
*/
|
|
4
|
+
import type EventBus from './event-bus';
|
|
5
|
+
import type { AssetStatus, AssetDefinition, AssetHandle, AssetsResource, AssetEvents, AssetConfigurator } from './asset-types';
|
|
6
|
+
/**
|
|
7
|
+
* Manages asset loading and access for ECSpresso
|
|
8
|
+
*/
|
|
9
|
+
export default class AssetManager<AssetTypes extends Record<string, unknown> = Record<string, never>> {
|
|
10
|
+
private readonly assets;
|
|
11
|
+
private readonly groups;
|
|
12
|
+
private eventBus;
|
|
13
|
+
/**
|
|
14
|
+
* Set the event bus for asset events
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
setEventBus(eventBus: EventBus<AssetEvents>): void;
|
|
18
|
+
/**
|
|
19
|
+
* Register an asset definition
|
|
20
|
+
*/
|
|
21
|
+
register<K extends string, T>(key: K, definition: AssetDefinition<T>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Load all assets marked as eager
|
|
24
|
+
*/
|
|
25
|
+
loadEagerAssets(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Load a single asset by key
|
|
28
|
+
*/
|
|
29
|
+
loadAsset<K extends keyof AssetTypes>(key: K): Promise<AssetTypes[K]>;
|
|
30
|
+
/**
|
|
31
|
+
* Load all assets in a group
|
|
32
|
+
*/
|
|
33
|
+
loadAssetGroup(groupName: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Get a loaded asset. Throws if not loaded.
|
|
36
|
+
*/
|
|
37
|
+
get<K extends keyof AssetTypes>(key: K): AssetTypes[K];
|
|
38
|
+
/**
|
|
39
|
+
* Get a loaded asset or undefined
|
|
40
|
+
*/
|
|
41
|
+
getOrUndefined<K extends keyof AssetTypes>(key: K): AssetTypes[K] | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Get a handle to an asset with status information
|
|
44
|
+
*/
|
|
45
|
+
getHandle<K extends keyof AssetTypes>(key: K): AssetHandle<AssetTypes[K]>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the status of an asset
|
|
48
|
+
*/
|
|
49
|
+
getStatus<K extends keyof AssetTypes>(key: K): AssetStatus;
|
|
50
|
+
/**
|
|
51
|
+
* Check if an asset is loaded
|
|
52
|
+
*/
|
|
53
|
+
isLoaded<K extends keyof AssetTypes>(key: K): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Check if all assets in a group are loaded
|
|
56
|
+
*/
|
|
57
|
+
isGroupLoaded(groupName: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Get the loading progress of a group (0-1)
|
|
60
|
+
*/
|
|
61
|
+
getGroupProgress(groupName: string): number;
|
|
62
|
+
/**
|
|
63
|
+
* Get detailed group progress
|
|
64
|
+
*/
|
|
65
|
+
getGroupProgressDetails(groupName: string): {
|
|
66
|
+
loaded: number;
|
|
67
|
+
total: number;
|
|
68
|
+
progress: number;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Check group progress and emit events
|
|
72
|
+
*/
|
|
73
|
+
private checkGroupProgress;
|
|
74
|
+
/**
|
|
75
|
+
* Create the $assets resource object
|
|
76
|
+
*/
|
|
77
|
+
createResource(): AssetsResource<AssetTypes>;
|
|
78
|
+
/**
|
|
79
|
+
* Get all registered asset keys
|
|
80
|
+
*/
|
|
81
|
+
getKeys(): Array<keyof AssetTypes>;
|
|
82
|
+
/**
|
|
83
|
+
* Get all group names
|
|
84
|
+
*/
|
|
85
|
+
getGroupNames(): string[];
|
|
86
|
+
/**
|
|
87
|
+
* Get all asset keys in a group
|
|
88
|
+
*/
|
|
89
|
+
getGroupKeys(groupName: string): Array<keyof AssetTypes>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Implementation of AssetConfigurator for builder pattern
|
|
93
|
+
*/
|
|
94
|
+
export declare class AssetConfiguratorImpl<A extends Record<string, unknown>> implements AssetConfigurator<A> {
|
|
95
|
+
private readonly manager;
|
|
96
|
+
constructor(manager: AssetManager<A>);
|
|
97
|
+
add<K extends string, T>(key: K, loader: () => Promise<T>): AssetConfigurator<A & Record<K, T>>;
|
|
98
|
+
addWithConfig<K extends string, T>(key: K, definition: AssetDefinition<T>): AssetConfigurator<A & Record<K, T>>;
|
|
99
|
+
addGroup<G extends string, T extends Record<string, () => Promise<unknown>>>(groupName: G, assets: T): AssetConfigurator<A & {
|
|
100
|
+
[K in keyof T]: Awaited<ReturnType<T[K]>>;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Get the underlying manager
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
getManager(): AssetManager<A>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create a new AssetConfigurator for builder pattern usage
|
|
110
|
+
*/
|
|
111
|
+
export declare function createAssetConfigurator<A extends Record<string, unknown> = Record<string, never>>(manager?: AssetManager<A>): AssetConfiguratorImpl<A>;
|