ecspresso 0.5.0 → 0.6.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 +192 -9
- package/dist/bundle.d.ts +5 -2
- package/dist/bundles/renderers/pixi.d.ts +248 -0
- package/dist/bundles/renderers/pixi.js +4 -0
- package/dist/bundles/renderers/pixi.js.map +12 -0
- package/dist/bundles/utils/timers.d.ts +113 -0
- package/dist/bundles/utils/timers.js +4 -0
- package/dist/bundles/utils/timers.js.map +12 -0
- package/dist/ecspresso.d.ts +122 -3
- package/dist/entity-manager.d.ts +19 -3
- package/dist/hierarchy-manager.d.ts +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +12 -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,9 @@ 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
|
|
18
21
|
|
|
19
22
|
## Installation
|
|
20
23
|
|
|
@@ -212,6 +215,35 @@ world.addSystem('physics')
|
|
|
212
215
|
.build();
|
|
213
216
|
```
|
|
214
217
|
|
|
218
|
+
### System Groups
|
|
219
|
+
|
|
220
|
+
Organize systems into groups that can be enabled/disabled at runtime:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Assign systems to groups
|
|
224
|
+
world.addSystem('renderSprites')
|
|
225
|
+
.inGroup('rendering')
|
|
226
|
+
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
227
|
+
.setProcess((queries) => { /* ... */ })
|
|
228
|
+
.and()
|
|
229
|
+
.addSystem('renderParticles')
|
|
230
|
+
.inGroup('rendering')
|
|
231
|
+
.inGroup('effects') // Systems can belong to multiple groups
|
|
232
|
+
.setProcess(() => { /* ... */ })
|
|
233
|
+
.build();
|
|
234
|
+
|
|
235
|
+
// Disable/enable groups at runtime
|
|
236
|
+
world.disableSystemGroup('rendering'); // All rendering systems skip
|
|
237
|
+
world.enableSystemGroup('rendering'); // Resume rendering
|
|
238
|
+
|
|
239
|
+
// Query group state
|
|
240
|
+
world.isSystemGroupEnabled('rendering'); // true/false
|
|
241
|
+
world.getSystemsInGroup('rendering'); // ['renderSprites', 'renderParticles']
|
|
242
|
+
|
|
243
|
+
// If a system belongs to multiple groups, disabling ANY group skips the system
|
|
244
|
+
world.disableSystemGroup('effects'); // renderParticles won't run
|
|
245
|
+
```
|
|
246
|
+
|
|
215
247
|
## Advanced Features
|
|
216
248
|
|
|
217
249
|
### Bundles
|
|
@@ -316,7 +348,8 @@ Create resources lazily with factory functions:
|
|
|
316
348
|
```typescript
|
|
317
349
|
interface Resources {
|
|
318
350
|
config: { difficulty: string; soundEnabled: boolean };
|
|
319
|
-
|
|
351
|
+
database: Database;
|
|
352
|
+
cache: { db: Database };
|
|
320
353
|
}
|
|
321
354
|
|
|
322
355
|
const world = new ECSpresso<Components, {}, Resources>();
|
|
@@ -328,15 +361,87 @@ world.addResource('config', () => ({
|
|
|
328
361
|
}));
|
|
329
362
|
|
|
330
363
|
// Async factory
|
|
331
|
-
world.addResource('
|
|
332
|
-
|
|
333
|
-
return { textures };
|
|
364
|
+
world.addResource('database', async () => {
|
|
365
|
+
return await connectToDatabase();
|
|
334
366
|
});
|
|
335
367
|
|
|
336
|
-
//
|
|
368
|
+
// Factory with dependencies - initialized after dependencies are ready
|
|
369
|
+
world.addResource('cache', {
|
|
370
|
+
dependsOn: ['database'],
|
|
371
|
+
factory: (ecs) => ({
|
|
372
|
+
db: ecs.getResource('database')
|
|
373
|
+
})
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Initialize all resources (respects dependency order)
|
|
337
377
|
await world.initializeResources();
|
|
338
378
|
```
|
|
339
379
|
|
|
380
|
+
**Dependency Features:**
|
|
381
|
+
- Resources are initialized in topological order (dependencies first)
|
|
382
|
+
- Circular dependencies throw a descriptive error at initialization time
|
|
383
|
+
- Existing patterns (direct values, simple factories) work unchanged
|
|
384
|
+
|
|
385
|
+
### Resource Builder
|
|
386
|
+
|
|
387
|
+
Add resources fluently during ECSpresso construction using `withResource()`:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
const world = ECSpresso
|
|
391
|
+
.create<Components, Events, Resources>()
|
|
392
|
+
.withBundle(physicsBundle)
|
|
393
|
+
.withResource('config', { debug: true, maxEntities: 1000 })
|
|
394
|
+
.withResource('score', () => ({ value: 0 }))
|
|
395
|
+
.withResource('cache', {
|
|
396
|
+
dependsOn: ['database'],
|
|
397
|
+
factory: (ecs) => createCache(ecs.getResource('database'))
|
|
398
|
+
})
|
|
399
|
+
.build();
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
This chains naturally with `withBundle()`, `withAssets()`, and `withScreens()`.
|
|
403
|
+
|
|
404
|
+
### Resource Disposal
|
|
405
|
+
|
|
406
|
+
Resources can define cleanup logic with `onDispose` callbacks, useful for removing event listeners, closing connections, or releasing resources:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// Factory with disposal callback
|
|
410
|
+
world.addResource('keyboard', {
|
|
411
|
+
factory: () => {
|
|
412
|
+
const handler = (e: KeyboardEvent) => { /* ... */ };
|
|
413
|
+
window.addEventListener('keydown', handler);
|
|
414
|
+
return { handler };
|
|
415
|
+
},
|
|
416
|
+
onDispose: (resource) => {
|
|
417
|
+
window.removeEventListener('keydown', resource.handler);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Or with the builder pattern
|
|
422
|
+
const world = ECSpresso
|
|
423
|
+
.create<Components, Events, Resources>()
|
|
424
|
+
.withResource('database', {
|
|
425
|
+
factory: async () => await connectToDatabase(),
|
|
426
|
+
onDispose: async (db) => await db.close()
|
|
427
|
+
})
|
|
428
|
+
.build();
|
|
429
|
+
|
|
430
|
+
// Dispose a single resource
|
|
431
|
+
await world.disposeResource('keyboard');
|
|
432
|
+
|
|
433
|
+
// Dispose all resources in reverse dependency order
|
|
434
|
+
// (dependents are disposed before their dependencies)
|
|
435
|
+
await world.disposeResources();
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Disposal Features:**
|
|
439
|
+
- `onDispose` receives the resource value and the ECSpresso instance as context
|
|
440
|
+
- `disposeResources()` disposes in reverse topological order (dependents first)
|
|
441
|
+
- Only initialized resources have their `onDispose` called
|
|
442
|
+
- Supports both sync and async disposal callbacks
|
|
443
|
+
- `removeResource()` still exists for removal without disposal
|
|
444
|
+
|
|
340
445
|
### System Lifecycle
|
|
341
446
|
|
|
342
447
|
Systems can have initialization and cleanup hooks:
|
|
@@ -446,6 +551,32 @@ world.getChildAt(root.id, 0); // child.id
|
|
|
446
551
|
world.getChildIndex(root.id, child2.id); // 1
|
|
447
552
|
```
|
|
448
553
|
|
|
554
|
+
#### Parent-First Traversal
|
|
555
|
+
|
|
556
|
+
Iterate the hierarchy with guaranteed parent-first order (useful for transform propagation):
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
// Callback-based traversal
|
|
560
|
+
world.forEachInHierarchy((entityId, parentId, depth) => {
|
|
561
|
+
// Parents are always visited before their children
|
|
562
|
+
console.log(`Entity ${entityId} at depth ${depth}, parent: ${parentId}`);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Filter to specific subtrees
|
|
566
|
+
world.forEachInHierarchy(
|
|
567
|
+
(entityId, parentId, depth) => {
|
|
568
|
+
// Only visits entities under root.id
|
|
569
|
+
},
|
|
570
|
+
{ roots: [root.id] }
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Generator-based traversal (supports early termination)
|
|
574
|
+
for (const { entityId, parentId, depth } of world.hierarchyIterator()) {
|
|
575
|
+
if (depth > 2) break; // Stop at depth 2
|
|
576
|
+
console.log(entityId);
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
449
580
|
#### Cascade Deletion
|
|
450
581
|
|
|
451
582
|
When removing entities, descendants are automatically removed by default:
|
|
@@ -771,19 +902,71 @@ world.withBundle(conflictingBundle); // TypeScript prevents this
|
|
|
771
902
|
|
|
772
903
|
## Component Callbacks
|
|
773
904
|
|
|
774
|
-
React to component changes with callbacks:
|
|
905
|
+
React to component changes with callbacks. Both methods return an unsubscribe function:
|
|
775
906
|
|
|
776
907
|
```typescript
|
|
777
|
-
// Listen for component additions
|
|
778
|
-
world.
|
|
908
|
+
// Listen for component additions - returns unsubscribe function
|
|
909
|
+
const unsubAdd = world.onComponentAdded('health', (value, entity) => {
|
|
779
910
|
console.log(`Health added to entity ${entity.id}:`, value);
|
|
780
911
|
});
|
|
781
912
|
|
|
782
|
-
|
|
913
|
+
// Listen for component removals
|
|
914
|
+
const unsubRemove = world.onComponentRemoved('health', (oldValue, entity) => {
|
|
783
915
|
console.log(`Health removed from entity ${entity.id}:`, oldValue);
|
|
784
916
|
});
|
|
917
|
+
|
|
918
|
+
// Unsubscribe when done
|
|
919
|
+
unsubAdd();
|
|
920
|
+
unsubRemove();
|
|
921
|
+
|
|
922
|
+
// Also available on entityManager directly
|
|
923
|
+
world.entityManager.onComponentAdded('position', (value, entity) => {
|
|
924
|
+
// ...
|
|
925
|
+
});
|
|
785
926
|
```
|
|
786
927
|
|
|
928
|
+
## Reactive Queries
|
|
929
|
+
|
|
930
|
+
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:
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
// Add a reactive query with enter/exit callbacks
|
|
934
|
+
world.addReactiveQuery('enemies', {
|
|
935
|
+
with: ['position', 'enemy'],
|
|
936
|
+
without: ['dead'],
|
|
937
|
+
onEnter: (entity) => {
|
|
938
|
+
// Called when entity starts matching the query
|
|
939
|
+
console.log(`Enemy ${entity.id} appeared at`, entity.components.position);
|
|
940
|
+
spawnHealthBar(entity.id);
|
|
941
|
+
},
|
|
942
|
+
onExit: (entityId) => {
|
|
943
|
+
// Called when entity stops matching (receives ID since entity may be removed)
|
|
944
|
+
console.log(`Enemy ${entityId} gone`);
|
|
945
|
+
removeHealthBar(entityId);
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// Triggers: spawning matching entity, adding required component,
|
|
950
|
+
// removing excluded component
|
|
951
|
+
const enemy = world.spawn({ position: { x: 0, y: 0 }, enemy: true }); // onEnter fires
|
|
952
|
+
|
|
953
|
+
// Triggers: removing required component, adding excluded component,
|
|
954
|
+
// removing entity
|
|
955
|
+
world.entityManager.addComponent(enemy.id, 'dead', true); // onExit fires
|
|
956
|
+
|
|
957
|
+
// Existing matching entities trigger onEnter when query is added
|
|
958
|
+
world.spawn({ position: { x: 10, y: 10 }, enemy: true });
|
|
959
|
+
world.addReactiveQuery('positioned', {
|
|
960
|
+
with: ['position'],
|
|
961
|
+
onEnter: (entity) => { /* Called for all existing entities with position */ },
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Remove reactive query when no longer needed
|
|
965
|
+
const removed = world.removeReactiveQuery('enemies'); // returns true if existed
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
**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.
|
|
969
|
+
|
|
787
970
|
## Error Handling
|
|
788
971
|
|
|
789
972
|
ECSpresso provides clear, contextual error messages for common issues:
|
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,248 @@
|
|
|
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
|
+
import type { Application, ApplicationOptions, Container, Sprite, Graphics } from 'pixi.js';
|
|
8
|
+
import Bundle from '../../bundle';
|
|
9
|
+
/**
|
|
10
|
+
* Local transform relative to parent (or world if no parent)
|
|
11
|
+
*/
|
|
12
|
+
export interface LocalTransform {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
rotation: number;
|
|
16
|
+
scaleX: number;
|
|
17
|
+
scaleY: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Computed world transform (accumulated from parent chain)
|
|
21
|
+
*/
|
|
22
|
+
export interface WorldTransform {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
rotation: number;
|
|
26
|
+
scaleX: number;
|
|
27
|
+
scaleY: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* PixiJS Sprite component
|
|
31
|
+
*/
|
|
32
|
+
export interface PixiSprite {
|
|
33
|
+
sprite: Sprite;
|
|
34
|
+
anchor?: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* PixiJS Graphics component
|
|
41
|
+
*/
|
|
42
|
+
export interface PixiGraphics {
|
|
43
|
+
graphics: Graphics;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* PixiJS Container component
|
|
47
|
+
*/
|
|
48
|
+
export interface PixiContainer {
|
|
49
|
+
container: Container;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Visibility and alpha component
|
|
53
|
+
*/
|
|
54
|
+
export interface PixiVisible {
|
|
55
|
+
visible: boolean;
|
|
56
|
+
alpha?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Aggregate component types for PixiJS bundle.
|
|
60
|
+
* Users should extend this interface with their own component types.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* interface GameComponents extends PixiComponentTypes {
|
|
65
|
+
* velocity: { x: number; y: number };
|
|
66
|
+
* player: true;
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export interface PixiComponentTypes {
|
|
71
|
+
localTransform: LocalTransform;
|
|
72
|
+
worldTransform: WorldTransform;
|
|
73
|
+
pixiSprite: PixiSprite;
|
|
74
|
+
pixiGraphics: PixiGraphics;
|
|
75
|
+
pixiContainer: PixiContainer;
|
|
76
|
+
pixiVisible: PixiVisible;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Events emitted by the PixiJS bundle
|
|
80
|
+
*/
|
|
81
|
+
export interface PixiEventTypes {
|
|
82
|
+
hierarchyChanged: {
|
|
83
|
+
entityId: number;
|
|
84
|
+
oldParent: number | null;
|
|
85
|
+
newParent: number | null;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resources provided by the PixiJS bundle
|
|
90
|
+
*/
|
|
91
|
+
export interface PixiResourceTypes {
|
|
92
|
+
pixiApp: Application;
|
|
93
|
+
pixiRootContainer: Container;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Common options shared between both initialization modes
|
|
97
|
+
*/
|
|
98
|
+
interface PixiBundleCommonOptions {
|
|
99
|
+
/** Optional custom root container (defaults to app.stage) */
|
|
100
|
+
rootContainer?: Container;
|
|
101
|
+
/** System group name (default: 'pixi-renderer') */
|
|
102
|
+
systemGroup?: string;
|
|
103
|
+
/** Priority for transform propagation system (default: 1000) */
|
|
104
|
+
transformPriority?: number;
|
|
105
|
+
/** Priority for render sync system (default: 500) */
|
|
106
|
+
renderSyncPriority?: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Options when providing a pre-initialized PixiJS Application
|
|
110
|
+
*/
|
|
111
|
+
export interface PixiBundleAppOptions extends PixiBundleCommonOptions {
|
|
112
|
+
/** The PixiJS Application instance (already initialized) */
|
|
113
|
+
app: Application;
|
|
114
|
+
init?: never;
|
|
115
|
+
container?: never;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Options when letting the bundle create and manage the PixiJS Application
|
|
119
|
+
*/
|
|
120
|
+
export interface PixiBundleManagedOptions extends PixiBundleCommonOptions {
|
|
121
|
+
app?: never;
|
|
122
|
+
/** PixiJS ApplicationOptions - bundle will create and initialize the Application */
|
|
123
|
+
init: Partial<ApplicationOptions>;
|
|
124
|
+
/** Container element to append the canvas to, or CSS selector string */
|
|
125
|
+
container?: HTMLElement | string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Configuration options for the PixiJS bundle.
|
|
129
|
+
*
|
|
130
|
+
* Supports two modes:
|
|
131
|
+
* 1. **Pre-initialized**: Pass an already-initialized Application via `app`
|
|
132
|
+
* 2. **Managed**: Pass `init` options and the bundle creates the Application during `ecs.initialize()`
|
|
133
|
+
*
|
|
134
|
+
* @example Pre-initialized mode (full control)
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const app = new Application();
|
|
137
|
+
* await app.init({ resizeTo: window });
|
|
138
|
+
* const ecs = ECSpresso.create<...>()
|
|
139
|
+
* .withBundle(createPixiBundle({ app }))
|
|
140
|
+
* .build();
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @example Managed mode (convenience)
|
|
144
|
+
* ```typescript
|
|
145
|
+
* const ecs = ECSpresso.create<...>()
|
|
146
|
+
* .withBundle(createPixiBundle({
|
|
147
|
+
* init: { background: '#1099bb', resizeTo: window },
|
|
148
|
+
* container: document.body,
|
|
149
|
+
* }))
|
|
150
|
+
* .build();
|
|
151
|
+
* await ecs.initialize(); // Application created here
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export type PixiBundleOptions = PixiBundleAppOptions | PixiBundleManagedOptions;
|
|
155
|
+
/**
|
|
156
|
+
* Default local transform values
|
|
157
|
+
*/
|
|
158
|
+
export declare const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform>;
|
|
159
|
+
/**
|
|
160
|
+
* Default world transform values
|
|
161
|
+
*/
|
|
162
|
+
export declare const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform>;
|
|
163
|
+
interface PositionOption {
|
|
164
|
+
x?: number;
|
|
165
|
+
y?: number;
|
|
166
|
+
}
|
|
167
|
+
interface TransformOptions {
|
|
168
|
+
rotation?: number;
|
|
169
|
+
scale?: number | {
|
|
170
|
+
x: number;
|
|
171
|
+
y: number;
|
|
172
|
+
};
|
|
173
|
+
visible?: boolean;
|
|
174
|
+
alpha?: number;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create components for a sprite entity.
|
|
178
|
+
* Returns an object suitable for spreading into spawn().
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const player = ecs.spawn({
|
|
183
|
+
* ...createSpriteComponents(new Sprite(texture), { x: 100, y: 100 }),
|
|
184
|
+
* velocity: { x: 0, y: 0 },
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export declare function createSpriteComponents(sprite: Sprite, position?: PositionOption, options?: TransformOptions & {
|
|
189
|
+
anchor?: {
|
|
190
|
+
x: number;
|
|
191
|
+
y: number;
|
|
192
|
+
};
|
|
193
|
+
}): Pick<PixiComponentTypes, 'pixiSprite' | 'localTransform' | 'worldTransform' | 'pixiVisible'>;
|
|
194
|
+
/**
|
|
195
|
+
* Create components for a graphics entity.
|
|
196
|
+
* Returns an object suitable for spreading into spawn().
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* const rect = ecs.spawn({
|
|
201
|
+
* ...createGraphicsComponents(graphics, { x: 50, y: 50 }),
|
|
202
|
+
* });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export declare function createGraphicsComponents(graphics: Graphics, position?: PositionOption, options?: TransformOptions): Pick<PixiComponentTypes, 'pixiGraphics' | 'localTransform' | 'worldTransform' | 'pixiVisible'>;
|
|
206
|
+
/**
|
|
207
|
+
* Create components for a container entity.
|
|
208
|
+
* Returns an object suitable for spreading into spawn().
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* const group = ecs.spawn({
|
|
213
|
+
* ...createContainerComponents(new Container(), { x: 0, y: 0 }),
|
|
214
|
+
* });
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export declare function createContainerComponents(container: Container, position?: PositionOption, options?: TransformOptions): Pick<PixiComponentTypes, 'pixiContainer' | 'localTransform' | 'worldTransform' | 'pixiVisible'>;
|
|
218
|
+
/**
|
|
219
|
+
* Create a PixiJS rendering bundle for ECSpresso.
|
|
220
|
+
*
|
|
221
|
+
* This bundle provides:
|
|
222
|
+
* - Transform propagation system (computes world transforms from hierarchy)
|
|
223
|
+
* - Render sync system (updates PixiJS objects from ECS components)
|
|
224
|
+
* - Scene graph management (mirrors ECS hierarchy in PixiJS scene graph)
|
|
225
|
+
*
|
|
226
|
+
* @example Pre-initialized mode
|
|
227
|
+
* ```typescript
|
|
228
|
+
* const app = new Application();
|
|
229
|
+
* await app.init({ resizeTo: window });
|
|
230
|
+
*
|
|
231
|
+
* const ecs = ECSpresso.create<GameComponents, {}, {}>()
|
|
232
|
+
* .withBundle(createPixiBundle({ app }))
|
|
233
|
+
* .build();
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @example Managed mode
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const ecs = ECSpresso.create<GameComponents, {}, {}>()
|
|
239
|
+
* .withBundle(createPixiBundle({
|
|
240
|
+
* init: { background: '#1099bb', resizeTo: window },
|
|
241
|
+
* container: document.body,
|
|
242
|
+
* }))
|
|
243
|
+
* .build();
|
|
244
|
+
* await ecs.initialize();
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export declare function createPixiBundle(options: PixiBundleOptions): Bundle<PixiComponentTypes, PixiEventTypes, PixiResourceTypes>;
|
|
248
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var O=Object.create;var{getPrototypeOf:I,defineProperty:j,getOwnPropertyNames:T}=Object;var f=Object.prototype.hasOwnProperty;var u=(H,J,K)=>{K=H!=null?O(I(H)):{};let F=J||!H||!H.__esModule?j(K,"default",{value:H,enumerable:!0}):K;for(let R of T(H))if(!f.call(F,R))j(F,R,{get:()=>H[R],enumerable:!0});return F};var b=((H)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(H,{get:(J,K)=>(typeof require<"u"?require:J)[K]}):H)(function(H){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+H+'" is not supported')});class k{_label;_ecspresso;_bundle;queries={};processFunction;detachFunction;initializeFunction;eventHandlers;_priority=0;_isRegistered=!1;_groups=[];_inScreens;_excludeScreens;_requiredAssets;constructor(H,J=null,K=null){this._label=H;this._ecspresso=J;this._bundle=K}get label(){return this._label}get bundle(){return this._bundle}get ecspresso(){return this._ecspresso}_autoRegister(){if(this._isRegistered||!this._ecspresso)return;let H=this._buildSystemObject();G(H,this._ecspresso),this._isRegistered=!0}_buildSystemObject(){return this._createSystemObject()}_createSystemObject(){let H={label:this._label,entityQueries:this.queries,priority:this._priority};if(this.processFunction)H.process=this.processFunction;if(this.detachFunction)H.onDetach=this.detachFunction;if(this.initializeFunction)H.onInitialize=this.initializeFunction;if(this.eventHandlers)H.eventHandlers=this.eventHandlers;if(this._groups.length>0)H.groups=[...this._groups];if(this._inScreens)H.inScreens=this._inScreens;if(this._excludeScreens)H.excludeScreens=this._excludeScreens;if(this._requiredAssets)H.requiredAssets=this._requiredAssets;return H}setPriority(H){return this._priority=H,this}inGroup(H){if(!this._groups.includes(H))this._groups.push(H);return this}inScreens(H){return this._inScreens=[...H],this}excludeScreens(H){return this._excludeScreens=[...H],this}requiresAssets(H){return this._requiredAssets=[...H],this}addQuery(H,J){let K=this;return K.queries={...this.queries,[H]:J},K}setProcess(H){return this.processFunction=H,this}registerAndContinue(){if(!this._ecspresso)throw Error(`Cannot register system '${this._label}': SystemBuilder is not attached to an ECSpresso instance. Use Bundle.addSystem() or ECSpresso.addSystem() instead.`);return this._autoRegister(),this._ecspresso}and(){if(this._ecspresso)return this._autoRegister(),this._ecspresso;if(this._bundle)return this._bundle;throw Error(`Cannot use and() on system '${this._label}': not attached to ECSpresso or Bundle.`)}setOnDetach(H){return this.detachFunction=H,this}setOnInitialize(H){return this.initializeFunction=H,this}setEventHandlers(H){return this.eventHandlers=H,this}build(H){let J=this._createSystemObject();if(this._ecspresso)G(J,this._ecspresso);if(H)G(J,H);return this}}function G(H,J){J._registerSystem(H)}function c(H,J){return new k(H,J)}function x(H,J){return new k(H,null,J)}function y(){return`bundle_${Date.now().toString(36)}_${Math.random().toString(36).substring(2,9)}`}class E{_systems=[];_resources=new Map;_assets=new Map;_assetGroups=new Map;_screens=new Map;_id;constructor(H){this._id=H||y()}get id(){return this._id}set id(H){this._id=H}addSystem(H){if(typeof H==="string"){let J=x(H,this);return this._systems.push(J),J}else return this._systems.push(H),H}addResource(H,J){return this._resources.set(H,J),this}addAsset(H,J,K){return this._assets.set(H,{loader:J,eager:K?.eager??!0,group:K?.group}),this}addAssetGroup(H,J){let K=new Map;for(let[F,R]of Object.entries(J))K.set(F,R),this._assets.set(F,{loader:R,eager:!1,group:H});return this._assetGroups.set(H,K),this}addScreen(H,J){return this._screens.set(H,J),this}getAssets(){return new Map(this._assets)}getScreens(){return new Map(this._screens)}_setResource(H,J){this._resources.set(H,J)}_setAsset(H,J){this._assets.set(H,J)}_setScreen(H,J){this._screens.set(H,J)}getSystems(){return this._systems.map((H)=>H.build())}registerSystemsWithEcspresso(H){for(let J of this._systems)J.build(H)}getResources(){return new Map(this._resources)}getResource(H){return this._resources.get(H)}getSystemBuilders(){return[...this._systems]}hasResource(H){return this._resources.has(H)}}function n(H,...J){if(J.length===0)return new E(H);let K=new E(H);for(let F of J){for(let R of F.getSystemBuilders())K.addSystem(R);for(let[R,Y]of F.getResources().entries())K._setResource(R,Y);for(let[R,Y]of F.getAssets().entries())K._setAsset(R,Y);for(let[R,Y]of F.getScreens().entries())K._setScreen(R,Y)}return K}async function m(H){let{Application:J}=await import("pixi.js"),K=new J;return await K.init(H),K}var t={x:0,y:0,rotation:0,scaleX:1,scaleY:1},o={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function L(H,J){let K=J?.scale,F=typeof K==="number"?K:K?.x??1,R=typeof K==="number"?K:K?.y??1;return{x:H?.x??0,y:H?.y??0,rotation:J?.rotation??0,scaleX:F,scaleY:R}}function V(H,J){let K=J?.scale,F=typeof K==="number"?K:K?.x??1,R=typeof K==="number"?K:K?.y??1;return{x:H?.x??0,y:H?.y??0,rotation:J?.rotation??0,scaleX:F,scaleY:R}}function B(H){return{visible:H?.visible??!0,alpha:H?.alpha}}function e(H,J,K){return{pixiSprite:{sprite:H,anchor:K?.anchor},localTransform:L(J,K),worldTransform:V(J,K),pixiVisible:B(K)}}function HH(H,J,K){return{pixiGraphics:{graphics:H},localTransform:L(J,K),worldTransform:V(J,K),pixiVisible:B(K)}}function JH(H,J,K){return{pixiContainer:{container:H},localTransform:L(J,K),worldTransform:V(J,K),pixiVisible:B(K)}}function KH(H){let{rootContainer:J,systemGroup:K="pixi-renderer",transformPriority:F=1000,renderSyncPriority:R=500}=H,Y=new E("pixi-renderer");if("init"in H&&H.init!==void 0){let{init:_,container:Q}=H;Y.addResource("pixiApp",async()=>{let U=await m(_);if(Q){let D=typeof Q==="string"?document.querySelector(Q):Q;if(D)D.appendChild(U.canvas);else if(typeof Q==="string")console.warn(`PixiJS bundle: container selector "${Q}" not found`)}return U}),Y.addResource("pixiRootContainer",{dependsOn:["pixiApp"],factory:(U)=>J??U.getResource("pixiApp").stage})}else{let _=H.app;Y.addResource("pixiApp",_),Y.addResource("pixiRootContainer",J??_.stage)}let q=new Map;function W(_,Q){let U=q.get(_);if(U)return U;let D=Q.entityManager.getComponent(_,"pixiSprite");if(D)return q.set(_,D.sprite),D.sprite;let z=Q.entityManager.getComponent(_,"pixiGraphics");if(z)return q.set(_,z.graphics),z.graphics;let $=Q.entityManager.getComponent(_,"pixiContainer");if($)return q.set(_,$.container),$.container;return null}function A(_,Q,U){let D=U.getResource("pixiRootContainer"),z=U.getParent(_),M=(z!==null?W(z,U):null)??D;if(Q.parent!==M)M.addChild(Q)}function N(_){let Q=q.get(_);if(Q)Q.removeFromParent(),q.delete(_)}function g(_,Q){let U=q.get(_);if(!U)return;let D=Q.getResource("pixiRootContainer"),z=Q.getParent(_),M=(z!==null?W(z,Q):null)??D;if(U.parent!==M)U.removeFromParent(),M.addChild(U)}return Y.addSystem("pixi-transform-propagation").setPriority(F).inGroup(K).setProcess((_,Q,U)=>{U.forEachInHierarchy((z,$)=>{let M=U.entityManager.getComponent(z,"localTransform"),Z=U.entityManager.getComponent(z,"worldTransform");if(!M||!Z)return;if($===null)Z.x=M.x,Z.y=M.y,Z.rotation=M.rotation,Z.scaleX=M.scaleX,Z.scaleY=M.scaleY;else{let X=U.entityManager.getComponent($,"worldTransform");if(X){let P=M.x*X.scaleX,v=M.y*X.scaleY,S=Math.cos(X.rotation),C=Math.sin(X.rotation),w=P*S-v*C,h=P*C+v*S;Z.x=X.x+w,Z.y=X.y+h,Z.rotation=X.rotation+M.rotation,Z.scaleX=X.scaleX*M.scaleX,Z.scaleY=X.scaleY*M.scaleY}else Z.x=M.x,Z.y=M.y,Z.rotation=M.rotation,Z.scaleX=M.scaleX,Z.scaleY=M.scaleY}});let D=U.getEntitiesWithQuery(["localTransform","worldTransform"]);for(let z of D)if(U.getParent(z.id)===null&&U.getChildren(z.id).length===0){let{localTransform:M,worldTransform:Z}=z.components;Z.x=M.x,Z.y=M.y,Z.rotation=M.rotation,Z.scaleX=M.scaleX,Z.scaleY=M.scaleY}}).and(),Y.addSystem("pixi-render-sync").setPriority(R).inGroup(K).addQuery("sprites",{with:["pixiSprite","worldTransform"]}).addQuery("graphics",{with:["pixiGraphics","worldTransform"]}).addQuery("containers",{with:["pixiContainer","worldTransform"]}).setProcess((_,Q,U)=>{for(let D of _.sprites){let{pixiSprite:z,worldTransform:$}=D.components,{sprite:M,anchor:Z}=z;if(M.position.set($.x,$.y),M.rotation=$.rotation,M.scale.set($.scaleX,$.scaleY),Z)M.anchor.set(Z.x,Z.y);let X=U.entityManager.getComponent(D.id,"pixiVisible");if(X){if(M.visible=X.visible,X.alpha!==void 0)M.alpha=X.alpha}}for(let D of _.graphics){let{pixiGraphics:z,worldTransform:$}=D.components,{graphics:M}=z;M.position.set($.x,$.y),M.rotation=$.rotation,M.scale.set($.scaleX,$.scaleY);let Z=U.entityManager.getComponent(D.id,"pixiVisible");if(Z){if(M.visible=Z.visible,Z.alpha!==void 0)M.alpha=Z.alpha}}for(let D of _.containers){let{pixiContainer:z,worldTransform:$}=D.components,{container:M}=z;M.position.set($.x,$.y),M.rotation=$.rotation,M.scale.set($.scaleX,$.scaleY);let Z=U.entityManager.getComponent(D.id,"pixiVisible");if(Z){if(M.visible=Z.visible,Z.alpha!==void 0)M.alpha=Z.alpha}}}).and(),Y.addSystem("pixi-scene-graph-manager").setPriority(9999).inGroup(K).setOnInitialize((_)=>{_.addReactiveQuery("pixi-sprites",{with:["pixiSprite"],onEnter:(Q)=>{let U=Q.components.pixiSprite.sprite;q.set(Q.id,U),A(Q.id,U,_)},onExit:(Q)=>{N(Q)}}),_.addReactiveQuery("pixi-graphics",{with:["pixiGraphics"],onEnter:(Q)=>{let U=Q.components.pixiGraphics.graphics;q.set(Q.id,U),A(Q.id,U,_)},onExit:(Q)=>{N(Q)}}),_.addReactiveQuery("pixi-containers",{with:["pixiContainer"],onEnter:(Q)=>{let U=Q.components.pixiContainer.container;q.set(Q.id,U),A(Q.id,U,_)},onExit:(Q)=>{N(Q)}}),_.on("hierarchyChanged",({entityId:Q})=>{g(Q,_)})}).and(),Y}export{e as createSpriteComponents,KH as createPixiBundle,HH as createGraphicsComponents,JH as createContainerComponents,o as DEFAULT_WORLD_TRANSFORM,t as DEFAULT_LOCAL_TRANSFORM};
|
|
2
|
+
|
|
3
|
+
//# debugId=3807489A54FF20AF64756E2164756E21
|
|
4
|
+
//# sourceMappingURL=pixi.js.map
|