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 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
- assets: { textures: any[] };
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('assets', async () => {
332
- const textures = await loadTextures();
333
- return { textures };
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/removals
778
- world.entityManager.onComponentAdded('health', (value, entity) => {
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
- world.entityManager.onComponentRemoved('health', (oldValue, entity) => {
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 or a factory function that returns the resource
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]>)): this;
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>;