emeraldengine 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2359 -968
- package/dist/types/index.d.ts +72 -32
- package/dist/types/src/Animator.d.ts +50 -0
- package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
- package/dist/types/src/Camera.d.ts +122 -0
- package/dist/types/src/CameraController.d.ts +107 -0
- package/dist/types/src/CanvasText.d.ts +91 -0
- package/dist/types/src/CollisionLayers.d.ts +58 -0
- package/dist/types/{Color.d.ts → src/Color.d.ts} +5 -6
- package/dist/types/src/Coroutine.d.ts +111 -0
- package/dist/types/src/DebugOverlay.d.ts +86 -0
- package/dist/types/src/Drawable.d.ts +271 -0
- package/dist/types/src/Easing.d.ts +22 -0
- package/dist/types/src/Emerald.d.ts +420 -0
- package/dist/types/src/EmeraldDB.d.ts +159 -0
- package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
- package/dist/types/src/GLUtils.d.ts +4 -0
- package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
- package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +60 -23
- package/dist/types/src/Interpolator.d.ts +66 -0
- package/dist/types/src/Material.d.ts +87 -0
- package/dist/types/src/MathUtils.d.ts +80 -0
- package/dist/types/src/ParticleEmitter.d.ts +131 -0
- package/dist/types/{Physics.d.ts → src/Physics.d.ts} +88 -20
- package/dist/types/src/Pool.d.ts +53 -0
- package/dist/types/src/PostEffects.d.ts +68 -0
- package/dist/types/src/PostProcessor.d.ts +124 -0
- package/dist/types/src/RenderTarget.d.ts +56 -0
- package/dist/types/src/Scene.d.ts +62 -0
- package/dist/types/src/ScreenEffects.d.ts +111 -0
- package/dist/types/src/Serializer.d.ts +86 -0
- package/dist/types/src/Shaders.d.ts +2 -0
- package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
- package/dist/types/src/SpatialGrid.d.ts +50 -0
- package/dist/types/src/SpriteBatch.d.ts +89 -0
- package/dist/types/src/StateMachine.d.ts +59 -0
- package/dist/types/src/Storage.d.ts +89 -0
- package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
- package/dist/types/src/TextureAtlas.d.ts +55 -0
- package/dist/types/src/Tilemap.d.ts +91 -0
- package/dist/types/src/Time.d.ts +53 -0
- package/dist/types/src/Timer.d.ts +54 -0
- package/dist/types/src/Transform.d.ts +94 -0
- package/dist/types/src/Tween.d.ts +81 -0
- package/dist/types/src/UI.d.ts +121 -0
- package/dist/types/src/components/Behaviour.d.ts +71 -0
- package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
- package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
- package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
- package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
- package/dist/types/src/components/Collider.d.ts +96 -0
- package/dist/types/src/components/GameObject.d.ts +135 -0
- package/dist/types/src/components/RigidBody.d.ts +183 -0
- package/dist/types/src/importers/Aseprite.d.ts +79 -0
- package/dist/types/src/importers/TiledMap.d.ts +62 -0
- package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
- package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
- package/dist/types/src/managers/AssetManager.d.ts +116 -0
- package/dist/types/src/managers/AudioManager.d.ts +259 -0
- package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
- package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
- package/dist/types/src/managers/GLManager.d.ts +84 -0
- package/dist/types/src/managers/GLState.d.ts +37 -0
- package/dist/types/src/managers/IDManager.d.ts +31 -0
- package/dist/types/src/managers/InputManager.d.ts +290 -0
- package/dist/types/src/managers/NetworkManager.d.ts +93 -0
- package/dist/types/src/managers/RenderStats.d.ts +34 -0
- package/dist/types/src/managers/SceneManager.d.ts +44 -0
- package/dist/types/src/managers/ShaderManager.d.ts +55 -0
- package/dist/types/src/managers/TextureManager.d.ts +102 -0
- package/dist/types/src/particlesystem/Particle.d.ts +64 -0
- package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
- package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
- package/index.js +72 -0
- package/package.json +74 -60
- package/src/Animator.js +95 -0
- package/src/BitmapText.js +6 -5
- package/src/Camera.js +183 -0
- package/src/CameraController.js +192 -0
- package/src/CanvasText.js +281 -0
- package/src/CollisionLayers.js +86 -0
- package/src/Color.js +18 -18
- package/src/Coroutine.js +259 -0
- package/src/DebugOverlay.js +246 -0
- package/src/Drawable.js +842 -555
- package/src/Easing.js +57 -0
- package/src/Emerald.js +1150 -459
- package/src/EmeraldDB.js +328 -0
- package/src/FPSCounter.js +43 -43
- package/src/GLUtils.js +60 -67
- package/src/Instance.js +41 -5
- package/src/InstancedTexture.js +251 -118
- package/src/Interpolator.js +124 -0
- package/src/Material.js +202 -0
- package/src/MathUtils.js +133 -0
- package/src/ParticleEmitter.js +284 -0
- package/src/Physics.js +186 -5
- package/src/Pool.js +85 -0
- package/src/PostEffects.js +296 -0
- package/src/PostProcessor.js +304 -0
- package/src/RenderTarget.js +134 -0
- package/src/Scene.js +115 -83
- package/src/ScreenEffects.js +266 -0
- package/src/Serializer.js +131 -0
- package/src/Shaders.js +150 -165
- package/src/Shapes.js +118 -129
- package/src/SpatialGrid.js +111 -0
- package/src/SpriteBatch.js +299 -0
- package/src/StateMachine.js +82 -0
- package/src/Storage.js +175 -47
- package/src/Texture.js +58 -67
- package/src/TextureAtlas.js +96 -0
- package/src/Tilemap.js +274 -0
- package/src/Time.js +51 -6
- package/src/Timer.js +99 -0
- package/src/Transform.js +100 -7
- package/src/Tween.js +160 -0
- package/src/UI.js +394 -0
- package/src/components/Behaviour.js +90 -0
- package/src/components/BoxCollider.js +22 -3
- package/src/components/CircleCollider.js +19 -3
- package/src/components/CircleColliderDebug.js +24 -24
- package/src/components/Collider.js +140 -34
- package/src/components/GameObject.js +130 -21
- package/src/components/RigidBody.js +123 -2
- package/src/importers/Aseprite.js +142 -0
- package/src/importers/TiledMap.js +158 -0
- package/src/lights/DirectionalLight.js +6 -15
- package/src/lights/PointLight.js +4 -4
- package/src/managers/AssetManager.js +239 -0
- package/src/managers/AudioManager.js +565 -146
- package/src/managers/EventManager.js +488 -477
- package/src/managers/GLManager.js +57 -0
- package/src/managers/GLState.js +70 -0
- package/src/managers/IDManager.js +24 -2
- package/src/managers/InputManager.js +779 -0
- package/src/managers/NetworkManager.js +178 -0
- package/src/managers/RenderStats.js +34 -0
- package/src/managers/SceneManager.js +30 -0
- package/src/managers/ShaderManager.js +0 -2
- package/src/managers/TextureManager.js +218 -0
- package/src/particlesystem/Particle.js +82 -7
- package/src/particlesystem/ParticleSettings.js +21 -3
- package/src/particlesystem/Particles.js +80 -31
- package/dist/types/Drawable.d.ts +0 -157
- package/dist/types/Emerald.d.ts +0 -73
- package/dist/types/GLUtils.d.ts +0 -6
- package/dist/types/Scene.d.ts +0 -39
- package/dist/types/Shaders.d.ts +0 -4
- package/dist/types/Storage.d.ts +0 -46
- package/dist/types/Time.d.ts +0 -22
- package/dist/types/Transform.d.ts +0 -41
- package/dist/types/components/BoxColliderDebug.d.ts +0 -19
- package/dist/types/components/CircleColliderDebug.d.ts +0 -19
- package/dist/types/components/Collider.d.ts +0 -53
- package/dist/types/components/GameObject.d.ts +0 -72
- package/dist/types/components/RigidBody.d.ts +0 -104
- package/dist/types/managers/AudioManager.d.ts +0 -60
- package/dist/types/managers/GLManager.d.ts +0 -47
- package/dist/types/managers/IDManager.d.ts +0 -21
- package/dist/types/managers/SceneManager.d.ts +0 -22
- package/dist/types/particlesystem/Particle.d.ts +0 -42
|
@@ -1,146 +1,565 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @class AudioManager
|
|
3
|
-
* @description Manages
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* @
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|