ecspresso 0.11.0 → 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 (96) hide show
  1. package/README.md +200 -148
  2. package/dist/asset-manager.d.ts +1 -1
  3. package/dist/asset-types.d.ts +2 -2
  4. package/dist/command-buffer.d.ts +34 -24
  5. package/dist/ecspresso-builder.d.ts +100 -72
  6. package/dist/ecspresso.d.ts +257 -122
  7. package/dist/entity-manager.d.ts +57 -47
  8. package/dist/index.d.ts +5 -4
  9. package/dist/plugin.d.ts +61 -0
  10. package/dist/{bundles → plugins}/audio.d.ts +27 -47
  11. package/dist/{bundles → plugins}/bounds.d.ts +17 -25
  12. package/dist/{bundles → plugins}/camera.d.ts +8 -9
  13. package/dist/{bundles → plugins}/collision.d.ts +22 -26
  14. package/dist/plugins/coroutine.d.ts +126 -0
  15. package/dist/{bundles → plugins}/diagnostics.d.ts +5 -4
  16. package/dist/{bundles → plugins}/input.d.ts +9 -15
  17. package/dist/plugins/particles.d.ts +225 -0
  18. package/dist/{bundles → plugins}/physics2D.d.ts +27 -23
  19. package/dist/{bundles → plugins}/renderers/renderer2D.d.ts +40 -39
  20. package/dist/{bundles → plugins}/spatial-index.d.ts +11 -10
  21. package/dist/plugins/sprite-animation.d.ts +150 -0
  22. package/dist/{bundles → plugins}/state-machine.d.ts +50 -104
  23. package/dist/plugins/timers.d.ts +151 -0
  24. package/dist/{bundles → plugins}/transform.d.ts +18 -19
  25. package/dist/{bundles → plugins}/tween.d.ts +36 -71
  26. package/dist/resource-manager.d.ts +32 -7
  27. package/dist/screen-manager.d.ts +17 -11
  28. package/dist/screen-types.d.ts +5 -2
  29. package/dist/src/index.js +2 -2
  30. package/dist/src/index.js.map +17 -17
  31. package/dist/src/plugins/audio.js +4 -0
  32. package/dist/src/plugins/audio.js.map +10 -0
  33. package/dist/src/plugins/bounds.js +4 -0
  34. package/dist/src/plugins/bounds.js.map +10 -0
  35. package/dist/src/plugins/camera.js +4 -0
  36. package/dist/src/plugins/camera.js.map +10 -0
  37. package/dist/src/plugins/collision.js +4 -0
  38. package/dist/src/plugins/collision.js.map +11 -0
  39. package/dist/src/plugins/coroutine.js +4 -0
  40. package/dist/src/plugins/coroutine.js.map +10 -0
  41. package/dist/src/plugins/diagnostics.js +5 -0
  42. package/dist/src/plugins/diagnostics.js.map +10 -0
  43. package/dist/src/plugins/input.js +4 -0
  44. package/dist/src/plugins/input.js.map +10 -0
  45. package/dist/src/plugins/particles.js +4 -0
  46. package/dist/src/plugins/particles.js.map +10 -0
  47. package/dist/src/plugins/physics2D.js +4 -0
  48. package/dist/src/plugins/physics2D.js.map +11 -0
  49. package/dist/src/plugins/renderers/renderer2D.js +4 -0
  50. package/dist/src/plugins/renderers/renderer2D.js.map +10 -0
  51. package/dist/src/plugins/spatial-index.js +4 -0
  52. package/dist/src/plugins/spatial-index.js.map +11 -0
  53. package/dist/src/plugins/sprite-animation.js +4 -0
  54. package/dist/src/plugins/sprite-animation.js.map +10 -0
  55. package/dist/src/plugins/state-machine.js +4 -0
  56. package/dist/src/plugins/state-machine.js.map +10 -0
  57. package/dist/src/plugins/timers.js +4 -0
  58. package/dist/src/plugins/timers.js.map +10 -0
  59. package/dist/src/plugins/transform.js +4 -0
  60. package/dist/src/plugins/transform.js.map +10 -0
  61. package/dist/src/plugins/tween.js +4 -0
  62. package/dist/src/plugins/tween.js.map +11 -0
  63. package/dist/system-builder.d.ts +66 -97
  64. package/dist/type-utils.d.ts +218 -27
  65. package/dist/types.d.ts +52 -24
  66. package/dist/utils/check-required-cycle.d.ts +1 -1
  67. package/dist/utils/narrowphase.d.ts +7 -7
  68. package/package.json +53 -45
  69. package/dist/bundle.d.ts +0 -173
  70. package/dist/bundles/timers.d.ts +0 -173
  71. package/dist/src/bundles/audio.js +0 -4
  72. package/dist/src/bundles/audio.js.map +0 -10
  73. package/dist/src/bundles/bounds.js +0 -4
  74. package/dist/src/bundles/bounds.js.map +0 -10
  75. package/dist/src/bundles/camera.js +0 -4
  76. package/dist/src/bundles/camera.js.map +0 -10
  77. package/dist/src/bundles/collision.js +0 -4
  78. package/dist/src/bundles/collision.js.map +0 -11
  79. package/dist/src/bundles/diagnostics.js +0 -5
  80. package/dist/src/bundles/diagnostics.js.map +0 -10
  81. package/dist/src/bundles/input.js +0 -4
  82. package/dist/src/bundles/input.js.map +0 -10
  83. package/dist/src/bundles/physics2D.js +0 -4
  84. package/dist/src/bundles/physics2D.js.map +0 -11
  85. package/dist/src/bundles/renderers/renderer2D.js +0 -4
  86. package/dist/src/bundles/renderers/renderer2D.js.map +0 -10
  87. package/dist/src/bundles/spatial-index.js +0 -4
  88. package/dist/src/bundles/spatial-index.js.map +0 -11
  89. package/dist/src/bundles/state-machine.js +0 -4
  90. package/dist/src/bundles/state-machine.js.map +0 -10
  91. package/dist/src/bundles/timers.js +0 -4
  92. package/dist/src/bundles/timers.js.map +0 -10
  93. package/dist/src/bundles/transform.js +0 -4
  94. package/dist/src/bundles/transform.js.map +0 -10
  95. package/dist/src/bundles/tween.js +0 -4
  96. package/dist/src/bundles/tween.js.map +0 -11
package/dist/bundle.d.ts DELETED
@@ -1,173 +0,0 @@
1
- import { SystemBuilderWithBundle } from './system-builder';
2
- import type ECSpresso from './ecspresso';
3
- import type { ResourceFactoryWithDeps } from './resource-manager';
4
- import type { AssetDefinition } from './asset-types';
5
- import type { ScreenDefinition } from './screen-types';
6
- import type { BundlesAreCompatible } from './type-utils';
7
- import type { QueryDefinition } from './types';
8
- /**
9
- * Bundle class that encapsulates a set of components, resources, events, and systems
10
- * that can be merged into a ECSpresso instance
11
- */
12
- export default class Bundle<ComponentTypes extends Record<string, any> = {}, EventTypes extends Record<string, any> = {}, ResourceTypes extends Record<string, any> = {}, AssetTypes extends Record<string, unknown> = {}, ScreenStates extends Record<string, ScreenDefinition<any, any>> = {}, Labels extends string = never, Groups extends string = never, AssetGroupNames extends string = never, ReactiveQueryNames extends string = never> {
13
- private _systems;
14
- private _resources;
15
- private _assets;
16
- private _assetGroups;
17
- private _screens;
18
- private _disposeCallbacks;
19
- private _requiredComponents;
20
- private _id;
21
- constructor(id?: string);
22
- /**
23
- * Get the unique ID of this bundle
24
- */
25
- get id(): string;
26
- /**
27
- * Set the ID of this bundle
28
- * @internal Used by combineBundles
29
- */
30
- set id(value: string);
31
- /**
32
- * Add a system to this bundle, by label (creating a new builder) or by reusing an existing one
33
- */
34
- addSystem<Q extends Record<string, QueryDefinition<ComponentTypes>>, BL extends string, BG extends string, L extends string, SG extends string>(builder: SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, Q, BL, BG, L, SG, any, any>): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, Q, Labels, Groups, L, SG, AssetGroupNames, ReactiveQueryNames>;
35
- addSystem<L extends string>(label: L): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, {}, Labels, Groups, L, never, AssetGroupNames, ReactiveQueryNames>;
36
- /**
37
- * Add a resource to this bundle
38
- * @param label The resource key
39
- * @param resource The resource value, a factory function, or a factory with dependencies
40
- */
41
- addResource<K extends keyof ResourceTypes>(label: K, resource: ResourceTypes[K] | ((ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates>) => ResourceTypes[K] | Promise<ResourceTypes[K]>) | ResourceFactoryWithDeps<ResourceTypes[K], ECSpresso<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates>, keyof ResourceTypes & string>): this;
42
- /**
43
- * Add an asset to this bundle
44
- * @param key The asset key
45
- * @param loader Function that loads and returns the asset
46
- * @param options Optional asset configuration
47
- */
48
- addAsset<K extends string, T>(key: K, loader: () => Promise<T>, options?: {
49
- eager?: boolean;
50
- group?: string;
51
- }): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes & Record<K, T>, ScreenStates, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
52
- /**
53
- * Add a group of assets to this bundle
54
- * @param groupName The group name
55
- * @param assets Object mapping asset keys to loader functions
56
- */
57
- addAssetGroup<GN extends string, T extends Record<string, () => Promise<unknown>>>(groupName: GN, assets: T): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes & {
58
- [K in keyof T]: Awaited<ReturnType<T[K]>>;
59
- }, ScreenStates, Labels, Groups, AssetGroupNames | GN, ReactiveQueryNames>;
60
- /**
61
- * Add a screen to this bundle
62
- * @param name The screen name
63
- * @param definition The screen definition
64
- */
65
- addScreen<K extends string, Config extends Record<string, unknown>, State extends Record<string, unknown>>(name: K, definition: ScreenDefinition<Config, State, ECSpresso<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, Record<string, ScreenDefinition>>>): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates & Record<K, ScreenDefinition<Config, State>>, Labels, Groups, AssetGroupNames, ReactiveQueryNames>;
66
- /**
67
- * Get all asset definitions in this bundle
68
- */
69
- getAssets(): Map<string, AssetDefinition<unknown>>;
70
- /**
71
- * Get all screen definitions in this bundle
72
- */
73
- getScreens(): Map<string, ScreenDefinition<any, any>>;
74
- /**
75
- * Register a dispose callback for a component type in this bundle.
76
- * Called when a component is removed (explicit removal, entity destruction, or replacement).
77
- * @param componentName The component type to register disposal for
78
- * @param callback Function receiving the component value being disposed
79
- * @returns This bundle for method chaining
80
- */
81
- registerDispose<K extends keyof ComponentTypes>(componentName: K, callback: (value: ComponentTypes[K]) => void): this;
82
- /**
83
- * Get all registered dispose callbacks in this bundle
84
- */
85
- getDisposeCallbacks(): Map<string, (value: unknown) => void>;
86
- /**
87
- * Register a required component relationship.
88
- * When an entity gains `trigger`, the `required` component is auto-added
89
- * (using `factory` for the default value) if not already present.
90
- * @param trigger The component whose presence triggers auto-addition
91
- * @param required The component to auto-add
92
- * @param factory Function that creates the default value for the required component
93
- * @returns This bundle for method chaining
94
- */
95
- registerRequired<Trigger extends keyof ComponentTypes, Required extends keyof ComponentTypes>(trigger: Trigger, required: Required, factory: (triggerValue: ComponentTypes[Trigger]) => ComponentTypes[Required]): this;
96
- /**
97
- * Declare reactive query names that this bundle will register at runtime.
98
- * This is a pure type-level operation with no runtime cost.
99
- */
100
- withReactiveQueryNames<N extends string>(): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, Labels, Groups, AssetGroupNames, ReactiveQueryNames | N>;
101
- /**
102
- * Get all registered required component mappings in this bundle
103
- */
104
- getRequiredComponents(): Map<string, Array<{
105
- component: string;
106
- factory: (triggerValue: any) => unknown;
107
- }>>;
108
- /**
109
- * Check for circular dependencies in the required components graph
110
- * @throws Error if adding the new edge would create a cycle
111
- */
112
- private _checkRequiredCycle;
113
- /**
114
- * Internal method to set a dispose callback
115
- * @internal Used by mergeBundles
116
- */
117
- _setDisposeCallback(name: string, callback: (value: unknown) => void): void;
118
- /**
119
- * Internal method to set a resource
120
- * @internal Used by mergeBundles
121
- */
122
- _setResource(key: string, value: unknown): void;
123
- /**
124
- * Internal method to set an asset definition
125
- * @internal Used by mergeBundles
126
- */
127
- _setAsset(key: string, definition: AssetDefinition<unknown>): void;
128
- /**
129
- * Internal method to set a screen definition
130
- * @internal Used by mergeBundles
131
- */
132
- _setScreen(name: string, definition: ScreenDefinition<any, any>): void;
133
- /**
134
- * Internal method to add a required component entry
135
- * @internal Used by mergeBundles
136
- */
137
- _addRequired(trigger: string, component: string, factory: (triggerValue: any) => unknown): void;
138
- /**
139
- * Get all systems defined in this bundle
140
- * Returns built System objects instead of SystemBuilders
141
- */
142
- getSystems(): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, any, any, any, any, any, any, any>[];
143
- /**
144
- * Register all systems in this bundle with an ECSpresso instance
145
- * @internal Used by ECSpresso when adding a bundle
146
- */
147
- registerSystemsWithEcspresso(ecspresso: ECSpresso<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates>): void;
148
- /**
149
- * Get all resources defined in this bundle
150
- */
151
- getResources(): Map<keyof ResourceTypes, ResourceTypes[keyof ResourceTypes]>;
152
- /**
153
- * Get a specific resource by key
154
- * @param key The resource key
155
- * @returns The resource value or undefined if not found
156
- */
157
- getResource<K extends keyof ResourceTypes>(key: K): ResourceTypes[K];
158
- /**
159
- * Get all system builders in this bundle
160
- */
161
- getSystemBuilders(): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, any, any, any, any, any, any, any>[];
162
- /**
163
- * Check if this bundle has a specific resource
164
- * @param key The resource key to check
165
- * @returns True if the resource exists
166
- */
167
- hasResource<K extends keyof ResourceTypes>(key: K): boolean;
168
- }
169
- /**
170
- * Function that merges multiple bundles into a single bundle
171
- */
172
- export declare function mergeBundles<C1 extends Record<string, any>, E1 extends Record<string, any>, R1 extends Record<string, any>, A1 extends Record<string, unknown>, S1 extends Record<string, ScreenDefinition<any, any>>, L1 extends string, G1 extends string, AG1 extends string, RQ1 extends string, C2 extends Record<string, any>, E2 extends Record<string, any>, R2 extends Record<string, any>, A2 extends Record<string, unknown>, S2 extends Record<string, ScreenDefinition<any, any>>, L2 extends string, G2 extends string, AG2 extends string, RQ2 extends string>(id: string, bundle1: Bundle<C1, E1, R1, A1, S1, L1, G1, AG1, RQ1>, bundle2: BundlesAreCompatible<C1, C2, E1, E2, R1, R2, A1, A2, S1, S2> extends true ? Bundle<C2, E2, R2, A2, S2, L2, G2, AG2, RQ2> : never): Bundle<C1 & C2, E1 & E2, R1 & R2, A1 & A2, S1 & S2, L1 | L2, G1 | G2, AG1 | AG2, RQ1 | RQ2>;
173
- export declare function mergeBundles<ComponentTypes extends Record<string, any>, EventTypes extends Record<string, any>, ResourceTypes extends Record<string, any>, AssetTypes extends Record<string, unknown>, ScreenStates extends Record<string, ScreenDefinition<any, any>>, Labels extends string, Groups extends string, AssetGroupNamesParam extends string, ReactiveQueryNamesParam extends string>(id: string, ...bundles: Array<Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, Labels, Groups, AssetGroupNamesParam, ReactiveQueryNamesParam>>): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates, Labels, Groups, AssetGroupNamesParam, ReactiveQueryNamesParam>;
@@ -1,173 +0,0 @@
1
- /**
2
- * Timer Bundle for ECSpresso
3
- *
4
- * Provides ECS-native timers following the "data, not callbacks" philosophy.
5
- * Timers are components processed each frame, automatically cleaned up when entities are removed.
6
- */
7
- import { Bundle } from 'ecspresso';
8
- import type { SystemPhase } from 'ecspresso';
9
- /**
10
- * Data structure published when a timer completes.
11
- * Use this type when defining timer completion events in your EventTypes interface.
12
- *
13
- * @example
14
- * ```typescript
15
- * interface Events {
16
- * hideMessage: TimerEventData;
17
- * spawnWave: TimerEventData;
18
- * }
19
- * ```
20
- */
21
- export interface TimerEventData {
22
- /** The entity ID that the timer belongs to */
23
- entityId: number;
24
- /** The timer's configured duration in seconds */
25
- duration: number;
26
- /** The actual elapsed time (may exceed duration slightly) */
27
- elapsed: number;
28
- }
29
- /**
30
- * Extracts event names from EventTypes that have TimerEventData as their payload.
31
- * This ensures only compatible events can be used with timer.onComplete.
32
- */
33
- export type TimerEventName<EventTypes extends Record<string, any>> = {
34
- [K in keyof EventTypes]: EventTypes[K] extends TimerEventData ? K : never;
35
- }[keyof EventTypes];
36
- /**
37
- * Timer component data structure.
38
- * Use `justFinished` to detect timer completion in your systems.
39
- *
40
- * @template EventTypes The event types from your ECS
41
- */
42
- export interface Timer<EventTypes extends Record<string, any>> {
43
- /** Time accumulated so far (seconds) */
44
- elapsed: number;
45
- /** Target duration (seconds) */
46
- duration: number;
47
- /** Whether timer repeats after completion */
48
- repeat: boolean;
49
- /** Whether timer is currently running */
50
- active: boolean;
51
- /** True for one frame after timer completes */
52
- justFinished: boolean;
53
- /** Optional event name to publish when timer completes. Must be an event with TimerEventData payload. */
54
- onComplete?: TimerEventName<EventTypes>;
55
- }
56
- /**
57
- * Component types provided by the timer bundle.
58
- * Included automatically via `.withBundle(createTimerBundle<Events>())`.
59
- * The EventTypes generic constrains which events can be used with `onComplete`.
60
- *
61
- * @template EventTypes The event types from your ECS
62
- *
63
- * @example
64
- * ```typescript
65
- * const ecs = ECSpresso.create()
66
- * .withBundle(createTimerBundle<{ respawn: TimerEventData }>())
67
- * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()
68
- * .build();
69
- * ```
70
- */
71
- export interface TimerComponentTypes<EventTypes extends Record<string, any>> {
72
- timer: Timer<EventTypes>;
73
- }
74
- /**
75
- * Configuration options for the timer bundle.
76
- */
77
- export interface TimerBundleOptions<G extends string = 'timers'> {
78
- /** System group name (default: 'timers') */
79
- systemGroup?: G;
80
- /** Priority for timer update system (default: 0) */
81
- priority?: number;
82
- /** Execution phase (default: 'preUpdate') */
83
- phase?: SystemPhase;
84
- }
85
- /**
86
- * Options for timer creation
87
- *
88
- * @template EventTypes The event types from your ECS
89
- */
90
- export interface TimerOptions<EventTypes extends Record<string, any>> {
91
- /** Event name to publish when timer completes. Must be an event with TimerEventData payload. */
92
- onComplete?: TimerEventName<EventTypes>;
93
- }
94
- /**
95
- * Create a one-shot timer that fires once after the specified duration.
96
- *
97
- * @template EventTypes The event types from your ECS (must be explicitly provided)
98
- * @param duration Duration in seconds until the timer completes
99
- * @param options Optional configuration including event name
100
- * @returns Component object suitable for spreading into spawn()
101
- *
102
- * @example
103
- * ```typescript
104
- * // Timer without event
105
- * ecs.spawn({
106
- * ...createTimer<GameEvents>(2),
107
- * explosion: true,
108
- * });
109
- *
110
- * // Timer that publishes an event on completion
111
- * ecs.spawn({
112
- * ...createTimer<GameEvents>(1.5, { onComplete: 'hideMessage' }),
113
- * });
114
- * ```
115
- */
116
- export declare function createTimer<EventTypes extends Record<string, any>>(duration: number, options?: TimerOptions<EventTypes>): Pick<TimerComponentTypes<EventTypes>, 'timer'>;
117
- /**
118
- * Create a repeating timer that fires every `duration` seconds.
119
- *
120
- * @template EventTypes The event types from your ECS (must be explicitly provided)
121
- * @param duration Duration in seconds between each timer completion
122
- * @param options Optional configuration including event name
123
- * @returns Component object suitable for spreading into spawn()
124
- *
125
- * @example
126
- * ```typescript
127
- * // Timer without event
128
- * ecs.spawn({
129
- * ...createRepeatingTimer<GameEvents>(5),
130
- * spawner: true,
131
- * });
132
- *
133
- * // Repeating timer that publishes an event each cycle
134
- * ecs.spawn({
135
- * ...createRepeatingTimer<GameEvents>(3, { onComplete: 'spawnWave' }),
136
- * });
137
- * ```
138
- */
139
- export declare function createRepeatingTimer<EventTypes extends Record<string, any>>(duration: number, options?: TimerOptions<EventTypes>): Pick<TimerComponentTypes<EventTypes>, 'timer'>;
140
- /**
141
- * Create a timer bundle for ECSpresso.
142
- *
143
- * This bundle provides:
144
- * - Timer update system that processes all timer components each frame
145
- * - `justFinished` flag pattern for one-frame completion detection
146
- * - Automatic cleanup when entities are removed
147
- *
148
- * @example
149
- * ```typescript
150
- * const ecs = ECSpresso
151
- * .create<Components, Events, Resources>()
152
- * .withBundle(createTimerBundle())
153
- * .build();
154
- *
155
- * // Spawn entity with timer
156
- * ecs.spawn({
157
- * ...createRepeatingTimer(5),
158
- * spawner: true,
159
- * });
160
- *
161
- * // React to timer completion in a system
162
- * ecs.addSystem('spawn-on-timer')
163
- * .addQuery('spawners', { with: ['timer', 'spawner'] })
164
- * .setProcess((queries, _dt, ecs) => {
165
- * for (const { components } of queries.spawners) {
166
- * if (components.timer.justFinished) {
167
- * ecs.spawn({ enemy: true });
168
- * }
169
- * }
170
- * });
171
- * ```
172
- */
173
- export declare function createTimerBundle<EventTypes extends Record<string, any>, G extends string = 'timers'>(options?: TimerBundleOptions<G>): Bundle<TimerComponentTypes<EventTypes>, EventTypes, {}, {}, {}, 'timer-update', G>;
@@ -1,4 +0,0 @@
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
@@ -1,10 +0,0 @@
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
- }
@@ -1,4 +0,0 @@
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
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/bundles/bounds.ts"],
4
- "sourcesContent": [
5
- "/**\n * Bounds Bundle for ECSpresso\n *\n * Provides screen bounds enforcement for entities with transforms.\n * Reads worldTransform for position checking; modifies localTransform for corrections.\n * Supports destroy, clamp, and wrap behaviors.\n */\n\nimport { Bundle } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\n\n// ==================== Component Types ====================\n\n/**\n * Component that marks an entity for destruction when outside bounds.\n */\nexport interface DestroyOutOfBounds {\n\t/** Extra padding beyond bounds before destruction (default: 0) */\n\tpadding?: number;\n}\n\n/**\n * Component that clamps an entity's position to stay within bounds.\n */\nexport interface ClampToBounds {\n\t/** Margin to shrink the valid area (default: 0) */\n\tmargin?: number;\n}\n\n/**\n * Component that wraps an entity's position to the opposite edge.\n */\nexport interface WrapAtBounds {\n\t/** Padding beyond bounds before wrapping (default: 0) */\n\tpadding?: number;\n}\n\n/**\n * Component types provided by the bounds bundle.\n * Included automatically via `.withBundle(createBoundsBundle())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withBundle(createTransformBundle())\n * .withBundle(createBoundsBundle({ width: 800, height: 600 }))\n * .withComponentTypes<{ sprite: Sprite }>()\n * .build();\n * ```\n */\nexport interface BoundsComponentTypes {\n\tdestroyOutOfBounds: DestroyOutOfBounds;\n\tclampToBounds: ClampToBounds;\n\twrapAtBounds: WrapAtBounds;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Bounds rectangle definition.\n */\nexport interface BoundsRect {\n\t/** Left edge x coordinate (default: 0) */\n\tx?: number;\n\t/** Top edge y coordinate (default: 0) */\n\ty?: number;\n\t/** Width of the bounds area */\n\twidth: number;\n\t/** Height of the bounds area */\n\theight: number;\n}\n\n/**\n * Resource types provided by the bounds bundle.\n */\nexport interface BoundsResourceTypes {\n\tbounds: BoundsRect;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when an entity exits bounds.\n */\nexport interface EntityOutOfBoundsEvent {\n\t/** The entity that exited bounds */\n\tentityId: number;\n\t/** The edge the entity exited through */\n\texitEdge: 'top' | 'bottom' | 'left' | 'right';\n}\n\n/**\n * Event types provided by the bounds bundle.\n */\nexport interface BoundsEventTypes {\n\tentityOutOfBounds: EntityOutOfBoundsEvent;\n}\n\n// ==================== Bundle Options ====================\n\n/**\n * Configuration options for the bounds bundle.\n */\nexport interface BoundsBundleOptions<G extends string = 'physics'> {\n\t/** System group name (default: 'physics') */\n\tsystemGroup?: G;\n\t/** Priority for bounds systems (default: 50) */\n\tpriority?: number;\n\t/** Resource key for bounds rectangle (default: 'bounds') */\n\tboundsResourceKey?: string;\n\t/** Whether to auto-remove entities when out of bounds (default: true) */\n\tautoRemove?: boolean;\n\t/** Execution phase (default: 'postUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a bounds rectangle resource.\n *\n * @param width The width of the bounds area\n * @param height The height of the bounds area\n * @param x The left edge x coordinate (default: 0)\n * @param y The top edge y coordinate (default: 0)\n * @returns Bounds rectangle suitable for use as a resource\n *\n * @example\n * ```typescript\n * ECSpresso.create()\n * .withResource('bounds', createBounds(800, 600))\n * .build();\n * ```\n */\nexport function createBounds(width: number, height: number, x?: number, y?: number): BoundsRect {\n\tconst bounds: BoundsRect = { width, height };\n\tif (x !== undefined) bounds.x = x;\n\tif (y !== undefined) bounds.y = y;\n\treturn bounds;\n}\n\n/**\n * Create a destroyOutOfBounds component.\n *\n * @param padding Extra padding beyond bounds before destruction\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createDestroyOutOfBounds(20),\n * });\n * ```\n */\nexport function createDestroyOutOfBounds(padding?: number): Pick<BoundsComponentTypes, 'destroyOutOfBounds'> {\n\treturn {\n\t\tdestroyOutOfBounds: padding !== undefined ? { padding } : {},\n\t};\n}\n\n/**\n * Create a clampToBounds component.\n *\n * @param margin Margin to shrink the valid area\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createClampToBounds(30),\n * });\n * ```\n */\nexport function createClampToBounds(margin?: number): Pick<BoundsComponentTypes, 'clampToBounds'> {\n\treturn {\n\t\tclampToBounds: margin !== undefined ? { margin } : {},\n\t};\n}\n\n/**\n * Create a wrapAtBounds component.\n *\n * @param padding Padding beyond bounds before wrapping\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createWrapAtBounds(10),\n * });\n * ```\n */\nexport function createWrapAtBounds(padding?: number): Pick<BoundsComponentTypes, 'wrapAtBounds'> {\n\treturn {\n\t\twrapAtBounds: padding !== undefined ? { padding } : {},\n\t};\n}\n\n// ==================== Internal Types ====================\n\ntype CombinedComponentTypes = BoundsComponentTypes & TransformComponentTypes;\n\n// ==================== Bundle Factory ====================\n\n/**\n * Create a bounds bundle for ECSpresso.\n *\n * This bundle provides:\n * - Destroy out of bounds system - removes entities that exit bounds\n * - Clamp to bounds system - constrains entities within bounds\n * - Wrap at bounds system - wraps entities to opposite edge\n *\n * Uses worldTransform for position checking (world-space) and modifies\n * localTransform for corrections. Works best with entities that don't\n * have parent transforms (orphan entities).\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withResource('bounds', createBounds(800, 600))\n * .withBundle(createTransformBundle())\n * .withBundle(createBoundsBundle())\n * .build();\n *\n * // Entity that gets destroyed when leaving screen\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createDestroyOutOfBounds(),\n * });\n * ```\n */\nexport function createBoundsBundle<ResourceTypes extends BoundsResourceTypes = BoundsResourceTypes, G extends string = 'physics'>(\n\toptions?: BoundsBundleOptions<G>\n): Bundle<CombinedComponentTypes, BoundsEventTypes, ResourceTypes, {}, {}, 'bounds-destroy' | 'bounds-clamp' | 'bounds-wrap', G> {\n\tconst {\n\t\tsystemGroup = 'physics',\n\t\tpriority = 50,\n\t\tboundsResourceKey = 'bounds',\n\t\tautoRemove = true,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\tconst bundle = new Bundle<CombinedComponentTypes, BoundsEventTypes, ResourceTypes>('bounds');\n\n\t// Destroy out of bounds system\n\tbundle\n\t\t.addSystem('bounds-destroy')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('entities', {\n\t\t\twith: ['worldTransform', 'destroyOutOfBounds'],\n\t\t})\n\t\t.setProcess((queries, _deltaTime, ecs) => {\n\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\tconst minX = bounds.x ?? 0;\n\t\t\tconst minY = bounds.y ?? 0;\n\t\t\tconst maxX = minX + bounds.width;\n\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\tfor (const entity of queries.entities) {\n\t\t\t\tconst { worldTransform, destroyOutOfBounds } = entity.components;\n\t\t\t\tconst padding = destroyOutOfBounds.padding ?? 0;\n\n\t\t\t\tconst exitEdge = getExitEdge(worldTransform, minX, minY, maxX, maxY, padding);\n\t\t\t\tif (!exitEdge) continue;\n\n\t\t\t\tecs.eventBus.publish('entityOutOfBounds', {\n\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\texitEdge,\n\t\t\t\t});\n\n\t\t\t\tif (autoRemove) {\n\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t// Clamp to bounds system\n\tbundle\n\t\t.addSystem('bounds-clamp')\n\t\t.setPriority(priority - 1)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('entities', {\n\t\t\twith: ['localTransform', 'worldTransform', 'clampToBounds'],\n\t\t})\n\t\t.setProcess((queries, _deltaTime, ecs) => {\n\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\tconst minX = bounds.x ?? 0;\n\t\t\tconst minY = bounds.y ?? 0;\n\t\t\tconst maxX = minX + bounds.width;\n\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\tfor (const entity of queries.entities) {\n\t\t\t\tconst { localTransform, worldTransform, clampToBounds } = entity.components;\n\t\t\t\tconst margin = clampToBounds.margin ?? 0;\n\n\t\t\t\tconst clampedMinX = minX + margin;\n\t\t\t\tconst clampedMinY = minY + margin;\n\t\t\t\tconst clampedMaxX = maxX - margin;\n\t\t\t\tconst clampedMaxY = maxY - margin;\n\n\t\t\t\t// Calculate world-space correction and apply to local transform\n\t\t\t\t// For entities without parents, this is equivalent to direct position clamping\n\t\t\t\tlet deltaX = 0;\n\t\t\t\tlet deltaY = 0;\n\n\t\t\t\tif (worldTransform.x < clampedMinX) deltaX = clampedMinX - worldTransform.x;\n\t\t\t\tif (worldTransform.x > clampedMaxX) deltaX = clampedMaxX - worldTransform.x;\n\t\t\t\tif (worldTransform.y < clampedMinY) deltaY = clampedMinY - worldTransform.y;\n\t\t\t\tif (worldTransform.y > clampedMaxY) deltaY = clampedMaxY - worldTransform.y;\n\n\t\t\t\tif (deltaX !== 0 || deltaY !== 0) {\n\t\t\t\t\tlocalTransform.x += deltaX;\n\t\t\t\t\tlocalTransform.y += deltaY;\n\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t// Wrap at bounds system\n\tbundle\n\t\t.addSystem('bounds-wrap')\n\t\t.setPriority(priority - 2)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('entities', {\n\t\t\twith: ['localTransform', 'worldTransform', 'wrapAtBounds'],\n\t\t})\n\t\t.setProcess((queries, _deltaTime, ecs) => {\n\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\tconst minX = bounds.x ?? 0;\n\t\t\tconst minY = bounds.y ?? 0;\n\t\t\tconst maxX = minX + bounds.width;\n\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\tfor (const entity of queries.entities) {\n\t\t\t\tconst { localTransform, worldTransform, wrapAtBounds } = entity.components;\n\t\t\t\tconst padding = wrapAtBounds.padding ?? 0;\n\n\t\t\t\tlet deltaX = 0;\n\t\t\t\tlet deltaY = 0;\n\t\t\t\tconst boundsWidth = maxX - minX;\n\t\t\t\tconst boundsHeight = maxY - minY;\n\n\t\t\t\t// Wrap horizontally\n\t\t\t\tif (worldTransform.x > maxX + padding) {\n\t\t\t\t\tdeltaX = -(boundsWidth + 2 * padding);\n\t\t\t\t} else if (worldTransform.x < minX - padding) {\n\t\t\t\t\tdeltaX = boundsWidth + 2 * padding;\n\t\t\t\t}\n\n\t\t\t\t// Wrap vertically\n\t\t\t\tif (worldTransform.y > maxY + padding) {\n\t\t\t\t\tdeltaY = -(boundsHeight + 2 * padding);\n\t\t\t\t} else if (worldTransform.y < minY - padding) {\n\t\t\t\t\tdeltaY = boundsHeight + 2 * padding;\n\t\t\t\t}\n\n\t\t\t\tif (deltaX !== 0 || deltaY !== 0) {\n\t\t\t\t\tlocalTransform.x += deltaX;\n\t\t\t\t\tlocalTransform.y += deltaY;\n\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\treturn bundle as Bundle<CombinedComponentTypes, BoundsEventTypes, ResourceTypes, {}, {}, 'bounds-destroy' | 'bounds-clamp' | 'bounds-wrap', G>;\n}\n\n/**\n * Determine which edge an entity has exited through, if any.\n */\nfunction getExitEdge(\n\ttransform: { x: number; y: number },\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tpadding: number\n): 'top' | 'bottom' | 'left' | 'right' | null {\n\tif (transform.x > maxX + padding) return 'right';\n\tif (transform.x < minX - padding) return 'left';\n\tif (transform.y > maxY + padding) return 'bottom';\n\tif (transform.y < minY - padding) return 'top';\n\treturn null;\n}\n"
6
- ],
7
- "mappings": "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",
8
- "debugId": "88C2C3EC79B631F264756E2164756E21",
9
- "names": []
10
- }
@@ -1,4 +0,0 @@
1
- var{defineProperty:T,getOwnPropertyNames:M,getOwnPropertyDescriptor:B}=Object,v=Object.prototype.hasOwnProperty;var q=new WeakMap,k=(j)=>{var C=q.get(j),A;if(C)return C;if(C=T({},"__esModule",{value:!0}),j&&typeof j==="object"||typeof j==="function")M(j).map((J)=>!v.call(C,J)&&T(C,J,{get:()=>j[J],enumerable:!(A=B(j,J))||A.enumerable}));return q.set(j,C),C};var X=(j,C)=>{for(var A in C)T(j,A,{get:C[A],enumerable:!0,configurable:!0,set:(J)=>C[A]=()=>J})};var Y=(j,C)=>()=>(j&&(C=j(j=0)),C);var g=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(C,A)=>(typeof require<"u"?require:C)[A]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{Bundle as F}from"ecspresso";var h={x:0,y:0,zoom:1,rotation:0},f={x:0,y:0,zoom:1,rotation:0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:800,viewportHeight:600};function y(j=0,C=0,A=1,J=0){return{camera:{x:j,y:C,zoom:A,rotation:J}}}function u(j,C){return{cameraFollow:{target:j,smoothing:C?.smoothing??5,deadzoneX:C?.deadzoneX??0,deadzoneY:C?.deadzoneY??0,offsetX:C?.offsetX??0,offsetY:C?.offsetY??0}}}function p(j){return{cameraShake:{trauma:j?.trauma??0,traumaDecay:j?.traumaDecay??1,maxOffsetX:j?.maxOffsetX??10,maxOffsetY:j?.maxOffsetY??10,maxRotation:j?.maxRotation??0.05}}}function d(j,C,A,J){return{cameraBounds:{minX:j,minY:C,maxX:A,maxY:J}}}function l(j,C,A){let J=j.entityManager.getComponent(C,"cameraShake");if(!J)return;J.trauma=Math.min(1,Math.max(0,J.trauma+A))}function m(j,C,A){let J=j-(A.x+A.shakeOffsetX),U=C-(A.y+A.shakeOffsetY),V=-(A.rotation+A.shakeRotation),P=Math.cos(V),Q=Math.sin(V),Z=J*P-U*Q,O=J*Q+U*P;return{x:Z*A.zoom+A.viewportWidth/2,y:O*A.zoom+A.viewportHeight/2}}function c(j,C,A){let J=(j-A.viewportWidth/2)/A.zoom,U=(C-A.viewportHeight/2)/A.zoom,V=A.rotation+A.shakeRotation,P=Math.cos(V),Q=Math.sin(V),Z=J*P-U*Q,O=J*Q+U*P;return{x:Z+A.x+A.shakeOffsetX,y:O+A.y+A.shakeOffsetY}}function i(j){let{viewportWidth:C=800,viewportHeight:A=600,systemGroup:J="camera",phase:U="postUpdate",randomFn:V=Math.random}=j??{},P=new F("camera");return P.addResource("cameraState",{x:0,y:0,zoom:1,rotation:0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:C,viewportHeight:A}),P.addSystem("camera-follow").setPriority(400).inPhase(U).inGroup(J).addQuery("cameras",{with:["camera","cameraFollow"]}).setProcess((Q,Z,O)=>{let E=O.entityManager,z=Math.min(1,Z);for(let N of Q.cameras){let{camera:L,cameraFollow:K}=N.components,R;try{R=E.getComponent(K.target,"worldTransform")}catch{continue}if(!R)continue;let D=R.x+K.offsetX,I=R.y+K.offsetY,_=D-L.x,$=I-L.y,W=Math.abs(_),b=Math.abs($);if(W>K.deadzoneX){let H=_>0?1:-1,S=_-H*K.deadzoneX,G=Math.min(1,K.smoothing*z);L.x+=S*G}if(b>K.deadzoneY){let H=$>0?1:-1,S=$-H*K.deadzoneY,G=Math.min(1,K.smoothing*z);L.y+=S*G}}}).and(),P.addSystem("camera-shake-update").setPriority(390).inPhase(U).inGroup(J).addQuery("shakeCameras",{with:["camera","cameraShake"]}).setProcess((Q,Z)=>{for(let O of Q.shakeCameras){let{cameraShake:E}=O.components;E.trauma=Math.max(0,E.trauma-E.traumaDecay*Z)}}).and(),P.addSystem("camera-bounds").setPriority(380).inPhase(U).inGroup(J).addQuery("boundedCameras",{with:["camera","cameraBounds"]}).setProcess((Q,Z,O)=>{let E=O.getResource("cameraState");for(let z of Q.boundedCameras){let{camera:N,cameraBounds:L}=z.components,K=E.viewportWidth/(2*N.zoom),R=E.viewportHeight/(2*N.zoom),D=L.minX+K,I=L.maxX-K,_=L.minY+R,$=L.maxY-R;if(D>I)N.x=(L.minX+L.maxX)/2;else N.x=Math.max(D,Math.min(I,N.x));if(_>$)N.y=(L.minY+L.maxY)/2;else N.y=Math.max(_,Math.min($,N.y))}}).and(),P.addSystem("camera-state-sync").setPriority(370).inPhase(U).inGroup(J).setProcess((Q,Z,O)=>{let E=O.getResource("cameraState"),N=O.getEntitiesWithQuery(["camera"])[0];if(!N){E.x=0,E.y=0,E.zoom=1,E.rotation=0,E.shakeOffsetX=0,E.shakeOffsetY=0,E.shakeRotation=0;return}let L=N.components.camera;E.x=L.x,E.y=L.y,E.zoom=L.zoom,E.rotation=L.rotation;let K=O.entityManager.getComponent(N.id,"cameraShake");if(K&&K.trauma>0){let R=K.trauma*K.trauma;E.shakeOffsetX=K.maxOffsetX*R*(V()*2-1),E.shakeOffsetY=K.maxOffsetY*R*(V()*2-1),E.shakeRotation=K.maxRotation*R*(V()*2-1)}else E.shakeOffsetX=0,E.shakeOffsetY=0,E.shakeRotation=0}).and(),P}export{m as worldToScreen,c as screenToWorld,p as createCameraShake,u as createCameraFollow,i as createCameraBundle,d as createCameraBounds,y as createCamera,l as addTrauma,f as DEFAULT_CAMERA_STATE,h as DEFAULT_CAMERA};
2
-
3
- //# debugId=4B3DDFC6FF7BF00364756E2164756E21
4
- //# sourceMappingURL=camera.js.map
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/bundles/camera.ts"],
4
- "sourcesContent": [
5
- "/**\n * Camera / Viewport Bundle for ECSpresso\n *\n * Provides a camera entity with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, and logical viewport dimensions.\n *\n * This bundle is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { Bundle } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\ntype CombinedComponentTypes = CameraComponentTypes & TransformComponentTypes;\n\n// ==================== Resource Types ====================\n\nexport interface CameraState {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Bundle Options ====================\n\nexport interface CameraBundleOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nexport const DEFAULT_CAMERA: Readonly<Camera> = {\n\tx: 0,\n\ty: 0,\n\tzoom: 1,\n\trotation: 0,\n};\n\nexport const DEFAULT_CAMERA_STATE: Readonly<CameraState> = {\n\tx: 0,\n\ty: 0,\n\tzoom: 1,\n\trotation: 0,\n\tshakeOffsetX: 0,\n\tshakeOffsetY: 0,\n\tshakeRotation: 0,\n\tviewportWidth: 800,\n\tviewportHeight: 600,\n};\n\n// ==================== Helper Functions ====================\n\nexport function createCamera(\n\tx = 0,\n\ty = 0,\n\tzoom = 1,\n\trotation = 0,\n): Pick<CameraComponentTypes, 'camera'> {\n\treturn {\n\t\tcamera: { x, y, zoom, rotation },\n\t};\n}\n\nexport function createCameraFollow(\n\ttarget: number,\n\toptions?: Partial<Omit<CameraFollow, 'target'>>,\n): Pick<CameraComponentTypes, 'cameraFollow'> {\n\treturn {\n\t\tcameraFollow: {\n\t\t\ttarget,\n\t\t\tsmoothing: options?.smoothing ?? 5,\n\t\t\tdeadzoneX: options?.deadzoneX ?? 0,\n\t\t\tdeadzoneY: options?.deadzoneY ?? 0,\n\t\t\toffsetX: options?.offsetX ?? 0,\n\t\t\toffsetY: options?.offsetY ?? 0,\n\t\t},\n\t};\n}\n\nexport function createCameraShake(\n\toptions?: Partial<CameraShake>,\n): Pick<CameraComponentTypes, 'cameraShake'> {\n\treturn {\n\t\tcameraShake: {\n\t\t\ttrauma: options?.trauma ?? 0,\n\t\t\ttraumaDecay: options?.traumaDecay ?? 1,\n\t\t\tmaxOffsetX: options?.maxOffsetX ?? 10,\n\t\t\tmaxOffsetY: options?.maxOffsetY ?? 10,\n\t\t\tmaxRotation: options?.maxRotation ?? 0.05,\n\t\t},\n\t};\n}\n\nexport function createCameraBounds(\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n): Pick<CameraComponentTypes, 'cameraBounds'> {\n\treturn {\n\t\tcameraBounds: { minX, minY, maxX, maxY },\n\t};\n}\n\nexport function addTrauma<\n\tC extends CombinedComponentTypes,\n\tR extends CameraResourceTypes,\n>(\n\tecs: ECSpresso<C, any, R>,\n\tentityId: number,\n\tamount: number,\n): void {\n\tconst shake = ecs.entityManager.getComponent(entityId, 'cameraShake');\n\tif (!shake) return;\n\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n}\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Bundle Factory ====================\n\nexport function createCameraBundle<G extends string = 'camera'>(\n\toptions?: CameraBundleOptions<G>,\n): Bundle<CombinedComponentTypes, {}, CameraResourceTypes, {}, {}, 'camera-follow' | 'camera-shake-update' | 'camera-bounds' | 'camera-state-sync', G> {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\tconst bundle = new Bundle<CombinedComponentTypes, {}, CameraResourceTypes>('camera');\n\n\tbundle.addResource('cameraState', {\n\t\tx: 0,\n\t\ty: 0,\n\t\tzoom: 1,\n\t\trotation: 0,\n\t\tshakeOffsetX: 0,\n\t\tshakeOffsetY: 0,\n\t\tshakeRotation: 0,\n\t\tviewportWidth,\n\t\tviewportHeight,\n\t});\n\n\t// camera-follow: priority 400 (after transform propagation at 500)\n\tbundle\n\t\t.addSystem('camera-follow')\n\t\t.setPriority(400)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('cameras', {\n\t\t\twith: ['camera', 'cameraFollow'],\n\t\t})\n\t\t.setProcess((queries, deltaTime, ecs) => {\n\t\t\tconst em = ecs.entityManager;\n\t\t\tconst t = Math.min(1, deltaTime);\n\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\tlet targetWorld;\n\t\t\t\ttry {\n\t\t\t\t\ttargetWorld = em.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!targetWorld) continue;\n\n\t\t\t\tconst goalX = targetWorld.x + cameraFollow.offsetX;\n\t\t\t\tconst goalY = targetWorld.y + cameraFollow.offsetY;\n\t\t\t\tconst dx = goalX - camera.x;\n\t\t\t\tconst dy = goalY - camera.y;\n\n\t\t\t\tconst absDx = Math.abs(dx);\n\t\t\t\tconst absDy = Math.abs(dy);\n\n\t\t\t\tif (absDx > cameraFollow.deadzoneX) {\n\t\t\t\t\tconst sign = dx > 0 ? 1 : -1;\n\t\t\t\t\tconst excessX = dx - sign * cameraFollow.deadzoneX;\n\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\tcamera.x += excessX * factor;\n\t\t\t\t}\n\t\t\t\tif (absDy > cameraFollow.deadzoneY) {\n\t\t\t\t\tconst sign = dy > 0 ? 1 : -1;\n\t\t\t\t\tconst excessY = dy - sign * cameraFollow.deadzoneY;\n\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\tcamera.y += excessY * factor;\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t// camera-shake-update: priority 390\n\tbundle\n\t\t.addSystem('camera-shake-update')\n\t\t.setPriority(390)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('shakeCameras', {\n\t\t\twith: ['camera', 'cameraShake'],\n\t\t})\n\t\t.setProcess((queries, deltaTime) => {\n\t\t\tfor (const entity of queries.shakeCameras) {\n\t\t\t\tconst { cameraShake } = entity.components;\n\t\t\t\tcameraShake.trauma = Math.max(0, cameraShake.trauma - cameraShake.traumaDecay * deltaTime);\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t// camera-bounds: priority 380\n\tbundle\n\t\t.addSystem('camera-bounds')\n\t\t.setPriority(380)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('boundedCameras', {\n\t\t\twith: ['camera', 'cameraBounds'],\n\t\t})\n\t\t.setProcess((queries, _deltaTime, ecs) => {\n\t\t\tconst state = ecs.getResource('cameraState');\n\t\t\tfor (const entity of queries.boundedCameras) {\n\t\t\t\tconst { camera, cameraBounds } = entity.components;\n\t\t\t\tconst halfW = state.viewportWidth / (2 * camera.zoom);\n\t\t\t\tconst halfH = state.viewportHeight / (2 * camera.zoom);\n\n\t\t\t\tconst effectiveMinX = cameraBounds.minX + halfW;\n\t\t\t\tconst effectiveMaxX = cameraBounds.maxX - halfW;\n\t\t\t\tconst effectiveMinY = cameraBounds.minY + halfH;\n\t\t\t\tconst effectiveMaxY = cameraBounds.maxY - halfH;\n\n\t\t\t\tif (effectiveMinX > effectiveMaxX) {\n\t\t\t\t\tcamera.x = (cameraBounds.minX + cameraBounds.maxX) / 2;\n\t\t\t\t} else {\n\t\t\t\t\tcamera.x = Math.max(effectiveMinX, Math.min(effectiveMaxX, camera.x));\n\t\t\t\t}\n\n\t\t\t\tif (effectiveMinY > effectiveMaxY) {\n\t\t\t\t\tcamera.y = (cameraBounds.minY + cameraBounds.maxY) / 2;\n\t\t\t\t} else {\n\t\t\t\t\tcamera.y = Math.max(effectiveMinY, Math.min(effectiveMaxY, camera.y));\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t// camera-state-sync: priority 370\n\tbundle\n\t\t.addSystem('camera-state-sync')\n\t\t.setPriority(370)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.setProcess((_queries, _deltaTime, ecs) => {\n\t\t\tconst state = ecs.getResource('cameraState');\n\t\t\tconst cameras = ecs.getEntitiesWithQuery(['camera']);\n\t\t\tconst first = cameras[0];\n\n\t\t\tif (!first) {\n\t\t\t\tstate.x = 0;\n\t\t\t\tstate.y = 0;\n\t\t\t\tstate.zoom = 1;\n\t\t\t\tstate.rotation = 0;\n\t\t\t\tstate.shakeOffsetX = 0;\n\t\t\t\tstate.shakeOffsetY = 0;\n\t\t\t\tstate.shakeRotation = 0;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst camera = first.components.camera;\n\t\t\tstate.x = camera.x;\n\t\t\tstate.y = camera.y;\n\t\t\tstate.zoom = camera.zoom;\n\t\t\tstate.rotation = camera.rotation;\n\n\t\t\tconst shake = ecs.entityManager.getComponent(first.id, 'cameraShake');\n\t\t\tif (shake && shake.trauma > 0) {\n\t\t\t\tconst intensity = shake.trauma * shake.trauma;\n\t\t\t\tstate.shakeOffsetX = shake.maxOffsetX * intensity * (randomFn() * 2 - 1);\n\t\t\t\tstate.shakeOffsetY = shake.maxOffsetY * intensity * (randomFn() * 2 - 1);\n\t\t\t\tstate.shakeRotation = shake.maxRotation * intensity * (randomFn() * 2 - 1);\n\t\t\t} else {\n\t\t\t\tstate.shakeOffsetX = 0;\n\t\t\t\tstate.shakeOffsetY = 0;\n\t\t\t\tstate.shakeRotation = 0;\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\treturn bundle as Bundle<CombinedComponentTypes, {}, CameraResourceTypes, {}, {}, 'camera-follow' | 'camera-shake-update' | 'camera-bounds' | 'camera-state-sync', G>;\n}\n"
6
- ],
7
- "mappings": "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",
8
- "debugId": "4B3DDFC6FF7BF00364756E2164756E21",
9
- "names": []
10
- }
@@ -1,4 +0,0 @@
1
- var{defineProperty:H,getOwnPropertyNames:A,getOwnPropertyDescriptor:k}=Object,v=Object.prototype.hasOwnProperty;var K=new WeakMap,f=(z)=>{var J=K.get(z),O;if(J)return J;if(J=H({},"__esModule",{value:!0}),z&&typeof z==="object"||typeof z==="function")A(z).map((Q)=>!v.call(J,Q)&&H(J,Q,{get:()=>z[Q],enumerable:!(O=k(z,Q))||O.enumerable}));return K.set(z,J),J};var u=(z,J)=>{for(var O in J)H(z,O,{get:J[O],enumerable:!0,configurable:!0,set:(Q)=>J[O]=()=>Q})};var h=(z,J)=>()=>(z&&(J=z(z=0)),J);var p=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,O)=>(typeof require<"u"?require:J)[O]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{Bundle as B}from"ecspresso";function W(z,J,O,Q,Z,$,N){if(!$&&!N)return null;let F={entityId:z,x:J,y:O,layer:Q,collidesWith:Z};if($)F.x+=$.offsetX??0,F.y+=$.offsetY??0,F.aabb={halfWidth:$.width/2,halfHeight:$.height/2};if(N)F.x+=N.offsetX??0,F.y+=N.offsetY??0,F.circle={radius:N.radius};return F}function q(z){return z("spatialIndex")??null}function C(z,J,O,Q,Z,$,N,F){let j=Z-z,M=$-J,V=O+N-Math.abs(j),E=Q+F-Math.abs(M);if(V<=0||E<=0)return null;if(V<E)return{normalX:j>=0?1:-1,normalY:0,depth:V};return{normalX:0,normalY:M>=0?1:-1,depth:E}}function X(z,J,O,Q,Z,$){let N=Q-z,F=Z-J,j=N*N+F*F,M=O+$;if(j>=M*M)return null;let V=Math.sqrt(j);if(V===0)return{normalX:1,normalY:0,depth:M};return{normalX:N/V,normalY:F/V,depth:M-V}}function T(z,J,O,Q,Z,$,N){let F=Math.max(z-O,Math.min(Z,z+O)),j=Math.max(J-Q,Math.min($,J+Q)),M=Z-F,V=$-j,E=M*M+V*V;if(E>=N*N)return null;if(E===0){let U=Z-(z-O),G=z+O-Z,D=$-(J-Q),P=J+Q-$,R=Math.min(U,G,D,P);if(R===G)return{normalX:1,normalY:0,depth:G+N};if(R===U)return{normalX:-1,normalY:0,depth:U+N};if(R===P)return{normalX:0,normalY:1,depth:P+N};return{normalX:0,normalY:-1,depth:D+N}}let _=Math.sqrt(E);return{normalX:M/_,normalY:V/_,depth:N-_}}function L(z,J){if(z.aabb&&J.aabb)return C(z.x,z.y,z.aabb.halfWidth,z.aabb.halfHeight,J.x,J.y,J.aabb.halfWidth,J.aabb.halfHeight);if(z.circle&&J.circle)return X(z.x,z.y,z.circle.radius,J.x,J.y,J.circle.radius);if(z.aabb&&J.circle)return T(z.x,z.y,z.aabb.halfWidth,z.aabb.halfHeight,J.x,J.y,J.circle.radius);if(z.circle&&J.aabb){let O=T(J.x,J.y,J.aabb.halfWidth,J.aabb.halfHeight,z.x,z.y,z.circle.radius);if(!O)return null;return{normalX:-O.normalX,normalY:-O.normalY,depth:O.depth}}return null}var g=new Set;function S(z,J,O,Q){if(J)m(z,J,O,Q);else Y(z,O,Q)}function Y(z,J,O){for(let Q=0;Q<z.length;Q++){let Z=z[Q];for(let $=Q+1;$<z.length;$++){let N=z[$];if(!Z.collidesWith.includes(N.layer)&&!N.collidesWith.includes(Z.layer))continue;let F=L(Z,N);if(!F)continue;J(Z,N,F,O)}}}function m(z,J,O,Q){let Z=new Map;for(let $=0;$<z.length;$++){let N=z[$];Z.set(N.entityId,N)}for(let $=0;$<z.length;$++){let N=z[$],F=N.aabb?N.aabb.halfWidth:N.circle?N.circle.radius:0,j=N.aabb?N.aabb.halfHeight:N.circle?N.circle.radius:0;g.clear(),J.queryRectInto(N.x-F,N.y-j,N.x+F,N.y+j,g);for(let M of g){if(M<=N.entityId)continue;let V=Z.get(M);if(!V)continue;if(!N.collidesWith.includes(V.layer)&&!V.collidesWith.includes(N.layer))continue;let E=L(N,V);if(!E)continue;O(N,V,E,Q)}}}function s(z,J,O,Q){let Z={width:z,height:J};if(O!==void 0)Z.offsetX=O;if(Q!==void 0)Z.offsetY=Q;return{aabbCollider:Z}}function r(z,J,O){let Q={radius:z};if(J!==void 0)Q.offsetX=J;if(O!==void 0)Q.offsetY=O;return{circleCollider:Q}}function w(z,J){return{collisionLayer:{layer:z,collidesWith:J}}}function l(z){let J={};for(let O of Object.keys(z)){let Q=z[O];J[O]=()=>w(O,Q)}return J}function I(z){let J=z.indexOf(":");if(J===-1)throw Error(`Invalid collision pair key "${z}": must contain a colon separator (e.g. "player:enemy")`);let O=z.slice(0,J),Q=z.slice(J+1);if(O===""||Q==="")throw Error(`Invalid collision pair key "${z}": layer names must not be empty`);return[O,Q]}function o(z){let J=new Map,O=new Set;for(let Q of Object.keys(z))I(Q),O.add(Q);for(let Q of Object.keys(z)){let[Z,$]=I(Q),N=z[Q];if(!N)continue;J.set(Q,{callback:N,swapped:!1});let F=`${$}:${Z}`;if(F!==Q&&!O.has(F))J.set(F,{callback:N,swapped:!0})}return function(Z,$){let N=J.get(Z.layerA+":"+Z.layerB);if(!N)return;if(N.swapped)N.callback(Z.entityB,Z.entityA,$);else N.callback(Z.entityA,Z.entityB,$)}}function x(z,J,O,Q){Q.publish("collision",{entityA:z.entityId,entityB:J.entityId,layerA:z.layer,layerB:J.layer,normal:{x:O.normalX,y:O.normalY},depth:O.depth})}function c(z){let{systemGroup:J="physics",priority:O=0,phase:Q="postUpdate"}=z,Z=new B("collision");return Z.addSystem("collision-detection").setPriority(O).inPhase(Q).inGroup(J).addQuery("collidables",{with:["worldTransform","collisionLayer"]}).setProcess(($,N,F)=>{let j=[];for(let V of $.collidables){let{worldTransform:E,collisionLayer:_}=V.components,U=W(V.id,E.x,E.y,_.layer,_.collidesWith,F.entityManager.getComponent(V.id,"aabbCollider"),F.entityManager.getComponent(V.id,"circleCollider"));if(U)j.push(U)}let M=q(F.tryGetResource.bind(F));S(j,M,x,F.eventBus)}).and(),Z}export{l as defineCollisionLayers,o as createCollisionPairHandler,w as createCollisionLayer,c as createCollisionBundle,r as createCircleCollider,s as createAABBCollider};
2
-
3
- //# debugId=7E185AB0A21012C364756E2164756E21
4
- //# sourceMappingURL=collision.js.map