ecspresso 0.5.0 → 0.7.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 -9
- package/dist/bundle.d.ts +5 -2
- package/dist/bundles/renderers/pixi.d.ts +235 -0
- package/dist/bundles/renderers/pixi.js +4 -0
- package/dist/bundles/renderers/pixi.js.map +13 -0
- package/dist/bundles/utils/bounds.d.ts +186 -0
- package/dist/bundles/utils/collision.d.ts +201 -0
- package/dist/bundles/utils/movement.d.ts +83 -0
- package/dist/bundles/utils/timers.d.ts +169 -0
- package/dist/bundles/utils/timers.js +4 -0
- package/dist/bundles/utils/timers.js.map +12 -0
- package/dist/bundles/utils/transform.d.ts +148 -0
- package/dist/command-buffer.d.ts +90 -0
- package/dist/ecspresso.d.ts +137 -3
- package/dist/entity-manager.d.ts +19 -3
- package/dist/hierarchy-manager.d.ts +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +13 -11
- package/dist/reactive-query-manager.d.ts +59 -0
- package/dist/resource-manager.d.ts +37 -5
- package/dist/system-builder.d.ts +8 -0
- package/dist/types.d.ts +22 -0
- package/package.json +23 -3
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
15
15
|
- **Screen Management**: Game state/screen transitions with overlay support
|
|
16
16
|
- **Entity Hierarchy**: Parent-child relationships with traversal and cascade deletion
|
|
17
17
|
- **Query System**: Powerful entity filtering with helper type utilities
|
|
18
|
+
- **Reactive Queries**: Enter/exit callbacks when entities match or unmatch queries
|
|
19
|
+
- **System Groups**: Enable/disable groups of systems at runtime
|
|
20
|
+
- **Component Lifecycle**: Callbacks for component add/remove with unsubscribe support
|
|
21
|
+
- **Command Buffer**: Deferred structural changes for safe entity/component operations during systems
|
|
22
|
+
- **Timer Bundle**: ECS-native timers with event-based completion notifications
|
|
18
23
|
|
|
19
24
|
## Installation
|
|
20
25
|
|
|
@@ -212,6 +217,35 @@ world.addSystem('physics')
|
|
|
212
217
|
.build();
|
|
213
218
|
```
|
|
214
219
|
|
|
220
|
+
### System Groups
|
|
221
|
+
|
|
222
|
+
Organize systems into groups that can be enabled/disabled at runtime:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// Assign systems to groups
|
|
226
|
+
world.addSystem('renderSprites')
|
|
227
|
+
.inGroup('rendering')
|
|
228
|
+
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
229
|
+
.setProcess((queries) => { /* ... */ })
|
|
230
|
+
.and()
|
|
231
|
+
.addSystem('renderParticles')
|
|
232
|
+
.inGroup('rendering')
|
|
233
|
+
.inGroup('effects') // Systems can belong to multiple groups
|
|
234
|
+
.setProcess(() => { /* ... */ })
|
|
235
|
+
.build();
|
|
236
|
+
|
|
237
|
+
// Disable/enable groups at runtime
|
|
238
|
+
world.disableSystemGroup('rendering'); // All rendering systems skip
|
|
239
|
+
world.enableSystemGroup('rendering'); // Resume rendering
|
|
240
|
+
|
|
241
|
+
// Query group state
|
|
242
|
+
world.isSystemGroupEnabled('rendering'); // true/false
|
|
243
|
+
world.getSystemsInGroup('rendering'); // ['renderSprites', 'renderParticles']
|
|
244
|
+
|
|
245
|
+
// If a system belongs to multiple groups, disabling ANY group skips the system
|
|
246
|
+
world.disableSystemGroup('effects'); // renderParticles won't run
|
|
247
|
+
```
|
|
248
|
+
|
|
215
249
|
## Advanced Features
|
|
216
250
|
|
|
217
251
|
### Bundles
|
|
@@ -316,7 +350,8 @@ Create resources lazily with factory functions:
|
|
|
316
350
|
```typescript
|
|
317
351
|
interface Resources {
|
|
318
352
|
config: { difficulty: string; soundEnabled: boolean };
|
|
319
|
-
|
|
353
|
+
database: Database;
|
|
354
|
+
cache: { db: Database };
|
|
320
355
|
}
|
|
321
356
|
|
|
322
357
|
const world = new ECSpresso<Components, {}, Resources>();
|
|
@@ -328,15 +363,87 @@ world.addResource('config', () => ({
|
|
|
328
363
|
}));
|
|
329
364
|
|
|
330
365
|
// Async factory
|
|
331
|
-
world.addResource('
|
|
332
|
-
|
|
333
|
-
|
|
366
|
+
world.addResource('database', async () => {
|
|
367
|
+
return await connectToDatabase();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Factory with dependencies - initialized after dependencies are ready
|
|
371
|
+
world.addResource('cache', {
|
|
372
|
+
dependsOn: ['database'],
|
|
373
|
+
factory: (ecs) => ({
|
|
374
|
+
db: ecs.getResource('database')
|
|
375
|
+
})
|
|
334
376
|
});
|
|
335
377
|
|
|
336
|
-
// Initialize all resources
|
|
378
|
+
// Initialize all resources (respects dependency order)
|
|
337
379
|
await world.initializeResources();
|
|
338
380
|
```
|
|
339
381
|
|
|
382
|
+
**Dependency Features:**
|
|
383
|
+
- Resources are initialized in topological order (dependencies first)
|
|
384
|
+
- Circular dependencies throw a descriptive error at initialization time
|
|
385
|
+
- Existing patterns (direct values, simple factories) work unchanged
|
|
386
|
+
|
|
387
|
+
### Resource Builder
|
|
388
|
+
|
|
389
|
+
Add resources fluently during ECSpresso construction using `withResource()`:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const world = ECSpresso
|
|
393
|
+
.create<Components, Events, Resources>()
|
|
394
|
+
.withBundle(physicsBundle)
|
|
395
|
+
.withResource('config', { debug: true, maxEntities: 1000 })
|
|
396
|
+
.withResource('score', () => ({ value: 0 }))
|
|
397
|
+
.withResource('cache', {
|
|
398
|
+
dependsOn: ['database'],
|
|
399
|
+
factory: (ecs) => createCache(ecs.getResource('database'))
|
|
400
|
+
})
|
|
401
|
+
.build();
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
This chains naturally with `withBundle()`, `withAssets()`, and `withScreens()`.
|
|
405
|
+
|
|
406
|
+
### Resource Disposal
|
|
407
|
+
|
|
408
|
+
Resources can define cleanup logic with `onDispose` callbacks, useful for removing event listeners, closing connections, or releasing resources:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Factory with disposal callback
|
|
412
|
+
world.addResource('keyboard', {
|
|
413
|
+
factory: () => {
|
|
414
|
+
const handler = (e: KeyboardEvent) => { /* ... */ };
|
|
415
|
+
window.addEventListener('keydown', handler);
|
|
416
|
+
return { handler };
|
|
417
|
+
},
|
|
418
|
+
onDispose: (resource) => {
|
|
419
|
+
window.removeEventListener('keydown', resource.handler);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Or with the builder pattern
|
|
424
|
+
const world = ECSpresso
|
|
425
|
+
.create<Components, Events, Resources>()
|
|
426
|
+
.withResource('database', {
|
|
427
|
+
factory: async () => await connectToDatabase(),
|
|
428
|
+
onDispose: async (db) => await db.close()
|
|
429
|
+
})
|
|
430
|
+
.build();
|
|
431
|
+
|
|
432
|
+
// Dispose a single resource
|
|
433
|
+
await world.disposeResource('keyboard');
|
|
434
|
+
|
|
435
|
+
// Dispose all resources in reverse dependency order
|
|
436
|
+
// (dependents are disposed before their dependencies)
|
|
437
|
+
await world.disposeResources();
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Disposal Features:**
|
|
441
|
+
- `onDispose` receives the resource value and the ECSpresso instance as context
|
|
442
|
+
- `disposeResources()` disposes in reverse topological order (dependents first)
|
|
443
|
+
- Only initialized resources have their `onDispose` called
|
|
444
|
+
- Supports both sync and async disposal callbacks
|
|
445
|
+
- `removeResource()` still exists for removal without disposal
|
|
446
|
+
|
|
340
447
|
### System Lifecycle
|
|
341
448
|
|
|
342
449
|
Systems can have initialization and cleanup hooks:
|
|
@@ -446,6 +553,32 @@ world.getChildAt(root.id, 0); // child.id
|
|
|
446
553
|
world.getChildIndex(root.id, child2.id); // 1
|
|
447
554
|
```
|
|
448
555
|
|
|
556
|
+
#### Parent-First Traversal
|
|
557
|
+
|
|
558
|
+
Iterate the hierarchy with guaranteed parent-first order (useful for transform propagation):
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// Callback-based traversal
|
|
562
|
+
world.forEachInHierarchy((entityId, parentId, depth) => {
|
|
563
|
+
// Parents are always visited before their children
|
|
564
|
+
console.log(`Entity ${entityId} at depth ${depth}, parent: ${parentId}`);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Filter to specific subtrees
|
|
568
|
+
world.forEachInHierarchy(
|
|
569
|
+
(entityId, parentId, depth) => {
|
|
570
|
+
// Only visits entities under root.id
|
|
571
|
+
},
|
|
572
|
+
{ roots: [root.id] }
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// Generator-based traversal (supports early termination)
|
|
576
|
+
for (const { entityId, parentId, depth } of world.hierarchyIterator()) {
|
|
577
|
+
if (depth > 2) break; // Stop at depth 2
|
|
578
|
+
console.log(entityId);
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
449
582
|
#### Cascade Deletion
|
|
450
583
|
|
|
451
584
|
When removing entities, descendants are automatically removed by default:
|
|
@@ -771,17 +904,254 @@ world.withBundle(conflictingBundle); // TypeScript prevents this
|
|
|
771
904
|
|
|
772
905
|
## Component Callbacks
|
|
773
906
|
|
|
774
|
-
React to component changes with callbacks:
|
|
907
|
+
React to component changes with callbacks. Both methods return an unsubscribe function:
|
|
775
908
|
|
|
776
909
|
```typescript
|
|
777
|
-
// Listen for component additions
|
|
778
|
-
world.
|
|
910
|
+
// Listen for component additions - returns unsubscribe function
|
|
911
|
+
const unsubAdd = world.onComponentAdded('health', (value, entity) => {
|
|
779
912
|
console.log(`Health added to entity ${entity.id}:`, value);
|
|
780
913
|
});
|
|
781
914
|
|
|
782
|
-
|
|
915
|
+
// Listen for component removals
|
|
916
|
+
const unsubRemove = world.onComponentRemoved('health', (oldValue, entity) => {
|
|
783
917
|
console.log(`Health removed from entity ${entity.id}:`, oldValue);
|
|
784
918
|
});
|
|
919
|
+
|
|
920
|
+
// Unsubscribe when done
|
|
921
|
+
unsubAdd();
|
|
922
|
+
unsubRemove();
|
|
923
|
+
|
|
924
|
+
// Also available on entityManager directly
|
|
925
|
+
world.entityManager.onComponentAdded('position', (value, entity) => {
|
|
926
|
+
// ...
|
|
927
|
+
});
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
## Reactive Queries
|
|
931
|
+
|
|
932
|
+
Get callbacks when entities enter or exit a query match. Unlike regular queries that you poll during `update()`, reactive queries push notifications when the entity's components change:
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
// Add a reactive query with enter/exit callbacks
|
|
936
|
+
world.addReactiveQuery('enemies', {
|
|
937
|
+
with: ['position', 'enemy'],
|
|
938
|
+
without: ['dead'],
|
|
939
|
+
onEnter: (entity) => {
|
|
940
|
+
// Called when entity starts matching the query
|
|
941
|
+
console.log(`Enemy ${entity.id} appeared at`, entity.components.position);
|
|
942
|
+
spawnHealthBar(entity.id);
|
|
943
|
+
},
|
|
944
|
+
onExit: (entityId) => {
|
|
945
|
+
// Called when entity stops matching (receives ID since entity may be removed)
|
|
946
|
+
console.log(`Enemy ${entityId} gone`);
|
|
947
|
+
removeHealthBar(entityId);
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Triggers: spawning matching entity, adding required component,
|
|
952
|
+
// removing excluded component
|
|
953
|
+
const enemy = world.spawn({ position: { x: 0, y: 0 }, enemy: true }); // onEnter fires
|
|
954
|
+
|
|
955
|
+
// Triggers: removing required component, adding excluded component,
|
|
956
|
+
// removing entity
|
|
957
|
+
world.entityManager.addComponent(enemy.id, 'dead', true); // onExit fires
|
|
958
|
+
|
|
959
|
+
// Existing matching entities trigger onEnter when query is added
|
|
960
|
+
world.spawn({ position: { x: 10, y: 10 }, enemy: true });
|
|
961
|
+
world.addReactiveQuery('positioned', {
|
|
962
|
+
with: ['position'],
|
|
963
|
+
onEnter: (entity) => { /* Called for all existing entities with position */ },
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Remove reactive query when no longer needed
|
|
967
|
+
const removed = world.removeReactiveQuery('enemies'); // returns true if existed
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
**Note:** Component replacement (calling `addComponent` with a component that already exists) does NOT trigger enter/exit callbacks since the entity's query match status doesn't change.
|
|
971
|
+
|
|
972
|
+
## Command Buffer
|
|
973
|
+
|
|
974
|
+
The command buffer allows you to queue structural changes (entity creation, removal, component changes) that execute at the end of the update cycle. This prevents issues when modifying entities during system iteration.
|
|
975
|
+
|
|
976
|
+
```typescript
|
|
977
|
+
// Queue commands during system execution
|
|
978
|
+
world.addSystem('combat')
|
|
979
|
+
.addQuery('enemies', { with: ['enemy', 'health'] })
|
|
980
|
+
.setProcess((queries, dt, ecs) => {
|
|
981
|
+
for (const entity of queries.enemies) {
|
|
982
|
+
if (entity.components.health.value <= 0) {
|
|
983
|
+
// Queue removal - doesn't execute immediately
|
|
984
|
+
ecs.commands.removeEntity(entity.id);
|
|
985
|
+
|
|
986
|
+
// Queue spawning an explosion
|
|
987
|
+
ecs.commands.spawn({
|
|
988
|
+
position: entity.components.position,
|
|
989
|
+
explosion: true,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// Commands execute automatically at end of update()
|
|
994
|
+
})
|
|
995
|
+
.build();
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### Available Commands
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
// Entity operations
|
|
1002
|
+
ecs.commands.spawn({ position: { x: 0, y: 0 } });
|
|
1003
|
+
ecs.commands.spawnChild(parentId, { position: { x: 10, y: 0 } });
|
|
1004
|
+
ecs.commands.removeEntity(entityId);
|
|
1005
|
+
ecs.commands.removeEntity(entityId, { cascade: false }); // Orphan children
|
|
1006
|
+
|
|
1007
|
+
// Component operations
|
|
1008
|
+
ecs.commands.addComponent(entityId, 'velocity', { x: 5, y: 0 });
|
|
1009
|
+
ecs.commands.addComponents(entityId, { velocity: { x: 5, y: 0 }, health: { value: 100 } });
|
|
1010
|
+
ecs.commands.removeComponent(entityId, 'velocity');
|
|
1011
|
+
|
|
1012
|
+
// Hierarchy operations
|
|
1013
|
+
ecs.commands.setParent(childId, parentId);
|
|
1014
|
+
ecs.commands.removeParent(childId);
|
|
1015
|
+
|
|
1016
|
+
// Utility
|
|
1017
|
+
ecs.commands.length; // Number of queued commands
|
|
1018
|
+
ecs.commands.clear(); // Discard all queued commands
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
Commands execute in FIFO order. If a command fails (e.g., entity doesn't exist), it logs a warning and continues with remaining commands.
|
|
1022
|
+
|
|
1023
|
+
## Built-in Bundles
|
|
1024
|
+
|
|
1025
|
+
ECSpresso provides optional utility bundles for common game development needs:
|
|
1026
|
+
|
|
1027
|
+
| Bundle | Import | Description |
|
|
1028
|
+
|--------|--------|-------------|
|
|
1029
|
+
| **Transform** | `ecspresso/bundles/utils/transform` | Hierarchical transform propagation (local/world transforms) |
|
|
1030
|
+
| **Movement** | `ecspresso/bundles/utils/movement` | Velocity-based movement integration |
|
|
1031
|
+
| **Bounds** | `ecspresso/bundles/utils/bounds` | Screen bounds enforcement (destroy, clamp, wrap) |
|
|
1032
|
+
| **Collision** | `ecspresso/bundles/utils/collision` | Layer-based AABB/circle collision detection with events |
|
|
1033
|
+
| **Timers** | `ecspresso/bundles/utils/timers` | ECS-native timers with event-based completion |
|
|
1034
|
+
| **PixiJS Renderer** | `ecspresso/bundles/renderers/pixi` | Automated PixiJS scene graph wiring |
|
|
1035
|
+
|
|
1036
|
+
## Timer Bundle
|
|
1037
|
+
|
|
1038
|
+
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.
|
|
1039
|
+
|
|
1040
|
+
```typescript
|
|
1041
|
+
import {
|
|
1042
|
+
createTimerBundle,
|
|
1043
|
+
createTimer,
|
|
1044
|
+
createRepeatingTimer,
|
|
1045
|
+
type TimerComponentTypes,
|
|
1046
|
+
type TimerEventData
|
|
1047
|
+
} from 'ecspresso/bundles/utils/timers';
|
|
1048
|
+
|
|
1049
|
+
// Define events that use TimerEventData payload
|
|
1050
|
+
interface Events {
|
|
1051
|
+
hideMessage: TimerEventData;
|
|
1052
|
+
spawnWave: TimerEventData;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Extend components with timer support
|
|
1056
|
+
interface Components extends TimerComponentTypes<Events> {
|
|
1057
|
+
position: { x: number; y: number };
|
|
1058
|
+
messageDisplay: true;
|
|
1059
|
+
spawner: true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Create world with timer bundle
|
|
1063
|
+
const world = ECSpresso
|
|
1064
|
+
.create<Components, Events, Resources>()
|
|
1065
|
+
.withBundle(createTimerBundle<Events>())
|
|
1066
|
+
.build();
|
|
1067
|
+
|
|
1068
|
+
// One-shot timer without event (poll justFinished)
|
|
1069
|
+
world.spawn({
|
|
1070
|
+
...createTimer<Events>(2.0),
|
|
1071
|
+
messageDisplay: true,
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// One-shot timer with completion event
|
|
1075
|
+
world.spawn({
|
|
1076
|
+
...createTimer<Events>(1.5, { onComplete: 'hideMessage' }),
|
|
1077
|
+
messageDisplay: true,
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Repeating timer with event
|
|
1081
|
+
world.spawn({
|
|
1082
|
+
...createRepeatingTimer<Events>(5.0, { onComplete: 'spawnWave' }),
|
|
1083
|
+
spawner: true,
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Subscribe to timer events
|
|
1087
|
+
world.on('hideMessage', (data) => {
|
|
1088
|
+
console.log(`Timer on entity ${data.entityId} completed after ${data.elapsed}s`);
|
|
1089
|
+
});
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Timer Event Data
|
|
1093
|
+
|
|
1094
|
+
Events used with timer `onComplete` must have `TimerEventData` as their payload type:
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
interface TimerEventData {
|
|
1098
|
+
entityId: number; // Entity the timer belongs to
|
|
1099
|
+
duration: number; // Timer's configured duration
|
|
1100
|
+
elapsed: number; // Actual elapsed time (may exceed duration slightly)
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### Polling vs Events
|
|
1105
|
+
|
|
1106
|
+
You can use timers in two ways:
|
|
1107
|
+
|
|
1108
|
+
```typescript
|
|
1109
|
+
// 1. Polling with justFinished flag
|
|
1110
|
+
world.addSystem('timerPolling')
|
|
1111
|
+
.addQuery('timers', { with: ['timer', 'messageDisplay'] })
|
|
1112
|
+
.setProcess((queries) => {
|
|
1113
|
+
for (const entity of queries.timers) {
|
|
1114
|
+
if (entity.components.timer.justFinished) {
|
|
1115
|
+
// Timer just completed this frame
|
|
1116
|
+
hideMessage(entity.id);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
.build();
|
|
1121
|
+
|
|
1122
|
+
// 2. Event-based (decoupled)
|
|
1123
|
+
world.addSystem('timerEvents')
|
|
1124
|
+
.setEventHandlers({
|
|
1125
|
+
hideMessage: {
|
|
1126
|
+
handler: (data, ecs) => {
|
|
1127
|
+
// React to timer completion
|
|
1128
|
+
ecs.commands.removeEntity(data.entityId);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
})
|
|
1132
|
+
.build();
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
### Timer Properties
|
|
1136
|
+
|
|
1137
|
+
```typescript
|
|
1138
|
+
interface Timer {
|
|
1139
|
+
elapsed: number; // Time accumulated (seconds)
|
|
1140
|
+
duration: number; // Target duration (seconds)
|
|
1141
|
+
repeat: boolean; // Whether timer repeats
|
|
1142
|
+
active: boolean; // Whether timer is running
|
|
1143
|
+
justFinished: boolean; // True for one frame after completion
|
|
1144
|
+
onComplete?: string; // Event name to publish (optional)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Control timer at runtime
|
|
1148
|
+
const entity = world.spawn({ ...createTimer<Events>(5.0), myTimer: true });
|
|
1149
|
+
const timer = entity.components.timer;
|
|
1150
|
+
|
|
1151
|
+
timer.active = false; // Pause
|
|
1152
|
+
timer.active = true; // Resume
|
|
1153
|
+
timer.elapsed = 0; // Reset
|
|
1154
|
+
timer.duration = 10; // Change duration
|
|
785
1155
|
```
|
|
786
1156
|
|
|
787
1157
|
## Error Handling
|
package/dist/bundle.d.ts
CHANGED
|
@@ -31,9 +31,12 @@ export default class Bundle<ComponentTypes extends Record<string, any> = {}, Eve
|
|
|
31
31
|
/**
|
|
32
32
|
* Add a resource to this bundle
|
|
33
33
|
* @param label The resource key
|
|
34
|
-
* @param resource The resource value
|
|
34
|
+
* @param resource The resource value, a factory function, or a factory with dependencies
|
|
35
35
|
*/
|
|
36
|
-
addResource<K extends keyof ResourceTypes>(label: K, resource: ResourceTypes[K] | ((ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]>)
|
|
36
|
+
addResource<K extends keyof ResourceTypes>(label: K, resource: ResourceTypes[K] | ((ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]>) | {
|
|
37
|
+
dependsOn: readonly string[];
|
|
38
|
+
factory: (ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]>;
|
|
39
|
+
}): this;
|
|
37
40
|
/**
|
|
38
41
|
* Add an asset to this bundle
|
|
39
42
|
* @param key The asset key
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PixiJS Renderer Bundle for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* An opt-in PixiJS rendering bundle that automates scene graph wiring.
|
|
5
|
+
* Import from 'ecspresso/bundles/renderers/pixi'
|
|
6
|
+
*
|
|
7
|
+
* Note: This bundle requires the transform bundle for transform propagation.
|
|
8
|
+
* Add createTransformBundle() before this bundle.
|
|
9
|
+
*/
|
|
10
|
+
import type { Application, ApplicationOptions, Container, Sprite, Graphics } from 'pixi.js';
|
|
11
|
+
import Bundle from '../../bundle';
|
|
12
|
+
import type { LocalTransform, WorldTransform, TransformComponentTypes } from '../utils/transform';
|
|
13
|
+
export type { LocalTransform, WorldTransform, TransformComponentTypes };
|
|
14
|
+
export { createTransform, createLocalTransform, createWorldTransform } from '../utils/transform';
|
|
15
|
+
/**
|
|
16
|
+
* PixiJS Sprite component
|
|
17
|
+
*/
|
|
18
|
+
export interface PixiSprite {
|
|
19
|
+
sprite: Sprite;
|
|
20
|
+
anchor?: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* PixiJS Graphics component
|
|
27
|
+
*/
|
|
28
|
+
export interface PixiGraphics {
|
|
29
|
+
graphics: Graphics;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* PixiJS Container component
|
|
33
|
+
*/
|
|
34
|
+
export interface PixiContainer {
|
|
35
|
+
container: Container;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Visibility and alpha component
|
|
39
|
+
*/
|
|
40
|
+
export interface PixiVisible {
|
|
41
|
+
visible: boolean;
|
|
42
|
+
alpha?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Aggregate component types for PixiJS bundle.
|
|
46
|
+
* Users should extend this interface with their own component types.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* interface GameComponents extends PixiComponentTypes {
|
|
51
|
+
* velocity: { x: number; y: number };
|
|
52
|
+
* player: true;
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export interface PixiComponentTypes extends TransformComponentTypes {
|
|
57
|
+
pixiSprite: PixiSprite;
|
|
58
|
+
pixiGraphics: PixiGraphics;
|
|
59
|
+
pixiContainer: PixiContainer;
|
|
60
|
+
pixiVisible: PixiVisible;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Events emitted by the PixiJS bundle
|
|
64
|
+
*/
|
|
65
|
+
export interface PixiEventTypes {
|
|
66
|
+
hierarchyChanged: {
|
|
67
|
+
entityId: number;
|
|
68
|
+
oldParent: number | null;
|
|
69
|
+
newParent: number | null;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resources provided by the PixiJS bundle
|
|
74
|
+
*/
|
|
75
|
+
export interface PixiResourceTypes {
|
|
76
|
+
pixiApp: Application;
|
|
77
|
+
pixiRootContainer: Container;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Common options shared between both initialization modes
|
|
81
|
+
*/
|
|
82
|
+
interface PixiBundleCommonOptions {
|
|
83
|
+
/** Optional custom root container (defaults to app.stage) */
|
|
84
|
+
rootContainer?: Container;
|
|
85
|
+
/** System group name (default: 'pixi-renderer') */
|
|
86
|
+
systemGroup?: string;
|
|
87
|
+
/** Priority for render sync system (default: 500) */
|
|
88
|
+
renderSyncPriority?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Options when providing a pre-initialized PixiJS Application
|
|
92
|
+
*/
|
|
93
|
+
export interface PixiBundleAppOptions extends PixiBundleCommonOptions {
|
|
94
|
+
/** The PixiJS Application instance (already initialized) */
|
|
95
|
+
app: Application;
|
|
96
|
+
init?: never;
|
|
97
|
+
container?: never;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Options when letting the bundle create and manage the PixiJS Application
|
|
101
|
+
*/
|
|
102
|
+
export interface PixiBundleManagedOptions extends PixiBundleCommonOptions {
|
|
103
|
+
app?: never;
|
|
104
|
+
/** PixiJS ApplicationOptions - bundle will create and initialize the Application */
|
|
105
|
+
init: Partial<ApplicationOptions>;
|
|
106
|
+
/** Container element to append the canvas to, or CSS selector string */
|
|
107
|
+
container?: HTMLElement | string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Configuration options for the PixiJS bundle.
|
|
111
|
+
*
|
|
112
|
+
* Supports two modes:
|
|
113
|
+
* 1. **Pre-initialized**: Pass an already-initialized Application via `app`
|
|
114
|
+
* 2. **Managed**: Pass `init` options and the bundle creates the Application during `ecs.initialize()`
|
|
115
|
+
*
|
|
116
|
+
* @example Pre-initialized mode (full control)
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const app = new Application();
|
|
119
|
+
* await app.init({ resizeTo: window });
|
|
120
|
+
* const ecs = ECSpresso.create<...>()
|
|
121
|
+
* .withBundle(createTransformBundle())
|
|
122
|
+
* .withBundle(createPixiBundle({ app }))
|
|
123
|
+
* .build();
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example Managed mode (convenience)
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const ecs = ECSpresso.create<...>()
|
|
129
|
+
* .withBundle(createTransformBundle())
|
|
130
|
+
* .withBundle(createPixiBundle({
|
|
131
|
+
* init: { background: '#1099bb', resizeTo: window },
|
|
132
|
+
* container: document.body,
|
|
133
|
+
* }))
|
|
134
|
+
* .build();
|
|
135
|
+
* await ecs.initialize(); // Application created here
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export type PixiBundleOptions = PixiBundleAppOptions | PixiBundleManagedOptions;
|
|
139
|
+
/**
|
|
140
|
+
* Default local transform values
|
|
141
|
+
*/
|
|
142
|
+
export declare const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform>;
|
|
143
|
+
/**
|
|
144
|
+
* Default world transform values
|
|
145
|
+
*/
|
|
146
|
+
export declare const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform>;
|
|
147
|
+
interface PositionOption {
|
|
148
|
+
x?: number;
|
|
149
|
+
y?: number;
|
|
150
|
+
}
|
|
151
|
+
interface TransformOptions {
|
|
152
|
+
rotation?: number;
|
|
153
|
+
scale?: number | {
|
|
154
|
+
x: number;
|
|
155
|
+
y: number;
|
|
156
|
+
};
|
|
157
|
+
visible?: boolean;
|
|
158
|
+
alpha?: number;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create components for a sprite entity.
|
|
162
|
+
* Returns an object suitable for spreading into spawn().
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const player = ecs.spawn({
|
|
167
|
+
* ...createSpriteComponents(new Sprite(texture), { x: 100, y: 100 }),
|
|
168
|
+
* velocity: { x: 0, y: 0 },
|
|
169
|
+
* });
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export declare function createSpriteComponents(sprite: Sprite, position?: PositionOption, options?: TransformOptions & {
|
|
173
|
+
anchor?: {
|
|
174
|
+
x: number;
|
|
175
|
+
y: number;
|
|
176
|
+
};
|
|
177
|
+
}): Pick<PixiComponentTypes, 'pixiSprite' | 'localTransform' | 'worldTransform' | 'pixiVisible'>;
|
|
178
|
+
/**
|
|
179
|
+
* Create components for a graphics entity.
|
|
180
|
+
* Returns an object suitable for spreading into spawn().
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const rect = ecs.spawn({
|
|
185
|
+
* ...createGraphicsComponents(graphics, { x: 50, y: 50 }),
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export declare function createGraphicsComponents(graphics: Graphics, position?: PositionOption, options?: TransformOptions): Pick<PixiComponentTypes, 'pixiGraphics' | 'localTransform' | 'worldTransform' | 'pixiVisible'>;
|
|
190
|
+
/**
|
|
191
|
+
* Create components for a container entity.
|
|
192
|
+
* Returns an object suitable for spreading into spawn().
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const group = ecs.spawn({
|
|
197
|
+
* ...createContainerComponents(new Container(), { x: 0, y: 0 }),
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export declare function createContainerComponents(container: Container, position?: PositionOption, options?: TransformOptions): Pick<PixiComponentTypes, 'pixiContainer' | 'localTransform' | 'worldTransform' | 'pixiVisible'>;
|
|
202
|
+
/**
|
|
203
|
+
* Create a PixiJS rendering bundle for ECSpresso.
|
|
204
|
+
*
|
|
205
|
+
* This bundle provides:
|
|
206
|
+
* - Render sync system (updates PixiJS objects from ECS components)
|
|
207
|
+
* - Scene graph management (mirrors ECS hierarchy in PixiJS scene graph)
|
|
208
|
+
*
|
|
209
|
+
* **Important**: This bundle requires the transform bundle for transform propagation.
|
|
210
|
+
* Add `createTransformBundle()` before this bundle.
|
|
211
|
+
*
|
|
212
|
+
* @example Pre-initialized mode
|
|
213
|
+
* ```typescript
|
|
214
|
+
* const app = new Application();
|
|
215
|
+
* await app.init({ resizeTo: window });
|
|
216
|
+
*
|
|
217
|
+
* const ecs = ECSpresso.create<GameComponents, {}, {}>()
|
|
218
|
+
* .withBundle(createTransformBundle())
|
|
219
|
+
* .withBundle(createPixiBundle({ app }))
|
|
220
|
+
* .build();
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* @example Managed mode
|
|
224
|
+
* ```typescript
|
|
225
|
+
* const ecs = ECSpresso.create<GameComponents, {}, {}>()
|
|
226
|
+
* .withBundle(createTransformBundle())
|
|
227
|
+
* .withBundle(createPixiBundle({
|
|
228
|
+
* init: { background: '#1099bb', resizeTo: window },
|
|
229
|
+
* container: document.body,
|
|
230
|
+
* }))
|
|
231
|
+
* .build();
|
|
232
|
+
* await ecs.initialize();
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export declare function createPixiBundle(options: PixiBundleOptions): Bundle<PixiComponentTypes, PixiEventTypes, PixiResourceTypes>;
|