emeraldengine 2.2.1 → 3.1.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 (208) hide show
  1. package/README.md +2198 -968
  2. package/dist/types/index.d.ts +75 -32
  3. package/dist/types/src/Animator.d.ts +50 -0
  4. package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
  5. package/dist/types/src/Camera.d.ts +122 -0
  6. package/dist/types/src/CameraController.d.ts +107 -0
  7. package/dist/types/src/CanvasText.d.ts +91 -0
  8. package/dist/types/src/CollisionLayers.d.ts +58 -0
  9. package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
  10. package/dist/types/src/Coroutine.d.ts +111 -0
  11. package/dist/types/src/DebugOverlay.d.ts +86 -0
  12. package/dist/types/src/Drawable.d.ts +271 -0
  13. package/dist/types/src/Easing.d.ts +22 -0
  14. package/dist/types/src/Emerald.d.ts +420 -0
  15. package/dist/types/src/EmeraldDB.d.ts +159 -0
  16. package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
  17. package/dist/types/src/GLUtils.d.ts +4 -0
  18. package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
  19. package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
  20. package/dist/types/src/Interpolator.d.ts +66 -0
  21. package/dist/types/src/Material.d.ts +87 -0
  22. package/dist/types/src/MathUtils.d.ts +81 -0
  23. package/dist/types/src/ParticleEmitter.d.ts +131 -0
  24. package/dist/types/src/Physics.d.ts +337 -0
  25. package/dist/types/src/Pool.d.ts +53 -0
  26. package/dist/types/src/PostEffects.d.ts +68 -0
  27. package/dist/types/src/PostProcessor.d.ts +124 -0
  28. package/dist/types/src/RenderTarget.d.ts +56 -0
  29. package/dist/types/src/Scene.d.ts +62 -0
  30. package/dist/types/src/ScreenEffects.d.ts +111 -0
  31. package/dist/types/src/Serializer.d.ts +86 -0
  32. package/dist/types/src/Shaders.d.ts +2 -0
  33. package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
  34. package/dist/types/src/SpatialGrid.d.ts +50 -0
  35. package/dist/types/src/SpriteBatch.d.ts +89 -0
  36. package/dist/types/src/StateMachine.d.ts +59 -0
  37. package/dist/types/src/Storage.d.ts +89 -0
  38. package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
  39. package/dist/types/src/TextureAtlas.d.ts +55 -0
  40. package/dist/types/src/Tilemap.d.ts +91 -0
  41. package/dist/types/src/Time.d.ts +53 -0
  42. package/dist/types/src/Timer.d.ts +54 -0
  43. package/dist/types/src/Transform.d.ts +94 -0
  44. package/dist/types/src/Tween.d.ts +81 -0
  45. package/dist/types/src/UI.d.ts +121 -0
  46. package/dist/types/src/components/Behaviour.d.ts +71 -0
  47. package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
  48. package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
  49. package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
  50. package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
  51. package/dist/types/src/components/Collider.d.ts +102 -0
  52. package/dist/types/src/components/GameObject.d.ts +135 -0
  53. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  54. package/dist/types/src/components/RigidBody.d.ts +456 -0
  55. package/dist/types/src/importers/Aseprite.d.ts +79 -0
  56. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  57. package/dist/types/src/importers/TiledMap.d.ts +62 -0
  58. package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
  59. package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
  60. package/dist/types/src/managers/AssetManager.d.ts +116 -0
  61. package/dist/types/src/managers/AudioManager.d.ts +259 -0
  62. package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
  63. package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
  64. package/dist/types/src/managers/GLManager.d.ts +84 -0
  65. package/dist/types/src/managers/GLState.d.ts +37 -0
  66. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  67. package/dist/types/src/managers/IDManager.d.ts +31 -0
  68. package/dist/types/src/managers/InputManager.d.ts +381 -0
  69. package/dist/types/src/managers/NetworkManager.d.ts +93 -0
  70. package/dist/types/src/managers/RenderStats.d.ts +34 -0
  71. package/dist/types/src/managers/SceneManager.d.ts +44 -0
  72. package/dist/types/src/managers/ShaderManager.d.ts +55 -0
  73. package/dist/types/src/managers/TextureManager.d.ts +102 -0
  74. package/dist/types/src/particlesystem/Particle.d.ts +64 -0
  75. package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
  76. package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
  77. package/dist/types/src/physics/AABB.d.ts +92 -0
  78. package/dist/types/src/physics/Body.d.ts +435 -0
  79. package/dist/types/src/physics/BodyType.d.ts +6 -0
  80. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  81. package/dist/types/src/physics/Collision.d.ts +102 -0
  82. package/dist/types/src/physics/Contact.d.ts +206 -0
  83. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  84. package/dist/types/src/physics/Distance.d.ts +54 -0
  85. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  86. package/dist/types/src/physics/Fixture.d.ts +221 -0
  87. package/dist/types/src/physics/Island.d.ts +52 -0
  88. package/dist/types/src/physics/Joint.d.ts +59 -0
  89. package/dist/types/src/physics/Math2D.d.ts +371 -0
  90. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  91. package/dist/types/src/physics/Settings.d.ts +22 -0
  92. package/dist/types/src/physics/Shapes.d.ts +207 -0
  93. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  94. package/dist/types/src/physics/World.d.ts +274 -0
  95. package/dist/types/src/physics/index.d.ts +34 -0
  96. package/index.js +78 -0
  97. package/package.json +73 -60
  98. package/src/Animator.js +95 -0
  99. package/src/BitmapText.js +6 -5
  100. package/src/Camera.js +183 -0
  101. package/src/CameraController.js +192 -0
  102. package/src/CanvasText.js +281 -0
  103. package/src/CollisionLayers.js +86 -0
  104. package/src/Color.js +26 -18
  105. package/src/Coroutine.js +259 -0
  106. package/src/DebugOverlay.js +246 -0
  107. package/src/Drawable.js +842 -582
  108. package/src/Easing.js +57 -0
  109. package/src/Emerald.js +1150 -459
  110. package/src/EmeraldDB.js +328 -0
  111. package/src/FPSCounter.js +43 -43
  112. package/src/GLUtils.js +60 -67
  113. package/src/Instance.js +41 -5
  114. package/src/InstancedTexture.js +308 -127
  115. package/src/Interpolator.js +124 -0
  116. package/src/Material.js +202 -0
  117. package/src/MathUtils.js +134 -0
  118. package/src/ParticleEmitter.js +284 -0
  119. package/src/Physics.js +431 -40
  120. package/src/Pool.js +85 -0
  121. package/src/PostEffects.js +296 -0
  122. package/src/PostProcessor.js +304 -0
  123. package/src/RenderTarget.js +134 -0
  124. package/src/Scene.js +115 -83
  125. package/src/ScreenEffects.js +266 -0
  126. package/src/Serializer.js +131 -0
  127. package/src/Shaders.js +150 -165
  128. package/src/Shapes.js +118 -129
  129. package/src/SpatialGrid.js +111 -0
  130. package/src/SpriteBatch.js +299 -0
  131. package/src/StateMachine.js +82 -0
  132. package/src/Storage.js +175 -47
  133. package/src/Texture.js +58 -67
  134. package/src/TextureAtlas.js +96 -0
  135. package/src/Tilemap.js +274 -0
  136. package/src/Time.js +51 -6
  137. package/src/Timer.js +99 -0
  138. package/src/Transform.js +100 -7
  139. package/src/Tween.js +160 -0
  140. package/src/UI.js +394 -0
  141. package/src/components/Behaviour.js +90 -0
  142. package/src/components/BoxCollider.js +29 -12
  143. package/src/components/BoxColliderDebug.js +3 -4
  144. package/src/components/CircleCollider.js +26 -12
  145. package/src/components/CircleColliderDebug.js +25 -24
  146. package/src/components/Collider.js +150 -34
  147. package/src/components/GameObject.js +130 -21
  148. package/src/components/PolygonCollider.js +55 -0
  149. package/src/components/RigidBody.js +556 -8
  150. package/src/importers/Aseprite.js +142 -0
  151. package/src/importers/ForgeLevel.js +581 -0
  152. package/src/importers/TiledMap.js +158 -0
  153. package/src/lights/DirectionalLight.js +6 -15
  154. package/src/lights/PointLight.js +4 -4
  155. package/src/managers/AssetManager.js +239 -0
  156. package/src/managers/AudioManager.js +565 -146
  157. package/src/managers/EventManager.js +488 -477
  158. package/src/managers/GLManager.js +57 -0
  159. package/src/managers/GLState.js +70 -0
  160. package/src/managers/Gamepad.js +126 -0
  161. package/src/managers/IDManager.js +24 -2
  162. package/src/managers/InputManager.js +905 -0
  163. package/src/managers/NetworkManager.js +178 -0
  164. package/src/managers/RenderStats.js +34 -0
  165. package/src/managers/SceneManager.js +30 -0
  166. package/src/managers/ShaderManager.js +0 -2
  167. package/src/managers/TextureManager.js +218 -0
  168. package/src/particlesystem/Particle.js +82 -7
  169. package/src/particlesystem/ParticleSettings.js +21 -3
  170. package/src/particlesystem/Particles.js +80 -31
  171. package/src/physics/AABB.js +207 -0
  172. package/src/physics/Body.js +862 -0
  173. package/src/physics/BodyType.js +16 -0
  174. package/src/physics/BroadPhase.js +641 -0
  175. package/src/physics/Collision.js +534 -0
  176. package/src/physics/Contact.js +500 -0
  177. package/src/physics/ContactSolver.js +526 -0
  178. package/src/physics/Distance.js +403 -0
  179. package/src/physics/DistanceJoint.js +227 -0
  180. package/src/physics/Fixture.js +346 -0
  181. package/src/physics/Island.js +203 -0
  182. package/src/physics/Joint.js +78 -0
  183. package/src/physics/Math2D.js +573 -0
  184. package/src/physics/RevoluteJoint.js +278 -0
  185. package/src/physics/Settings.js +78 -0
  186. package/src/physics/Shapes.js +549 -0
  187. package/src/physics/TimeOfImpact.js +87 -0
  188. package/src/physics/World.js +731 -0
  189. package/src/physics/index.js +79 -0
  190. package/dist/types/Drawable.d.ts +0 -157
  191. package/dist/types/Emerald.d.ts +0 -73
  192. package/dist/types/GLUtils.d.ts +0 -6
  193. package/dist/types/Physics.d.ts +0 -139
  194. package/dist/types/Scene.d.ts +0 -39
  195. package/dist/types/Shaders.d.ts +0 -4
  196. package/dist/types/Storage.d.ts +0 -46
  197. package/dist/types/Time.d.ts +0 -22
  198. package/dist/types/Transform.d.ts +0 -41
  199. package/dist/types/components/BoxColliderDebug.d.ts +0 -19
  200. package/dist/types/components/CircleColliderDebug.d.ts +0 -19
  201. package/dist/types/components/Collider.d.ts +0 -53
  202. package/dist/types/components/GameObject.d.ts +0 -72
  203. package/dist/types/components/RigidBody.d.ts +0 -104
  204. package/dist/types/managers/AudioManager.d.ts +0 -60
  205. package/dist/types/managers/GLManager.d.ts +0 -47
  206. package/dist/types/managers/IDManager.d.ts +0 -21
  207. package/dist/types/managers/SceneManager.d.ts +0 -22
  208. package/dist/types/particlesystem/Particle.d.ts +0 -42
@@ -1,146 +1,565 @@
1
- /**
2
- * @class AudioManager
3
- * @description Manages the audio for the game
4
- */
5
- class AudioManager {
6
- constructor() {
7
- this.sounds = new Map();
8
- }
9
-
10
- /**
11
- * @method add
12
- * @description Adds an audio file to the manager
13
- * @param {string} path - The path to the audio file
14
- * @param {string} name - The name of the audio file
15
- */
16
- add(path, name) {
17
- if (!this.sounds.has(name)) {
18
- const audio = new Audio(path);
19
- this.sounds.set(name, {
20
- audio,
21
- id: Math.random().toString(36).substring(7),
22
- playPromise: null,
23
- });
24
- return true;
25
- }
26
- return false;
27
- }
28
-
29
- /**
30
- * @method remove
31
- * @description Removes an audio file from the manager
32
- * @param {string} name - The name of the audio file
33
- */
34
- remove(name) {
35
- return this.sounds.delete(name);
36
- }
37
-
38
- /**
39
- * @method play
40
- * @description Plays an audio file
41
- * @param {string} name - The name of the audio file
42
- */
43
- async play(name) {
44
- const sound = this.sounds.get(name);
45
- if (sound) {
46
- try {
47
- // Wait for any existing play promise to resolve before starting a new one
48
- if (sound.playPromise) {
49
- await sound.playPromise;
50
- }
51
-
52
- sound.audio.pause();
53
- sound.audio.currentTime = 0;
54
-
55
- // Store the play promise to handle future conflicts
56
- sound.playPromise = sound.audio.play();
57
- await sound.playPromise;
58
-
59
- // Clear the promise once it's resolved
60
- sound.playPromise = null;
61
- return true;
62
- } catch (error) {
63
- // Handle the interrupt error gracefully
64
- console.warn(`Audio play interrupted for ${name}:`, error);
65
- sound.playPromise = null;
66
- return false;
67
- }
68
- }
69
- return false;
70
- }
71
-
72
- /**
73
- * @method stop
74
- * @description Stops an audio file
75
- * @param {string} name - The name of the audio file
76
- */
77
- async stop(name) {
78
- const sound = this.sounds.get(name);
79
- if (sound) {
80
- try {
81
- // Wait for any existing play promise to resolve before stopping
82
- if (sound.playPromise) {
83
- await sound.playPromise;
84
- }
85
-
86
- sound.audio.pause();
87
- sound.audio.currentTime = 0;
88
- sound.playPromise = null;
89
- return true;
90
- } catch (error) {
91
- // Handle any errors gracefully
92
- console.warn(`Audio stop interrupted for ${name}:`, error);
93
- sound.audio.pause();
94
- sound.audio.currentTime = 0;
95
- sound.playPromise = null;
96
- return false;
97
- }
98
- }
99
- return false;
100
- }
101
-
102
- /**
103
- * @method stopAll
104
- * @description Stops all audio files
105
- */
106
- async stopAll() {
107
- const stopPromises = Array.from(this.sounds.values()).map(async (sound) => {
108
- try {
109
- if (sound.playPromise) {
110
- await sound.playPromise;
111
- }
112
- sound.audio.pause();
113
- sound.audio.currentTime = 0;
114
- sound.playPromise = null;
115
- } catch (error) {
116
- console.warn("Audio stop interrupted:", error);
117
- sound.audio.pause();
118
- sound.audio.currentTime = 0;
119
- sound.playPromise = null;
120
- }
121
- });
122
-
123
- await Promise.all(stopPromises);
124
- }
125
-
126
- /**
127
- * @method playExclusive
128
- * @description Plays an audio file after stopping all other audio files
129
- * @param {string} name - The name of the audio file
130
- */
131
- async playExclusive(name) {
132
- await this.stopAll();
133
- return this.play(name);
134
- }
135
-
136
- /**
137
- * @method getSound
138
- * @description Returns an audio file from the manager
139
- * @param {string} name - The name of the audio file
140
- */
141
- getSound(name) {
142
- return this.sounds.get(name);
143
- }
144
- }
145
-
146
- export default AudioManager;
1
+ /**
2
+ * @class AudioManager
3
+ * @description Manages audio for the game with per-sound volume, looping,
4
+ * overlapping one-shot playback for sound effects, named mix buses
5
+ * (master / music / sfx and any custom bus) and volume fades / crossfades.
6
+ *
7
+ * Final playback volume for a sound is
8
+ * `masterVolume * busVolume(sound.bus) * sound.volume`, so an options menu can
9
+ * expose independent Music / SFX sliders that route through the buses.
10
+ *
11
+ * Fades are advanced by `update(dt)`. When `autoTick` is true (the default) and
12
+ * `requestAnimationFrame` exists, the manager drives its own fades and you do
13
+ * NOT need to call `update`. Pass `{ autoTick: false }` to advance fades
14
+ * manually (e.g. from your game loop or in tests).
15
+ *
16
+ * @param {Object} [options]
17
+ * @param {boolean} [options.autoTick=true] - Self-drive fades via requestAnimationFrame
18
+ */
19
+ class AudioManager {
20
+ constructor(options = {}) {
21
+ this.sounds = new Map();
22
+ this.masterVolume = 1.0;
23
+
24
+ this.buses = new Map([
25
+ ["music", 1.0],
26
+ ["sfx", 1.0],
27
+ ]);
28
+ this.defaultBus = "sfx";
29
+
30
+ this.listener = { x: 0, y: 0 };
31
+ this.refDistance = 100;
32
+ this.maxDistance = 800;
33
+ /** @private */
34
+ this._audioCtx = null;
35
+
36
+ /** @private */
37
+ this._fades = [];
38
+ this.autoTick = options.autoTick !== false;
39
+ /** @private */
40
+ this._tickId = null;
41
+ /** @private */
42
+ this._lastTick = 0;
43
+ }
44
+
45
+ /**
46
+ * @method getBusVolume
47
+ * @description Returns the 0..1 volume of a named bus (1 if it doesn't exist).
48
+ * @param {string} bus - The bus name (e.g. "music", "sfx")
49
+ * @returns {number}
50
+ */
51
+ getBusVolume(bus) {
52
+ const v = this.buses.get(bus);
53
+ return v == null ? 1 : v;
54
+ }
55
+
56
+ /**
57
+ * @method setBusVolume
58
+ * @description Sets the volume (0..1) of a named bus, creating it if needed,
59
+ * and applies it immediately to any currently-playing tracked sounds on it.
60
+ * @param {string} bus - The bus name
61
+ * @param {number} volume - The volume (clamped to 0..1)
62
+ */
63
+ setBusVolume(bus, volume) {
64
+ this.buses.set(bus, Math.max(0, Math.min(1, volume)));
65
+ this._applyLiveVolumes(bus);
66
+ }
67
+
68
+ /**
69
+ * @method setSoundBus
70
+ * @description Routes a registered sound through a named bus.
71
+ * @param {string} name - The registered sound name
72
+ * @param {string} bus - The bus name
73
+ */
74
+ setSoundBus(name, bus) {
75
+ const sound = this.sounds.get(name);
76
+ if (sound) sound.bus = bus;
77
+ }
78
+
79
+ /**
80
+ * @method _busVolumeFor
81
+ * @description Combined master * bus multiplier for a sound (before fade/per-sound volume).
82
+ * @private
83
+ */
84
+ _busVolumeFor(sound) {
85
+ return this.masterVolume * this.getBusVolume(sound.bus || this.defaultBus);
86
+ }
87
+
88
+ /**
89
+ * @method _effectiveVolume
90
+ * @description Final 0..1 volume for a tracked sound element.
91
+ * @private
92
+ */
93
+ _effectiveVolume(sound) {
94
+ return this._busVolumeFor(sound) * sound.volume * (sound._fadeMul ?? 1);
95
+ }
96
+
97
+ /**
98
+ * @method _applyLiveVolumes
99
+ * @description Re-applies the effective volume to the primary element of
100
+ * tracked sounds (optionally filtered to one bus). Overlapping one-shots are
101
+ * fire-and-forget and not retargeted.
102
+ * @private
103
+ */
104
+ _applyLiveVolumes(bus) {
105
+ for (const sound of this.sounds.values()) {
106
+ if (bus != null && (sound.bus || this.defaultBus) !== bus) continue;
107
+ sound.audio.volume = this._effectiveVolume(sound);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @method setListener
113
+ * @description Sets the world-space listener position (usually the camera or
114
+ * player) used by playSpatial.
115
+ * @param {number} x
116
+ * @param {number} y
117
+ */
118
+ setListener(x, y) {
119
+ this.listener.x = x;
120
+ this.listener.y = y;
121
+ }
122
+
123
+ /**
124
+ * @method setSpatialRange
125
+ * @description Configures the distance attenuation model for playSpatial.
126
+ * @param {number} refDistance - Distance within which volume is full
127
+ * @param {number} maxDistance - Distance at/after which the sound is silent
128
+ */
129
+ setSpatialRange(refDistance, maxDistance) {
130
+ this.refDistance = Math.max(0, refDistance);
131
+ this.maxDistance = Math.max(this.refDistance + 0.0001, maxDistance);
132
+ }
133
+
134
+ /**
135
+ * @method computeSpatial
136
+ * @description Pure helper: returns { volume, pan } for a source at `position`
137
+ * relative to the current listener and distance model. `volume` is 0..1 (before
138
+ * master/per-sound multipliers), `pan` is -1 (left) .. 1 (right). Exposed for
139
+ * testing and custom routing.
140
+ * @param {{x:number,y:number}} position
141
+ * @returns {{volume:number, pan:number, distance:number}}
142
+ */
143
+ computeSpatial(position) {
144
+ const dx = position.x - this.listener.x;
145
+ const dy = position.y - this.listener.y;
146
+ const distance = Math.sqrt(dx * dx + dy * dy);
147
+
148
+ let volume;
149
+ if (distance <= this.refDistance) {
150
+ volume = 1;
151
+ } else if (distance >= this.maxDistance) {
152
+ volume = 0;
153
+ } else {
154
+ volume =
155
+ 1 -
156
+ (distance - this.refDistance) / (this.maxDistance - this.refDistance);
157
+ }
158
+
159
+ const pan = Math.max(-1, Math.min(1, dx / this.maxDistance));
160
+ return { volume, pan, distance };
161
+ }
162
+
163
+ /**
164
+ * @method playSpatial
165
+ * @description Plays a one-shot, overlapping copy of a sound positioned in the
166
+ * world: volume falls off with distance from the listener and the sound pans
167
+ * left/right. Uses Web Audio for true stereo panning when available, falling
168
+ * back to volume-only otherwise.
169
+ * @param {string} name - The registered sound name
170
+ * @param {{x:number,y:number}} position - World-space source position
171
+ * @returns {boolean} - Whether playback started
172
+ */
173
+ playSpatial(name, position) {
174
+ const sound = this.sounds.get(name);
175
+ if (!sound) return false;
176
+
177
+ const { volume, pan } = this.computeSpatial(position);
178
+ if (volume <= 0) return false;
179
+
180
+ const node = sound.audio.cloneNode();
181
+ node.loop = false;
182
+ const finalVolume = this._busVolumeFor(sound) * sound.volume * volume;
183
+
184
+ const ctx = this._getAudioContext();
185
+ if (ctx && typeof ctx.createStereoPanner === "function") {
186
+ try {
187
+ const source = ctx.createMediaElementSource(node);
188
+ const panner = ctx.createStereoPanner();
189
+ const gain = ctx.createGain();
190
+ panner.pan.value = pan;
191
+ gain.gain.value = finalVolume;
192
+ source.connect(panner);
193
+ panner.connect(gain);
194
+ gain.connect(ctx.destination);
195
+ node
196
+ .play()
197
+ .catch((error) =>
198
+ console.warn(`Spatial audio play interrupted for ${name}:`, error)
199
+ );
200
+ return true;
201
+ } catch (err) {}
202
+ }
203
+
204
+ node.volume = finalVolume;
205
+ const result = node.play();
206
+ if (result && typeof result.catch === "function") {
207
+ result.catch((error) =>
208
+ console.warn(`Spatial audio play interrupted for ${name}:`, error)
209
+ );
210
+ }
211
+ return true;
212
+ }
213
+
214
+ /** @private */
215
+ _getAudioContext() {
216
+ if (this._audioCtx) return this._audioCtx;
217
+ const Ctx =
218
+ typeof window !== "undefined" &&
219
+ (window.AudioContext || window.webkitAudioContext);
220
+ if (!Ctx) return null;
221
+ try {
222
+ this._audioCtx = new Ctx();
223
+ } catch (err) {
224
+ this._audioCtx = null;
225
+ }
226
+ return this._audioCtx;
227
+ }
228
+
229
+ /**
230
+ * @method add
231
+ * @description Adds an audio file to the manager
232
+ * @param {string} path - The path to the audio file
233
+ * @param {string} name - The name of the audio file
234
+ * @param {Object} [options] - { volume = 1, loop = false, bus = "sfx" }
235
+ */
236
+ add(path, name, options = {}) {
237
+ if (!this.sounds.has(name)) {
238
+ const { volume = 1, loop = false, bus = this.defaultBus } = options;
239
+ const audio = new Audio(path);
240
+ audio.loop = loop;
241
+ this.sounds.set(name, {
242
+ audio,
243
+ path,
244
+ volume,
245
+ loop,
246
+ bus,
247
+ _fadeMul: 1,
248
+ id: Math.random().toString(36).substring(7),
249
+ playPromise: null,
250
+ });
251
+ return true;
252
+ }
253
+ return false;
254
+ }
255
+
256
+ /**
257
+ * @method remove
258
+ * @description Removes an audio file from the manager
259
+ * @param {string} name - The name of the audio file
260
+ */
261
+ remove(name) {
262
+ return this.sounds.delete(name);
263
+ }
264
+
265
+ /**
266
+ * @method setMasterVolume
267
+ * @description Sets the master volume applied to all sounds (0..1)
268
+ * @param {number} volume - The master volume
269
+ */
270
+ setMasterVolume(volume) {
271
+ this.masterVolume = Math.max(0, Math.min(1, volume));
272
+ this._applyLiveVolumes();
273
+ }
274
+
275
+ /**
276
+ * @method setVolume
277
+ * @description Sets the per-sound volume (0..1)
278
+ * @param {string} name - The name of the audio file
279
+ * @param {number} volume - The volume
280
+ */
281
+ setVolume(name, volume) {
282
+ const sound = this.sounds.get(name);
283
+ if (sound) sound.volume = Math.max(0, Math.min(1, volume));
284
+ }
285
+
286
+ /**
287
+ * @method setLoop
288
+ * @description Sets whether a sound loops
289
+ * @param {string} name - The name of the audio file
290
+ * @param {boolean} loop - Whether to loop
291
+ */
292
+ setLoop(name, loop) {
293
+ const sound = this.sounds.get(name);
294
+ if (sound) {
295
+ sound.loop = loop;
296
+ sound.audio.loop = loop;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * @method play
302
+ * @description Plays an audio file from the start
303
+ * @param {string} name - The name of the audio file
304
+ */
305
+ async play(name) {
306
+ const sound = this.sounds.get(name);
307
+ if (sound) {
308
+ try {
309
+ if (sound.playPromise) {
310
+ await sound.playPromise;
311
+ }
312
+
313
+ sound.audio.pause();
314
+ sound.audio.currentTime = 0;
315
+ sound.audio.volume = this._effectiveVolume(sound);
316
+ sound.audio.loop = sound.loop;
317
+
318
+ sound.playPromise = sound.audio.play();
319
+ await sound.playPromise;
320
+
321
+ sound.playPromise = null;
322
+ return true;
323
+ } catch (error) {
324
+ console.warn(`Audio play interrupted for ${name}:`, error);
325
+ sound.playPromise = null;
326
+ return false;
327
+ }
328
+ }
329
+ return false;
330
+ }
331
+
332
+ /**
333
+ * @method playOverlap
334
+ * @description Plays a one-shot copy of the sound that can overlap with other
335
+ * instances of the same sound (ideal for rapid sound effects). The cloned
336
+ * node is not tracked or interruptible.
337
+ * @param {string} name - The name of the audio file
338
+ */
339
+ playOverlap(name) {
340
+ const sound = this.sounds.get(name);
341
+ if (!sound) return false;
342
+ const node = sound.audio.cloneNode();
343
+ node.volume = this._effectiveVolume(sound);
344
+ node.loop = false;
345
+ const result = node.play();
346
+ if (result && typeof result.catch === "function") {
347
+ result.catch((error) =>
348
+ console.warn(`Audio overlap play interrupted for ${name}:`, error)
349
+ );
350
+ }
351
+ return true;
352
+ }
353
+
354
+ /**
355
+ * @method stop
356
+ * @description Stops an audio file
357
+ * @param {string} name - The name of the audio file
358
+ */
359
+ async stop(name) {
360
+ const sound = this.sounds.get(name);
361
+ if (sound) {
362
+ try {
363
+ if (sound.playPromise) {
364
+ await sound.playPromise;
365
+ }
366
+
367
+ sound.audio.pause();
368
+ sound.audio.currentTime = 0;
369
+ sound.playPromise = null;
370
+ return true;
371
+ } catch (error) {
372
+ console.warn(`Audio stop interrupted for ${name}:`, error);
373
+ sound.audio.pause();
374
+ sound.audio.currentTime = 0;
375
+ sound.playPromise = null;
376
+ return false;
377
+ }
378
+ }
379
+ return false;
380
+ }
381
+
382
+ /**
383
+ * @method stopAll
384
+ * @description Stops all audio files
385
+ */
386
+ async stopAll() {
387
+ const stopPromises = Array.from(this.sounds.values()).map(async (sound) => {
388
+ try {
389
+ if (sound.playPromise) {
390
+ await sound.playPromise;
391
+ }
392
+ sound.audio.pause();
393
+ sound.audio.currentTime = 0;
394
+ sound.playPromise = null;
395
+ } catch (error) {
396
+ console.warn("Audio stop interrupted:", error);
397
+ sound.audio.pause();
398
+ sound.audio.currentTime = 0;
399
+ sound.playPromise = null;
400
+ }
401
+ });
402
+
403
+ await Promise.all(stopPromises);
404
+ }
405
+
406
+ /**
407
+ * @method playExclusive
408
+ * @description Plays an audio file after stopping all other audio files
409
+ * @param {string} name - The name of the audio file
410
+ */
411
+ async playExclusive(name) {
412
+ await this.stopAll();
413
+ return this.play(name);
414
+ }
415
+
416
+ /**
417
+ * @method fadeTo
418
+ * @description Smoothly fades a sound's volume multiplier to a target (0..1)
419
+ * over `duration` seconds. Starting playback first (e.g. `play(name)`) and
420
+ * fading from 0 gives a fade-in; fading to 0 then stopping gives a fade-out.
421
+ * @param {string} name - The registered sound name
422
+ * @param {number} target - Target multiplier (0..1)
423
+ * @param {number} duration - Fade time in seconds (<=0 applies instantly)
424
+ * @param {Function} [onDone] - Called when the fade completes
425
+ * @returns {boolean} - Whether the sound exists
426
+ */
427
+ fadeTo(name, target, duration, onDone) {
428
+ const sound = this.sounds.get(name);
429
+ if (!sound) return false;
430
+ const to = Math.max(0, Math.min(1, target));
431
+ this._removeFadesFor(name);
432
+ if (!(duration > 0)) {
433
+ sound._fadeMul = to;
434
+ sound.audio.volume = this._effectiveVolume(sound);
435
+ if (onDone) onDone();
436
+ return true;
437
+ }
438
+ this._fades.push({
439
+ name,
440
+ sound,
441
+ from: sound._fadeMul ?? 1,
442
+ to,
443
+ t: 0,
444
+ duration,
445
+ onDone: onDone || null,
446
+ });
447
+ this._maybeStartTicker();
448
+ return true;
449
+ }
450
+
451
+ /**
452
+ * @method fadeIn
453
+ * @description Convenience: sets the fade multiplier to 0, starts playback and
454
+ * fades up to full over `duration` seconds.
455
+ * @param {string} name - The registered sound name
456
+ * @param {number} duration - Fade time in seconds
457
+ */
458
+ fadeIn(name, duration) {
459
+ const sound = this.sounds.get(name);
460
+ if (!sound) return false;
461
+ sound._fadeMul = 0;
462
+ sound.audio.volume = 0;
463
+ this.play(name);
464
+ return this.fadeTo(name, 1, duration);
465
+ }
466
+
467
+ /**
468
+ * @method fadeOut
469
+ * @description Fades a sound down to silence over `duration` seconds, then
470
+ * stops it (and resets its fade multiplier to 1 for next time).
471
+ * @param {string} name - The registered sound name
472
+ * @param {number} duration - Fade time in seconds
473
+ */
474
+ fadeOut(name, duration) {
475
+ return this.fadeTo(name, 0, duration, () => {
476
+ this.stop(name);
477
+ const sound = this.sounds.get(name);
478
+ if (sound) sound._fadeMul = 1;
479
+ });
480
+ }
481
+
482
+ /**
483
+ * @method crossfade
484
+ * @description Fades `fromName` out while fading `toName` in over `duration`
485
+ * seconds (ideal for switching music tracks). Starts `toName` playing.
486
+ * @param {string} fromName - Track to fade out (may be null)
487
+ * @param {string} toName - Track to fade in
488
+ * @param {number} duration - Fade time in seconds
489
+ */
490
+ crossfade(fromName, toName, duration) {
491
+ if (fromName) this.fadeOut(fromName, duration);
492
+ if (toName) this.fadeIn(toName, duration);
493
+ }
494
+
495
+ /**
496
+ * @method _removeFadesFor
497
+ * @description Cancels any in-flight fades for a sound name.
498
+ * @private
499
+ */
500
+ _removeFadesFor(name) {
501
+ for (let i = this._fades.length - 1; i >= 0; i--) {
502
+ if (this._fades[i].name === name) this._fades.splice(i, 1);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * @method update
508
+ * @description Advances active volume fades by `dt` seconds. Call this from
509
+ * your loop when the manager was created with `{ autoTick: false }`; otherwise
510
+ * the manager advances fades itself.
511
+ * @param {number} dt - Elapsed seconds since the last update
512
+ */
513
+ update(dt) {
514
+ if (!this._fades.length) return;
515
+ for (let i = this._fades.length - 1; i >= 0; i--) {
516
+ const f = this._fades[i];
517
+ f.t += dt;
518
+ const k = f.duration > 0 ? Math.min(1, f.t / f.duration) : 1;
519
+ f.sound._fadeMul = f.from + (f.to - f.from) * k;
520
+ f.sound.audio.volume = this._effectiveVolume(f.sound);
521
+ if (k >= 1) {
522
+ this._fades.splice(i, 1);
523
+ if (f.onDone) f.onDone();
524
+ }
525
+ }
526
+ }
527
+
528
+ /**
529
+ * @method _maybeStartTicker
530
+ * @description Lazily starts the self-driven RAF ticker (browser + autoTick).
531
+ * @private
532
+ */
533
+ _maybeStartTicker() {
534
+ if (!this.autoTick || this._tickId != null) return;
535
+ const raf =
536
+ typeof globalThis !== "undefined" && globalThis.requestAnimationFrame
537
+ ? globalThis.requestAnimationFrame.bind(globalThis)
538
+ : null;
539
+ if (!raf) return;
540
+ this._lastTick = 0;
541
+ const tick = (now) => {
542
+ if (!this._lastTick) this._lastTick = now;
543
+ const dt = (now - this._lastTick) / 1000;
544
+ this._lastTick = now;
545
+ this.update(dt);
546
+ if (this._fades.length) {
547
+ this._tickId = raf(tick);
548
+ } else {
549
+ this._tickId = null;
550
+ }
551
+ };
552
+ this._tickId = raf(tick);
553
+ }
554
+
555
+ /**
556
+ * @method getSound
557
+ * @description Returns an audio file from the manager
558
+ * @param {string} name - The name of the audio file
559
+ */
560
+ getSound(name) {
561
+ return this.sounds.get(name);
562
+ }
563
+ }
564
+
565
+ export default AudioManager;