ecspresso 0.4.3 → 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 +379 -0
- 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 +29 -2
- package/dist/types.d.ts +40 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,9 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
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
|
|
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
|
|
14
17
|
- **Query System**: Powerful entity filtering with helper type utilities
|
|
15
18
|
|
|
16
19
|
## Installation
|
|
@@ -277,6 +280,19 @@ interface Events {
|
|
|
277
280
|
|
|
278
281
|
const world = new ECSpresso<Components, Events>();
|
|
279
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
|
+
|
|
280
296
|
// Handle events in systems
|
|
281
297
|
world.addSystem('gameLogic')
|
|
282
298
|
.setEventHandlers({
|
|
@@ -341,6 +357,369 @@ world.addSystem('gameSystem')
|
|
|
341
357
|
await world.initialize();
|
|
342
358
|
```
|
|
343
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
|
+
|
|
344
723
|
## Type Safety
|
|
345
724
|
|
|
346
725
|
ECSpresso provides comprehensive TypeScript support:
|
|
@@ -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>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset management types for ECSpresso ECS framework
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Status of an asset in the loading lifecycle
|
|
6
|
+
*/
|
|
7
|
+
export type AssetStatus = 'pending' | 'loading' | 'loaded' | 'failed';
|
|
8
|
+
/**
|
|
9
|
+
* Definition for an asset including its loader and configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface AssetDefinition<T> {
|
|
12
|
+
readonly loader: () => Promise<T>;
|
|
13
|
+
readonly eager?: boolean;
|
|
14
|
+
readonly group?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Handle to an asset that provides status information and access methods
|
|
18
|
+
*/
|
|
19
|
+
export interface AssetHandle<T> {
|
|
20
|
+
readonly status: AssetStatus;
|
|
21
|
+
readonly isLoaded: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Get the asset value. Throws if asset is not loaded.
|
|
24
|
+
*/
|
|
25
|
+
get(): T;
|
|
26
|
+
/**
|
|
27
|
+
* Get the asset value if loaded, undefined otherwise.
|
|
28
|
+
*/
|
|
29
|
+
getOrUndefined(): T | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resource interface for accessing assets in systems
|
|
33
|
+
* Exposed as $assets resource
|
|
34
|
+
*/
|
|
35
|
+
export interface AssetsResource<A extends Record<string, unknown>> {
|
|
36
|
+
/**
|
|
37
|
+
* Get the loading status of an asset
|
|
38
|
+
*/
|
|
39
|
+
getStatus<K extends keyof A>(key: K): AssetStatus;
|
|
40
|
+
/**
|
|
41
|
+
* Check if an asset is loaded
|
|
42
|
+
*/
|
|
43
|
+
isLoaded<K extends keyof A>(key: K): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Check if all assets in a group are loaded
|
|
46
|
+
*/
|
|
47
|
+
isGroupLoaded(groupName: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Get the loading progress of a group (0-1)
|
|
50
|
+
*/
|
|
51
|
+
getGroupProgress(groupName: string): number;
|
|
52
|
+
/**
|
|
53
|
+
* Get a loaded asset. Throws if not loaded.
|
|
54
|
+
*/
|
|
55
|
+
get<K extends keyof A>(key: K): A[K];
|
|
56
|
+
/**
|
|
57
|
+
* Get a loaded asset or undefined if not loaded
|
|
58
|
+
*/
|
|
59
|
+
getOrUndefined<K extends keyof A>(key: K): A[K] | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Get a handle to an asset with status information
|
|
62
|
+
*/
|
|
63
|
+
getHandle<K extends keyof A>(key: K): AssetHandle<A[K]>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Events emitted by the asset system
|
|
67
|
+
*/
|
|
68
|
+
export interface AssetEvents {
|
|
69
|
+
assetLoaded: {
|
|
70
|
+
key: string;
|
|
71
|
+
};
|
|
72
|
+
assetFailed: {
|
|
73
|
+
key: string;
|
|
74
|
+
error: Error;
|
|
75
|
+
};
|
|
76
|
+
assetGroupLoaded: {
|
|
77
|
+
group: string;
|
|
78
|
+
};
|
|
79
|
+
assetGroupProgress: {
|
|
80
|
+
group: string;
|
|
81
|
+
progress: number;
|
|
82
|
+
loaded: number;
|
|
83
|
+
total: number;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Configuration for asset definitions during builder setup
|
|
88
|
+
*/
|
|
89
|
+
export interface AssetConfigurator<A extends Record<string, unknown>> {
|
|
90
|
+
/**
|
|
91
|
+
* Add a single eager asset
|
|
92
|
+
*/
|
|
93
|
+
add<K extends string, T>(key: K, loader: () => Promise<T>): AssetConfigurator<A & Record<K, T>>;
|
|
94
|
+
/**
|
|
95
|
+
* Add a single asset with full configuration
|
|
96
|
+
*/
|
|
97
|
+
addWithConfig<K extends string, T>(key: K, definition: AssetDefinition<T>): AssetConfigurator<A & Record<K, T>>;
|
|
98
|
+
/**
|
|
99
|
+
* Add a group of assets that can be loaded together
|
|
100
|
+
*/
|
|
101
|
+
addGroup<G extends string, T extends Record<string, () => Promise<unknown>>>(groupName: G, assets: T): AssetConfigurator<A & {
|
|
102
|
+
[K in keyof T]: Awaited<ReturnType<T[K]>>;
|
|
103
|
+
}>;
|
|
104
|
+
}
|