ecspresso 0.10.2 → 0.12.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.
Files changed (94) hide show
  1. package/README.md +256 -148
  2. package/dist/asset-manager.d.ts +16 -16
  3. package/dist/asset-types.d.ts +18 -16
  4. package/dist/command-buffer.d.ts +30 -20
  5. package/dist/ecspresso-builder.d.ts +193 -0
  6. package/dist/ecspresso.d.ts +323 -209
  7. package/dist/entity-manager.d.ts +76 -30
  8. package/dist/event-bus.d.ts +6 -1
  9. package/dist/index.d.ts +6 -13
  10. package/dist/plugin.d.ts +61 -0
  11. package/dist/plugins/audio.d.ts +273 -0
  12. package/dist/{bundles/utils → plugins}/bounds.d.ts +20 -26
  13. package/dist/plugins/camera.d.ts +88 -0
  14. package/dist/plugins/collision.d.ts +285 -0
  15. package/dist/plugins/coroutine.d.ts +126 -0
  16. package/dist/plugins/diagnostics.d.ts +49 -0
  17. package/dist/{bundles/utils → plugins}/input.d.ts +22 -29
  18. package/dist/plugins/particles.d.ts +225 -0
  19. package/dist/plugins/physics2D.d.ts +163 -0
  20. package/dist/plugins/renderers/renderer2D.d.ts +262 -0
  21. package/dist/plugins/spatial-index.d.ts +58 -0
  22. package/dist/plugins/sprite-animation.d.ts +150 -0
  23. package/dist/plugins/state-machine.d.ts +244 -0
  24. package/dist/plugins/timers.d.ts +151 -0
  25. package/dist/{bundles/utils → plugins}/transform.d.ts +21 -22
  26. package/dist/plugins/tween.d.ts +162 -0
  27. package/dist/reactive-query-manager.d.ts +14 -3
  28. package/dist/resource-manager.d.ts +64 -23
  29. package/dist/screen-manager.d.ts +21 -15
  30. package/dist/screen-types.d.ts +15 -11
  31. package/dist/src/index.js +4 -0
  32. package/dist/src/index.js.map +25 -0
  33. package/dist/src/plugins/audio.js +4 -0
  34. package/dist/src/plugins/audio.js.map +10 -0
  35. package/dist/src/plugins/bounds.js +4 -0
  36. package/dist/src/plugins/bounds.js.map +10 -0
  37. package/dist/src/plugins/camera.js +4 -0
  38. package/dist/src/plugins/camera.js.map +10 -0
  39. package/dist/src/plugins/collision.js +4 -0
  40. package/dist/src/plugins/collision.js.map +11 -0
  41. package/dist/src/plugins/coroutine.js +4 -0
  42. package/dist/src/plugins/coroutine.js.map +10 -0
  43. package/dist/src/plugins/diagnostics.js +5 -0
  44. package/dist/src/plugins/diagnostics.js.map +10 -0
  45. package/dist/src/plugins/input.js +4 -0
  46. package/dist/src/plugins/input.js.map +10 -0
  47. package/dist/src/plugins/particles.js +4 -0
  48. package/dist/src/plugins/particles.js.map +10 -0
  49. package/dist/src/plugins/physics2D.js +4 -0
  50. package/dist/src/plugins/physics2D.js.map +11 -0
  51. package/dist/src/plugins/renderers/renderer2D.js +4 -0
  52. package/dist/src/plugins/renderers/renderer2D.js.map +10 -0
  53. package/dist/src/plugins/spatial-index.js +4 -0
  54. package/dist/src/plugins/spatial-index.js.map +11 -0
  55. package/dist/src/plugins/sprite-animation.js +4 -0
  56. package/dist/src/plugins/sprite-animation.js.map +10 -0
  57. package/dist/src/plugins/state-machine.js +4 -0
  58. package/dist/src/plugins/state-machine.js.map +10 -0
  59. package/dist/src/plugins/timers.js +4 -0
  60. package/dist/src/plugins/timers.js.map +10 -0
  61. package/dist/src/plugins/transform.js +4 -0
  62. package/dist/src/plugins/transform.js.map +10 -0
  63. package/dist/src/plugins/tween.js +4 -0
  64. package/dist/src/plugins/tween.js.map +11 -0
  65. package/dist/system-builder.d.ts +75 -112
  66. package/dist/type-utils.d.ts +247 -7
  67. package/dist/types.d.ts +58 -39
  68. package/dist/utils/check-required-cycle.d.ts +12 -0
  69. package/dist/utils/easing.d.ts +71 -0
  70. package/dist/utils/math.d.ts +67 -0
  71. package/dist/utils/narrowphase.d.ts +63 -0
  72. package/dist/utils/spatial-hash.d.ts +53 -0
  73. package/package.json +65 -27
  74. package/dist/bundle.d.ts +0 -123
  75. package/dist/bundles/renderers/renderer2D.d.ts +0 -220
  76. package/dist/bundles/renderers/renderer2D.js +0 -4
  77. package/dist/bundles/renderers/renderer2D.js.map +0 -10
  78. package/dist/bundles/utils/bounds.js +0 -4
  79. package/dist/bundles/utils/bounds.js.map +0 -10
  80. package/dist/bundles/utils/collision.d.ts +0 -204
  81. package/dist/bundles/utils/collision.js +0 -4
  82. package/dist/bundles/utils/collision.js.map +0 -10
  83. package/dist/bundles/utils/input.js +0 -4
  84. package/dist/bundles/utils/input.js.map +0 -10
  85. package/dist/bundles/utils/movement.d.ts +0 -86
  86. package/dist/bundles/utils/movement.js +0 -4
  87. package/dist/bundles/utils/movement.js.map +0 -10
  88. package/dist/bundles/utils/timers.d.ts +0 -172
  89. package/dist/bundles/utils/timers.js +0 -4
  90. package/dist/bundles/utils/timers.js.map +0 -10
  91. package/dist/bundles/utils/transform.js +0 -4
  92. package/dist/bundles/utils/transform.js.map +0 -10
  93. package/dist/index.js +0 -4
  94. package/dist/index.js.map +0 -22
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Bounds Bundle for ECSpresso
2
+ * Bounds Plugin for ECSpresso
3
3
  *
4
4
  * Provides screen bounds enforcement for entities with transforms.
5
5
  * Reads worldTransform for position checking; modifies localTransform for corrections.
6
6
  * Supports destroy, clamp, and wrap behaviors.
7
7
  */
8
- import { Bundle } from 'ecspresso';
9
- import type { SystemPhase } from 'ecspresso';
10
- import type { TransformComponentTypes } from './transform';
8
+ import { type Plugin, type BasePluginOptions } from 'ecspresso';
9
+ import type { WorldConfigFrom } from '../type-utils';
10
+ import type { TransformWorldConfig } from './transform';
11
11
  /**
12
12
  * Component that marks an entity for destruction when outside bounds.
13
13
  */
@@ -30,14 +30,16 @@ export interface WrapAtBounds {
30
30
  padding?: number;
31
31
  }
32
32
  /**
33
- * Component types provided by the bounds bundle.
34
- * Extend your component types with this interface.
33
+ * Component types provided by the bounds plugin.
34
+ * Included automatically via `.withPlugin(createBoundsPlugin())`.
35
35
  *
36
36
  * @example
37
37
  * ```typescript
38
- * interface GameComponents extends TransformComponentTypes, BoundsComponentTypes {
39
- * sprite: Sprite;
40
- * }
38
+ * const ecs = ECSpresso.create()
39
+ * .withPlugin(createTransformPlugin())
40
+ * .withPlugin(createBoundsPlugin({ width: 800, height: 600 }))
41
+ * .withComponentTypes<{ sprite: Sprite }>()
42
+ * .build();
41
43
  * ```
42
44
  */
43
45
  export interface BoundsComponentTypes {
@@ -59,7 +61,7 @@ export interface BoundsRect {
59
61
  height: number;
60
62
  }
61
63
  /**
62
- * Resource types provided by the bounds bundle.
64
+ * Resource types provided by the bounds plugin.
63
65
  */
64
66
  export interface BoundsResourceTypes {
65
67
  bounds: BoundsRect;
@@ -74,25 +76,19 @@ export interface EntityOutOfBoundsEvent {
74
76
  exitEdge: 'top' | 'bottom' | 'left' | 'right';
75
77
  }
76
78
  /**
77
- * Event types provided by the bounds bundle.
79
+ * Event types provided by the bounds plugin.
78
80
  */
79
81
  export interface BoundsEventTypes {
80
82
  entityOutOfBounds: EntityOutOfBoundsEvent;
81
83
  }
82
84
  /**
83
- * Configuration options for the bounds bundle.
85
+ * Configuration options for the bounds plugin.
84
86
  */
85
- export interface BoundsBundleOptions {
86
- /** System group name (default: 'physics') */
87
- systemGroup?: string;
88
- /** Priority for bounds systems (default: 50) */
89
- priority?: number;
87
+ export interface BoundsPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {
90
88
  /** Resource key for bounds rectangle (default: 'bounds') */
91
89
  boundsResourceKey?: string;
92
90
  /** Whether to auto-remove entities when out of bounds (default: true) */
93
91
  autoRemove?: boolean;
94
- /** Execution phase (default: 'postUpdate') */
95
- phase?: SystemPhase;
96
92
  }
97
93
  /**
98
94
  * Create a bounds rectangle resource.
@@ -156,11 +152,10 @@ export declare function createClampToBounds(margin?: number): Pick<BoundsCompone
156
152
  * ```
157
153
  */
158
154
  export declare function createWrapAtBounds(padding?: number): Pick<BoundsComponentTypes, 'wrapAtBounds'>;
159
- type CombinedComponentTypes = BoundsComponentTypes & TransformComponentTypes;
160
155
  /**
161
- * Create a bounds bundle for ECSpresso.
156
+ * Create a bounds plugin for ECSpresso.
162
157
  *
163
- * This bundle provides:
158
+ * This plugin provides:
164
159
  * - Destroy out of bounds system - removes entities that exit bounds
165
160
  * - Clamp to bounds system - constrains entities within bounds
166
161
  * - Wrap at bounds system - wraps entities to opposite edge
@@ -174,8 +169,8 @@ type CombinedComponentTypes = BoundsComponentTypes & TransformComponentTypes;
174
169
  * const ecs = ECSpresso
175
170
  * .create<Components, Events, Resources>()
176
171
  * .withResource('bounds', createBounds(800, 600))
177
- * .withBundle(createTransformBundle())
178
- * .withBundle(createBoundsBundle())
172
+ * .withPlugin(createTransformPlugin())
173
+ * .withPlugin(createBoundsPlugin())
179
174
  * .build();
180
175
  *
181
176
  * // Entity that gets destroyed when leaving screen
@@ -185,5 +180,4 @@ type CombinedComponentTypes = BoundsComponentTypes & TransformComponentTypes;
185
180
  * });
186
181
  * ```
187
182
  */
188
- export declare function createBoundsBundle<ResourceTypes extends BoundsResourceTypes = BoundsResourceTypes>(options?: BoundsBundleOptions): Bundle<CombinedComponentTypes, BoundsEventTypes, ResourceTypes>;
189
- export {};
183
+ export declare function createBoundsPlugin<ResourceTypes extends BoundsResourceTypes = BoundsResourceTypes, G extends string = 'physics'>(options?: BoundsPluginOptions<G>): Plugin<WorldConfigFrom<BoundsComponentTypes, BoundsEventTypes, ResourceTypes>, TransformWorldConfig, 'bounds-destroy' | 'bounds-clamp' | 'bounds-wrap', G>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Camera / Viewport Plugin for ECSpresso
3
+ *
4
+ * Provides a camera entity with world/screen coordinate conversion, smooth follow,
5
+ * trauma-based shake, bounds clamping, and logical viewport dimensions.
6
+ *
7
+ * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying
8
+ * cameraState to a container/stage transform) is the consumer's responsibility.
9
+ *
10
+ * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.
11
+ * It reads the target entity's worldTransform for follow, but doesn't participate
12
+ * in the transform hierarchy itself.
13
+ */
14
+ import { type Plugin } from 'ecspresso';
15
+ import type { SystemPhase } from 'ecspresso';
16
+ import type ECSpresso from 'ecspresso';
17
+ import type { WorldConfigFrom } from '../type-utils';
18
+ import type { TransformWorldConfig } from './transform';
19
+ export interface Camera {
20
+ x: number;
21
+ y: number;
22
+ zoom: number;
23
+ rotation: number;
24
+ }
25
+ export interface CameraFollow {
26
+ target: number;
27
+ smoothing: number;
28
+ deadzoneX: number;
29
+ deadzoneY: number;
30
+ offsetX: number;
31
+ offsetY: number;
32
+ }
33
+ export interface CameraShake {
34
+ trauma: number;
35
+ traumaDecay: number;
36
+ maxOffsetX: number;
37
+ maxOffsetY: number;
38
+ maxRotation: number;
39
+ }
40
+ export interface CameraBounds {
41
+ minX: number;
42
+ minY: number;
43
+ maxX: number;
44
+ maxY: number;
45
+ }
46
+ export interface CameraComponentTypes {
47
+ camera: Camera;
48
+ cameraFollow: CameraFollow;
49
+ cameraShake: CameraShake;
50
+ cameraBounds: CameraBounds;
51
+ }
52
+ export interface CameraState {
53
+ x: number;
54
+ y: number;
55
+ zoom: number;
56
+ rotation: number;
57
+ shakeOffsetX: number;
58
+ shakeOffsetY: number;
59
+ shakeRotation: number;
60
+ viewportWidth: number;
61
+ viewportHeight: number;
62
+ }
63
+ export interface CameraResourceTypes {
64
+ cameraState: CameraState;
65
+ }
66
+ export interface CameraPluginOptions<G extends string = 'camera'> {
67
+ viewportWidth?: number;
68
+ viewportHeight?: number;
69
+ systemGroup?: G;
70
+ phase?: SystemPhase;
71
+ randomFn?: () => number;
72
+ }
73
+ export declare const DEFAULT_CAMERA: Readonly<Camera>;
74
+ export declare const DEFAULT_CAMERA_STATE: Readonly<CameraState>;
75
+ export declare function createCamera(x?: number, y?: number, zoom?: number, rotation?: number): Pick<CameraComponentTypes, 'camera'>;
76
+ export declare function createCameraFollow(target: number, options?: Partial<Omit<CameraFollow, 'target'>>): Pick<CameraComponentTypes, 'cameraFollow'>;
77
+ export declare function createCameraShake(options?: Partial<CameraShake>): Pick<CameraComponentTypes, 'cameraShake'>;
78
+ export declare function createCameraBounds(minX: number, minY: number, maxX: number, maxY: number): Pick<CameraComponentTypes, 'cameraBounds'>;
79
+ export declare function addTrauma<Cfg extends WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>>(ecs: ECSpresso<Cfg>, entityId: number, amount: number): void;
80
+ export declare function worldToScreen(worldX: number, worldY: number, state: CameraState): {
81
+ x: number;
82
+ y: number;
83
+ };
84
+ export declare function screenToWorld(screenX: number, screenY: number, state: CameraState): {
85
+ x: number;
86
+ y: number;
87
+ };
88
+ export declare function createCameraPlugin<G extends string = 'camera'>(options?: CameraPluginOptions<G>): Plugin<WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>, TransformWorldConfig, 'camera-follow' | 'camera-shake-update' | 'camera-bounds' | 'camera-state-sync', G>;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Collision Plugin for ECSpresso
3
+ *
4
+ * Provides layer-based collision detection with events.
5
+ * Uses worldTransform for position (world-space collision).
6
+ * Supports AABB and circle colliders.
7
+ */
8
+ import { type Plugin, type BasePluginOptions } from 'ecspresso';
9
+ import type { WorldConfigFrom } from '../type-utils';
10
+ import type { TransformWorldConfig } from './transform';
11
+ /**
12
+ * Axis-Aligned Bounding Box collider.
13
+ */
14
+ export interface AABBCollider {
15
+ /** Width of the bounding box */
16
+ width: number;
17
+ /** Height of the bounding box */
18
+ height: number;
19
+ /** X offset from entity position (default: 0) */
20
+ offsetX?: number;
21
+ /** Y offset from entity position (default: 0) */
22
+ offsetY?: number;
23
+ }
24
+ /**
25
+ * Circle collider.
26
+ */
27
+ export interface CircleCollider {
28
+ /** Radius of the circle */
29
+ radius: number;
30
+ /** X offset from entity position (default: 0) */
31
+ offsetX?: number;
32
+ /** Y offset from entity position (default: 0) */
33
+ offsetY?: number;
34
+ }
35
+ /**
36
+ * Collision layer configuration.
37
+ */
38
+ export interface CollisionLayer<L extends string = never> {
39
+ /** The layer this entity belongs to */
40
+ layer: L;
41
+ /** Layers this entity can collide with */
42
+ collidesWith: readonly L[];
43
+ }
44
+ /**
45
+ * Component types provided by the collision plugin.
46
+ * Included automatically via `.withPlugin(createCollisionPlugin())`.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const ecs = ECSpresso.create()
51
+ * .withPlugin(createCollisionPlugin())
52
+ * .withComponentTypes<{ sprite: Sprite; enemy: boolean }>()
53
+ * .build();
54
+ * ```
55
+ */
56
+ export interface CollisionComponentTypes<L extends string = never> {
57
+ aabbCollider: AABBCollider;
58
+ circleCollider: CircleCollider;
59
+ collisionLayer: CollisionLayer<L>;
60
+ }
61
+ /**
62
+ * Event fired when two entities collide.
63
+ */
64
+ export interface CollisionEvent<L extends string = never> {
65
+ /** First entity in the collision */
66
+ entityA: number;
67
+ /** Second entity in the collision */
68
+ entityB: number;
69
+ /** Layer of the first entity */
70
+ layerA: L;
71
+ /** Layer of the second entity */
72
+ layerB: L;
73
+ /** Contact normal pointing from entityA toward entityB */
74
+ normal: {
75
+ x: number;
76
+ y: number;
77
+ };
78
+ /** Penetration depth (positive = overlapping) */
79
+ depth: number;
80
+ }
81
+ /**
82
+ * Event types provided by the collision plugin.
83
+ */
84
+ export interface CollisionEventTypes<L extends string = never> {
85
+ collision: CollisionEvent<L>;
86
+ }
87
+ /**
88
+ * Configuration options for the collision plugin.
89
+ */
90
+ export interface CollisionPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {
91
+ /** Name of the collision event (default: 'collision') */
92
+ collisionEventName?: string;
93
+ }
94
+ /**
95
+ * Create an AABB collider component.
96
+ *
97
+ * @param width Width of the bounding box
98
+ * @param height Height of the bounding box
99
+ * @param offsetX X offset from entity position
100
+ * @param offsetY Y offset from entity position
101
+ * @returns Component object suitable for spreading into spawn()
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * ecs.spawn({
106
+ * ...createTransform(100, 200),
107
+ * ...createAABBCollider(50, 30),
108
+ * });
109
+ * ```
110
+ */
111
+ export declare function createAABBCollider(width: number, height: number, offsetX?: number, offsetY?: number): {
112
+ aabbCollider: AABBCollider;
113
+ };
114
+ /**
115
+ * Create a circle collider component.
116
+ *
117
+ * @param radius Radius of the circle
118
+ * @param offsetX X offset from entity position
119
+ * @param offsetY Y offset from entity position
120
+ * @returns Component object suitable for spreading into spawn()
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * ecs.spawn({
125
+ * ...createTransform(100, 200),
126
+ * ...createCircleCollider(25),
127
+ * });
128
+ * ```
129
+ */
130
+ export declare function createCircleCollider(radius: number, offsetX?: number, offsetY?: number): {
131
+ circleCollider: CircleCollider;
132
+ };
133
+ /**
134
+ * Create a collision layer component.
135
+ *
136
+ * @param layer The layer this entity belongs to
137
+ * @param collidesWith Layers this entity can collide with
138
+ * @returns Component object suitable for spreading into spawn()
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * ecs.spawn({
143
+ * ...createTransform(100, 200),
144
+ * ...createAABBCollider(50, 30),
145
+ * ...createCollisionLayer('player', ['enemy', 'obstacle']),
146
+ * });
147
+ * ```
148
+ */
149
+ export declare function createCollisionLayer<L extends string>(layer: L, collidesWith: readonly L[]): Pick<CollisionComponentTypes<L>, 'collisionLayer'>;
150
+ /**
151
+ * Layer factory result from defineCollisionLayers.
152
+ */
153
+ export type LayerFactories<T extends Record<string, readonly string[]>> = {
154
+ [K in keyof T]: () => Pick<CollisionComponentTypes<Extract<keyof T, string>>, 'collisionLayer'>;
155
+ };
156
+ /**
157
+ * Extract layer names from a `defineCollisionLayers` result for use with
158
+ * `createCollisionPairHandler`'s `L` type parameter.
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });
163
+ * type Layer = LayersOf<typeof layers>;
164
+ * const handler = createCollisionPairHandler<ECS, Layer>({
165
+ * 'player:enemy': (playerId, enemyId, ecs) => { ... },
166
+ * });
167
+ * ```
168
+ */
169
+ export type LayersOf<T> = Extract<keyof T, string>;
170
+ /**
171
+ * Define collision layer relationships and get factory functions.
172
+ *
173
+ * @param rules Object mapping layer names to arrays of layers they collide with
174
+ * @returns Object with factory functions for each layer
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * const layers = defineCollisionLayers({
179
+ * player: ['enemy', 'enemyProjectile'],
180
+ * playerProjectile: ['enemy'],
181
+ * enemy: ['playerProjectile'],
182
+ * enemyProjectile: ['player'],
183
+ * });
184
+ *
185
+ * // Usage
186
+ * ecs.spawn({
187
+ * ...createTransform(100, 200),
188
+ * ...createAABBCollider(50, 30),
189
+ * ...layers.player(),
190
+ * });
191
+ * ```
192
+ */
193
+ /**
194
+ * Validates that all `collidesWith` values reference actual layer keys.
195
+ * Catches typos at compile time.
196
+ */
197
+ type ValidateCollidesWith<T> = {
198
+ [K in keyof T]: T[K] extends readonly (infer V)[] ? [V] extends [Extract<keyof T, string>] ? T[K] : readonly Extract<keyof T, string>[] : never;
199
+ };
200
+ export declare function defineCollisionLayers<const T extends Record<string, readonly string[]>>(rules: T & ValidateCollidesWith<T>): LayerFactories<T>;
201
+ /**
202
+ * Callback for a collision pair handler.
203
+ *
204
+ * @param firstEntityId Entity belonging to the first layer in the pair key
205
+ * @param secondEntityId Entity belonging to the second layer in the pair key
206
+ * @param ecs The ECS world instance (passed through from the subscriber)
207
+ */
208
+ export type CollisionPairCallback<W = unknown> = (firstEntityId: number, secondEntityId: number, ecs: W) => void;
209
+ /**
210
+ * Create a collision pair handler that routes collision events to
211
+ * layer-pair-specific callbacks.
212
+ *
213
+ * Registering `"a:b"` automatically handles both `(layerA=a, layerB=b)` and
214
+ * `(layerA=b, layerB=a)`. Entity arguments are swapped to match the declared
215
+ * key order. If both `"a:b"` and `"b:a"` are explicitly registered, each gets
216
+ * its own handler with no implicit reverse.
217
+ *
218
+ * @typeParam W - The ECS world type (e.g. `ECSpresso<C, E, R>`). Defaults to `unknown`.
219
+ * @typeParam L - Union of valid layer names. Defaults to `string`.
220
+ * Provide specific layer names for compile-time key validation:
221
+ * `createCollisionPairHandler<ECS, keyof typeof layers>({...})`
222
+ *
223
+ * @param pairs Object mapping `"layerA:layerB"` keys to callbacks
224
+ * @returns A dispatch function to call with collision event data and ECS instance
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * // Basic usage:
229
+ * const handler = createCollisionPairHandler<ECS>({
230
+ * 'playerProjectile:enemy': (projectileId, enemyId, ecs) => {
231
+ * ecs.commands.removeEntity(projectileId);
232
+ * },
233
+ * });
234
+ *
235
+ * // With layer name validation:
236
+ * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });
237
+ * type Layer = LayersOf<typeof layers>;
238
+ * const handler = createCollisionPairHandler<ECS, Layer>({
239
+ * 'player:enemy': (playerId, enemyId, ecs) => { ... },
240
+ * });
241
+ *
242
+ * ecs.eventBus.subscribe('collision', (data) => handler({ data, ecs }));
243
+ * ```
244
+ */
245
+ export declare function createCollisionPairHandler<W = unknown, L extends string = string>(pairs: {
246
+ [K in `${L}:${L}`]?: CollisionPairCallback<W>;
247
+ }): (ctx: {
248
+ data: CollisionEvent<L>;
249
+ ecs: W;
250
+ }) => void;
251
+ /**
252
+ * Create a collision plugin for ECSpresso.
253
+ *
254
+ * This plugin provides:
255
+ * - Collision detection between entities with colliders
256
+ * - AABB-AABB, circle-circle, and AABB-circle collision
257
+ * - Layer-based filtering for collision pairs
258
+ * - Deduplication of A-B / B-A collisions
259
+ * - Automatic broadphase acceleration when spatialIndex resource is present
260
+ *
261
+ * Uses worldTransform for position (world-space collision detection).
262
+ * The `layers` parameter is required for type inference — at runtime the
263
+ * plugin does not consume it.
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });
268
+ * const ecs = ECSpresso
269
+ * .create()
270
+ * .withPlugin(createTransformPlugin())
271
+ * .withPlugin(createCollisionPlugin({ layers }))
272
+ * .build();
273
+ *
274
+ * // Entity with collision
275
+ * ecs.spawn({
276
+ * ...createTransform(100, 200),
277
+ * ...createAABBCollider(50, 30),
278
+ * ...layers.player(),
279
+ * });
280
+ * ```
281
+ */
282
+ export declare function createCollisionPlugin<L extends string, G extends string = 'physics'>(options: CollisionPluginOptions<G> & {
283
+ layers: LayerFactories<Record<L, readonly string[]>>;
284
+ }): Plugin<WorldConfigFrom<CollisionComponentTypes<L>, CollisionEventTypes<L>>, TransformWorldConfig, 'collision-detection', G>;
285
+ export {};
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Coroutine Plugin for ECSpresso
3
+ *
4
+ * ES6 generator-based coroutines for multi-step, frame-spanning scripted sequences.
5
+ * A `coroutine` component holds a live generator. A system ticks all generators each
6
+ * frame via `.next(dt)`. Helper generators (`waitSeconds`, `waitFrames`, `waitUntil`,
7
+ * `waitForEvent`, `parallel`, `race`) compose via `yield*`.
8
+ */
9
+ import { type Plugin, type BasePluginOptions } from 'ecspresso';
10
+ import type { EventsOfWorld, AnyECSpresso } from 'ecspresso';
11
+ import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
12
+ /**
13
+ * Yields void, returns void, receives deltaTime (number) via `.next(dt)`.
14
+ * First `.next(dt)` initializes the generator (runs to first yield, dt discarded per JS spec).
15
+ * Subsequent `.next(dt)` resume from yield with dt as the yield expression value.
16
+ */
17
+ export type CoroutineGenerator = Generator<void, void, number>;
18
+ export interface CoroutineEventData {
19
+ entityId: number;
20
+ }
21
+ export interface CoroutineState {
22
+ generator: CoroutineGenerator;
23
+ onComplete?: (data: CoroutineEventData) => void;
24
+ }
25
+ export interface CoroutineComponentTypes {
26
+ coroutine: CoroutineState;
27
+ }
28
+ export interface CoroutinePluginOptions<G extends string = 'coroutines'> extends BasePluginOptions<G> {
29
+ }
30
+ export interface CoroutineOptions {
31
+ onComplete?: (data: CoroutineEventData) => void;
32
+ }
33
+ /**
34
+ * Create a coroutine component for spawning or adding to an entity.
35
+ *
36
+ * @param generator - The generator function to drive
37
+ * @param options - Optional configuration (onComplete event)
38
+ * @returns Component object suitable for spreading into spawn()
39
+ */
40
+ export declare function createCoroutine(generator: CoroutineGenerator, options?: CoroutineOptions): Pick<CoroutineComponentTypes, 'coroutine'>;
41
+ /**
42
+ * Wait for a specified number of seconds. Accumulates dt until elapsed >= seconds.
43
+ * If seconds <= 0, returns immediately.
44
+ */
45
+ export declare function waitSeconds(seconds: number): CoroutineGenerator;
46
+ /**
47
+ * Wait for a specified number of frames. Yields `frames` times.
48
+ * If frames <= 0, returns immediately.
49
+ */
50
+ export declare function waitFrames(frames: number): CoroutineGenerator;
51
+ /**
52
+ * Wait until a predicate returns true. Yields each frame until predicate is satisfied.
53
+ * User closes over ecs if needed for state checks.
54
+ */
55
+ export declare function waitUntil(predicate: () => boolean): CoroutineGenerator;
56
+ /**
57
+ * Run multiple coroutines in parallel. Completes when all finish.
58
+ * Initializes all sub-generators, ticks all each frame.
59
+ * Empty array = immediate return.
60
+ */
61
+ export declare function parallel(...coroutines: CoroutineGenerator[]): CoroutineGenerator;
62
+ /**
63
+ * Run multiple coroutines, completing when the first one finishes.
64
+ * Calls `.return()` on remaining generators (triggers finally blocks).
65
+ * Empty array = immediate return.
66
+ */
67
+ export declare function race(...coroutines: CoroutineGenerator[]): CoroutineGenerator;
68
+ /**
69
+ * Wait until a matching event fires on the event bus.
70
+ * Subscribes via eventBus.subscribe, yields until event received, unsubscribes in finally block.
71
+ *
72
+ * @param eventBus - Object with subscribe method (typically ecs.eventBus)
73
+ * @param eventType - Event type name to listen for
74
+ * @param filter - Optional predicate to filter events
75
+ */
76
+ export declare function waitForEvent<ET extends Record<string, any>, E extends keyof ET & string>(eventBus: {
77
+ subscribe(type: E, cb: (data: ET[E]) => void): () => void;
78
+ }, eventType: E, filter?: (data: ET[E]) => boolean): CoroutineGenerator;
79
+ /**
80
+ * Structural interface for ECS methods used by cancelCoroutine.
81
+ */
82
+ export interface CoroutineWorld {
83
+ getComponent(entityId: number, componentName: string): unknown | undefined;
84
+ commands: {
85
+ removeComponent(entityId: number, componentName: string): void;
86
+ };
87
+ }
88
+ /**
89
+ * Cancel a running coroutine on an entity. Calls generator.return() (triggers finally blocks)
90
+ * and queues component removal.
91
+ *
92
+ * @returns true if the entity had a coroutine that was cancelled, false otherwise
93
+ */
94
+ export declare function cancelCoroutine(ecs: CoroutineWorld, entityId: number): boolean;
95
+ /**
96
+ * Type-safe coroutine helpers that validate event names against a world's event types.
97
+ * Use `createCoroutineHelpers<typeof ecs>()` to get compile-time validation.
98
+ */
99
+ export interface CoroutineHelpers<W extends AnyECSpresso> {
100
+ createCoroutine: (generator: CoroutineGenerator, options?: {
101
+ onComplete?: (data: CoroutineEventData) => void;
102
+ }) => Pick<CoroutineComponentTypes, 'coroutine'>;
103
+ waitForEvent: <E extends keyof EventsOfWorld<W> & string>(eventBus: {
104
+ subscribe(type: E, cb: (data: EventsOfWorld<W>[E]) => void): () => void;
105
+ }, eventType: E, filter?: (data: EventsOfWorld<W>[E]) => boolean) => CoroutineGenerator;
106
+ }
107
+ /**
108
+ * Create typed coroutine helpers that validate event names at compile time.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const { createCoroutine, waitForEvent } = createCoroutineHelpers<typeof ecs>();
113
+ * ecs.spawn({ ...createCoroutine(myGen(), { onComplete: (data) => console.log(data.entityId) }) });
114
+ * ```
115
+ */
116
+ export declare function createCoroutineHelpers<W extends AnyECSpresso>(_world?: W): CoroutineHelpers<W>;
117
+ /**
118
+ * Create a coroutine plugin for ECSpresso.
119
+ *
120
+ * This plugin provides:
121
+ * - Coroutine system that ticks all generator-based coroutines each frame
122
+ * - Automatic cleanup via dispose callback (triggers generator finally blocks)
123
+ * - `onComplete` callback invocation
124
+ * - Component removal on completion
125
+ */
126
+ export declare function createCoroutinePlugin<G extends string = 'coroutines'>(options?: CoroutinePluginOptions<G>): Plugin<WorldConfigFrom<CoroutineComponentTypes>, EmptyConfig, 'coroutine-update', G>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Diagnostics Plugin for ECSpresso
3
+ *
4
+ * Runtime diagnostics: FPS, entity count, per-system timing, per-phase timing,
5
+ * and an optional DOM overlay for visual debugging.
6
+ */
7
+ import { type Plugin } from 'ecspresso';
8
+ import type { SystemPhase } from 'ecspresso';
9
+ import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
10
+ export interface DiagnosticsData {
11
+ fps: number;
12
+ entityCount: number;
13
+ systemTimings: ReadonlyMap<string, number>;
14
+ phaseTimings: Readonly<Record<SystemPhase, number>>;
15
+ averageFrameTime: number;
16
+ }
17
+ export interface DiagnosticsResourceTypes {
18
+ diagnostics: DiagnosticsData;
19
+ }
20
+ export interface DiagnosticsPluginOptions<G extends string = 'diagnostics'> {
21
+ /** System group name (default: 'diagnostics') */
22
+ systemGroup?: G;
23
+ /** Enable timing collection on initialize (default: true) */
24
+ enableTimingOnInit?: boolean;
25
+ /** Number of frames to sample for FPS average (default: 60) */
26
+ fpsSampleCount?: number;
27
+ }
28
+ export interface DiagnosticsOverlayOptions {
29
+ /** Corner position (default: 'top-left') */
30
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
31
+ /** Milliseconds between DOM updates (default: 200) */
32
+ updateInterval?: number;
33
+ /** Show per-system timings (default: true) */
34
+ showSystemTimings?: boolean;
35
+ /** Maximum systems to show in overlay (default: 10) */
36
+ maxSystemsShown?: number;
37
+ }
38
+ export declare function createDiagnosticsPlugin<G extends string = 'diagnostics'>(options?: DiagnosticsPluginOptions<G>): Plugin<WorldConfigFrom<{}, {}, DiagnosticsResourceTypes>, EmptyConfig, 'diagnostics-collect', G>;
39
+ /**
40
+ * Create a DOM overlay that displays diagnostics data.
41
+ * Returns a cleanup function that removes the element and clears the interval.
42
+ *
43
+ * @param ecs An ECSpresso instance with the diagnostics resource
44
+ * @param options Overlay configuration
45
+ * @returns Cleanup function
46
+ */
47
+ export declare function createDiagnosticsOverlay<R extends DiagnosticsResourceTypes>(ecs: {
48
+ getResource<K extends keyof R>(key: K): R[K];
49
+ }, options?: DiagnosticsOverlayOptions): () => void;