ecspresso 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -17
- package/dist/asset-manager.d.ts +15 -15
- package/dist/asset-types.d.ts +16 -14
- package/dist/bundle.d.ts +66 -16
- package/dist/bundles/audio.d.ts +293 -0
- package/dist/bundles/{utils/bounds.d.ts → bounds.d.ts} +9 -7
- package/dist/bundles/camera.d.ts +89 -0
- package/dist/bundles/collision.d.ts +289 -0
- package/dist/bundles/diagnostics.d.ts +48 -0
- package/dist/bundles/{utils/input.d.ts → input.d.ts} +16 -17
- package/dist/bundles/physics2D.d.ts +159 -0
- package/dist/bundles/renderers/renderer2D.d.ts +65 -24
- package/dist/bundles/spatial-index.d.ts +57 -0
- package/dist/bundles/state-machine.d.ts +298 -0
- package/dist/bundles/{utils/timers.d.ts → timers.d.ts} +9 -8
- package/dist/bundles/{utils/transform.d.ts → transform.d.ts} +10 -10
- package/dist/bundles/tween.d.ts +197 -0
- package/dist/command-buffer.d.ts +20 -20
- package/dist/ecspresso-builder.d.ts +165 -0
- package/dist/ecspresso.d.ts +157 -178
- package/dist/entity-manager.d.ts +76 -40
- package/dist/event-bus.d.ts +6 -1
- package/dist/index.d.ts +1 -9
- package/dist/reactive-query-manager.d.ts +14 -3
- package/dist/resource-manager.d.ts +35 -19
- package/dist/screen-manager.d.ts +4 -4
- package/dist/screen-types.d.ts +12 -11
- package/dist/src/bundles/audio.js +4 -0
- package/dist/src/bundles/audio.js.map +10 -0
- package/dist/src/bundles/bounds.js +4 -0
- package/dist/src/bundles/bounds.js.map +10 -0
- package/dist/src/bundles/camera.js +4 -0
- package/dist/src/bundles/camera.js.map +10 -0
- package/dist/src/bundles/collision.js +4 -0
- package/dist/src/bundles/collision.js.map +11 -0
- package/dist/src/bundles/diagnostics.js +5 -0
- package/dist/src/bundles/diagnostics.js.map +10 -0
- package/dist/src/bundles/input.js +4 -0
- package/dist/src/bundles/input.js.map +10 -0
- package/dist/src/bundles/physics2D.js +4 -0
- package/dist/src/bundles/physics2D.js.map +11 -0
- package/dist/src/bundles/renderers/renderer2D.js +4 -0
- package/dist/src/bundles/renderers/renderer2D.js.map +10 -0
- package/dist/src/bundles/spatial-index.js +4 -0
- package/dist/src/bundles/spatial-index.js.map +11 -0
- package/dist/src/bundles/state-machine.js +4 -0
- package/dist/src/bundles/state-machine.js.map +10 -0
- package/dist/src/bundles/timers.js +4 -0
- package/dist/src/bundles/timers.js.map +10 -0
- package/dist/src/bundles/transform.js +4 -0
- package/dist/src/bundles/transform.js.map +10 -0
- package/dist/src/bundles/tween.js +4 -0
- package/dist/src/bundles/tween.js.map +11 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +25 -0
- package/dist/system-builder.d.ts +36 -42
- package/dist/type-utils.d.ts +52 -3
- package/dist/types.d.ts +10 -19
- package/dist/utils/check-required-cycle.d.ts +12 -0
- package/dist/utils/easing.d.ts +71 -0
- package/dist/utils/math.d.ts +67 -0
- package/dist/utils/narrowphase.d.ts +63 -0
- package/dist/utils/spatial-hash.d.ts +53 -0
- package/package.json +50 -20
- package/dist/bundles/renderers/renderer2D.js +0 -4
- package/dist/bundles/renderers/renderer2D.js.map +0 -10
- package/dist/bundles/utils/bounds.js +0 -4
- package/dist/bundles/utils/bounds.js.map +0 -10
- package/dist/bundles/utils/collision.d.ts +0 -204
- package/dist/bundles/utils/collision.js +0 -4
- package/dist/bundles/utils/collision.js.map +0 -10
- package/dist/bundles/utils/input.js +0 -4
- package/dist/bundles/utils/input.js.map +0 -10
- package/dist/bundles/utils/movement.d.ts +0 -86
- package/dist/bundles/utils/movement.js +0 -4
- package/dist/bundles/utils/movement.js.map +0 -10
- package/dist/bundles/utils/timers.js +0 -4
- package/dist/bundles/utils/timers.js.map +0 -10
- package/dist/bundles/utils/transform.js +0 -4
- package/dist/bundles/utils/transform.js.map +0 -10
- package/dist/index.js +0 -4
- package/dist/index.js.map +0 -22
package/dist/entity-manager.d.ts
CHANGED
|
@@ -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,7 +30,38 @@ 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>;
|
|
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
|
|
51
|
+
*/
|
|
52
|
+
registerDispose<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, callback: (value: ComponentTypes[ComponentName]) => void): void;
|
|
53
|
+
/**
|
|
54
|
+
* Get all registered dispose callbacks.
|
|
55
|
+
* @internal Used by ECSpresso for bundle installation
|
|
56
|
+
*/
|
|
57
|
+
getDisposeCallbacks(): Map<keyof ComponentTypes, (value: any) => void>;
|
|
58
|
+
/**
|
|
59
|
+
* Invoke the dispose callback for a component, if registered.
|
|
60
|
+
* Errors are caught and logged to prevent blocking removal.
|
|
61
|
+
*/
|
|
62
|
+
private invokeDispose;
|
|
63
|
+
private resolveEntity;
|
|
64
|
+
private resolveEntityId;
|
|
29
65
|
addComponent<ComponentName extends keyof ComponentTypes>(entityOrId: number | Entity<ComponentTypes>, componentName: ComponentName, data: ComponentTypes[ComponentName]): this;
|
|
30
66
|
/**
|
|
31
67
|
* Add multiple components to an entity at once
|
|
@@ -36,7 +72,7 @@ export default class EntityManager<ComponentTypes> {
|
|
|
36
72
|
[K in keyof ComponentTypes]?: ComponentTypes[K];
|
|
37
73
|
}>(entityOrId: number | Entity<ComponentTypes>, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): this;
|
|
38
74
|
removeComponent<ComponentName extends keyof ComponentTypes>(entityOrId: number | Entity<ComponentTypes>, componentName: ComponentName): this;
|
|
39
|
-
getComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): ComponentTypes[ComponentName] |
|
|
75
|
+
getComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): ComponentTypes[ComponentName] | undefined;
|
|
40
76
|
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
77
|
/**
|
|
42
78
|
* Check if an entity's direct parent has all specified components
|
|
@@ -62,6 +98,11 @@ export default class EntityManager<ComponentTypes> {
|
|
|
62
98
|
* @returns Unsubscribe function to remove the callback
|
|
63
99
|
*/
|
|
64
100
|
onComponentRemoved<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (oldValue: ComponentTypes[ComponentName], entity: Entity<ComponentTypes>) => void): () => void;
|
|
101
|
+
onAfterComponentAdded(hook: (entityId: number, componentName: keyof ComponentTypes) => void): () => void;
|
|
102
|
+
onAfterEntityMutated(hook: (entityId: number) => void): () => void;
|
|
103
|
+
onAfterComponentRemoved(hook: (entityId: number, componentName: keyof ComponentTypes) => void): () => void;
|
|
104
|
+
onBeforeEntityRemoved(hook: (entityId: number) => void): () => void;
|
|
105
|
+
onAfterParentChanged(hook: (childId: number) => void): () => void;
|
|
65
106
|
/**
|
|
66
107
|
* The current monotonic change sequence value.
|
|
67
108
|
* Each markChanged call increments this before stamping.
|
|
@@ -69,10 +110,10 @@ export default class EntityManager<ComponentTypes> {
|
|
|
69
110
|
get changeSeq(): number;
|
|
70
111
|
/**
|
|
71
112
|
* Mark a component as changed on an entity, stamping the next sequence number.
|
|
72
|
-
* @param
|
|
113
|
+
* @param entityOrId The entity or entity ID
|
|
73
114
|
* @param componentName The component that changed
|
|
74
115
|
*/
|
|
75
|
-
markChanged<K extends keyof ComponentTypes>(
|
|
116
|
+
markChanged<K extends keyof ComponentTypes>(entityOrId: number | Entity<ComponentTypes>, componentName: K): void;
|
|
76
117
|
/**
|
|
77
118
|
* Get the sequence number at which a component was last changed on an entity
|
|
78
119
|
* @param entityId The entity ID
|
|
@@ -80,96 +121,91 @@ export default class EntityManager<ComponentTypes> {
|
|
|
80
121
|
* @returns The sequence number when last changed, or -1 if never changed
|
|
81
122
|
*/
|
|
82
123
|
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
124
|
/**
|
|
89
125
|
* Create an entity as a child of another entity with initial components
|
|
90
|
-
* @param
|
|
126
|
+
* @param parentOrId The parent entity or entity ID
|
|
91
127
|
* @param components Initial components to add
|
|
92
128
|
* @returns The created child entity
|
|
93
129
|
*/
|
|
94
130
|
spawnChild<T extends {
|
|
95
131
|
[K in keyof ComponentTypes]?: ComponentTypes[K];
|
|
96
|
-
}>(
|
|
132
|
+
}>(parentOrId: number | Entity<ComponentTypes>, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): FilteredEntity<ComponentTypes, keyof T & keyof ComponentTypes>;
|
|
97
133
|
/**
|
|
98
134
|
* Set the parent of an entity
|
|
99
|
-
* @param
|
|
100
|
-
* @param
|
|
135
|
+
* @param childOrId The entity or entity ID to set as a child
|
|
136
|
+
* @param parentOrId The entity or entity ID to set as the parent
|
|
101
137
|
*/
|
|
102
|
-
setParent(
|
|
138
|
+
setParent(childOrId: number | Entity<ComponentTypes>, parentOrId: number | Entity<ComponentTypes>): this;
|
|
103
139
|
/**
|
|
104
140
|
* Remove the parent relationship for an entity (orphan it)
|
|
105
|
-
* @param
|
|
141
|
+
* @param childOrId The entity or entity ID to orphan
|
|
106
142
|
* @returns true if a parent was removed, false if entity had no parent
|
|
107
143
|
*/
|
|
108
|
-
removeParent(
|
|
144
|
+
removeParent(childOrId: number | Entity<ComponentTypes>): boolean;
|
|
109
145
|
/**
|
|
110
146
|
* Get the parent of an entity
|
|
111
|
-
* @param
|
|
147
|
+
* @param entityOrId The entity or entity ID to get the parent of
|
|
112
148
|
* @returns The parent entity ID, or null if no parent
|
|
113
149
|
*/
|
|
114
|
-
getParent(
|
|
150
|
+
getParent(entityOrId: number | Entity<ComponentTypes>): number | null;
|
|
115
151
|
/**
|
|
116
152
|
* Get all children of an entity in insertion order
|
|
117
|
-
* @param
|
|
153
|
+
* @param parentOrId The parent entity or entity ID
|
|
118
154
|
* @returns Readonly array of child entity IDs
|
|
119
155
|
*/
|
|
120
|
-
getChildren(
|
|
156
|
+
getChildren(parentOrId: number | Entity<ComponentTypes>): readonly number[];
|
|
121
157
|
/**
|
|
122
158
|
* Get a child at a specific index
|
|
123
|
-
* @param
|
|
159
|
+
* @param parentOrId The parent entity or entity ID
|
|
124
160
|
* @param index The index of the child
|
|
125
161
|
* @returns The child entity ID, or null if index is out of bounds
|
|
126
162
|
*/
|
|
127
|
-
getChildAt(
|
|
163
|
+
getChildAt(parentOrId: number | Entity<ComponentTypes>, index: number): number | null;
|
|
128
164
|
/**
|
|
129
165
|
* Get the index of a child within its parent's children list
|
|
130
|
-
* @param
|
|
131
|
-
* @param
|
|
166
|
+
* @param parentOrId The parent entity or entity ID
|
|
167
|
+
* @param childOrId The child entity or entity ID to find
|
|
132
168
|
* @returns The index of the child, or -1 if not found
|
|
133
169
|
*/
|
|
134
|
-
getChildIndex(
|
|
170
|
+
getChildIndex(parentOrId: number | Entity<ComponentTypes>, childOrId: number | Entity<ComponentTypes>): number;
|
|
135
171
|
/**
|
|
136
172
|
* Get all ancestors of an entity in order [parent, grandparent, ...]
|
|
137
|
-
* @param
|
|
173
|
+
* @param entityOrId The entity or entity ID to get ancestors of
|
|
138
174
|
* @returns Readonly array of ancestor entity IDs
|
|
139
175
|
*/
|
|
140
|
-
getAncestors(
|
|
176
|
+
getAncestors(entityOrId: number | Entity<ComponentTypes>): readonly number[];
|
|
141
177
|
/**
|
|
142
178
|
* Get all descendants of an entity in depth-first order
|
|
143
|
-
* @param
|
|
179
|
+
* @param entityOrId The entity or entity ID to get descendants of
|
|
144
180
|
* @returns Readonly array of descendant entity IDs
|
|
145
181
|
*/
|
|
146
|
-
getDescendants(
|
|
182
|
+
getDescendants(entityOrId: number | Entity<ComponentTypes>): readonly number[];
|
|
147
183
|
/**
|
|
148
184
|
* Get the root ancestor of an entity (topmost parent), or self if no parent
|
|
149
|
-
* @param
|
|
185
|
+
* @param entityOrId The entity or entity ID to get the root of
|
|
150
186
|
* @returns The root entity ID
|
|
151
187
|
*/
|
|
152
|
-
getRoot(
|
|
188
|
+
getRoot(entityOrId: number | Entity<ComponentTypes>): number;
|
|
153
189
|
/**
|
|
154
190
|
* Get siblings of an entity (other children of the same parent)
|
|
155
|
-
* @param
|
|
191
|
+
* @param entityOrId The entity or entity ID to get siblings of
|
|
156
192
|
* @returns Readonly array of sibling entity IDs
|
|
157
193
|
*/
|
|
158
|
-
getSiblings(
|
|
194
|
+
getSiblings(entityOrId: number | Entity<ComponentTypes>): readonly number[];
|
|
159
195
|
/**
|
|
160
196
|
* Check if an entity is a descendant of another entity
|
|
161
|
-
* @param
|
|
162
|
-
* @param
|
|
163
|
-
* @returns true if
|
|
197
|
+
* @param entityOrId The potential descendant (entity or ID)
|
|
198
|
+
* @param ancestorOrId The potential ancestor (entity or ID)
|
|
199
|
+
* @returns true if entityOrId is a descendant of ancestorOrId
|
|
164
200
|
*/
|
|
165
|
-
isDescendantOf(
|
|
201
|
+
isDescendantOf(entityOrId: number | Entity<ComponentTypes>, ancestorOrId: number | Entity<ComponentTypes>): boolean;
|
|
166
202
|
/**
|
|
167
203
|
* Check if an entity is an ancestor of another entity
|
|
168
|
-
* @param
|
|
169
|
-
* @param
|
|
170
|
-
* @returns true if
|
|
204
|
+
* @param entityOrId The potential ancestor (entity or ID)
|
|
205
|
+
* @param descendantOrId The potential descendant (entity or ID)
|
|
206
|
+
* @returns true if entityOrId is an ancestor of descendantOrId
|
|
171
207
|
*/
|
|
172
|
-
isAncestorOf(
|
|
208
|
+
isAncestorOf(entityOrId: number | Entity<ComponentTypes>, descendantOrId: number | Entity<ComponentTypes>): boolean;
|
|
173
209
|
/**
|
|
174
210
|
* Get all root entities (entities that have children but no parent)
|
|
175
211
|
* @returns Readonly array of root entity IDs
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -4,16 +4,8 @@ import Bundle, { mergeBundles } from './bundle';
|
|
|
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
11
|
export { SystemBuilder };
|
|
@@ -20,7 +20,7 @@ export interface ReactiveQueryDefinition<ComponentTypes extends Record<string, a
|
|
|
20
20
|
/**
|
|
21
21
|
* Manages reactive queries that trigger callbacks when entities enter/exit query matches
|
|
22
22
|
*/
|
|
23
|
-
export default class ReactiveQueryManager<ComponentTypes extends Record<string, any
|
|
23
|
+
export default class ReactiveQueryManager<ComponentTypes extends Record<string, any>, QueryNames extends string = string> {
|
|
24
24
|
private queries;
|
|
25
25
|
private entityManager;
|
|
26
26
|
/** Whether any registered query uses parentHas */
|
|
@@ -35,17 +35,22 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
|
|
|
35
35
|
* @param name Unique name for the query
|
|
36
36
|
* @param definition Query definition with callbacks
|
|
37
37
|
*/
|
|
38
|
-
addQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never>(name:
|
|
38
|
+
addQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never>(name: QueryNames, definition: ReactiveQueryDefinition<ComponentTypes, WithComponents, WithoutComponents, OptionalComponents>): void;
|
|
39
39
|
/**
|
|
40
40
|
* Remove a reactive query
|
|
41
41
|
* @param name Name of the query to remove
|
|
42
42
|
* @returns true if the query existed and was removed
|
|
43
43
|
*/
|
|
44
|
-
removeQuery(name:
|
|
44
|
+
removeQuery(name: QueryNames): boolean;
|
|
45
45
|
/**
|
|
46
46
|
* Check if an entity matches a query definition
|
|
47
47
|
*/
|
|
48
48
|
private entityMatchesQuery;
|
|
49
|
+
/**
|
|
50
|
+
* Apply enter/exit transitions for a single query against an entity.
|
|
51
|
+
* Fires onEnter when entity starts matching, onExit when it stops.
|
|
52
|
+
*/
|
|
53
|
+
private _applyQueryTransition;
|
|
49
54
|
/**
|
|
50
55
|
* Called when a component is added to an entity
|
|
51
56
|
* Checks all queries for potential enter/exit events
|
|
@@ -66,6 +71,12 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
|
|
|
66
71
|
* Fires enter/exit callbacks as appropriate based on current state vs tracked state
|
|
67
72
|
*/
|
|
68
73
|
recheckEntity(entity: Entity<ComponentTypes>): void;
|
|
74
|
+
/**
|
|
75
|
+
* Recheck an entity and its children against all queries.
|
|
76
|
+
* Used after component mutations to handle both the entity's own queries
|
|
77
|
+
* and parentHas queries on its children.
|
|
78
|
+
*/
|
|
79
|
+
recheckEntityAndChildren(entity: Entity<ComponentTypes>): void;
|
|
69
80
|
/**
|
|
70
81
|
* Recheck all children of a parent entity against parentHas queries.
|
|
71
82
|
* Called when a component is added/removed from a parent entity.
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resource factory with declared dependencies and optional disposal callback
|
|
3
3
|
*/
|
|
4
|
-
interface ResourceFactoryWithDeps<T> {
|
|
5
|
-
dependsOn?: readonly
|
|
6
|
-
factory: (context
|
|
7
|
-
onDispose?: (resource: T, context
|
|
4
|
+
export interface ResourceFactoryWithDeps<T, Context = unknown, D extends string = string> {
|
|
5
|
+
dependsOn?: readonly D[];
|
|
6
|
+
factory: (context: Context) => T | Promise<T>;
|
|
7
|
+
onDispose?: (resource: T, context: Context) => void | Promise<void>;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* When Context is unknown (default), context args are optional.
|
|
11
|
+
* When Context is a specific type (e.g. ECSpresso<...>), context is required.
|
|
12
|
+
*/
|
|
13
|
+
type ContextArgs<Context> = unknown extends Context ? [context?: Context] : [context: Context];
|
|
14
|
+
export default class ResourceManager<ResourceTypes extends Record<string, any> = Record<string, any>, Context = unknown> {
|
|
10
15
|
private resources;
|
|
11
16
|
private resourceFactories;
|
|
12
17
|
private resourceDependencies;
|
|
@@ -18,20 +23,31 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
|
|
|
18
23
|
* @param resource The resource value, a factory function, or a factory with dependencies
|
|
19
24
|
* @returns The resource manager instance for chaining
|
|
20
25
|
*/
|
|
21
|
-
add<K extends keyof ResourceTypes>(label: K, resource: ResourceTypes[K] | ((context
|
|
26
|
+
add<K extends keyof ResourceTypes>(label: K, resource: ResourceTypes[K] | ((context: Context) => ResourceTypes[K] | Promise<ResourceTypes[K]>) | ResourceFactoryWithDeps<ResourceTypes[K], Context, keyof ResourceTypes & string>): this;
|
|
22
27
|
/**
|
|
23
28
|
* Improved detection of factory functions vs direct values/classes
|
|
24
29
|
* @private
|
|
25
30
|
*/
|
|
26
31
|
private _isFactoryFunction;
|
|
32
|
+
/**
|
|
33
|
+
* Try to get a resource from the manager.
|
|
34
|
+
* Returns the resource value if it exists, or undefined if not found.
|
|
35
|
+
* Like `get`, initializes factory resources on first access.
|
|
36
|
+
* @param label The resource key
|
|
37
|
+
* @param context Context to pass to factory functions (usually the ECSpresso instance)
|
|
38
|
+
* @returns The resource value, or undefined if not found
|
|
39
|
+
* @see get — the throwing alternative
|
|
40
|
+
*/
|
|
41
|
+
tryGet<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): ResourceTypes[K] | undefined;
|
|
27
42
|
/**
|
|
28
43
|
* Get a resource from the manager
|
|
29
44
|
* @param label The resource key
|
|
30
|
-
* @param context
|
|
45
|
+
* @param context Context to pass to factory functions (usually the ECSpresso instance)
|
|
31
46
|
* @returns The resource value
|
|
32
47
|
* @throws Error if resource not found
|
|
48
|
+
* @see tryGet — the non-throwing alternative
|
|
33
49
|
*/
|
|
34
|
-
get<K extends keyof ResourceTypes>(label: K,
|
|
50
|
+
get<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): ResourceTypes[K];
|
|
35
51
|
/**
|
|
36
52
|
* Check if a resource exists
|
|
37
53
|
* @param label The resource key
|
|
@@ -48,7 +64,7 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
|
|
|
48
64
|
* Get all resource keys
|
|
49
65
|
* @returns Array of resource keys
|
|
50
66
|
*/
|
|
51
|
-
getKeys(): Array<
|
|
67
|
+
getKeys(): Array<keyof ResourceTypes>;
|
|
52
68
|
/**
|
|
53
69
|
* Check if a resource needs to be initialized
|
|
54
70
|
* @param label The resource key
|
|
@@ -59,40 +75,40 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
|
|
|
59
75
|
* Get all resource keys that need to be initialized
|
|
60
76
|
* @returns Array of resource keys that need initialization
|
|
61
77
|
*/
|
|
62
|
-
getPendingInitializationKeys(): Array<
|
|
78
|
+
getPendingInitializationKeys(): Array<keyof ResourceTypes>;
|
|
63
79
|
/**
|
|
64
80
|
* Initialize a specific resource if it's a factory function
|
|
65
81
|
* @param label The resource key
|
|
66
|
-
* @param context
|
|
82
|
+
* @param context Context to pass to factory functions
|
|
67
83
|
* @returns Promise that resolves when the resource is initialized
|
|
68
84
|
*/
|
|
69
|
-
initializeResource<K extends keyof ResourceTypes>(label: K,
|
|
85
|
+
initializeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<void>;
|
|
70
86
|
/**
|
|
71
87
|
* Initialize specific resources or all resources that haven't been initialized yet.
|
|
72
88
|
* Resources are initialized in topological order based on their dependencies.
|
|
73
|
-
* @param context
|
|
89
|
+
* @param context Context to pass to factory functions (usually the ECSpresso instance)
|
|
74
90
|
* @param keys Optional array of resource keys to initialize
|
|
75
91
|
* @returns Promise that resolves when the specified resources are initialized
|
|
76
92
|
*/
|
|
77
|
-
initializeResources<K extends keyof ResourceTypes>(
|
|
93
|
+
initializeResources<K extends keyof ResourceTypes>(...args: [...ContextArgs<Context>, ...K[]]): Promise<void>;
|
|
78
94
|
/**
|
|
79
95
|
* Get the dependencies of a resource
|
|
80
96
|
* @param label The resource key
|
|
81
97
|
* @returns Array of resource keys that this resource depends on
|
|
82
98
|
*/
|
|
83
|
-
getDependencies<K extends keyof ResourceTypes>(label: K): readonly string[];
|
|
99
|
+
getDependencies<K extends keyof ResourceTypes>(label: K): readonly (keyof ResourceTypes & string)[];
|
|
84
100
|
/**
|
|
85
101
|
* Dispose a single resource, calling its onDispose callback if it exists
|
|
86
102
|
* @param label The resource key to dispose
|
|
87
|
-
* @param context
|
|
103
|
+
* @param context Context to pass to the onDispose callback
|
|
88
104
|
* @returns True if the resource existed and was disposed, false if it didn't exist
|
|
89
105
|
*/
|
|
90
|
-
disposeResource<K extends keyof ResourceTypes>(label: K,
|
|
106
|
+
disposeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<boolean>;
|
|
91
107
|
/**
|
|
92
108
|
* Dispose all initialized resources in reverse dependency order.
|
|
93
109
|
* Resources that depend on others are disposed first.
|
|
94
|
-
* @param context
|
|
110
|
+
* @param context Context to pass to onDispose callbacks
|
|
95
111
|
*/
|
|
96
|
-
disposeResources(
|
|
112
|
+
disposeResources(...args: ContextArgs<Context>): Promise<void>;
|
|
97
113
|
}
|
|
98
114
|
export {};
|
package/dist/screen-manager.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export default class ScreenManager<Screens extends Record<string, ScreenDefiniti
|
|
|
19
19
|
* Set dependencies for screen transitions
|
|
20
20
|
* @internal
|
|
21
21
|
*/
|
|
22
|
-
setDependencies(eventBus: EventBus<ScreenEvents
|
|
22
|
+
setDependencies(eventBus: EventBus<ScreenEvents<keyof Screens & string>>, assetManager: AssetManager<any> | null, ecs: ECSpresso<any, any, any, any, any>): void;
|
|
23
23
|
/**
|
|
24
24
|
* Register a screen definition
|
|
25
25
|
*/
|
|
@@ -100,10 +100,10 @@ export default class ScreenManager<Screens extends Record<string, ScreenDefiniti
|
|
|
100
100
|
/**
|
|
101
101
|
* Implementation of ScreenConfigurator for builder pattern
|
|
102
102
|
*/
|
|
103
|
-
export declare class ScreenConfiguratorImpl<Screens extends Record<string, ScreenDefinition<any, any
|
|
103
|
+
export declare class ScreenConfiguratorImpl<Screens extends Record<string, ScreenDefinition<any, any>>, W = unknown> implements ScreenConfigurator<Screens, W> {
|
|
104
104
|
private readonly manager;
|
|
105
105
|
constructor(manager: ScreenManager<Screens>);
|
|
106
|
-
add<K extends string, Config extends Record<string, unknown>, State extends Record<string, unknown>>(name: K, definition: ScreenDefinition<Config, State>): ScreenConfigurator<Screens & Record<K, ScreenDefinition<Config, State
|
|
106
|
+
add<K extends string, Config extends Record<string, unknown>, State extends Record<string, unknown>>(name: K, definition: ScreenDefinition<Config, State, W>): ScreenConfigurator<Screens & Record<K, ScreenDefinition<Config, State, W>>, W>;
|
|
107
107
|
/**
|
|
108
108
|
* Get the underlying manager
|
|
109
109
|
* @internal
|
|
@@ -113,4 +113,4 @@ export declare class ScreenConfiguratorImpl<Screens extends Record<string, Scree
|
|
|
113
113
|
/**
|
|
114
114
|
* Create a new ScreenConfigurator for builder pattern usage
|
|
115
115
|
*/
|
|
116
|
-
export declare function createScreenConfigurator<Screens extends Record<string, ScreenDefinition<any, any>> = Record<string, never
|
|
116
|
+
export declare function createScreenConfigurator<Screens extends Record<string, ScreenDefinition<any, any>> = Record<string, never>, W = unknown>(manager?: ScreenManager<Screens>): ScreenConfiguratorImpl<Screens, W>;
|
package/dist/screen-types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type ECSpresso from './ecspresso';
|
|
|
5
5
|
/**
|
|
6
6
|
* Definition for a screen including its state, lifecycle hooks, and requirements
|
|
7
7
|
*/
|
|
8
|
-
export interface ScreenDefinition<Config extends Record<string, unknown> = Record<string, never>, State extends Record<string, unknown> = Record<string, never>> {
|
|
8
|
+
export interface ScreenDefinition<Config extends Record<string, unknown> = Record<string, never>, State extends Record<string, unknown> = Record<string, never>, W = ECSpresso<any, any, any, any, any>> {
|
|
9
9
|
/**
|
|
10
10
|
* Function to create initial state from config
|
|
11
11
|
*/
|
|
@@ -13,11 +13,11 @@ export interface ScreenDefinition<Config extends Record<string, unknown> = Recor
|
|
|
13
13
|
/**
|
|
14
14
|
* Lifecycle hook called when entering this screen
|
|
15
15
|
*/
|
|
16
|
-
readonly onEnter?: (config: Config, ecs:
|
|
16
|
+
readonly onEnter?: (config: Config, ecs: W) => void | Promise<void>;
|
|
17
17
|
/**
|
|
18
18
|
* Lifecycle hook called when exiting this screen
|
|
19
19
|
*/
|
|
20
|
-
readonly onExit?: (ecs:
|
|
20
|
+
readonly onExit?: (ecs: W) => void | Promise<void>;
|
|
21
21
|
/**
|
|
22
22
|
* Asset keys that must be loaded before entering this screen
|
|
23
23
|
*/
|
|
@@ -82,32 +82,33 @@ export interface ScreenResource<Screens extends Record<string, ScreenDefinition<
|
|
|
82
82
|
isCurrent(screenName: keyof Screens): boolean;
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
-
* Events emitted by the screen system
|
|
85
|
+
* Events emitted by the screen system.
|
|
86
|
+
* @typeParam S - Screen name type (defaults to `string` for backward compatibility)
|
|
86
87
|
*/
|
|
87
|
-
export interface ScreenEvents {
|
|
88
|
+
export interface ScreenEvents<S extends string = string> {
|
|
88
89
|
screenEnter: {
|
|
89
|
-
screen:
|
|
90
|
+
screen: S;
|
|
90
91
|
config: unknown;
|
|
91
92
|
};
|
|
92
93
|
screenExit: {
|
|
93
|
-
screen:
|
|
94
|
+
screen: S;
|
|
94
95
|
};
|
|
95
96
|
screenPush: {
|
|
96
|
-
screen:
|
|
97
|
+
screen: S;
|
|
97
98
|
config: unknown;
|
|
98
99
|
};
|
|
99
100
|
screenPop: {
|
|
100
|
-
screen:
|
|
101
|
+
screen: S;
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
105
|
* Configuration for screen definitions during builder setup
|
|
105
106
|
*/
|
|
106
|
-
export interface ScreenConfigurator<Screens extends Record<string, ScreenDefinition<any, any
|
|
107
|
+
export interface ScreenConfigurator<Screens extends Record<string, ScreenDefinition<any, any>>, W = unknown> {
|
|
107
108
|
/**
|
|
108
109
|
* Add a screen definition
|
|
109
110
|
*/
|
|
110
|
-
add<K extends string, Config extends Record<string, unknown>, State extends Record<string, unknown>>(name: K, definition: ScreenDefinition<Config, State>): ScreenConfigurator<Screens & Record<K, ScreenDefinition<Config, State
|
|
111
|
+
add<K extends string, Config extends Record<string, unknown>, State extends Record<string, unknown>>(name: K, definition: ScreenDefinition<Config, State, W>): ScreenConfigurator<Screens & Record<K, ScreenDefinition<Config, State, W>>, W>;
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
113
114
|
* Type-safe screen state getter result
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var{defineProperty:k,getOwnPropertyNames:V,getOwnPropertyDescriptor:I}=Object,S=Object.prototype.hasOwnProperty;var B=new WeakMap,r=(F)=>{var L=B.get(F),U;if(L)return L;if(L=k({},"__esModule",{value:!0}),F&&typeof F==="object"||typeof F==="function")V(F).map((Z)=>!S.call(L,Z)&&k(L,Z,{get:()=>F[Z],enumerable:!(U=I(F,Z))||U.enumerable}));return B.set(F,L),L};var h=(F,L)=>{for(var U in L)k(F,U,{get:L[U],enumerable:!0,configurable:!0,set:(Z)=>L[U]=()=>Z})};var i=(F,L)=>()=>(F&&(L=F(F=0)),L);var w=((F)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(F,{get:(L,U)=>(typeof require<"u"?require:L)[U]}):F)(function(F){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+F+'" is not supported')});import{Bundle as f}from"ecspresso";function u(F){return Object.freeze(F)}function C(F,L,U){return{audioSource:{sound:F,channel:L,volume:U?.volume??1,loop:U?.loop??!1,autoRemove:U?.autoRemove??!1,playing:!1,_soundId:-1}}}function v(F,L){return()=>import("howler").then(({Howl:U})=>new Promise((Z,N)=>{let _,P=!1;if(_=new U({src:Array.isArray(F)?F:[F],html5:L?.html5??!1,preload:L?.preload??!0,onload:()=>{P=!0,Z(_)},onloaderror:(X,K)=>N(K instanceof Error?K:Error(String(K)))}),!P&&_.state?.()==="loaded")Z(_)}))}function y(F){let{channels:L,systemGroup:U="audio",priority:Z=0,phase:N="update"}=F,_=new Map,P=new Map,X=new Map,K=1,G=!1,R=[];for(let[q,z]of Object.entries(L))_.set(q,z.volume),R.push(q);let x=R[0];function H(q,z){if(G)return 0;let J=_.get(z)??1;return q*J*K}function b(q){for(let J of P.values()){if(J.channel!==q)continue;J.howl.volume(H(J.individualVolume,q),J.soundId)}let z=X.get(q);if(z)z.howl.volume(H(z.individualVolume,q),z.soundId)}function T(){for(let q of R)b(q)}function O(q){let z=P.get(q);if(!z)return;z.howl.stop(q),P.delete(q)}let W=null,D=null,g={play(q,z){if(!D)return-1;let J=z?.channel??x,M=z?.volume??1,$=z?.loop??!1,Q=D(q);Q.volume(H(M,J)),Q.loop($);let Y=Q.play(),E={howl:Q,soundId:Y,channel:J,individualVolume:M,assetKey:q,entityId:-1};return P.set(Y,E),Q.once("end",()=>{P.delete(Y),W?.publish("soundEnded",{entityId:-1,soundId:Y,sound:q})},Y),Y},stop(q){O(q)},playMusic(q,z){if(!D)return;let J=z?.channel??x,M=z?.volume??1,$=z?.loop??!0,Q=X.get(J);if(Q)Q.howl.stop(Q.soundId),P.delete(Q.soundId);let Y=D(q);Y.volume(H(M,J)),Y.loop($);let E=Y.play(),A={howl:Y,soundId:E,channel:J,individualVolume:M,assetKey:q};X.set(J,A),P.set(E,{...A,entityId:-1}),Y.once("end",()=>{if(P.delete(E),X.get(J)?.soundId===E)X.delete(J)},E)},stopMusic(q){if(q!==void 0){let z=X.get(q);if(z)z.howl.stop(z.soundId),P.delete(z.soundId),X.delete(q)}else for(let[z,J]of X)J.howl.stop(J.soundId),P.delete(J.soundId),X.delete(z)},pauseMusic(q){if(q!==void 0){let z=X.get(q);if(z)z.howl.pause(z.soundId)}else for(let z of X.values())z.howl.pause(z.soundId)},resumeMusic(q){if(q!==void 0){let z=X.get(q);if(z)z.howl.play(z.soundId)}else for(let z of X.values())z.howl.play(z.soundId)},setChannelVolume(q,z){_.set(q,z),b(q)},getChannelVolume(q){return _.get(q)??1},setMasterVolume(q){K=q,T()},getMasterVolume(){return K},mute(){G=!0,T()},unmute(){G=!1,T()},toggleMute(){G=!G,T()},isMuted(){return G}},j=new f("audio");return j.addResource("audioState",g),j.registerDispose("audioSource",(q)=>{if(q._soundId!==-1)O(q._soundId)}),j.addSystem("audio-sync").setPriority(Z).inPhase(N).inGroup(U).setOnInitialize((q)=>{W=q.eventBus;let z=q.tryGetResource("$assets");if(z)D=(J)=>z.get(J);q.addReactiveQuery("audio-sources",{with:["audioSource"],onEnter:(J)=>{let M=J.components.audioSource;if(!D)return;if(M._soundId!==-1)return;let $=D(M.sound);$.volume(H(M.volume,M.channel)),$.loop(M.loop);let Q=$.play();M._soundId=Q,M.playing=!0;let Y={howl:$,soundId:Q,channel:M.channel,individualVolume:M.volume,assetKey:M.sound,entityId:J.id};P.set(Q,Y),$.once("end",()=>{if(P.delete(Q),M.playing=!1,W?.publish("soundEnded",{entityId:J.id,soundId:Q,sound:M.sound}),M.autoRemove)q.commands.removeEntity(J.id)},Q)},onExit:(J)=>{}})}).setEventHandlers({playSound:{handler(q,z){z.getResource("audioState").play(q.sound,{channel:q.channel,volume:q.volume,loop:q.loop})}},stopMusic:{handler(q,z){z.getResource("audioState").stopMusic(q.channel)}}}).setOnDetach(()=>{for(let q of P.values())q.howl.stop(q.soundId);P.clear(),X.clear(),W=null,D=null}).and(),j.withReactiveQueryNames()}function l(F){return{bundle:y(F),createAudioSource:C,loadSound:v}}export{v as loadSound,u as defineAudioChannels,C as createAudioSource,l as createAudioKit,y as createAudioBundle};
|
|
2
|
+
|
|
3
|
+
//# debugId=08A2E01637CA322364756E2164756E21
|
|
4
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bundles/audio.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Audio Bundle 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 { Bundle } from 'ecspresso';\nimport type { SystemPhase, AssetsOfWorld } 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 bundle.\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 bundle.\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 bundle.\n */\nexport interface AudioResourceTypes<Ch extends string = string> {\n\taudioState: AudioState<Ch>;\n}\n\n// ==================== Bundle Options ====================\n\n/**\n * Configuration options for the audio bundle.\n */\nexport interface AudioBundleOptions<Ch extends string, G extends string = 'audio'> {\n\t/** Channel definitions from defineAudioChannels */\n\tchannels: Readonly<Record<Ch, AudioChannelConfig>>;\n\t/** System group name (default: 'audio') */\n\tsystemGroup?: G;\n\t/** Priority for audio sync system (default: 0) */\n\tpriority?: number;\n\t/** Execution phase (default: 'update') */\n\tphase?: SystemPhase;\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 as unknown as { state(): string }).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// ==================== Bundle Factory ====================\n\n/**\n * Create an audio bundle 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 * .withBundle(createAudioBundle({ channels }))\n * .build();\n *\n * await ecs.initialize();\n * const audio = ecs.getResource('audioState');\n * audio.play('explosion', { channel: 'sfx' });\n * ```\n */\nexport function createAudioBundle<Ch extends string, G extends string = 'audio'>(\n\toptions: AudioBundleOptions<Ch, G>\n): Bundle<AudioComponentTypes<Ch>, AudioEventTypes<Ch>, AudioResourceTypes<Ch>, {}, {}, 'audio-sync', G, never, 'audio-sources'> {\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\t// Build bundle\n\tconst bundle = new Bundle<AudioComponentTypes<Ch>, AudioEventTypes<Ch>, AudioResourceTypes<Ch>>('audio');\n\n\tbundle.addResource('audioState', audioState);\n\n\t// Dispose callback: stop sounds when audioSource component is removed\n\tbundle.registerDispose('audioSource', (source: AudioSource<Ch>) => {\n\t\tif (source._soundId !== -1) {\n\t\t\tstopSoundById(source._soundId);\n\t\t}\n\t});\n\n\tbundle\n\t\t.addSystem('audio-sync')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.setOnInitialize((ecs) => {\n\t\t\teventBusRef = ecs.eventBus;\n\n\t\t\t// Resolve asset getter - works with $assets resource if available\n\t\t\tconst assets = ecs.tryGetResource<{ get(k: string): unknown }>('$assets');\n\t\t\tif (assets) {\n\t\t\t\tgetAsset = (key: string) => assets.get(key) as Howl;\n\t\t\t}\n\n\t\t\t// Register reactive query for audioSource components\n\t\t\tecs.addReactiveQuery('audio-sources', {\n\t\t\t\twith: ['audioSource'],\n\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\tconst source = entity.components.audioSource;\n\t\t\t\t\tif (!getAsset) return;\n\t\t\t\t\tif (source._soundId !== -1) return; // Already started\n\n\t\t\t\t\tconst howl = getAsset(source.sound);\n\t\t\t\t\thowl.volume(effectiveVolume(source.volume, source.channel));\n\t\t\t\t\thowl.loop(source.loop);\n\t\t\t\t\tconst soundId = howl.play();\n\n\t\t\t\t\tsource._soundId = soundId;\n\t\t\t\t\tsource.playing = true;\n\n\t\t\t\t\tconst entry: ActiveSound<Ch> = {\n\t\t\t\t\t\thowl,\n\t\t\t\t\t\tsoundId,\n\t\t\t\t\t\tchannel: source.channel,\n\t\t\t\t\t\tindividualVolume: source.volume,\n\t\t\t\t\t\tassetKey: source.sound,\n\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t};\n\t\t\t\t\tactiveSounds.set(soundId, entry);\n\n\t\t\t\t\thowl.once('end', () => {\n\t\t\t\t\t\tactiveSounds.delete(soundId);\n\t\t\t\t\t\tsource.playing = false;\n\n\t\t\t\t\t\teventBusRef?.publish('soundEnded', {\n\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\tsoundId,\n\t\t\t\t\t\t\tsound: source.sound,\n\t\t\t\t\t\t} satisfies SoundEndedEvent);\n\n\t\t\t\t\t\tif (source.autoRemove) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}, soundId);\n\t\t\t\t},\n\t\t\t\tonExit: (_entityId) => {\n\t\t\t\t\t// Cleanup handled by dispose callback\n\t\t\t\t},\n\t\t\t});\n\t\t})\n\t\t.setEventHandlers({\n\t\t\tplaySound: {\n\t\t\t\thandler(data, ecs) {\n\t\t\t\t\tconst audio = ecs.getResource('audioState');\n\t\t\t\t\taudio.play(data.sound, {\n\t\t\t\t\t\tchannel: data.channel,\n\t\t\t\t\t\tvolume: data.volume,\n\t\t\t\t\t\tloop: data.loop,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t},\n\t\t\tstopMusic: {\n\t\t\t\thandler(data, ecs) {\n\t\t\t\t\tconst audio = ecs.getResource('audioState');\n\t\t\t\t\taudio.stopMusic(data.channel);\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\t.setOnDetach(() => {\n\t\t\t// Stop all active sounds\n\t\t\tfor (const entry of activeSounds.values()) {\n\t\t\t\tentry.howl.stop(entry.soundId);\n\t\t\t}\n\t\t\tactiveSounds.clear();\n\t\t\tmusicByChannel.clear();\n\t\t\teventBusRef = null;\n\t\t\tgetAsset = null;\n\t\t})\n\t\t.and();\n\n\t// Declare reactive query names\n\tconst typedBundle = bundle.withReactiveQueryNames<'audio-sources'>();\n\n\treturn typedBundle as Bundle<AudioComponentTypes<Ch>, AudioEventTypes<Ch>, AudioResourceTypes<Ch>, {}, {}, 'audio-sync', G, never, 'audio-sources'>;\n}\n\n// ==================== Kit Pattern ====================\n\ntype AnyECSpresso = import('ecspresso').default<any, any, any, any, any, any, any>;\n\n/**\n * Kit result from createAudioKit.\n */\nexport interface AudioKit<W extends AnyECSpresso, Ch extends string, G extends string = 'audio'> {\n\tbundle: Bundle<AudioComponentTypes<Ch>, AudioEventTypes<Ch>, AudioResourceTypes<Ch>, {}, {}, 'audio-sync', G, never, 'audio-sources'>;\n\tcreateAudioSource: (\n\t\tsound: keyof AssetsOfWorld<W> & string,\n\t\tchannel: Ch,\n\t\toptions?: { volume?: number; loop?: boolean; autoRemove?: boolean }\n\t) => Pick<AudioComponentTypes<Ch>, 'audioSource'>;\n\tloadSound: typeof loadSound;\n}\n\n/**\n * Create a typed audio kit that captures the world type W and channel type Ch.\n *\n * The returned `createAudioSource` validates sound keys against the world's\n * asset types at compile time.\n *\n * @template W - Concrete ECS world type (e.g. `typeof ecs`)\n * @template Ch - Channel name union from defineAudioChannels\n * @template G - System group name (default: 'audio')\n * @param options - Bundle configuration including channel definitions\n * @returns A kit object with bundle, createAudioSource, loadSound\n *\n * @example\n * ```typescript\n * const channels = defineAudioChannels({ sfx: { volume: 1 }, music: { volume: 0.7 } });\n * type Ch = ChannelsOf<typeof channels>;\n * const kit = createAudioKit<typeof ecs, Ch>({ channels });\n *\n * const ecs = ECSpresso.create()\n * .withBundle(kit.bundle)\n * .withAssets(a => a.add('boom', loadSound('/sfx/boom.mp3')))\n * .build();\n *\n * // Type-safe: 'boom' must be a registered asset\n * kit.createAudioSource('boom', 'sfx');\n * ```\n */\nexport function createAudioKit<W extends AnyECSpresso, Ch extends string, G extends string = 'audio'>(\n\toptions: AudioBundleOptions<Ch, G>\n): AudioKit<W, Ch, G> {\n\treturn {\n\t\tbundle: createAudioBundle<Ch, G>(options),\n\t\tcreateAudioSource: createAudioSource as AudioKit<W, Ch, G>['createAudioSource'],\n\t\tloadSound,\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "08A2E01637CA322364756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var{defineProperty:B,getOwnPropertyNames:q,getOwnPropertyDescriptor:M}=Object,w=Object.prototype.hasOwnProperty;var R=new WeakMap,y=(j)=>{var k=R.get(j),z;if(k)return k;if(k=B({},"__esModule",{value:!0}),j&&typeof j==="object"||typeof j==="function")q(j).map((F)=>!w.call(k,F)&&B(k,F,{get:()=>j[F],enumerable:!(z=M(j,F))||z.enumerable}));return R.set(j,k),k};var x=(j,k)=>{for(var z in k)B(j,z,{get:k[z],enumerable:!0,configurable:!0,set:(F)=>k[z]=()=>F})};var b=(j,k)=>()=>(j&&(k=j(j=0)),k);var f=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,z)=>(typeof require<"u"?require:k)[z]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{Bundle as h}from"ecspresso";function c(j,k,z,F){let U={width:j,height:k};if(z!==void 0)U.x=z;if(F!==void 0)U.y=F;return U}function g(j){return{destroyOutOfBounds:j!==void 0?{padding:j}:{}}}function u(j){return{clampToBounds:j!==void 0?{margin:j}:{}}}function p(j){return{wrapAtBounds:j!==void 0?{padding:j}:{}}}function l(j){let{systemGroup:k="physics",priority:z=50,boundsResourceKey:F="bounds",autoRemove:U=!0,phase:S="postUpdate"}=j??{},W=new h("bounds");return W.addSystem("bounds-destroy").setPriority(z).inPhase(S).inGroup(k).addQuery("entities",{with:["worldTransform","destroyOutOfBounds"]}).setProcess((A,E,L)=>{let J=L.getResource(F),N=J.x??0,P=J.y??0,$=N+J.width,v=P+J.height;for(let Q of A.entities){let{worldTransform:V,destroyOutOfBounds:I}=Q.components,D=I.padding??0,C=T(V,N,P,$,v,D);if(!C)continue;if(L.eventBus.publish("entityOutOfBounds",{entityId:Q.id,exitEdge:C}),U)L.commands.removeEntity(Q.id)}}).and(),W.addSystem("bounds-clamp").setPriority(z-1).inPhase(S).inGroup(k).addQuery("entities",{with:["localTransform","worldTransform","clampToBounds"]}).setProcess((A,E,L)=>{let J=L.getResource(F),N=J.x??0,P=J.y??0,$=N+J.width,v=P+J.height;for(let Q of A.entities){let{localTransform:V,worldTransform:I,clampToBounds:D}=Q.components,C=D.margin??0,Z=N+C,_=P+C,H=$-C,O=v-C,G=0,K=0;if(I.x<Z)G=Z-I.x;if(I.x>H)G=H-I.x;if(I.y<_)K=_-I.y;if(I.y>O)K=O-I.y;if(G!==0||K!==0)V.x+=G,V.y+=K,L.markChanged(Q.id,"localTransform")}}).and(),W.addSystem("bounds-wrap").setPriority(z-2).inPhase(S).inGroup(k).addQuery("entities",{with:["localTransform","worldTransform","wrapAtBounds"]}).setProcess((A,E,L)=>{let J=L.getResource(F),N=J.x??0,P=J.y??0,$=N+J.width,v=P+J.height;for(let Q of A.entities){let{localTransform:V,worldTransform:I,wrapAtBounds:D}=Q.components,C=D.padding??0,Z=0,_=0,H=$-N,O=v-P;if(I.x>$+C)Z=-(H+2*C);else if(I.x<N-C)Z=H+2*C;if(I.y>v+C)_=-(O+2*C);else if(I.y<P-C)_=O+2*C;if(Z!==0||_!==0)V.x+=Z,V.y+=_,L.markChanged(Q.id,"localTransform")}}).and(),W}function T(j,k,z,F,U,S){if(j.x>F+S)return"right";if(j.x<k-S)return"left";if(j.y>U+S)return"bottom";if(j.y<z-S)return"top";return null}export{p as createWrapAtBounds,g as createDestroyOutOfBounds,u as createClampToBounds,l as createBoundsBundle,c as createBounds};
|
|
2
|
+
|
|
3
|
+
//# debugId=88C2C3EC79B631F264756E2164756E21
|
|
4
|
+
//# sourceMappingURL=bounds.js.map
|