ecspresso 0.12.9 → 0.13.1

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 (101) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +5 -5
  3. package/dist/plugin.d.ts +89 -22
  4. package/dist/plugins/ai/detection.d.ts +118 -0
  5. package/dist/plugins/ai/detection.js +4 -0
  6. package/dist/plugins/ai/detection.js.map +10 -0
  7. package/dist/plugins/{audio.d.ts → audio/audio.d.ts} +2 -3
  8. package/dist/plugins/{audio.js → audio/audio.js} +2 -2
  9. package/dist/plugins/audio/audio.js.map +10 -0
  10. package/dist/plugins/combat/health.d.ts +98 -0
  11. package/dist/plugins/combat/health.js +4 -0
  12. package/dist/plugins/combat/health.js.map +10 -0
  13. package/dist/plugins/combat/projectile.d.ts +115 -0
  14. package/dist/plugins/combat/projectile.js +4 -0
  15. package/dist/plugins/combat/projectile.js.map +10 -0
  16. package/dist/plugins/{diagnostics.d.ts → debug/diagnostics.d.ts} +1 -3
  17. package/dist/plugins/debug/diagnostics.js +5 -0
  18. package/dist/plugins/debug/diagnostics.js.map +10 -0
  19. package/dist/plugins/{input.d.ts → input/input.d.ts} +11 -3
  20. package/dist/plugins/input/input.js +4 -0
  21. package/dist/plugins/input/input.js.map +10 -0
  22. package/dist/plugins/input/selection.d.ts +114 -0
  23. package/dist/plugins/input/selection.js +4 -0
  24. package/dist/plugins/input/selection.js.map +11 -0
  25. package/dist/plugins/isometric/depth-sort.d.ts +44 -0
  26. package/dist/plugins/isometric/depth-sort.js +4 -0
  27. package/dist/plugins/isometric/depth-sort.js.map +10 -0
  28. package/dist/plugins/isometric/projection.d.ts +83 -0
  29. package/dist/plugins/isometric/projection.js +4 -0
  30. package/dist/plugins/isometric/projection.js.map +10 -0
  31. package/dist/plugins/{collision.d.ts → physics/collision.d.ts} +10 -9
  32. package/dist/plugins/physics/collision.js +4 -0
  33. package/dist/plugins/physics/collision.js.map +11 -0
  34. package/dist/plugins/{physics2D.d.ts → physics/physics2D.d.ts} +9 -6
  35. package/dist/plugins/physics/physics2D.js +4 -0
  36. package/dist/plugins/physics/physics2D.js.map +11 -0
  37. package/dist/plugins/physics/steering.d.ts +102 -0
  38. package/dist/plugins/physics/steering.js +4 -0
  39. package/dist/plugins/physics/steering.js.map +10 -0
  40. package/dist/plugins/{particles.d.ts → rendering/particles.d.ts} +4 -4
  41. package/dist/plugins/{particles.js → rendering/particles.js} +2 -2
  42. package/dist/plugins/rendering/particles.js.map +10 -0
  43. package/dist/plugins/{renderers → rendering}/renderer2D.d.ts +45 -14
  44. package/dist/plugins/rendering/renderer2D.js +4 -0
  45. package/dist/plugins/rendering/renderer2D.js.map +10 -0
  46. package/dist/plugins/{sprite-animation.d.ts → rendering/sprite-animation.d.ts} +2 -3
  47. package/dist/plugins/{sprite-animation.js → rendering/sprite-animation.js} +2 -2
  48. package/dist/plugins/rendering/sprite-animation.js.map +10 -0
  49. package/dist/plugins/{coroutine.d.ts → scripting/coroutine.d.ts} +2 -3
  50. package/dist/plugins/{coroutine.js → scripting/coroutine.js} +2 -2
  51. package/dist/plugins/scripting/coroutine.js.map +10 -0
  52. package/dist/plugins/{state-machine.d.ts → scripting/state-machine.d.ts} +2 -3
  53. package/dist/plugins/{state-machine.js → scripting/state-machine.js} +2 -2
  54. package/dist/plugins/scripting/state-machine.js.map +10 -0
  55. package/dist/plugins/{timers.d.ts → scripting/timers.d.ts} +2 -3
  56. package/dist/plugins/scripting/timers.js +4 -0
  57. package/dist/plugins/scripting/timers.js.map +10 -0
  58. package/dist/plugins/{tween.d.ts → scripting/tween.d.ts} +3 -4
  59. package/dist/plugins/{tween.js → scripting/tween.js} +2 -2
  60. package/dist/plugins/scripting/tween.js.map +11 -0
  61. package/dist/plugins/{bounds.d.ts → spatial/bounds.d.ts} +2 -3
  62. package/dist/plugins/spatial/bounds.js +4 -0
  63. package/dist/plugins/spatial/bounds.js.map +10 -0
  64. package/dist/plugins/{camera.d.ts → spatial/camera.d.ts} +43 -13
  65. package/dist/plugins/spatial/camera.js +4 -0
  66. package/dist/plugins/spatial/camera.js.map +10 -0
  67. package/dist/plugins/{spatial-index.d.ts → spatial/spatial-index.d.ts} +3 -6
  68. package/dist/plugins/spatial/spatial-index.js +4 -0
  69. package/dist/plugins/spatial/spatial-index.js.map +11 -0
  70. package/dist/plugins/{transform.d.ts → spatial/transform.d.ts} +3 -3
  71. package/dist/plugins/spatial/transform.js +4 -0
  72. package/dist/plugins/spatial/transform.js.map +10 -0
  73. package/dist/utils/narrowphase.d.ts +60 -19
  74. package/dist/utils/spatial-hash.d.ts +11 -1
  75. package/package.json +80 -49
  76. package/dist/plugins/audio.js.map +0 -10
  77. package/dist/plugins/bounds.js +0 -4
  78. package/dist/plugins/bounds.js.map +0 -10
  79. package/dist/plugins/camera.js +0 -4
  80. package/dist/plugins/camera.js.map +0 -10
  81. package/dist/plugins/collision.js +0 -4
  82. package/dist/plugins/collision.js.map +0 -11
  83. package/dist/plugins/coroutine.js.map +0 -10
  84. package/dist/plugins/diagnostics.js +0 -5
  85. package/dist/plugins/diagnostics.js.map +0 -10
  86. package/dist/plugins/input.js +0 -4
  87. package/dist/plugins/input.js.map +0 -10
  88. package/dist/plugins/particles.js.map +0 -10
  89. package/dist/plugins/physics2D.js +0 -4
  90. package/dist/plugins/physics2D.js.map +0 -11
  91. package/dist/plugins/renderers/renderer2D.js +0 -4
  92. package/dist/plugins/renderers/renderer2D.js.map +0 -10
  93. package/dist/plugins/spatial-index.js +0 -4
  94. package/dist/plugins/spatial-index.js.map +0 -11
  95. package/dist/plugins/sprite-animation.js.map +0 -10
  96. package/dist/plugins/state-machine.js.map +0 -10
  97. package/dist/plugins/timers.js +0 -4
  98. package/dist/plugins/timers.js.map +0 -10
  99. package/dist/plugins/transform.js +0 -4
  100. package/dist/plugins/transform.js.map +0 -10
  101. package/dist/plugins/tween.js.map +0 -11
package/dist/plugin.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type ECSpresso from './ecspresso';
2
2
  import type { SystemPhase } from './types';
3
- import type { WorldConfig, EmptyConfig, MergeConfigs, AnyECSpresso, ConfigOf } from './type-utils';
3
+ import type { WorldConfig, EmptyConfig, MergeConfigs, WithComponents, WithEvents, WithResources, WithAssets, WithScreens } from './type-utils';
4
4
  /**
5
5
  * Plugin interface for ECSpresso. A plugin is a plain object with an `install`
6
6
  * function that configures a world directly, plus phantom properties for
@@ -32,30 +32,97 @@ export interface BasePluginOptions<G extends string = string> {
32
32
  phase?: SystemPhase;
33
33
  }
34
34
  /**
35
- * Factory function to create a type-safe Plugin with phantom type parameters.
36
- * The type assertion adds phantom types without runtime cost.
35
+ * Fluent builder for defining plugins. Mirrors `ECSpressoBuilder`'s
36
+ * type-accumulator pattern: each `.withXxx<T>()` call threads `T` into the
37
+ * appropriate WorldConfig slot at the type level, with no runtime cost.
38
+ *
39
+ * Terminal call is `.install(fn)` which returns the finalized `Plugin<...>`.
37
40
  *
38
41
  * @example
39
42
  * ```typescript
40
- * // Option 1: Explicit config type param
41
- * const myPlugin = definePlugin<WorldConfigFrom<MyComponents, MyEvents, MyResources>>({
42
- * id: 'my-plugin',
43
- * install(world) { ... },
44
- * });
43
+ * const myPlugin = definePlugin('my-plugin')
44
+ * .withComponentTypes<MyComponents>()
45
+ * .withEventTypes<MyEvents>()
46
+ * .withResourceTypes<MyResources>()
47
+ * .install((world) => {
48
+ * world.addSystem('foo').setProcess(() => {});
49
+ * });
50
+ * ```
51
+ */
52
+ export declare class PluginBuilder<Cfg extends WorldConfig = EmptyConfig, Requires extends WorldConfig = EmptyConfig, Labels extends string = never, Groups extends string = never, AssetGroupNames extends string = never, ReactiveQueryNames extends string = never> {
53
+ private readonly _id;
54
+ constructor(_id: string);
55
+ /**
56
+ * Declare component types this plugin provides.
57
+ * Pure type-level operation with no runtime cost.
58
+ */
59
+ withComponentTypes<T extends Record<string, any>>(): PluginBuilder<WithComponents<Cfg, T>, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
60
+ /**
61
+ * Declare event types this plugin provides.
62
+ * Pure type-level operation with no runtime cost.
63
+ */
64
+ withEventTypes<T extends Record<string, any>>(): PluginBuilder<WithEvents<Cfg, T>, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
65
+ /**
66
+ * Declare resource types this plugin provides.
67
+ * Pure type-level operation with no runtime cost.
68
+ */
69
+ withResourceTypes<T extends Record<string, any>>(): PluginBuilder<WithResources<Cfg, T>, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
70
+ /**
71
+ * Declare asset types this plugin provides.
72
+ * Pure type-level operation with no runtime cost.
73
+ */
74
+ withAssetTypes<T extends Record<string, unknown>>(): PluginBuilder<WithAssets<Cfg, T>, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
75
+ /**
76
+ * Declare screen types this plugin provides.
77
+ * Pure type-level operation with no runtime cost.
78
+ */
79
+ withScreenTypes<T extends Record<string, any>>(): PluginBuilder<WithScreens<Cfg, T>, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
80
+ /**
81
+ * Declare system labels this plugin registers.
82
+ * Pure type-level operation with no runtime cost.
83
+ */
84
+ withLabels<L extends string>(): PluginBuilder<Cfg, Requires, Labels | L, Groups, AssetGroupNames, ReactiveQueryNames>;
85
+ /**
86
+ * Declare system groups this plugin uses.
87
+ * Pure type-level operation with no runtime cost.
88
+ */
89
+ withGroups<G extends string>(): PluginBuilder<Cfg, Requires, Labels, Groups | G, AssetGroupNames, ReactiveQueryNames>;
90
+ /**
91
+ * Declare asset group names this plugin uses.
92
+ * Pure type-level operation with no runtime cost.
93
+ */
94
+ withAssetGroupNames<N extends string>(): PluginBuilder<Cfg, Requires, Labels, Groups, AssetGroupNames | N, ReactiveQueryNames>;
95
+ /**
96
+ * Declare reactive query names this plugin registers.
97
+ * Pure type-level operation with no runtime cost.
98
+ */
99
+ withReactiveQueryNames<N extends string>(): PluginBuilder<Cfg, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames | N>;
100
+ /**
101
+ * Declare dependencies this plugin requires from other plugins.
102
+ * Accepts a pre-built `WorldConfig` type (typically a named alias like
103
+ * `TransformWorldConfig`). The install callback will see these types
104
+ * merged into its world parameter.
105
+ * Pure type-level operation with no runtime cost.
106
+ */
107
+ requires<R extends WorldConfig>(): PluginBuilder<Cfg, R, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
108
+ /**
109
+ * Terminal method. Provide the install function and receive the finalized
110
+ * `Plugin<...>` object. The install function receives a world typed as
111
+ * `ECSpresso<MergeConfigs<Cfg, Requires>>` — meaning it can use both the
112
+ * types this plugin provides and the types it declared via `.requires<>()`.
113
+ */
114
+ install(install: (world: ECSpresso<MergeConfigs<Cfg, Requires>>) => void): Plugin<Cfg, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
115
+ }
116
+ /**
117
+ * Entry point for the fluent plugin builder. Pass the plugin id and chain
118
+ * type-accumulator methods, terminating with `.install(fn)`.
45
119
  *
46
- * // Option 2: Single world type param (extracts config automatically)
47
- * type MyWorld = typeof ecs;
48
- * const myPlugin = definePlugin<MyWorld>({
49
- * id: 'my-plugin',
50
- * install(world) { ... },
51
- * });
120
+ * @example
121
+ * ```typescript
122
+ * const myPlugin = definePlugin('my-plugin')
123
+ * .withComponentTypes<MyComponents>()
124
+ * .withResourceTypes<MyResources>()
125
+ * .install((world) => { ... });
52
126
  * ```
53
127
  */
54
- export declare function definePlugin<W extends AnyECSpresso, Requires extends WorldConfig = EmptyConfig, Labels extends string = never, Groups extends string = never, AssetGroupNames extends string = never, ReactiveQueryNames extends string = never>(config: {
55
- id: string;
56
- install: (world: W) => void;
57
- }): Plugin<ConfigOf<W>, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
58
- export declare function definePlugin<Cfg extends WorldConfig = EmptyConfig, Requires extends WorldConfig = EmptyConfig, Labels extends string = never, Groups extends string = never, AssetGroupNames extends string = never, ReactiveQueryNames extends string = never>(config: {
59
- id: string;
60
- install: (world: ECSpresso<MergeConfigs<Cfg, Requires>>) => void;
61
- }): Plugin<Cfg, Requires, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
128
+ export declare function definePlugin(id: string): PluginBuilder;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Detection Plugin for ECSpresso
3
+ *
4
+ * Provides automatic proximity detection for entities. Entities with a
5
+ * `detector` component get their `detectedEntities` populated each frame
6
+ * with nearby entities that match the configured collision layer filter,
7
+ * sorted by distance ascending (nearest first).
8
+ *
9
+ * Uses the spatial-index plugin for efficient range queries.
10
+ */
11
+ import { type BasePluginOptions } from 'ecspresso';
12
+ import type { WorldConfigFrom } from 'ecspresso';
13
+ import type { TransformWorldConfig } from '../spatial/transform';
14
+ import type { SpatialIndexResourceTypes } from '../spatial/spatial-index';
15
+ import type { CollisionComponentTypes } from '../physics/collision';
16
+ /**
17
+ * Configures proximity detection for an entity.
18
+ */
19
+ export interface Detector {
20
+ /** Detection radius in world units */
21
+ range: number;
22
+ /** Only detect entities on these collision layers */
23
+ layerFilter: readonly string[];
24
+ /** Maximum number of results to track (default: 32) */
25
+ maxResults: number;
26
+ }
27
+ /**
28
+ * A detected entity with its squared distance from the detector.
29
+ */
30
+ export interface DetectedEntry {
31
+ entityId: number;
32
+ distanceSq: number;
33
+ }
34
+ /**
35
+ * Auto-populated list of detected entities, sorted by distance ascending.
36
+ */
37
+ export interface DetectedEntities {
38
+ entities: readonly DetectedEntry[];
39
+ }
40
+ /**
41
+ * Component types provided by the detection plugin.
42
+ */
43
+ export interface DetectionComponentTypes {
44
+ detector: Detector;
45
+ detectedEntities: DetectedEntities;
46
+ }
47
+ /**
48
+ * Event fired when a new entity enters detection range.
49
+ */
50
+ export interface DetectionGainedEvent {
51
+ /** The entity doing the detecting */
52
+ entityId: number;
53
+ /** The entity that was detected */
54
+ detectedId: number;
55
+ }
56
+ /**
57
+ * Event fired when an entity leaves detection range.
58
+ */
59
+ export interface DetectionLostEvent {
60
+ /** The entity doing the detecting */
61
+ entityId: number;
62
+ /** The entity that was lost */
63
+ lostId: number;
64
+ }
65
+ /**
66
+ * Event types provided by the detection plugin.
67
+ */
68
+ export interface DetectionEventTypes {
69
+ detectionGained: DetectionGainedEvent;
70
+ detectionLost: DetectionLostEvent;
71
+ }
72
+ /**
73
+ * WorldConfig representing the detection plugin's provided types.
74
+ */
75
+ export type DetectionWorldConfig = WorldConfigFrom<DetectionComponentTypes, DetectionEventTypes>;
76
+ export interface DetectionPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {
77
+ }
78
+ /**
79
+ * Create a detector component.
80
+ *
81
+ * @param range Detection radius in world units
82
+ * @param layerFilter Only detect entities on these collision layers
83
+ * @param maxResults Maximum results to track (default: 32)
84
+ * @returns Component object suitable for spreading into spawn()
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * ecs.spawn({
89
+ * ...createDetector(300, ['enemy']),
90
+ * ...createLocalTransform(400, 400),
91
+ * });
92
+ * ```
93
+ */
94
+ export declare function createDetector(range: number, layerFilter: readonly string[], maxResults?: number): Pick<DetectionComponentTypes, 'detector'>;
95
+ /**
96
+ * Create a detection plugin for ECSpresso.
97
+ *
98
+ * Populates `detectedEntities` each frame with nearby entities matching
99
+ * the detector's layer filter, sorted by distance (nearest first).
100
+ * Publishes `detectionGained`/`detectionLost` events on transitions.
101
+ *
102
+ * Requires the spatial-index and transform plugins to be installed.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const ecs = ECSpresso.create()
107
+ * .withPlugin(createTransformPlugin())
108
+ * .withPlugin(createCollisionPlugin({ layers }))
109
+ * .withPlugin(createSpatialIndexPlugin())
110
+ * .withPlugin(createDetectionPlugin())
111
+ * .build();
112
+ *
113
+ * // Read nearest detected entity:
114
+ * const detected = ecs.getComponent(turretId, 'detectedEntities');
115
+ * const nearest = detected?.entities[0];
116
+ * ```
117
+ */
118
+ export declare function createDetectionPlugin<G extends string = 'ai'>(options?: DetectionPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, DetectionComponentTypes>, DetectionEventTypes>, TransformWorldConfig & WorldConfigFrom<Pick<CollisionComponentTypes<string>, "collisionLayer">> & WorldConfigFrom<{}, {}, SpatialIndexResourceTypes>, "detection-scan", G, never, never>;
@@ -0,0 +1,4 @@
1
+ var C=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(E,J)=>(typeof require<"u"?require:E)[J]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as b}from"ecspresso";function F(k,E,J=32){return{detector:{range:k,layerFilter:E,maxResults:J}}}function q(k,E){return k.distanceSq-E.distanceSq}function g(k){let{systemGroup:E="ai",priority:J=500,phase:G="update"}=k??{},V=new Map,K=new Set,W=new Set,Z=new WeakMap;return b("detection").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((_)=>{_.registerDispose("detector",({entityId:X})=>{V.delete(X)}),_.addSystem("detection-scan").setPriority(J).inPhase(G).inGroup(E).addQuery("detectors",{with:["detector","worldTransform"]}).setProcess(({queries:X,ecs:z})=>{let P=z.getResource("spatialIndex");for(let A of X.detectors){let{detector:H,worldTransform:O}=A.components;W.clear(),P.queryRadiusInto(O.x,O.y,H.range,W);let L=[],Q=Z.get(H.layerFilter);if(!Q)Q=new Set(H.layerFilter),Z.set(H.layerFilter,Q);for(let j of W){if(j===A.id)continue;if(!z.getEntity(j))continue;let N=z.getComponent(j,"collisionLayer");if(!N)continue;if(!Q.has(N.layer))continue;let Y=z.getComponent(j,"worldTransform");if(!Y)continue;let B=Y.x-O.x,D=Y.y-O.y;L.push({entityId:j,distanceSq:B*B+D*D})}L.sort(q);let U=L.length>H.maxResults?L.slice(0,H.maxResults):L,$=z.getComponent(A.id,"detectedEntities");if($)$.entities=U,z.markChanged(A.id,"detectedEntities");else z.addComponent(A.id,"detectedEntities",{entities:U});let M=V.get(A.id);K.clear();for(let j of U)K.add(j.entityId);if(M){for(let j of K)if(!M.has(j))z.eventBus.publish("detectionGained",{entityId:A.id,detectedId:j});for(let j of M)if(!K.has(j))z.eventBus.publish("detectionLost",{entityId:A.id,lostId:j});M.clear();for(let j of K)M.add(j)}else{let j=new Set;for(let N of U)j.add(N.entityId),z.eventBus.publish("detectionGained",{entityId:A.id,detectedId:N.entityId});V.set(A.id,j)}}})})}export{F as createDetector,g as createDetectionPlugin};
2
+
3
+ //# debugId=0908791952E86B8564756E2164756E21
4
+ //# sourceMappingURL=detection.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/ai/detection.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Detection Plugin for ECSpresso\n *\n * Provides automatic proximity detection for entities. Entities with a\n * `detector` component get their `detectedEntities` populated each frame\n * with nearby entities that match the configured collision layer filter,\n * sorted by distance ascending (nearest first).\n *\n * Uses the spatial-index plugin for efficient range queries.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SpatialIndexResourceTypes } from '../spatial/spatial-index';\nimport type { CollisionComponentTypes } from '../physics/collision';\n\n// ==================== Component Types ====================\n\n/**\n * Configures proximity detection for an entity.\n */\nexport interface Detector {\n\t/** Detection radius in world units */\n\trange: number;\n\t/** Only detect entities on these collision layers */\n\tlayerFilter: readonly string[];\n\t/** Maximum number of results to track (default: 32) */\n\tmaxResults: number;\n}\n\n/**\n * A detected entity with its squared distance from the detector.\n */\nexport interface DetectedEntry {\n\tentityId: number;\n\tdistanceSq: number;\n}\n\n/**\n * Auto-populated list of detected entities, sorted by distance ascending.\n */\nexport interface DetectedEntities {\n\tentities: readonly DetectedEntry[];\n}\n\n/**\n * Component types provided by the detection plugin.\n */\nexport interface DetectionComponentTypes {\n\tdetector: Detector;\n\tdetectedEntities: DetectedEntities;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when a new entity enters detection range.\n */\nexport interface DetectionGainedEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was detected */\n\tdetectedId: number;\n}\n\n/**\n * Event fired when an entity leaves detection range.\n */\nexport interface DetectionLostEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was lost */\n\tlostId: number;\n}\n\n/**\n * Event types provided by the detection plugin.\n */\nexport interface DetectionEventTypes {\n\tdetectionGained: DetectionGainedEvent;\n\tdetectionLost: DetectionLostEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the detection plugin's provided types.\n */\nexport type DetectionWorldConfig = WorldConfigFrom<DetectionComponentTypes, DetectionEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface DetectionPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a detector component.\n *\n * @param range Detection radius in world units\n * @param layerFilter Only detect entities on these collision layers\n * @param maxResults Maximum results to track (default: 32)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createDetector(300, ['enemy']),\n * ...createLocalTransform(400, 400),\n * });\n * ```\n */\nexport function createDetector(\n\trange: number,\n\tlayerFilter: readonly string[],\n\tmaxResults = 32,\n): Pick<DetectionComponentTypes, 'detector'> {\n\treturn { detector: { range, layerFilter, maxResults } };\n}\n\n// ==================== Plugin Factory ====================\n\nfunction compareByDistance(a: DetectedEntry, b: DetectedEntry): number {\n\treturn a.distanceSq - b.distanceSq;\n}\n\n/**\n * Create a detection plugin for ECSpresso.\n *\n * Populates `detectedEntities` each frame with nearby entities matching\n * the detector's layer filter, sorted by distance (nearest first).\n * Publishes `detectionGained`/`detectionLost` events on transitions.\n *\n * Requires the spatial-index and transform plugins to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin())\n * .withPlugin(createDetectionPlugin())\n * .build();\n *\n * // Read nearest detected entity:\n * const detected = ecs.getComponent(turretId, 'detectedEntities');\n * const nearest = detected?.entities[0];\n * ```\n */\nexport function createDetectionPlugin<G extends string = 'ai'>(\n\toptions?: DetectionPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 500,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Per-detector tracking of previous frame's detected set for event diffing\n\tconst previousSets = new Map<number, Set<number>>();\n\tconst currentSet = new Set<number>();\n\t// Reusable set for spatial index queries (avoids allocation per frame)\n\tconst candidateSet = new Set<number>();\n\t// Cache: layerFilter array → Set for O(1) lookups\n\tconst layerFilterCache = new WeakMap<readonly string[], Set<string>>();\n\n\treturn definePlugin('detection')\n\t\t.withComponentTypes<DetectionComponentTypes>()\n\t\t.withEventTypes<DetectionEventTypes>()\n\t\t.withLabels<'detection-scan'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<Pick<CollisionComponentTypes<string>, 'collisionLayer'>> &\n\t\t\tWorldConfigFrom<{}, {}, SpatialIndexResourceTypes>\n\t\t>()\n\t\t.install((world) => {\n\t\t\tworld.registerDispose('detector', ({ entityId }) => {\n\t\t\t\tpreviousSets.delete(entityId);\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('detection-scan')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('detectors', {\n\t\t\t\t\twith: ['detector', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst spatialIndex = ecs.getResource('spatialIndex');\n\n\t\t\t\t\tfor (const entity of queries.detectors) {\n\t\t\t\t\t\tconst { detector, worldTransform } = entity.components;\n\n\t\t\t\t\t\tcandidateSet.clear();\n\t\t\t\t\t\tspatialIndex.queryRadiusInto(worldTransform.x, worldTransform.y, detector.range, candidateSet);\n\n\t\t\t\t\t\t// Build sorted results, filtering by layer and excluding self\n\t\t\t\t\t\tconst entries: DetectedEntry[] = [];\n\n\t\t\t\t\t\tlet filterSet = layerFilterCache.get(detector.layerFilter);\n\t\t\t\t\t\tif (!filterSet) {\n\t\t\t\t\t\t\tfilterSet = new Set(detector.layerFilter);\n\t\t\t\t\t\t\tlayerFilterCache.set(detector.layerFilter, filterSet);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const candidateId of candidateSet) {\n\t\t\t\t\t\t\tif (candidateId === entity.id) continue;\n\t\t\t\t\t\t\tif (!ecs.getEntity(candidateId)) continue;\n\n\t\t\t\t\t\t\tconst layer = ecs.getComponent(candidateId, 'collisionLayer');\n\t\t\t\t\t\t\tif (!layer) continue;\n\t\t\t\t\t\t\tif (!filterSet.has(layer.layer)) continue;\n\n\t\t\t\t\t\t\tconst candidateTransform = ecs.getComponent(candidateId, 'worldTransform');\n\t\t\t\t\t\t\tif (!candidateTransform) continue;\n\n\t\t\t\t\t\t\tconst dx = candidateTransform.x - worldTransform.x;\n\t\t\t\t\t\t\tconst dy = candidateTransform.y - worldTransform.y;\n\t\t\t\t\t\t\tentries.push({ entityId: candidateId, distanceSq: dx * dx + dy * dy });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tentries.sort(compareByDistance);\n\t\t\t\t\t\tconst capped = entries.length > detector.maxResults\n\t\t\t\t\t\t\t? entries.slice(0, detector.maxResults)\n\t\t\t\t\t\t\t: entries;\n\n\t\t\t\t\t\t// Update or add the detectedEntities component\n\t\t\t\t\t\tconst existing = ecs.getComponent(entity.id, 'detectedEntities');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\t(existing as { entities: readonly DetectedEntry[] }).entities = capped;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'detectedEntities');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'detectedEntities', { entities: capped });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Diff against previous frame for events\n\t\t\t\t\t\tconst prev = previousSets.get(entity.id);\n\t\t\t\t\t\tcurrentSet.clear();\n\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\tcurrentSet.add(entry.entityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (prev) {\n\t\t\t\t\t\t\t// Detect gained\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tif (!prev.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tdetectedId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Detect lost\n\t\t\t\t\t\t\tfor (const id of prev) {\n\t\t\t\t\t\t\t\tif (!currentSet.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionLost', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tlostId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Update previous set in place\n\t\t\t\t\t\t\tprev.clear();\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tprev.add(id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// First frame — all are gained\n\t\t\t\t\t\t\tconst newSet = new Set<number>();\n\t\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\t\tnewSet.add(entry.entityId);\n\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tdetectedId: entry.entityId,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpreviousSets.set(entity.id, newSet);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "0908791952E86B8564756E2164756E21",
9
+ "names": []
10
+ }
@@ -5,9 +5,8 @@
5
5
  * User-defined channels with type-safe volume control, hybrid resource + component API,
6
6
  * and asset manager integration.
7
7
  */
8
- import { type Plugin, type BasePluginOptions } from 'ecspresso';
8
+ import { type BasePluginOptions } from 'ecspresso';
9
9
  import type { AssetsOfWorld, AnyECSpresso, ChannelOfWorld } from 'ecspresso';
10
- import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
11
10
  import type { Howl } from 'howler';
12
11
  /**
13
12
  * Configuration for a single audio channel.
@@ -243,7 +242,7 @@ export declare function loadSound(src: string | string[], options?: {
243
242
  * audio.play('explosion', { channel: 'sfx' });
244
243
  * ```
245
244
  */
246
- export declare function createAudioPlugin<Ch extends string, G extends string = 'audio'>(options: AudioPluginOptions<Ch, G>): Plugin<WorldConfigFrom<AudioComponentTypes<Ch>, AudioEventTypes<Ch>, AudioResourceTypes<Ch>>, EmptyConfig, 'audio-sync', G, never, 'audio-sources'>;
245
+ export declare function createAudioPlugin<Ch extends string, G extends string = 'audio'>(options: AudioPluginOptions<Ch, G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, AudioComponentTypes<Ch>>, AudioEventTypes<Ch>>, AudioResourceTypes<Ch>>, import("ecspresso").EmptyConfig, "audio-sync", G, never, "audio-sources">;
247
246
  /**
248
247
  * Typed helpers for the audio plugin.
249
248
  * Creates helpers that validate sound keys and channel names against the world type W.
@@ -1,4 +1,4 @@
1
- var A=((Q)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(Q,{get:($,Y)=>(typeof require<"u"?require:$)[Y]}):Q)(function(Q){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+Q+'" is not supported')});import{definePlugin as B}from"ecspresso";function I(Q){return Object.freeze(Q)}function S(Q,$,Y){return{audioSource:{sound:Q,channel:$,volume:Y?.volume??1,loop:Y?.loop??!1,autoRemove:Y?.autoRemove??!1,playing:!1,_soundId:-1}}}function f(Q,$){return()=>import("howler").then(({Howl:Y})=>new Promise((T,H)=>{let Z,K=!1;if(Z=new Y({src:Array.isArray(Q)?Q:[Q],html5:$?.html5??!1,preload:$?.preload??!0,onload:()=>{K=!0,T(Z)},onloaderror:(U,G)=>H(G instanceof Error?G:Error(String(G)))}),!K&&Z.state?.()==="loaded")T(Z)}))}function w(Q){let{channels:$,systemGroup:Y="audio",priority:T=0,phase:H="update"}=Q,Z=new Map,K=new Map,U=new Map,G=1,j=!1,N=[];for(let[z,q]of Object.entries($))Z.set(z,q.volume),N.push(z);let P=N[0];function E(z,q){if(j)return 0;let F=Z.get(q)??1;return z*F*G}function R(z){for(let F of K.values()){if(F.channel!==z)continue;F.howl.volume(E(F.individualVolume,z),F.soundId)}let q=U.get(z);if(q)q.howl.volume(E(q.individualVolume,z),q.soundId)}function W(){for(let z of N)R(z)}function b(z){let q=K.get(z);if(!q)return;q.howl.stop(z),K.delete(z)}let k=null,D=null,O={play(z,q){if(!D)return-1;let F=q?.channel??P,X=q?.volume??1,L=q?.loop??!1,M=D(z);M.volume(E(X,F)),M.loop(L);let J=M.play(),_={howl:M,soundId:J,channel:F,individualVolume:X,assetKey:z,entityId:-1};return K.set(J,_),M.once("end",()=>{K.delete(J),k?.publish("soundEnded",{entityId:-1,soundId:J,sound:z})},J),J},stop(z){b(z)},playMusic(z,q){if(!D)return;let F=q?.channel??P,X=q?.volume??1,L=q?.loop??!0,M=U.get(F);if(M)M.howl.stop(M.soundId),K.delete(M.soundId);let J=D(z);J.volume(E(X,F)),J.loop(L);let _=J.play(),x={howl:J,soundId:_,channel:F,individualVolume:X,assetKey:z};U.set(F,x),K.set(_,{...x,entityId:-1}),J.once("end",()=>{if(K.delete(_),U.get(F)?.soundId===_)U.delete(F)},_)},stopMusic(z){if(z!==void 0){let q=U.get(z);if(q)q.howl.stop(q.soundId),K.delete(q.soundId),U.delete(z)}else for(let[q,F]of U)F.howl.stop(F.soundId),K.delete(F.soundId),U.delete(q)},pauseMusic(z){if(z!==void 0){let q=U.get(z);if(q)q.howl.pause(q.soundId)}else for(let q of U.values())q.howl.pause(q.soundId)},resumeMusic(z){if(z!==void 0){let q=U.get(z);if(q)q.howl.play(q.soundId)}else for(let q of U.values())q.howl.play(q.soundId)},setChannelVolume(z,q){Z.set(z,q),R(z)},getChannelVolume(z){return Z.get(z)??1},setMasterVolume(z){G=z,W()},getMasterVolume(){return G},mute(){j=!0,W()},unmute(){j=!1,W()},toggleMute(){j=!j,W()},isMuted(){return j}};return B({id:"audio",install(z){z.addResource("audioState",O),z.registerDispose("audioSource",({value:q})=>{if(q._soundId!==-1)b(q._soundId)}),z.addSystem("audio-sync").setPriority(T).inPhase(H).inGroup(Y).setOnInitialize((q)=>{k=q.eventBus;let F=q.tryGetResource("$assets");if(F)D=(X)=>F.get(X);q.addReactiveQuery("audio-sources",{with:["audioSource"],onEnter:(X)=>{let L=X.components.audioSource;if(!D)return;if(L._soundId!==-1)return;let M=D(L.sound);M.volume(E(L.volume,L.channel)),M.loop(L.loop);let J=M.play();L._soundId=J,L.playing=!0;let _={howl:M,soundId:J,channel:L.channel,individualVolume:L.volume,assetKey:L.sound,entityId:X.id};K.set(J,_),M.once("end",()=>{if(K.delete(J),L.playing=!1,k?.publish("soundEnded",{entityId:X.id,soundId:J,sound:L.sound}),L.autoRemove)q.commands.removeEntity(X.id)},J)},onExit:(X)=>{}})}).setEventHandlers({playSound({data:q,ecs:F}){F.getResource("audioState").play(q.sound,{channel:q.channel,volume:q.volume,loop:q.loop})},stopMusic({data:q,ecs:F}){F.getResource("audioState").stopMusic(q.channel)}}).setOnDetach(()=>{for(let q of K.values())q.howl.stop(q.soundId);K.clear(),U.clear(),k=null,D=null})}})}function v(Q){return{createAudioSource:S}}export{f as loadSound,I as defineAudioChannels,S as createAudioSource,w as createAudioPlugin,v as createAudioHelpers};
1
+ var A=((Q)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(Q,{get:($,Y)=>(typeof require<"u"?require:$)[Y]}):Q)(function(Q){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+Q+'" is not supported')});import{definePlugin as B}from"ecspresso";function I(Q){return Object.freeze(Q)}function S(Q,$,Y){return{audioSource:{sound:Q,channel:$,volume:Y?.volume??1,loop:Y?.loop??!1,autoRemove:Y?.autoRemove??!1,playing:!1,_soundId:-1}}}function f(Q,$){return()=>import("howler").then(({Howl:Y})=>new Promise((T,H)=>{let Z,K=!1;if(Z=new Y({src:Array.isArray(Q)?Q:[Q],html5:$?.html5??!1,preload:$?.preload??!0,onload:()=>{K=!0,T(Z)},onloaderror:(U,G)=>H(G instanceof Error?G:Error(String(G)))}),!K&&Z.state?.()==="loaded")T(Z)}))}function w(Q){let{channels:$,systemGroup:Y="audio",priority:T=0,phase:H="update"}=Q,Z=new Map,K=new Map,U=new Map,G=1,j=!1,N=[];for(let[z,q]of Object.entries($))Z.set(z,q.volume),N.push(z);let P=N[0];function E(z,q){if(j)return 0;let F=Z.get(q)??1;return z*F*G}function R(z){for(let F of K.values()){if(F.channel!==z)continue;F.howl.volume(E(F.individualVolume,z),F.soundId)}let q=U.get(z);if(q)q.howl.volume(E(q.individualVolume,z),q.soundId)}function W(){for(let z of N)R(z)}function b(z){let q=K.get(z);if(!q)return;q.howl.stop(z),K.delete(z)}let k=null,D=null,O={play(z,q){if(!D)return-1;let F=q?.channel??P,X=q?.volume??1,L=q?.loop??!1,M=D(z);M.volume(E(X,F)),M.loop(L);let J=M.play(),_={howl:M,soundId:J,channel:F,individualVolume:X,assetKey:z,entityId:-1};return K.set(J,_),M.once("end",()=>{K.delete(J),k?.publish("soundEnded",{entityId:-1,soundId:J,sound:z})},J),J},stop(z){b(z)},playMusic(z,q){if(!D)return;let F=q?.channel??P,X=q?.volume??1,L=q?.loop??!0,M=U.get(F);if(M)M.howl.stop(M.soundId),K.delete(M.soundId);let J=D(z);J.volume(E(X,F)),J.loop(L);let _=J.play(),x={howl:J,soundId:_,channel:F,individualVolume:X,assetKey:z};U.set(F,x),K.set(_,{...x,entityId:-1}),J.once("end",()=>{if(K.delete(_),U.get(F)?.soundId===_)U.delete(F)},_)},stopMusic(z){if(z!==void 0){let q=U.get(z);if(q)q.howl.stop(q.soundId),K.delete(q.soundId),U.delete(z)}else for(let[q,F]of U)F.howl.stop(F.soundId),K.delete(F.soundId),U.delete(q)},pauseMusic(z){if(z!==void 0){let q=U.get(z);if(q)q.howl.pause(q.soundId)}else for(let q of U.values())q.howl.pause(q.soundId)},resumeMusic(z){if(z!==void 0){let q=U.get(z);if(q)q.howl.play(q.soundId)}else for(let q of U.values())q.howl.play(q.soundId)},setChannelVolume(z,q){Z.set(z,q),R(z)},getChannelVolume(z){return Z.get(z)??1},setMasterVolume(z){G=z,W()},getMasterVolume(){return G},mute(){j=!0,W()},unmute(){j=!1,W()},toggleMute(){j=!j,W()},isMuted(){return j}};return B("audio").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().withReactiveQueryNames().install((z)=>{z.addResource("audioState",O),z.registerDispose("audioSource",({value:q})=>{if(q._soundId!==-1)b(q._soundId)}),z.addSystem("audio-sync").setPriority(T).inPhase(H).inGroup(Y).setOnInitialize((q)=>{k=q.eventBus;let F=q.tryGetResource("$assets");if(F)D=(X)=>F.get(X);q.addReactiveQuery("audio-sources",{with:["audioSource"],onEnter:(X)=>{let L=X.components.audioSource;if(!D)return;if(L._soundId!==-1)return;let M=D(L.sound);M.volume(E(L.volume,L.channel)),M.loop(L.loop);let J=M.play();L._soundId=J,L.playing=!0;let _={howl:M,soundId:J,channel:L.channel,individualVolume:L.volume,assetKey:L.sound,entityId:X.id};K.set(J,_),M.once("end",()=>{if(K.delete(J),L.playing=!1,k?.publish("soundEnded",{entityId:X.id,soundId:J,sound:L.sound}),L.autoRemove)q.commands.removeEntity(X.id)},J)},onExit:(X)=>{}})}).setEventHandlers({playSound({data:q,ecs:F}){F.getResource("audioState").play(q.sound,{channel:q.channel,volume:q.volume,loop:q.loop})},stopMusic({data:q,ecs:F}){F.getResource("audioState").stopMusic(q.channel)}}).setOnDetach(()=>{for(let q of K.values())q.howl.stop(q.soundId);K.clear(),U.clear(),k=null,D=null})})}function v(Q){return{createAudioSource:S}}export{f as loadSound,I as defineAudioChannels,S as createAudioSource,w as createAudioPlugin,v as createAudioHelpers};
2
2
 
3
- //# debugId=94D9C0CFA427296F64756E2164756E21
3
+ //# debugId=FFF17E90E1B1E9CB64756E2164756E21
4
4
  //# sourceMappingURL=audio.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/audio/audio.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Audio Plugin for ECSpresso\n *\n * Web Audio API integration via Howler.js for sound effects and music playback.\n * User-defined channels with type-safe volume control, hybrid resource + component API,\n * and asset manager integration.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { AssetsOfWorld, AnyECSpresso, ChannelOfWorld } from 'ecspresso';\nimport type { Howl } from 'howler';\n\n// ==================== Channel Definition ====================\n\n/**\n * Configuration for a single audio channel.\n */\nexport interface AudioChannelConfig {\n\treadonly volume: number;\n}\n\n/**\n * Define audio channels with type-safe names and initial volumes.\n * Mirrors `defineCollisionLayers` pattern.\n *\n * @param channels Object mapping channel names to their configuration\n * @returns Frozen channel configuration with inferred channel name union\n *\n * @example\n * ```typescript\n * const channels = defineAudioChannels({\n * sfx: { volume: 1 },\n * music: { volume: 0.7 },\n * ui: { volume: 0.8 },\n * });\n * type Ch = ChannelsOf<typeof channels>; // 'sfx' | 'music' | 'ui'\n * ```\n */\nexport function defineAudioChannels<const T extends Record<string, AudioChannelConfig>>(\n\tchannels: T\n): Readonly<T> {\n\treturn Object.freeze(channels);\n}\n\n/**\n * Extract channel name union from a `defineAudioChannels` result.\n */\nexport type ChannelsOf<T> = T extends Record<infer K extends string, AudioChannelConfig> ? K : never;\n\n// ==================== Component Types ====================\n\n/**\n * Audio source component attached to entities for positional/entity-bound audio.\n */\nexport interface AudioSource<Ch extends string = string> {\n\t/** Asset key for the sound */\n\treadonly sound: string;\n\t/** Channel this sound plays on */\n\treadonly channel: Ch;\n\t/** Individual volume (0-1) */\n\tvolume: number;\n\t/** Whether sound loops */\n\tloop: boolean;\n\t/** Remove entity when sound ends (like timer autoRemove) */\n\tautoRemove: boolean;\n\t/** Whether sound is currently playing (system-managed) */\n\tplaying: boolean;\n\t/** Howler sound ID (system-managed, -1 = not started) */\n\t_soundId: number;\n}\n\n/**\n * Component types provided by the audio plugin.\n */\nexport interface AudioComponentTypes<Ch extends string = string> {\n\taudioSource: AudioSource<Ch>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event to trigger fire-and-forget sound playback from any system.\n */\nexport interface PlaySoundEvent<Ch extends string = string> {\n\t/** Asset key for the sound */\n\tsound: string;\n\t/** Channel to play on */\n\tchannel?: Ch;\n\t/** Individual volume (0-1) */\n\tvolume?: number;\n\t/** Whether sound loops */\n\tloop?: boolean;\n}\n\n/**\n * Event to stop music on a channel.\n */\nexport interface StopMusicEvent<Ch extends string = string> {\n\t/** Channel to stop music on. If omitted, stops all music. */\n\tchannel?: Ch;\n}\n\n/**\n * Event published when a sound finishes playing.\n */\nexport interface SoundEndedEvent {\n\t/** Entity ID if sound was entity-attached, -1 for fire-and-forget */\n\tentityId: number;\n\t/** Howler sound ID */\n\tsoundId: number;\n\t/** Asset key of the sound */\n\tsound: string;\n}\n\n/**\n * Event types provided by the audio plugin.\n */\nexport interface AudioEventTypes<Ch extends string = string> {\n\tplaySound: PlaySoundEvent<Ch>;\n\tstopMusic: StopMusicEvent<Ch>;\n\tsoundEnded: SoundEndedEvent;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Play options for fire-and-forget sound effects.\n */\nexport interface PlayOptions<Ch extends string = string> {\n\t/** Channel to play on (uses first defined channel if omitted) */\n\tchannel?: Ch;\n\t/** Individual volume (0-1, default: 1) */\n\tvolume?: number;\n\t/** Whether to loop (default: false) */\n\tloop?: boolean;\n}\n\n/**\n * Music playback options.\n */\nexport interface MusicOptions<Ch extends string = string> {\n\t/** Channel to play music on (uses first defined channel if omitted) */\n\tchannel?: Ch;\n\t/** Volume (0-1, default: 1) */\n\tvolume?: number;\n\t/** Whether to loop (default: true) */\n\tloop?: boolean;\n}\n\n/**\n * Audio state resource providing fire-and-forget SFX and music control.\n * Effective volume = individual * channel * master.\n */\nexport interface AudioState<Ch extends string = string> {\n\t/** Play a fire-and-forget sound effect. Returns the Howler sound ID. */\n\tplay(sound: string, options?: PlayOptions<Ch>): number;\n\t/** Stop a specific sound by its Howler sound ID. */\n\tstop(soundId: number): void;\n\n\t/** Play music on a channel. Stops any existing music on that channel first. */\n\tplayMusic(sound: string, options?: MusicOptions<Ch>): void;\n\t/** Stop music on a channel. If omitted, stops all music. */\n\tstopMusic(channel?: Ch): void;\n\t/** Pause music on a channel. If omitted, pauses all music. */\n\tpauseMusic(channel?: Ch): void;\n\t/** Resume music on a channel. If omitted, resumes all music. */\n\tresumeMusic(channel?: Ch): void;\n\n\t/** Set volume for a channel (0-1). */\n\tsetChannelVolume(channel: Ch, volume: number): void;\n\t/** Get current volume for a channel. */\n\tgetChannelVolume(channel: Ch): number;\n\t/** Set master volume (0-1). */\n\tsetMasterVolume(volume: number): void;\n\t/** Get current master volume. */\n\tgetMasterVolume(): number;\n\t/** Mute all audio. */\n\tmute(): void;\n\t/** Unmute all audio. */\n\tunmute(): void;\n\t/** Toggle mute state. */\n\ttoggleMute(): void;\n\t/** Check if audio is muted. */\n\tisMuted(): boolean;\n}\n\n/**\n * Resource types provided by the audio plugin.\n */\nexport interface AudioResourceTypes<Ch extends string = string> {\n\taudioState: AudioState<Ch>;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the audio plugin.\n */\nexport interface AudioPluginOptions<Ch extends string, G extends string = 'audio'> extends BasePluginOptions<G> {\n\t/** Channel definitions from defineAudioChannels */\n\tchannels: Readonly<Record<Ch, AudioChannelConfig>>;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create an audioSource component for entity-attached audio.\n *\n * @param sound Asset key for the sound\n * @param channel Channel to play on\n * @param options Optional configuration\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createAudioSource('explosion', 'sfx'),\n * ...createTransform(100, 200),\n * });\n * ```\n */\nexport function createAudioSource<Ch extends string>(\n\tsound: string,\n\tchannel: Ch,\n\toptions?: { volume?: number; loop?: boolean; autoRemove?: boolean }\n): Pick<AudioComponentTypes<Ch>, 'audioSource'> {\n\treturn {\n\t\taudioSource: {\n\t\t\tsound,\n\t\t\tchannel,\n\t\t\tvolume: options?.volume ?? 1,\n\t\t\tloop: options?.loop ?? false,\n\t\t\tautoRemove: options?.autoRemove ?? false,\n\t\t\tplaying: false,\n\t\t\t_soundId: -1,\n\t\t},\n\t};\n}\n\n/**\n * Create a loader function for use with the asset manager.\n * Returns a factory function that loads a Howl when called.\n *\n * @param src URL(s) for the sound file\n * @param options Optional Howl configuration\n * @returns Factory function compatible with asset manager's loader parameter\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withAssets(a => a\n * .add('explosion', loadSound('/sounds/explosion.mp3'))\n * .add('bgm', loadSound(['/sounds/bgm.webm', '/sounds/bgm.mp3']))\n * )\n * .build();\n * ```\n */\nexport function loadSound(\n\tsrc: string | string[],\n\toptions?: { html5?: boolean; preload?: boolean }\n): () => Promise<Howl> {\n\treturn () => import('howler').then(({ Howl: HowlClass }) =>\n\t\tnew Promise<Howl>((resolve, reject) => {\n\t\t\tlet howl: Howl;\n\t\t\tlet resolved = false;\n\t\t\thowl = new HowlClass({\n\t\t\t\tsrc: Array.isArray(src) ? src : [src],\n\t\t\t\thtml5: options?.html5 ?? false,\n\t\t\t\tpreload: options?.preload ?? true,\n\t\t\t\tonload: () => {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tresolve(howl);\n\t\t\t\t},\n\t\t\t\tonloaderror: (_id: number, err: unknown) => reject(\n\t\t\t\t\terr instanceof Error ? err : new Error(String(err))\n\t\t\t\t),\n\t\t\t});\n\t\t\t// If onload fired synchronously during construction (e.g. cached),\n\t\t\t// howl is now assigned and the promise is already resolved.\n\t\t\tif (!resolved && howl.state?.() === 'loaded') {\n\t\t\t\tresolve(howl);\n\t\t\t}\n\t\t})\n\t);\n}\n\n// ==================== Internal Types ====================\n\ninterface ActiveSound<Ch extends string> {\n\thowl: Howl;\n\tsoundId: number;\n\tchannel: Ch;\n\tindividualVolume: number;\n\tassetKey: string;\n\tentityId: number;\n}\n\ninterface MusicEntry<Ch extends string> {\n\thowl: Howl;\n\tsoundId: number;\n\tchannel: Ch;\n\tindividualVolume: number;\n\tassetKey: string;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an audio plugin for ECSpresso.\n *\n * Provides:\n * - `audioState` resource for fire-and-forget SFX and music\n * - `audioSource` component for entity-attached sounds\n * - Volume hierarchy: individual * channel * master\n * - `playSound` / `stopMusic` event handlers\n * - `soundEnded` event on completion\n * - Automatic cleanup on entity removal (dispose callback)\n *\n * Sounds must be preloaded through the asset pipeline (`loadSound` helper).\n *\n * @example\n * ```typescript\n * const channels = defineAudioChannels({\n * sfx: { volume: 1 },\n * music: { volume: 0.7 },\n * });\n *\n * const ecs = ECSpresso.create()\n * .withAssets(a => a.add('explosion', loadSound('/sfx/boom.mp3')))\n * .withPlugin(createAudioPlugin({ channels }))\n * .build();\n *\n * await ecs.initialize();\n * const audio = ecs.getResource('audioState');\n * audio.play('explosion', { channel: 'sfx' });\n * ```\n */\nexport function createAudioPlugin<Ch extends string, G extends string = 'audio'>(\n\toptions: AudioPluginOptions<Ch, G>\n) {\n\tconst {\n\t\tchannels: channelDefs,\n\t\tsystemGroup = 'audio',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options;\n\n\t// Closure state\n\tconst channelVolumes = new Map<Ch, number>();\n\tconst activeSounds = new Map<number, ActiveSound<Ch>>();\n\tconst musicByChannel = new Map<Ch, MusicEntry<Ch>>();\n\tlet masterVolume = 1;\n\tlet muted = false;\n\n\t// Initialize channel volumes from definitions\n\tconst channelNames: Ch[] = [];\n\tfor (const [name, config] of Object.entries(channelDefs) as Array<[Ch, AudioChannelConfig]>) {\n\t\tchannelVolumes.set(name, config.volume);\n\t\tchannelNames.push(name);\n\t}\n\n\tconst defaultChannel = channelNames[0] as Ch;\n\n\t// Volume computation\n\tfunction effectiveVolume(individualVol: number, channel: Ch): number {\n\t\tif (muted) return 0;\n\t\tconst chanVol = channelVolumes.get(channel) ?? 1;\n\t\treturn individualVol * chanVol * masterVolume;\n\t}\n\n\t// Propagate volume changes to all active sounds on a channel\n\tfunction propagateChannelVolume(channel: Ch): void {\n\t\tfor (const sound of activeSounds.values()) {\n\t\t\tif (sound.channel !== channel) continue;\n\t\t\tsound.howl.volume(effectiveVolume(sound.individualVolume, channel), sound.soundId);\n\t\t}\n\t\tconst music = musicByChannel.get(channel);\n\t\tif (music) {\n\t\t\tmusic.howl.volume(effectiveVolume(music.individualVolume, channel), music.soundId);\n\t\t}\n\t}\n\n\t// Propagate volume to all sounds across all channels\n\tfunction propagateAllVolumes(): void {\n\t\tfor (const ch of channelNames) {\n\t\t\tpropagateChannelVolume(ch);\n\t\t}\n\t}\n\n\t// Stop a sound by its Howler sound ID\n\tfunction stopSoundById(soundId: number): void {\n\t\tconst entry = activeSounds.get(soundId);\n\t\tif (!entry) return;\n\t\tentry.howl.stop(soundId);\n\t\tactiveSounds.delete(soundId);\n\t}\n\n\t// Event bus reference, set during initialization\n\tlet eventBusRef: { publish(event: string, data: unknown): void } | null = null;\n\n\t// Resolve Howl from asset key\n\tlet getAsset: ((key: string) => Howl) | null = null;\n\n\t// AudioState resource implementation\n\tconst audioState: AudioState<Ch> = {\n\t\tplay(sound, playOpts) {\n\t\t\tif (!getAsset) return -1;\n\t\t\tconst channel = playOpts?.channel ?? defaultChannel;\n\t\t\tconst individualVol = playOpts?.volume ?? 1;\n\t\t\tconst loop = playOpts?.loop ?? false;\n\n\t\t\tconst howl = getAsset(sound);\n\t\t\thowl.volume(effectiveVolume(individualVol, channel));\n\t\t\thowl.loop(loop);\n\t\t\tconst soundId = howl.play();\n\n\t\t\tconst entry: ActiveSound<Ch> = {\n\t\t\t\thowl,\n\t\t\t\tsoundId,\n\t\t\t\tchannel,\n\t\t\t\tindividualVolume: individualVol,\n\t\t\t\tassetKey: sound,\n\t\t\t\tentityId: -1,\n\t\t\t};\n\t\t\tactiveSounds.set(soundId, entry);\n\n\t\t\thowl.once('end', () => {\n\t\t\t\tactiveSounds.delete(soundId);\n\t\t\t\teventBusRef?.publish('soundEnded', {\n\t\t\t\t\tentityId: -1,\n\t\t\t\t\tsoundId,\n\t\t\t\t\tsound,\n\t\t\t\t} satisfies SoundEndedEvent);\n\t\t\t}, soundId);\n\n\t\t\treturn soundId;\n\t\t},\n\n\t\tstop(soundId) {\n\t\t\tstopSoundById(soundId);\n\t\t},\n\n\t\tplayMusic(sound, musicOpts) {\n\t\t\tif (!getAsset) return;\n\t\t\tconst channel = musicOpts?.channel ?? defaultChannel;\n\t\t\tconst individualVol = musicOpts?.volume ?? 1;\n\t\t\tconst loop = musicOpts?.loop ?? true;\n\n\t\t\t// Stop existing music on this channel\n\t\t\tconst existing = musicByChannel.get(channel);\n\t\t\tif (existing) {\n\t\t\t\texisting.howl.stop(existing.soundId);\n\t\t\t\tactiveSounds.delete(existing.soundId);\n\t\t\t}\n\n\t\t\tconst howl = getAsset(sound);\n\t\t\thowl.volume(effectiveVolume(individualVol, channel));\n\t\t\thowl.loop(loop);\n\t\t\tconst soundId = howl.play();\n\n\t\t\tconst entry: MusicEntry<Ch> = {\n\t\t\t\thowl,\n\t\t\t\tsoundId,\n\t\t\t\tchannel,\n\t\t\t\tindividualVolume: individualVol,\n\t\t\t\tassetKey: sound,\n\t\t\t};\n\t\t\tmusicByChannel.set(channel, entry);\n\t\t\tactiveSounds.set(soundId, {\n\t\t\t\t...entry,\n\t\t\t\tentityId: -1,\n\t\t\t});\n\n\t\t\thowl.once('end', () => {\n\t\t\t\tactiveSounds.delete(soundId);\n\t\t\t\tconst current = musicByChannel.get(channel);\n\t\t\t\tif (current?.soundId === soundId) {\n\t\t\t\t\tmusicByChannel.delete(channel);\n\t\t\t\t}\n\t\t\t}, soundId);\n\t\t},\n\n\t\tstopMusic(channel) {\n\t\t\tif (channel !== undefined) {\n\t\t\t\tconst entry = musicByChannel.get(channel);\n\t\t\t\tif (entry) {\n\t\t\t\t\tentry.howl.stop(entry.soundId);\n\t\t\t\t\tactiveSounds.delete(entry.soundId);\n\t\t\t\t\tmusicByChannel.delete(channel);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor (const [ch, entry] of musicByChannel) {\n\t\t\t\t\tentry.howl.stop(entry.soundId);\n\t\t\t\t\tactiveSounds.delete(entry.soundId);\n\t\t\t\t\tmusicByChannel.delete(ch);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tpauseMusic(channel) {\n\t\t\tif (channel !== undefined) {\n\t\t\t\tconst entry = musicByChannel.get(channel);\n\t\t\t\tif (entry) entry.howl.pause(entry.soundId);\n\t\t\t} else {\n\t\t\t\tfor (const entry of musicByChannel.values()) {\n\t\t\t\t\tentry.howl.pause(entry.soundId);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tresumeMusic(channel) {\n\t\t\tif (channel !== undefined) {\n\t\t\t\tconst entry = musicByChannel.get(channel);\n\t\t\t\tif (entry) entry.howl.play(entry.soundId);\n\t\t\t} else {\n\t\t\t\tfor (const entry of musicByChannel.values()) {\n\t\t\t\t\tentry.howl.play(entry.soundId);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tsetChannelVolume(channel, volume) {\n\t\t\tchannelVolumes.set(channel, volume);\n\t\t\tpropagateChannelVolume(channel);\n\t\t},\n\n\t\tgetChannelVolume(channel) {\n\t\t\treturn channelVolumes.get(channel) ?? 1;\n\t\t},\n\n\t\tsetMasterVolume(volume) {\n\t\t\tmasterVolume = volume;\n\t\t\tpropagateAllVolumes();\n\t\t},\n\n\t\tgetMasterVolume() {\n\t\t\treturn masterVolume;\n\t\t},\n\n\t\tmute() {\n\t\t\tmuted = true;\n\t\t\tpropagateAllVolumes();\n\t\t},\n\n\t\tunmute() {\n\t\t\tmuted = false;\n\t\t\tpropagateAllVolumes();\n\t\t},\n\n\t\ttoggleMute() {\n\t\t\tmuted = !muted;\n\t\t\tpropagateAllVolumes();\n\t\t},\n\n\t\tisMuted() {\n\t\t\treturn muted;\n\t\t},\n\t};\n\n\treturn definePlugin('audio')\n\t\t.withComponentTypes<AudioComponentTypes<Ch>>()\n\t\t.withEventTypes<AudioEventTypes<Ch>>()\n\t\t.withResourceTypes<AudioResourceTypes<Ch>>()\n\t\t.withLabels<'audio-sync'>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<'audio-sources'>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('audioState', audioState);\n\n\t\t\t// Dispose callback: stop sounds when audioSource component is removed\n\t\t\tworld.registerDispose('audioSource', ({ value: source }: { value: AudioSource<Ch>; entityId: number }) => {\n\t\t\t\tif (source._soundId !== -1) {\n\t\t\t\t\tstopSoundById(source._soundId);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('audio-sync')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\teventBusRef = ecs.eventBus;\n\n\t\t\t\t\t// Resolve asset getter - works with $assets resource if available\n\t\t\t\t\tconst assets = ecs.tryGetResource<{ get(k: string): unknown }>('$assets');\n\t\t\t\t\tif (assets) {\n\t\t\t\t\t\tgetAsset = (key: string) => assets.get(key) as Howl;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Register reactive query for audioSource components\n\t\t\t\t\tecs.addReactiveQuery('audio-sources', {\n\t\t\t\t\t\twith: ['audioSource'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst source = entity.components.audioSource;\n\t\t\t\t\t\t\tif (!getAsset) return;\n\t\t\t\t\t\t\tif (source._soundId !== -1) return; // Already started\n\n\t\t\t\t\t\t\tconst howl = getAsset(source.sound);\n\t\t\t\t\t\t\thowl.volume(effectiveVolume(source.volume, source.channel));\n\t\t\t\t\t\t\thowl.loop(source.loop);\n\t\t\t\t\t\t\tconst soundId = howl.play();\n\n\t\t\t\t\t\t\tsource._soundId = soundId;\n\t\t\t\t\t\t\tsource.playing = true;\n\n\t\t\t\t\t\t\tconst entry: ActiveSound<Ch> = {\n\t\t\t\t\t\t\t\thowl,\n\t\t\t\t\t\t\t\tsoundId,\n\t\t\t\t\t\t\t\tchannel: source.channel,\n\t\t\t\t\t\t\t\tindividualVolume: source.volume,\n\t\t\t\t\t\t\t\tassetKey: source.sound,\n\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tactiveSounds.set(soundId, entry);\n\n\t\t\t\t\t\t\thowl.once('end', () => {\n\t\t\t\t\t\t\t\tactiveSounds.delete(soundId);\n\t\t\t\t\t\t\t\tsource.playing = false;\n\n\t\t\t\t\t\t\t\teventBusRef?.publish('soundEnded', {\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tsoundId,\n\t\t\t\t\t\t\t\t\tsound: source.sound,\n\t\t\t\t\t\t\t\t} satisfies SoundEndedEvent);\n\n\t\t\t\t\t\t\t\tif (source.autoRemove) {\n\t\t\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}, soundId);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (_entityId) => {\n\t\t\t\t\t\t\t// Cleanup handled by dispose callback\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tplaySound({ data, ecs }) {\n\t\t\t\t\t\tconst audio = ecs.getResource('audioState');\n\t\t\t\t\t\taudio.play(data.sound, {\n\t\t\t\t\t\t\tchannel: data.channel,\n\t\t\t\t\t\t\tvolume: data.volume,\n\t\t\t\t\t\t\tloop: data.loop,\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tstopMusic({ data, ecs }) {\n\t\t\t\t\t\tconst audio = ecs.getResource('audioState');\n\t\t\t\t\t\taudio.stopMusic(data.channel);\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t// Stop all active sounds\n\t\t\t\t\tfor (const entry of activeSounds.values()) {\n\t\t\t\t\t\tentry.howl.stop(entry.soundId);\n\t\t\t\t\t}\n\t\t\t\t\tactiveSounds.clear();\n\t\t\t\t\tmusicByChannel.clear();\n\t\t\t\t\teventBusRef = null;\n\t\t\t\t\tgetAsset = null;\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Post-Build Helpers ====================\n\n/**\n * Typed helpers for the audio plugin.\n * Creates helpers that validate sound keys and channel names against the world type W.\n * Call after .build() using typeof ecs.\n *\n * @template W - Concrete ECS world type (e.g. `typeof ecs`)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createAudioPlugin({ channels }))\n * .withAssets(a => a.add('boom', loadSound('/sfx/boom.mp3')))\n * .build();\n *\n * const { createAudioSource } = createAudioHelpers<typeof ecs>();\n * // Type-safe: 'boom' must be a registered asset, 'sfx' a valid channel\n * createAudioSource('boom', 'sfx');\n * ```\n */\nexport interface AudioHelpers<W extends AnyECSpresso> {\n\tcreateAudioSource: (\n\t\tsound: keyof AssetsOfWorld<W> & string,\n\t\tchannel: ChannelOfWorld<W>,\n\t\toptions?: { volume?: number; loop?: boolean; autoRemove?: boolean },\n\t) => Pick<AudioComponentTypes<ChannelOfWorld<W>>, 'audioSource'>;\n}\n\nexport function createAudioHelpers<W extends AnyECSpresso>(_world?: W): AudioHelpers<W> {\n\treturn {\n\t\tcreateAudioSource: createAudioSource as AudioHelpers<W>['createAudioSource'],\n\t};\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "FFF17E90E1B1E9CB64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Health Plugin for ECSpresso
3
+ *
4
+ * Provides a standard health/damage/death lifecycle.
5
+ * Entities with a `health` component can receive `damage` events.
6
+ * When health reaches zero, an `entityDied` event is published.
7
+ * The plugin does NOT remove dead entities — game-specific logic
8
+ * decides when and how to handle death (animations, loot, etc).
9
+ */
10
+ import { type BasePluginOptions } from 'ecspresso';
11
+ import type { WorldConfigFrom } from 'ecspresso';
12
+ /**
13
+ * Health state for an entity.
14
+ */
15
+ export interface Health {
16
+ current: number;
17
+ max: number;
18
+ }
19
+ /**
20
+ * Component types provided by the health plugin.
21
+ */
22
+ export interface HealthComponentTypes {
23
+ health: Health;
24
+ }
25
+ /**
26
+ * Event requesting damage to an entity.
27
+ */
28
+ export interface DamageEvent {
29
+ entityId: number;
30
+ amount: number;
31
+ sourceId?: number;
32
+ }
33
+ /**
34
+ * Event fired when an entity's health reaches zero.
35
+ */
36
+ export interface EntityDiedEvent {
37
+ entityId: number;
38
+ killerId?: number;
39
+ }
40
+ /**
41
+ * Event types provided by the health plugin.
42
+ */
43
+ export interface HealthEventTypes {
44
+ damage: DamageEvent;
45
+ entityDied: EntityDiedEvent;
46
+ }
47
+ /**
48
+ * WorldConfig representing the health plugin's provided types.
49
+ * Used as the `Requires` type parameter by plugins that depend on health.
50
+ */
51
+ export type HealthWorldConfig = WorldConfigFrom<HealthComponentTypes, HealthEventTypes>;
52
+ export interface HealthPluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {
53
+ }
54
+ /**
55
+ * Create a health component at full HP.
56
+ *
57
+ * @param max Maximum (and initial) health
58
+ * @returns Component object suitable for spreading into spawn()
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * ecs.spawn({
63
+ * ...createHealth(100),
64
+ * ...createLocalTransform(200, 300),
65
+ * });
66
+ * ```
67
+ */
68
+ export declare function createHealth(max: number): Pick<HealthComponentTypes, 'health'>;
69
+ /**
70
+ * Create a health component with a specific current value.
71
+ *
72
+ * @param current Current health
73
+ * @param max Maximum health
74
+ * @returns Component object suitable for spreading into spawn()
75
+ */
76
+ export declare function createHealthWith(current: number, max: number): Pick<HealthComponentTypes, 'health'>;
77
+ /**
78
+ * Create a health plugin for ECSpresso.
79
+ *
80
+ * Provides event-driven damage processing. Subscribe to `damage` events
81
+ * to deal damage, and listen to `entityDied` events to react to deaths.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const ecs = ECSpresso.create()
86
+ * .withPlugin(createHealthPlugin())
87
+ * .build();
88
+ *
89
+ * // Deal damage:
90
+ * ecs.eventBus.publish('damage', { entityId: targetId, amount: 25 });
91
+ *
92
+ * // React to death:
93
+ * ecs.on('entityDied', ({ entityId }) => {
94
+ * ecs.commands.removeEntity(entityId);
95
+ * });
96
+ * ```
97
+ */
98
+ export declare function createHealthPlugin<G extends string = 'combat'>(options?: HealthPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, HealthComponentTypes>, HealthEventTypes>, import("ecspresso").EmptyConfig, "health-damage", G, never, never>;
@@ -0,0 +1,4 @@
1
+ var K=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,A)=>(typeof require<"u"?require:k)[A]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as J}from"ecspresso";function N(j){return{health:{current:j,max:j}}}function O(j,k){return{health:{current:j,max:k}}}function Q(j){let{systemGroup:k="combat"}=j??{};return J("health").withComponentTypes().withEventTypes().withLabels().withGroups().install((A)=>{A.addSystem("health-damage").inGroup(k).setEventHandlers({damage({data:q,ecs:F}){let z=F.getComponent(q.entityId,"health");if(!z)return;if(z.current<=0)return;if(z.current=Math.max(0,z.current-q.amount),F.markChanged(q.entityId,"health"),z.current<=0)F.eventBus.publish("entityDied",{entityId:q.entityId,killerId:q.sourceId})}})})}export{O as createHealthWith,Q as createHealthPlugin,N as createHealth};
2
+
3
+ //# debugId=F64127187EB3461D64756E2164756E21
4
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/combat/health.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Health Plugin for ECSpresso\n *\n * Provides a standard health/damage/death lifecycle.\n * Entities with a `health` component can receive `damage` events.\n * When health reaches zero, an `entityDied` event is published.\n * The plugin does NOT remove dead entities — game-specific logic\n * decides when and how to handle death (animations, loot, etc).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\n\n// ==================== Component Types ====================\n\n/**\n * Health state for an entity.\n */\nexport interface Health {\n\tcurrent: number;\n\tmax: number;\n}\n\n/**\n * Component types provided by the health plugin.\n */\nexport interface HealthComponentTypes {\n\thealth: Health;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event requesting damage to an entity.\n */\nexport interface DamageEvent {\n\tentityId: number;\n\tamount: number;\n\tsourceId?: number;\n}\n\n/**\n * Event fired when an entity's health reaches zero.\n */\nexport interface EntityDiedEvent {\n\tentityId: number;\n\tkillerId?: number;\n}\n\n/**\n * Event types provided by the health plugin.\n */\nexport interface HealthEventTypes {\n\tdamage: DamageEvent;\n\tentityDied: EntityDiedEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the health plugin's provided types.\n * Used as the `Requires` type parameter by plugins that depend on health.\n */\nexport type HealthWorldConfig = WorldConfigFrom<HealthComponentTypes, HealthEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface HealthPluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a health component at full HP.\n *\n * @param max Maximum (and initial) health\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createHealth(100),\n * ...createLocalTransform(200, 300),\n * });\n * ```\n */\nexport function createHealth(max: number): Pick<HealthComponentTypes, 'health'> {\n\treturn { health: { current: max, max } };\n}\n\n/**\n * Create a health component with a specific current value.\n *\n * @param current Current health\n * @param max Maximum health\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createHealthWith(current: number, max: number): Pick<HealthComponentTypes, 'health'> {\n\treturn { health: { current, max } };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a health plugin for ECSpresso.\n *\n * Provides event-driven damage processing. Subscribe to `damage` events\n * to deal damage, and listen to `entityDied` events to react to deaths.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createHealthPlugin())\n * .build();\n *\n * // Deal damage:\n * ecs.eventBus.publish('damage', { entityId: targetId, amount: 25 });\n *\n * // React to death:\n * ecs.on('entityDied', ({ entityId }) => {\n * ecs.commands.removeEntity(entityId);\n * });\n * ```\n */\nexport function createHealthPlugin<G extends string = 'combat'>(\n\toptions?: HealthPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'combat',\n\t} = options ?? {};\n\n\treturn definePlugin('health')\n\t\t.withComponentTypes<HealthComponentTypes>()\n\t\t.withEventTypes<HealthEventTypes>()\n\t\t.withLabels<'health-damage'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('health-damage')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tdamage({ data, ecs }) {\n\t\t\t\t\t\tconst health = ecs.getComponent(data.entityId, 'health');\n\t\t\t\t\t\tif (!health) return;\n\t\t\t\t\t\tif (health.current <= 0) return;\n\n\t\t\t\t\t\thealth.current = Math.max(0, health.current - data.amount);\n\t\t\t\t\t\tecs.markChanged(data.entityId, 'health');\n\n\t\t\t\t\t\tif (health.current <= 0) {\n\t\t\t\t\t\t\tecs.eventBus.publish('entityDied', {\n\t\t\t\t\t\t\t\tentityId: data.entityId,\n\t\t\t\t\t\t\t\tkillerId: data.sourceId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
6
+ ],
7
+ "mappings": "2PAUA,uBAAS,kBA2EF,SAAS,CAAY,CAAC,EAAmD,CAC/E,MAAO,CAAE,OAAQ,CAAE,QAAS,EAAK,KAAI,CAAE,EAUjC,SAAS,CAAgB,CAAC,EAAiB,EAAmD,CACpG,MAAO,CAAE,OAAQ,CAAE,UAAS,KAAI,CAAE,EA0B5B,SAAS,CAA+C,CAC9D,EACC,CACD,IACC,cAAc,UACX,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAyC,EACzC,eAAiC,EACjC,WAA4B,EAC5B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,eAAe,EACzB,QAAQ,CAAW,EACnB,iBAAiB,CACjB,MAAM,EAAG,OAAM,OAAO,CACrB,IAAM,EAAS,EAAI,aAAa,EAAK,SAAU,QAAQ,EACvD,GAAI,CAAC,EAAQ,OACb,GAAI,EAAO,SAAW,EAAG,OAKzB,GAHA,EAAO,QAAU,KAAK,IAAI,EAAG,EAAO,QAAU,EAAK,MAAM,EACzD,EAAI,YAAY,EAAK,SAAU,QAAQ,EAEnC,EAAO,SAAW,EACrB,EAAI,SAAS,QAAQ,aAAc,CAClC,SAAU,EAAK,SACf,SAAU,EAAK,QAChB,CAAC,EAGJ,CAAC,EACF",
8
+ "debugId": "F64127187EB3461D64756E2164756E21",
9
+ "names": []
10
+ }