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
@@ -15,6 +15,11 @@ export default class EntityManager<ComponentTypes> {
15
15
  * Hierarchy manager for parent-child relationships
16
16
  */
17
17
  private hierarchyManager;
18
+ /**
19
+ * Per-type component dispose callbacks.
20
+ * Called when a component is removed (explicit removal, entity destruction, or replacement).
21
+ */
22
+ private disposeCallbacks;
18
23
  /**
19
24
  * Per-entity per-component change sequence tracking.
20
25
  * Maps entityId -> (componentName -> sequence number when last changed)
@@ -25,24 +30,59 @@ export default class EntityManager<ComponentTypes> {
25
30
  * Each markChanged call increments this and stamps the new value.
26
31
  */
27
32
  private _changeSeq;
33
+ private _afterComponentAddedHooks;
34
+ private _afterEntityMutatedHooks;
35
+ private _afterComponentRemovedHooks;
36
+ private _beforeEntityRemovedHooks;
37
+ private _afterParentChangedHooks;
38
+ private _batchingDepth;
39
+ private _batchedEntityIds;
40
+ /** Component keys being added in the current addComponents batch, if any.
41
+ * Used by required component resolution to skip auto-adding explicitly provided components. */
42
+ _pendingBatchKeys: ReadonlySet<keyof ComponentTypes> | null;
43
+ get entityCount(): number;
28
44
  createEntity(): Entity<ComponentTypes>;
29
- addComponent<ComponentName extends keyof ComponentTypes>(entityOrId: number | Entity<ComponentTypes>, componentName: ComponentName, data: ComponentTypes[ComponentName]): this;
45
+ /**
46
+ * Register a dispose callback for a component type.
47
+ * Called when a component is removed (explicit removal, entity destruction, or replacement).
48
+ * Later registrations replace earlier ones for the same component type.
49
+ * @param componentName The component type to register disposal for
50
+ * @param callback Function receiving the component value being disposed and the entity ID
51
+ */
52
+ registerDispose<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, callback: (ctx: {
53
+ value: ComponentTypes[ComponentName];
54
+ entityId: number;
55
+ }) => void): void;
56
+ /**
57
+ * Get all registered dispose callbacks.
58
+ * @internal Used by ECSpresso for plugin installation
59
+ */
60
+ getDisposeCallbacks(): Map<keyof ComponentTypes, (ctx: {
61
+ value: unknown;
62
+ entityId: number;
63
+ }) => void>;
64
+ /**
65
+ * Invoke the dispose callback for a component, if registered.
66
+ * Errors are caught and logged to prevent blocking removal.
67
+ */
68
+ private invokeDispose;
69
+ addComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName, data: ComponentTypes[ComponentName]): this;
30
70
  /**
31
71
  * Add multiple components to an entity at once
32
- * @param entityOrId Entity or entity ID to add components to
72
+ * @param entityId Entity ID to add components to
33
73
  * @param components Object with component names as keys and component data as values
34
74
  */
35
75
  addComponents<T extends {
36
76
  [K in keyof ComponentTypes]?: ComponentTypes[K];
37
- }>(entityOrId: number | Entity<ComponentTypes>, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): this;
38
- removeComponent<ComponentName extends keyof ComponentTypes>(entityOrId: number | Entity<ComponentTypes>, componentName: ComponentName): this;
39
- getComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): ComponentTypes[ComponentName] | null;
77
+ }>(entityId: number, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): this;
78
+ removeComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): this;
79
+ getComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): ComponentTypes[ComponentName] | undefined;
40
80
  getEntitiesWithQuery<WithComponents extends keyof ComponentTypes = never, WithoutComponents extends keyof ComponentTypes = never>(required?: ReadonlyArray<WithComponents>, excluded?: ReadonlyArray<WithoutComponents>, changed?: ReadonlyArray<keyof ComponentTypes>, changeThreshold?: number, parentHas?: ReadonlyArray<keyof ComponentTypes>): Array<FilteredEntity<ComponentTypes, WithComponents extends never ? never : WithComponents, WithoutComponents extends never ? never : WithoutComponents>>;
41
81
  /**
42
82
  * Check if an entity's direct parent has all specified components
43
83
  */
44
84
  private parentHasComponents;
45
- removeEntity(entityOrId: number | Entity<ComponentTypes>, options?: RemoveEntityOptions): boolean;
85
+ removeEntity(entityId: number, options?: RemoveEntityOptions): boolean;
46
86
  /**
47
87
  * Internal method to remove a single entity without cascade logic
48
88
  */
@@ -54,14 +94,25 @@ export default class EntityManager<ComponentTypes> {
54
94
  * @param handler Function receiving the new component value and the entity
55
95
  * @returns Unsubscribe function to remove the callback
56
96
  */
57
- onComponentAdded<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (value: ComponentTypes[ComponentName], entity: Entity<ComponentTypes>) => void): () => void;
97
+ onComponentAdded<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (ctx: {
98
+ value: ComponentTypes[ComponentName];
99
+ entity: Entity<ComponentTypes>;
100
+ }) => void): () => void;
58
101
  /**
59
102
  * Register a callback when a specific component is removed from any entity
60
103
  * @param componentName The component key
61
104
  * @param handler Function receiving the old component value and the entity
62
105
  * @returns Unsubscribe function to remove the callback
63
106
  */
64
- onComponentRemoved<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (oldValue: ComponentTypes[ComponentName], entity: Entity<ComponentTypes>) => void): () => void;
107
+ onComponentRemoved<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (ctx: {
108
+ value: ComponentTypes[ComponentName];
109
+ entity: Entity<ComponentTypes>;
110
+ }) => void): () => void;
111
+ onAfterComponentAdded(hook: (entityId: number, componentName: keyof ComponentTypes) => void): () => void;
112
+ onAfterEntityMutated(hook: (entityId: number) => void): () => void;
113
+ onAfterComponentRemoved(hook: (entityId: number, componentName: keyof ComponentTypes) => void): () => void;
114
+ onBeforeEntityRemoved(hook: (entityId: number) => void): () => void;
115
+ onAfterParentChanged(hook: (childId: number) => void): () => void;
65
116
  /**
66
117
  * The current monotonic change sequence value.
67
118
  * Each markChanged call increments this before stamping.
@@ -80,11 +131,6 @@ export default class EntityManager<ComponentTypes> {
80
131
  * @returns The sequence number when last changed, or -1 if never changed
81
132
  */
82
133
  getChangeSeq<K extends keyof ComponentTypes>(entityId: number, componentName: K): number;
83
- /**
84
- * Clear all change sequences for an entity
85
- * @param entityId The entity ID
86
- */
87
- clearChangeSeqs(entityId: number): void;
88
134
  /**
89
135
  * Create an entity as a child of another entity with initial components
90
136
  * @param parentId The parent entity ID
@@ -93,80 +139,80 @@ export default class EntityManager<ComponentTypes> {
93
139
  */
94
140
  spawnChild<T extends {
95
141
  [K in keyof ComponentTypes]?: ComponentTypes[K];
96
- }>(parentId: number, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): FilteredEntity<ComponentTypes, keyof T & keyof ComponentTypes, never>;
142
+ }>(parentId: number, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): FilteredEntity<ComponentTypes, keyof T & keyof ComponentTypes>;
97
143
  /**
98
144
  * Set the parent of an entity
99
- * @param childId The entity to set as a child
100
- * @param parentId The entity to set as the parent
145
+ * @param childId The entity ID to set as a child
146
+ * @param parentId The entity ID to set as the parent
101
147
  */
102
148
  setParent(childId: number, parentId: number): this;
103
149
  /**
104
150
  * Remove the parent relationship for an entity (orphan it)
105
- * @param childId The entity to orphan
151
+ * @param childId The entity ID to orphan
106
152
  * @returns true if a parent was removed, false if entity had no parent
107
153
  */
108
154
  removeParent(childId: number): boolean;
109
155
  /**
110
156
  * Get the parent of an entity
111
- * @param entityId The entity to get the parent of
157
+ * @param entityId The entity ID to get the parent of
112
158
  * @returns The parent entity ID, or null if no parent
113
159
  */
114
160
  getParent(entityId: number): number | null;
115
161
  /**
116
162
  * Get all children of an entity in insertion order
117
- * @param parentId The parent entity
163
+ * @param parentId The parent entity ID
118
164
  * @returns Readonly array of child entity IDs
119
165
  */
120
166
  getChildren(parentId: number): readonly number[];
121
167
  /**
122
168
  * Get a child at a specific index
123
- * @param parentId The parent entity
169
+ * @param parentId The parent entity ID
124
170
  * @param index The index of the child
125
171
  * @returns The child entity ID, or null if index is out of bounds
126
172
  */
127
173
  getChildAt(parentId: number, index: number): number | null;
128
174
  /**
129
175
  * Get the index of a child within its parent's children list
130
- * @param parentId The parent entity
131
- * @param childId The child entity to find
176
+ * @param parentId The parent entity ID
177
+ * @param childId The child entity ID to find
132
178
  * @returns The index of the child, or -1 if not found
133
179
  */
134
180
  getChildIndex(parentId: number, childId: number): number;
135
181
  /**
136
182
  * Get all ancestors of an entity in order [parent, grandparent, ...]
137
- * @param entityId The entity to get ancestors of
183
+ * @param entityId The entity ID to get ancestors of
138
184
  * @returns Readonly array of ancestor entity IDs
139
185
  */
140
186
  getAncestors(entityId: number): readonly number[];
141
187
  /**
142
188
  * Get all descendants of an entity in depth-first order
143
- * @param entityId The entity to get descendants of
189
+ * @param entityId The entity ID to get descendants of
144
190
  * @returns Readonly array of descendant entity IDs
145
191
  */
146
192
  getDescendants(entityId: number): readonly number[];
147
193
  /**
148
194
  * Get the root ancestor of an entity (topmost parent), or self if no parent
149
- * @param entityId The entity to get the root of
195
+ * @param entityId The entity ID to get the root of
150
196
  * @returns The root entity ID
151
197
  */
152
198
  getRoot(entityId: number): number;
153
199
  /**
154
200
  * Get siblings of an entity (other children of the same parent)
155
- * @param entityId The entity to get siblings of
201
+ * @param entityId The entity ID to get siblings of
156
202
  * @returns Readonly array of sibling entity IDs
157
203
  */
158
204
  getSiblings(entityId: number): readonly number[];
159
205
  /**
160
206
  * Check if an entity is a descendant of another entity
161
- * @param entityId The potential descendant
162
- * @param ancestorId The potential ancestor
207
+ * @param entityId The potential descendant ID
208
+ * @param ancestorId The potential ancestor ID
163
209
  * @returns true if entityId is a descendant of ancestorId
164
210
  */
165
211
  isDescendantOf(entityId: number, ancestorId: number): boolean;
166
212
  /**
167
213
  * Check if an entity is an ancestor of another entity
168
- * @param entityId The potential ancestor
169
- * @param descendantId The potential descendant
214
+ * @param entityId The potential ancestor ID
215
+ * @param descendantId The potential descendant ID
170
216
  * @returns true if entityId is an ancestor of descendantId
171
217
  */
172
218
  isAncestorOf(entityId: number, descendantId: number): boolean;
@@ -17,7 +17,12 @@ export default class EventBus<EventTypes> {
17
17
  * Internal method to add an event handler
18
18
  */
19
19
  private addHandler;
20
- publish<E extends keyof EventTypes>(eventType: E, data?: EventTypes[E]): void;
20
+ /**
21
+ * Publish an event. Data is required unless EventTypes[E] extends void | undefined.
22
+ * Zero-allocation hot path: uses index-based iteration with a snapshot length
23
+ * so handlers added mid-publish are not called in the same publish cycle.
24
+ */
25
+ publish<E extends keyof EventTypes>(...[eventType, data]: EventTypes[E] extends void | undefined ? [eventType: E, data?: EventTypes[E]] : [eventType: E, data: EventTypes[E]]): void;
21
26
  clear(): void;
22
27
  clearEvent<E extends keyof EventTypes>(eventType: E): void;
23
28
  }
package/dist/index.d.ts CHANGED
@@ -1,21 +1,14 @@
1
1
  import ECSpresso from './ecspresso';
2
- import { SystemBuilder } from './system-builder';
3
- import Bundle, { mergeBundles } from './bundle';
2
+ import { SystemBuilder, type ProcessContext } from './system-builder';
3
+ import { type Plugin, type BasePluginOptions, definePlugin } from './plugin';
4
4
  export * from './types';
5
5
  export * from './asset-types';
6
6
  export * from './screen-types';
7
+ export * from './utils/math';
7
8
  export type { ReactiveQueryDefinition } from './reactive-query-manager';
8
- export { default as EntityManager } from './entity-manager';
9
- export { default as EventBus } from './event-bus';
10
- export { default as CommandBuffer } from './command-buffer';
11
- export { default as HierarchyManager } from './hierarchy-manager';
12
- /**
13
- * @internal ResourceManager is exported for testing purposes only.
14
- * Use ECSpresso resource methods instead: getResource(), addResource(), removeResource(), updateResource(), hasResource()
15
- */
16
- export { default as ResourceManager } from './resource-manager';
17
9
  export { default as AssetManager, createAssetConfigurator } from './asset-manager';
18
10
  export { default as ScreenManager, createScreenConfigurator } from './screen-manager';
19
- export { SystemBuilder };
20
- export { Bundle, mergeBundles };
11
+ export { SystemBuilder, type ProcessContext };
12
+ export { type Plugin, type BasePluginOptions, definePlugin };
13
+ export { directValue, type ResourceDirectValue } from './resource-manager';
21
14
  export default ECSpresso;
@@ -0,0 +1,61 @@
1
+ import type ECSpresso from './ecspresso';
2
+ import type { SystemPhase } from './types';
3
+ import type { WorldConfig, EmptyConfig, MergeConfigs, AnyECSpresso, ConfigOf } from './type-utils';
4
+ /**
5
+ * Plugin interface for ECSpresso. A plugin is a plain object with an `install`
6
+ * function that configures a world directly, plus phantom properties for
7
+ * compile-time type extraction.
8
+ *
9
+ * @typeParam Cfg - The WorldConfig this plugin provides (components, events, resources, etc.)
10
+ * @typeParam Requires - The WorldConfig this plugin requires from other plugins
11
+ */
12
+ export interface Plugin<Cfg extends WorldConfig = EmptyConfig, Requires extends WorldConfig = EmptyConfig, Labels extends string = never, Groups extends string = never, AssetGroupNames extends string = never, ReactiveQueryNames extends string = never> {
13
+ readonly id: string;
14
+ readonly install: (world: ECSpresso<MergeConfigs<Cfg, Requires>>) => void;
15
+ readonly _cfg?: Cfg;
16
+ readonly _requires?: Requires;
17
+ readonly _labels?: Labels;
18
+ readonly _groups?: Groups;
19
+ readonly _assetGroupNames?: AssetGroupNames;
20
+ readonly _reactiveQueryNames?: ReactiveQueryNames;
21
+ }
22
+ /**
23
+ * Common configuration options shared by most plugins.
24
+ * Plugin-specific options interfaces extend this with additional fields.
25
+ */
26
+ export interface BasePluginOptions<G extends string = string> {
27
+ /** System group name for all systems registered by this plugin */
28
+ systemGroup?: G;
29
+ /** Priority for the plugin's primary system (default varies per plugin) */
30
+ priority?: number;
31
+ /** Execution phase for the plugin's primary system */
32
+ phase?: SystemPhase;
33
+ }
34
+ /**
35
+ * Factory function to create a type-safe Plugin with phantom type parameters.
36
+ * The type assertion adds phantom types without runtime cost.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Option 1: Explicit config type param
41
+ * const myPlugin = definePlugin<WorldConfigFrom<MyComponents, MyEvents, MyResources>>({
42
+ * id: 'my-plugin',
43
+ * install(world) { ... },
44
+ * });
45
+ *
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
+ * });
52
+ * ```
53
+ */
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>;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Audio Plugin for ECSpresso
3
+ *
4
+ * Web Audio API integration via Howler.js for sound effects and music playback.
5
+ * User-defined channels with type-safe volume control, hybrid resource + component API,
6
+ * and asset manager integration.
7
+ */
8
+ import { type Plugin, type BasePluginOptions } from 'ecspresso';
9
+ import type { AssetsOfWorld, AnyECSpresso, ChannelOfWorld } from 'ecspresso';
10
+ import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
11
+ import type { Howl } from 'howler';
12
+ /**
13
+ * Configuration for a single audio channel.
14
+ */
15
+ export interface AudioChannelConfig {
16
+ readonly volume: number;
17
+ }
18
+ /**
19
+ * Define audio channels with type-safe names and initial volumes.
20
+ * Mirrors `defineCollisionLayers` pattern.
21
+ *
22
+ * @param channels Object mapping channel names to their configuration
23
+ * @returns Frozen channel configuration with inferred channel name union
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const channels = defineAudioChannels({
28
+ * sfx: { volume: 1 },
29
+ * music: { volume: 0.7 },
30
+ * ui: { volume: 0.8 },
31
+ * });
32
+ * type Ch = ChannelsOf<typeof channels>; // 'sfx' | 'music' | 'ui'
33
+ * ```
34
+ */
35
+ export declare function defineAudioChannels<const T extends Record<string, AudioChannelConfig>>(channels: T): Readonly<T>;
36
+ /**
37
+ * Extract channel name union from a `defineAudioChannels` result.
38
+ */
39
+ export type ChannelsOf<T> = T extends Record<infer K extends string, AudioChannelConfig> ? K : never;
40
+ /**
41
+ * Audio source component attached to entities for positional/entity-bound audio.
42
+ */
43
+ export interface AudioSource<Ch extends string = string> {
44
+ /** Asset key for the sound */
45
+ readonly sound: string;
46
+ /** Channel this sound plays on */
47
+ readonly channel: Ch;
48
+ /** Individual volume (0-1) */
49
+ volume: number;
50
+ /** Whether sound loops */
51
+ loop: boolean;
52
+ /** Remove entity when sound ends (like timer autoRemove) */
53
+ autoRemove: boolean;
54
+ /** Whether sound is currently playing (system-managed) */
55
+ playing: boolean;
56
+ /** Howler sound ID (system-managed, -1 = not started) */
57
+ _soundId: number;
58
+ }
59
+ /**
60
+ * Component types provided by the audio plugin.
61
+ */
62
+ export interface AudioComponentTypes<Ch extends string = string> {
63
+ audioSource: AudioSource<Ch>;
64
+ }
65
+ /**
66
+ * Event to trigger fire-and-forget sound playback from any system.
67
+ */
68
+ export interface PlaySoundEvent<Ch extends string = string> {
69
+ /** Asset key for the sound */
70
+ sound: string;
71
+ /** Channel to play on */
72
+ channel?: Ch;
73
+ /** Individual volume (0-1) */
74
+ volume?: number;
75
+ /** Whether sound loops */
76
+ loop?: boolean;
77
+ }
78
+ /**
79
+ * Event to stop music on a channel.
80
+ */
81
+ export interface StopMusicEvent<Ch extends string = string> {
82
+ /** Channel to stop music on. If omitted, stops all music. */
83
+ channel?: Ch;
84
+ }
85
+ /**
86
+ * Event published when a sound finishes playing.
87
+ */
88
+ export interface SoundEndedEvent {
89
+ /** Entity ID if sound was entity-attached, -1 for fire-and-forget */
90
+ entityId: number;
91
+ /** Howler sound ID */
92
+ soundId: number;
93
+ /** Asset key of the sound */
94
+ sound: string;
95
+ }
96
+ /**
97
+ * Event types provided by the audio plugin.
98
+ */
99
+ export interface AudioEventTypes<Ch extends string = string> {
100
+ playSound: PlaySoundEvent<Ch>;
101
+ stopMusic: StopMusicEvent<Ch>;
102
+ soundEnded: SoundEndedEvent;
103
+ }
104
+ /**
105
+ * Play options for fire-and-forget sound effects.
106
+ */
107
+ export interface PlayOptions<Ch extends string = string> {
108
+ /** Channel to play on (uses first defined channel if omitted) */
109
+ channel?: Ch;
110
+ /** Individual volume (0-1, default: 1) */
111
+ volume?: number;
112
+ /** Whether to loop (default: false) */
113
+ loop?: boolean;
114
+ }
115
+ /**
116
+ * Music playback options.
117
+ */
118
+ export interface MusicOptions<Ch extends string = string> {
119
+ /** Channel to play music on (uses first defined channel if omitted) */
120
+ channel?: Ch;
121
+ /** Volume (0-1, default: 1) */
122
+ volume?: number;
123
+ /** Whether to loop (default: true) */
124
+ loop?: boolean;
125
+ }
126
+ /**
127
+ * Audio state resource providing fire-and-forget SFX and music control.
128
+ * Effective volume = individual * channel * master.
129
+ */
130
+ export interface AudioState<Ch extends string = string> {
131
+ /** Play a fire-and-forget sound effect. Returns the Howler sound ID. */
132
+ play(sound: string, options?: PlayOptions<Ch>): number;
133
+ /** Stop a specific sound by its Howler sound ID. */
134
+ stop(soundId: number): void;
135
+ /** Play music on a channel. Stops any existing music on that channel first. */
136
+ playMusic(sound: string, options?: MusicOptions<Ch>): void;
137
+ /** Stop music on a channel. If omitted, stops all music. */
138
+ stopMusic(channel?: Ch): void;
139
+ /** Pause music on a channel. If omitted, pauses all music. */
140
+ pauseMusic(channel?: Ch): void;
141
+ /** Resume music on a channel. If omitted, resumes all music. */
142
+ resumeMusic(channel?: Ch): void;
143
+ /** Set volume for a channel (0-1). */
144
+ setChannelVolume(channel: Ch, volume: number): void;
145
+ /** Get current volume for a channel. */
146
+ getChannelVolume(channel: Ch): number;
147
+ /** Set master volume (0-1). */
148
+ setMasterVolume(volume: number): void;
149
+ /** Get current master volume. */
150
+ getMasterVolume(): number;
151
+ /** Mute all audio. */
152
+ mute(): void;
153
+ /** Unmute all audio. */
154
+ unmute(): void;
155
+ /** Toggle mute state. */
156
+ toggleMute(): void;
157
+ /** Check if audio is muted. */
158
+ isMuted(): boolean;
159
+ }
160
+ /**
161
+ * Resource types provided by the audio plugin.
162
+ */
163
+ export interface AudioResourceTypes<Ch extends string = string> {
164
+ audioState: AudioState<Ch>;
165
+ }
166
+ /**
167
+ * Configuration options for the audio plugin.
168
+ */
169
+ export interface AudioPluginOptions<Ch extends string, G extends string = 'audio'> extends BasePluginOptions<G> {
170
+ /** Channel definitions from defineAudioChannels */
171
+ channels: Readonly<Record<Ch, AudioChannelConfig>>;
172
+ }
173
+ /**
174
+ * Create an audioSource component for entity-attached audio.
175
+ *
176
+ * @param sound Asset key for the sound
177
+ * @param channel Channel to play on
178
+ * @param options Optional configuration
179
+ * @returns Component object suitable for spreading into spawn()
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * ecs.spawn({
184
+ * ...createAudioSource('explosion', 'sfx'),
185
+ * ...createTransform(100, 200),
186
+ * });
187
+ * ```
188
+ */
189
+ export declare function createAudioSource<Ch extends string>(sound: string, channel: Ch, options?: {
190
+ volume?: number;
191
+ loop?: boolean;
192
+ autoRemove?: boolean;
193
+ }): Pick<AudioComponentTypes<Ch>, 'audioSource'>;
194
+ /**
195
+ * Create a loader function for use with the asset manager.
196
+ * Returns a factory function that loads a Howl when called.
197
+ *
198
+ * @param src URL(s) for the sound file
199
+ * @param options Optional Howl configuration
200
+ * @returns Factory function compatible with asset manager's loader parameter
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * const ecs = ECSpresso.create()
205
+ * .withAssets(a => a
206
+ * .add('explosion', loadSound('/sounds/explosion.mp3'))
207
+ * .add('bgm', loadSound(['/sounds/bgm.webm', '/sounds/bgm.mp3']))
208
+ * )
209
+ * .build();
210
+ * ```
211
+ */
212
+ export declare function loadSound(src: string | string[], options?: {
213
+ html5?: boolean;
214
+ preload?: boolean;
215
+ }): () => Promise<Howl>;
216
+ /**
217
+ * Create an audio plugin for ECSpresso.
218
+ *
219
+ * Provides:
220
+ * - `audioState` resource for fire-and-forget SFX and music
221
+ * - `audioSource` component for entity-attached sounds
222
+ * - Volume hierarchy: individual * channel * master
223
+ * - `playSound` / `stopMusic` event handlers
224
+ * - `soundEnded` event on completion
225
+ * - Automatic cleanup on entity removal (dispose callback)
226
+ *
227
+ * Sounds must be preloaded through the asset pipeline (`loadSound` helper).
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * const channels = defineAudioChannels({
232
+ * sfx: { volume: 1 },
233
+ * music: { volume: 0.7 },
234
+ * });
235
+ *
236
+ * const ecs = ECSpresso.create()
237
+ * .withAssets(a => a.add('explosion', loadSound('/sfx/boom.mp3')))
238
+ * .withPlugin(createAudioPlugin({ channels }))
239
+ * .build();
240
+ *
241
+ * await ecs.initialize();
242
+ * const audio = ecs.getResource('audioState');
243
+ * audio.play('explosion', { channel: 'sfx' });
244
+ * ```
245
+ */
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'>;
247
+ /**
248
+ * Typed helpers for the audio plugin.
249
+ * Creates helpers that validate sound keys and channel names against the world type W.
250
+ * Call after .build() using typeof ecs.
251
+ *
252
+ * @template W - Concrete ECS world type (e.g. `typeof ecs`)
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const ecs = ECSpresso.create()
257
+ * .withPlugin(createAudioPlugin({ channels }))
258
+ * .withAssets(a => a.add('boom', loadSound('/sfx/boom.mp3')))
259
+ * .build();
260
+ *
261
+ * const { createAudioSource } = createAudioHelpers<typeof ecs>();
262
+ * // Type-safe: 'boom' must be a registered asset, 'sfx' a valid channel
263
+ * createAudioSource('boom', 'sfx');
264
+ * ```
265
+ */
266
+ export interface AudioHelpers<W extends AnyECSpresso> {
267
+ createAudioSource: (sound: keyof AssetsOfWorld<W> & string, channel: ChannelOfWorld<W>, options?: {
268
+ volume?: number;
269
+ loop?: boolean;
270
+ autoRemove?: boolean;
271
+ }) => Pick<AudioComponentTypes<ChannelOfWorld<W>>, 'audioSource'>;
272
+ }
273
+ export declare function createAudioHelpers<W extends AnyECSpresso>(_world?: W): AudioHelpers<W>;