kiwiengine 0.5.15 → 0.5.17
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/lib/asset/audio.js +109 -25
- package/lib/asset/audio.js.map +1 -1
- package/lib/types/asset/audio.d.ts +20 -0
- package/lib/types/asset/audio.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/asset/audio.ts +113 -25
package/lib/asset/audio.js
CHANGED
|
@@ -9,7 +9,9 @@ async function getAvailableContext() {
|
|
|
9
9
|
return audioContext;
|
|
10
10
|
}
|
|
11
11
|
let isPageVisible = !document.hidden;
|
|
12
|
-
window.addEventListener('visibilitychange', () =>
|
|
12
|
+
window.addEventListener('visibilitychange', () => {
|
|
13
|
+
isPageVisible = !document.hidden;
|
|
14
|
+
});
|
|
13
15
|
function createSafeStorage() {
|
|
14
16
|
const memory = (() => {
|
|
15
17
|
const m = new Map();
|
|
@@ -41,7 +43,19 @@ function createSafeStorage() {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
const safeStorage = createSafeStorage();
|
|
44
|
-
|
|
46
|
+
function clamp01(n) {
|
|
47
|
+
return Math.max(0, Math.min(1, n));
|
|
48
|
+
}
|
|
49
|
+
function readBool(key, defaultValue) {
|
|
50
|
+
const v = safeStorage.getItem(key);
|
|
51
|
+
if (v == null)
|
|
52
|
+
return defaultValue;
|
|
53
|
+
return v === '1' || v === 'true';
|
|
54
|
+
}
|
|
55
|
+
function writeBool(key, value) {
|
|
56
|
+
safeStorage.setItem(key, value ? '1' : '0');
|
|
57
|
+
}
|
|
58
|
+
export class Audio {
|
|
45
59
|
src;
|
|
46
60
|
#volume;
|
|
47
61
|
#loop;
|
|
@@ -56,17 +70,18 @@ class Audio {
|
|
|
56
70
|
#offset = 0;
|
|
57
71
|
constructor(src, volume, loop) {
|
|
58
72
|
this.src = src;
|
|
59
|
-
this.#volume = volume;
|
|
73
|
+
this.#volume = clamp01(volume);
|
|
60
74
|
this.#loop = loop;
|
|
61
75
|
this.play();
|
|
62
76
|
}
|
|
63
77
|
get volume() { return this.#volume; }
|
|
64
78
|
set volume(volume) {
|
|
65
|
-
this.#volume = volume;
|
|
79
|
+
this.#volume = clamp01(volume);
|
|
66
80
|
if (this.#gainNode)
|
|
67
|
-
this.#gainNode.gain.value =
|
|
81
|
+
this.#gainNode.gain.value = this.#volume;
|
|
68
82
|
}
|
|
69
83
|
async play() {
|
|
84
|
+
// On mobile, avoid starting audio while the page is hidden (often blocked / unreliable)
|
|
70
85
|
if (isMobile && !isPageVisible)
|
|
71
86
|
return;
|
|
72
87
|
if (!this.#audioBuffer) {
|
|
@@ -80,14 +95,17 @@ class Audio {
|
|
|
80
95
|
}
|
|
81
96
|
if (!this.#audioBuffer)
|
|
82
97
|
return;
|
|
98
|
+
// If already playing, restart cleanly
|
|
83
99
|
if (this.#isPlaying)
|
|
84
100
|
this.stop();
|
|
101
|
+
// If this is not a resume, reset offset to the beginning
|
|
85
102
|
if (!this.#isPaused)
|
|
86
103
|
this.#offset = 0;
|
|
87
104
|
this.#isPlaying = true;
|
|
88
105
|
this.#isPaused = false;
|
|
89
106
|
if (!this.#audioContext)
|
|
90
107
|
this.#audioContext = await getAvailableContext();
|
|
108
|
+
// If state changed while awaiting, bail out
|
|
91
109
|
if (!this.#isPlaying)
|
|
92
110
|
return;
|
|
93
111
|
if (!this.#gainNode) {
|
|
@@ -95,25 +113,39 @@ class Audio {
|
|
|
95
113
|
this.#gainNode.gain.value = this.#volume;
|
|
96
114
|
this.#gainNode.connect(this.#audioContext.destination);
|
|
97
115
|
}
|
|
116
|
+
else {
|
|
117
|
+
this.#gainNode.gain.value = this.#volume;
|
|
118
|
+
}
|
|
98
119
|
this.#source = this.#audioContext.createBufferSource();
|
|
99
120
|
this.#source.buffer = this.#audioBuffer;
|
|
100
121
|
this.#source.loop = this.#loop;
|
|
101
122
|
this.#source.connect(this.#gainNode);
|
|
102
123
|
this.#source.start(0, this.#offset);
|
|
103
124
|
this.#startTime = this.#audioContext.currentTime;
|
|
104
|
-
this.#source.onended = () => {
|
|
105
|
-
|
|
125
|
+
this.#source.onended = () => {
|
|
126
|
+
// Only auto-stop for one-shot sounds that were not paused and are not looping
|
|
127
|
+
if (!this.#isPaused && !this.#loop)
|
|
128
|
+
this.stop();
|
|
129
|
+
};
|
|
106
130
|
}
|
|
107
131
|
#clear() {
|
|
108
132
|
if (this.#source) {
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
// stop() can throw if already stopped; ignore safely
|
|
134
|
+
try {
|
|
135
|
+
this.#source.stop();
|
|
136
|
+
}
|
|
137
|
+
catch { /* noop */ }
|
|
138
|
+
try {
|
|
139
|
+
this.#source.disconnect();
|
|
140
|
+
}
|
|
141
|
+
catch { /* noop */ }
|
|
111
142
|
this.#source = undefined;
|
|
112
143
|
}
|
|
113
144
|
}
|
|
114
145
|
pause() {
|
|
115
146
|
if (this.#isPlaying && !this.#isPaused) {
|
|
116
147
|
if (this.#audioContext) {
|
|
148
|
+
// Track elapsed time so we can resume from the correct offset
|
|
117
149
|
this.#pauseTime = this.#audioContext.currentTime;
|
|
118
150
|
this.#offset += this.#pauseTime - this.#startTime;
|
|
119
151
|
}
|
|
@@ -123,6 +155,7 @@ class Audio {
|
|
|
123
155
|
}
|
|
124
156
|
}
|
|
125
157
|
stop() {
|
|
158
|
+
// Full stop resets the offset; next play starts from the beginning
|
|
126
159
|
this.#isPlaying = false;
|
|
127
160
|
this.#isPaused = false;
|
|
128
161
|
this.#offset = 0;
|
|
@@ -131,33 +164,68 @@ class Audio {
|
|
|
131
164
|
}
|
|
132
165
|
class MusicPlayer {
|
|
133
166
|
#volume = 0.7;
|
|
167
|
+
#enabled = true;
|
|
134
168
|
#currentAudio;
|
|
169
|
+
#pendingSrc;
|
|
135
170
|
constructor() {
|
|
136
|
-
const
|
|
137
|
-
this.#volume = Number.isNaN(
|
|
171
|
+
const storedVol = parseFloat(safeStorage.getItem('musicVolume') || '');
|
|
172
|
+
this.#volume = Number.isNaN(storedVol) ? this.#volume : clamp01(storedVol);
|
|
173
|
+
// Separate from volume: true/false music enable state
|
|
174
|
+
this.#enabled = readBool('musicEnabled', true);
|
|
138
175
|
if (isMobile) {
|
|
139
176
|
document.addEventListener('visibilitychange', () => {
|
|
140
|
-
if (document.hidden)
|
|
177
|
+
if (document.hidden) {
|
|
178
|
+
// When hidden, pause to avoid mobile audio issues
|
|
141
179
|
this.pause();
|
|
180
|
+
}
|
|
142
181
|
else {
|
|
143
182
|
isPageVisible = true;
|
|
144
|
-
|
|
183
|
+
// Only resume if enabled
|
|
184
|
+
if (this.#enabled)
|
|
185
|
+
this.#currentAudio?.play();
|
|
145
186
|
}
|
|
146
187
|
});
|
|
147
188
|
}
|
|
148
189
|
}
|
|
149
|
-
get
|
|
150
|
-
|
|
190
|
+
get enabled() { return this.#enabled; }
|
|
191
|
+
set enabled(v) {
|
|
192
|
+
this.#enabled = v;
|
|
193
|
+
writeBool('musicEnabled', v);
|
|
194
|
+
if (!v) {
|
|
195
|
+
// "Off" means: do not output music. Keep the state by pausing (resume later).
|
|
196
|
+
this.pause();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// When turning on, play the last requested track if any; otherwise just resume
|
|
200
|
+
if (this.#pendingSrc) {
|
|
201
|
+
const src = this.#pendingSrc;
|
|
202
|
+
this.#pendingSrc = undefined;
|
|
203
|
+
this.play(src);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
this.#currentAudio?.play();
|
|
207
|
+
}
|
|
151
208
|
}
|
|
209
|
+
enable() { this.enabled = true; }
|
|
210
|
+
disable() { this.enabled = false; }
|
|
211
|
+
toggle() { this.enabled = !this.enabled; }
|
|
212
|
+
get volume() { return this.#volume; }
|
|
152
213
|
set volume(volume) {
|
|
153
|
-
this.#volume = volume;
|
|
154
|
-
safeStorage.setItem('musicVolume', volume.toString());
|
|
214
|
+
this.#volume = clamp01(volume);
|
|
215
|
+
safeStorage.setItem('musicVolume', this.#volume.toString());
|
|
155
216
|
if (this.#currentAudio)
|
|
156
|
-
this.#currentAudio.volume = volume;
|
|
217
|
+
this.#currentAudio.volume = this.#volume;
|
|
157
218
|
}
|
|
158
219
|
play(src) {
|
|
159
|
-
if
|
|
220
|
+
// Remember the user's intent even if music is currently disabled
|
|
221
|
+
this.#pendingSrc = src;
|
|
222
|
+
if (!this.#enabled)
|
|
160
223
|
return;
|
|
224
|
+
// If it's the same track, resume instead of recreating the Audio
|
|
225
|
+
if (this.#currentAudio?.src === src) {
|
|
226
|
+
this.#currentAudio.play();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
161
229
|
this.#currentAudio?.stop();
|
|
162
230
|
this.#currentAudio = new Audio(src, this.#volume, true);
|
|
163
231
|
}
|
|
@@ -165,27 +233,43 @@ class MusicPlayer {
|
|
|
165
233
|
this.#currentAudio?.pause();
|
|
166
234
|
}
|
|
167
235
|
stop() {
|
|
236
|
+
// stop() is a hard reset (unlike pause)
|
|
168
237
|
this.#currentAudio?.stop();
|
|
169
238
|
this.#currentAudio = undefined;
|
|
239
|
+
this.#pendingSrc = undefined;
|
|
170
240
|
}
|
|
171
241
|
}
|
|
172
242
|
class SfxPlayer {
|
|
173
243
|
#volume = 1;
|
|
244
|
+
#enabled = true;
|
|
174
245
|
constructor() {
|
|
175
|
-
const
|
|
176
|
-
this.#volume = Number.isNaN(
|
|
246
|
+
const storedVol = parseFloat(safeStorage.getItem('sfxVolume') || '');
|
|
247
|
+
this.#volume = Number.isNaN(storedVol) ? this.#volume : clamp01(storedVol);
|
|
248
|
+
// Separate from volume: true/false SFX enable state
|
|
249
|
+
this.#enabled = readBool('sfxEnabled', true);
|
|
177
250
|
}
|
|
178
|
-
get
|
|
179
|
-
|
|
251
|
+
get enabled() { return this.#enabled; }
|
|
252
|
+
set enabled(v) {
|
|
253
|
+
this.#enabled = v;
|
|
254
|
+
writeBool('sfxEnabled', v);
|
|
180
255
|
}
|
|
256
|
+
enable() { this.enabled = true; }
|
|
257
|
+
disable() { this.enabled = false; }
|
|
258
|
+
toggle() { this.enabled = !this.enabled; }
|
|
259
|
+
get volume() { return this.#volume; }
|
|
181
260
|
set volume(volume) {
|
|
182
|
-
this.#volume = volume;
|
|
183
|
-
safeStorage.setItem('sfxVolume', volume.toString());
|
|
261
|
+
this.#volume = clamp01(volume);
|
|
262
|
+
safeStorage.setItem('sfxVolume', this.#volume.toString());
|
|
184
263
|
}
|
|
185
264
|
play(src) {
|
|
265
|
+
// If disabled, do not play any one-shot sounds
|
|
266
|
+
if (!this.#enabled)
|
|
267
|
+
return;
|
|
186
268
|
new Audio(src, this.#volume, false);
|
|
187
269
|
}
|
|
188
270
|
playRandom(...srcs) {
|
|
271
|
+
if (!this.#enabled)
|
|
272
|
+
return;
|
|
189
273
|
this.play(srcs[Math.floor(Math.random() * srcs.length)]);
|
|
190
274
|
}
|
|
191
275
|
}
|
package/lib/asset/audio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../../src/asset/audio.ts"],"names":[],"mappings":"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","sourcesContent":["import { isMobile } from '../utils/device'\nimport { audioLoader } from './loaders/audio'\n\nexport const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()\nwindow.addEventListener('mousedown', () => audioContext.resume())\nwindow.addEventListener('touchend', () => audioContext.resume())\n\nasync function getAvailableContext(): Promise<AudioContext> {\n if (audioContext.state === 'suspended') await audioContext.resume()\n return audioContext\n}\n\nlet isPageVisible = !document.hidden\nwindow.addEventListener('visibilitychange', () => isPageVisible = !document.hidden)\n\ntype BasicStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>\n\ntype SafeStorage = BasicStorage & {\n persistent: boolean\n}\n\nfunction createSafeStorage(): SafeStorage {\n const memory = (() => {\n const m = new Map<string, string>()\n const api: SafeStorage = {\n persistent: false,\n getItem: (k) => (m.has(k) ? m.get(k)! : null),\n setItem: (k, v) => { m.set(k, String(v)) },\n removeItem: (k) => { m.delete(k) },\n }\n return api\n })()\n\n if (typeof window === 'undefined') return memory\n\n try {\n const ls = window.localStorage\n const probeKey = '__safe_ls_probe__' + Math.random().toString(36).slice(2)\n ls.setItem(probeKey, '1')\n ls.removeItem(probeKey)\n\n const safe: SafeStorage = {\n persistent: true,\n getItem: (k) => ls.getItem(k),\n setItem: (k, v) => ls.setItem(k, v),\n removeItem: (k) => ls.removeItem(k),\n }\n return safe\n } catch {\n return memory\n }\n}\n\nconst safeStorage = createSafeStorage()\n\nclass Audio {\n src: string\n #volume: number\n #loop: boolean\n\n #audioBuffer?: AudioBuffer\n #audioContext?: AudioContext\n #gainNode?: GainNode\n #source?: AudioBufferSourceNode\n\n #isPlaying = false\n #isPaused = false\n #startTime = 0\n #pauseTime = 0\n #offset = 0\n\n constructor(src: string, volume: number, loop: boolean) {\n this.src = src\n this.#volume = volume\n this.#loop = loop\n this.play()\n }\n\n get volume() { return this.#volume }\n set volume(volume: number) {\n this.#volume = volume\n if (this.#gainNode) this.#gainNode.gain.value = Math.max(0, Math.min(1, volume))\n }\n\n async play() {\n if (isMobile && !isPageVisible) return\n\n if (!this.#audioBuffer) {\n if (audioLoader.checkCached(this.src)) {\n this.#audioBuffer = audioLoader.getCached(this.src)\n } else {\n console.info(`Audio not preloaded. Loading now: ${this.src}`)\n this.#audioBuffer = await audioLoader.load(this.src)\n }\n }\n if (!this.#audioBuffer) return\n\n if (this.#isPlaying) this.stop()\n if (!this.#isPaused) this.#offset = 0\n this.#isPlaying = true\n this.#isPaused = false\n if (!this.#audioContext) this.#audioContext = await getAvailableContext()\n if (!this.#isPlaying) return\n\n if (!this.#gainNode) {\n this.#gainNode = this.#audioContext.createGain()\n this.#gainNode.gain.value = this.#volume\n this.#gainNode.connect(this.#audioContext.destination)\n }\n\n this.#source = this.#audioContext.createBufferSource()\n this.#source.buffer = this.#audioBuffer\n this.#source.loop = this.#loop\n this.#source.connect(this.#gainNode)\n this.#source.start(0, this.#offset)\n this.#startTime = this.#audioContext.currentTime\n this.#source.onended = () => { if (!this.#isPaused && !this.#loop) this.stop() }\n }\n\n #clear(): void {\n if (this.#source) {\n this.#source.stop()\n this.#source.disconnect()\n this.#source = undefined\n }\n }\n\n pause() {\n if (this.#isPlaying && !this.#isPaused) {\n if (this.#audioContext) {\n this.#pauseTime = this.#audioContext.currentTime\n this.#offset += this.#pauseTime - this.#startTime\n }\n this.#isPaused = true\n this.#isPlaying = false\n this.#clear()\n }\n }\n\n stop() {\n this.#isPlaying = false\n this.#isPaused = false\n this.#offset = 0\n this.#clear()\n }\n}\n\nclass MusicPlayer {\n #volume = 0.7\n #currentAudio?: Audio\n\n constructor() {\n const stored = parseFloat(safeStorage.getItem('musicVolume') || '')\n this.#volume = Number.isNaN(stored) ? this.#volume : stored\n\n if (isMobile) {\n document.addEventListener('visibilitychange', () => {\n if (document.hidden) this.pause()\n else {\n isPageVisible = true\n this.#currentAudio?.play()\n }\n })\n }\n }\n\n get volume() {\n return this.#volume\n }\n\n set volume(volume: number) {\n this.#volume = volume\n safeStorage.setItem('musicVolume', volume.toString())\n if (this.#currentAudio) this.#currentAudio.volume = volume\n }\n\n play(src: string) {\n if (this.#currentAudio?.src === src) return\n this.#currentAudio?.stop()\n this.#currentAudio = new Audio(src, this.#volume, true)\n }\n\n pause() {\n this.#currentAudio?.pause()\n }\n\n stop() {\n this.#currentAudio?.stop()\n this.#currentAudio = undefined\n }\n}\n\nclass SfxPlayer {\n #volume = 1\n\n constructor() {\n const stored = parseFloat(safeStorage.getItem('sfxVolume') || '')\n this.#volume = Number.isNaN(stored) ? this.#volume : stored\n }\n\n get volume() {\n return this.#volume\n }\n\n set volume(volume: number) {\n this.#volume = volume\n safeStorage.setItem('sfxVolume', volume.toString())\n }\n\n play(src: string) {\n new Audio(src, this.#volume, false)\n }\n\n playRandom(...srcs: string[]) {\n this.play(srcs[Math.floor(Math.random() * srcs.length)])\n }\n}\n\nexport const musicPlayer = new MusicPlayer()\nexport const sfxPlayer = new SfxPlayer()\n"]}
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../../src/asset/audio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAK,MAAc,CAAC,kBAAkB,CAAC,EAAE,CAAA;AAC7F,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;AACjE,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;AAEhE,KAAK,UAAU,mBAAmB;IAChC,IAAI,YAAY,CAAC,KAAK,KAAK,WAAW;QAAE,MAAM,YAAY,CAAC,MAAM,EAAE,CAAA;IACnE,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,IAAI,aAAa,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAA;AACpC,MAAM,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAC/C,aAAa,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAA;AAClC,CAAC,CAAC,CAAA;AAQF,SAAS,iBAAiB;IACxB,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE;QACnB,MAAM,CAAC,GAAG,IAAI,GAAG,EAAkB,CAAA;QACnC,MAAM,GAAG,GAAgB;YACvB,UAAU,EAAE,KAAK;YACjB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAC7C,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC;YAC1C,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC;SACnC,CAAA;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,CAAC,EAAE,CAAA;IAEJ,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,MAAM,CAAA;IAEhD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,CAAC,YAAY,CAAA;QAC9B,MAAM,QAAQ,GAAG,mBAAmB,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAC1E,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;QACzB,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;QAEvB,MAAM,IAAI,GAAgB;YACxB,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7B,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;YACnC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;SACpC,CAAA;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAA;IACf,CAAC;AACH,CAAC;AAED,MAAM,WAAW,GAAG,iBAAiB,EAAE,CAAA;AAEvC,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AACpC,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW,EAAE,YAAqB;IAClD,MAAM,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,YAAY,CAAA;IAClC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,MAAM,CAAA;AAClC,CAAC;AAED,SAAS,SAAS,CAAC,GAAW,EAAE,KAAc;IAC5C,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,OAAO,KAAK;IAChB,GAAG,CAAQ;IACX,OAAO,CAAQ;IACf,KAAK,CAAS;IAEd,YAAY,CAAc;IAC1B,aAAa,CAAe;IAC5B,SAAS,CAAW;IACpB,OAAO,CAAwB;IAE/B,UAAU,GAAG,KAAK,CAAA;IAClB,SAAS,GAAG,KAAK,CAAA;IACjB,UAAU,GAAG,CAAC,CAAA;IACd,UAAU,GAAG,CAAC,CAAA;IACd,OAAO,GAAG,CAAC,CAAA;IAEX,YAAY,GAAW,EAAE,MAAc,EAAE,IAAa;QACpD,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC9B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,IAAI,EAAE,CAAA;IACb,CAAC;IAED,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,OAAO,CAAA,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,MAAc;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC9B,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAA;IAC9D,CAAC;IAED,KAAK,CAAC,IAAI;QACR,wFAAwF;QACxF,IAAI,QAAQ,IAAI,CAAC,aAAa;YAAE,OAAM;QAEtC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,IAAI,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACrD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,qCAAqC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;gBAC7D,IAAI,CAAC,YAAY,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACtD,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,YAAY;YAAE,OAAM;QAE9B,sCAAsC;QACtC,IAAI,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,IAAI,EAAE,CAAA;QAEhC,yDAAyD;QACzD,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,OAAO,GAAG,CAAC,CAAA;QAErC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAA;QAEtB,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,IAAI,CAAC,aAAa,GAAG,MAAM,mBAAmB,EAAE,CAAA;QAEzE,4CAA4C;QAC5C,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAM;QAE5B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,CAAA;YAChD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAA;YACxC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,CAAA;QACxD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAA;QAC1C,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,kBAAkB,EAAE,CAAA;QACtD,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,YAAY,CAAA;QACvC,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAA;QAC9B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACpC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,CAAA;QAEhD,IAAI,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE;YAC1B,8EAA8E;YAC9E,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,IAAI,EAAE,CAAA;QACjD,CAAC,CAAA;IACH,CAAC;IAED,MAAM;QACJ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,qDAAqD;YACrD,IAAI,CAAC;gBAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC;YAChD,IAAI,CAAC;gBAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC;YACtD,IAAI,CAAC,OAAO,GAAG,SAAS,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,8DAA8D;gBAC9D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,CAAA;gBAChD,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAA;YACnD,CAAC;YACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;YACrB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAA;YACvB,IAAI,CAAC,MAAM,EAAE,CAAA;QACf,CAAC;IACH,CAAC;IAED,IAAI;QACF,mEAAmE;QACnE,IAAI,CAAC,UAAU,GAAG,KAAK,CAAA;QACvB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAA;QACtB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAA;QAChB,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;CACF;AAED,MAAM,WAAW;IACf,OAAO,GAAG,GAAG,CAAA;IACb,QAAQ,GAAG,IAAI,CAAA;IACf,aAAa,CAAQ;IACrB,WAAW,CAAS;IAEpB;QACE,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,CAAA;QACtE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAE1E,sDAAsD;QACtD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC,CAAA;QAE9C,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE;gBACjD,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;oBACpB,kDAAkD;oBAClD,IAAI,CAAC,KAAK,EAAE,CAAA;gBACd,CAAC;qBAAM,CAAC;oBACN,aAAa,GAAG,IAAI,CAAA;oBACpB,yBAAyB;oBACzB,IAAI,IAAI,CAAC,QAAQ;wBAAE,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;gBAC/C,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAA,CAAC,CAAC;IACtC,IAAI,OAAO,CAAC,CAAU;QACpB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;QACjB,SAAS,CAAC,cAAc,EAAE,CAAC,CAAC,CAAA;QAE5B,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,8EAA8E;YAC9E,IAAI,CAAC,KAAK,EAAE,CAAA;YACZ,OAAM;QACR,CAAC;QAED,+EAA+E;QAC/E,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAA;YAC5B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAA;YAC5B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAChB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;QAC5B,CAAC;IACH,CAAC;IAED,MAAM,KAAK,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA,CAAC,CAAC;IAChC,OAAO,KAAK,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA,CAAC,CAAC;IAClC,MAAM,KAAK,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAA,CAAC,CAAC;IAEzC,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,OAAO,CAAA,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,MAAc;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC9B,WAAW,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC3D,IAAI,IAAI,CAAC,aAAa;YAAE,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAA;IAClE,CAAC;IAED,IAAI,CAAC,GAAW;QACd,iEAAiE;QACjE,IAAI,CAAC,WAAW,GAAG,GAAG,CAAA;QACtB,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAM;QAE1B,iEAAiE;QACjE,IAAI,IAAI,CAAC,aAAa,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC;YACpC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAA;YACzB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;QAC1B,IAAI,CAAC,aAAa,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACzD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,CAAA;IAC7B,CAAC;IAED,IAAI;QACF,wCAAwC;QACxC,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;QAC1B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;QAC9B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAA;IAC9B,CAAC;CACF;AAED,MAAM,SAAS;IACb,OAAO,GAAG,CAAC,CAAA;IACX,QAAQ,GAAG,IAAI,CAAA;IAEf;QACE,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAA;QACpE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAE1E,oDAAoD;QACpD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;IAC9C,CAAC;IAED,IAAI,OAAO,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAA,CAAC,CAAC;IACtC,IAAI,OAAO,CAAC,CAAU;QACpB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;QACjB,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;IAC5B,CAAC;IAED,MAAM,KAAK,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA,CAAC,CAAC;IAChC,OAAO,KAAK,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA,CAAC,CAAC;IAClC,MAAM,KAAK,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAA,CAAC,CAAC;IAEzC,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,OAAO,CAAA,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,MAAc;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC9B,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC3D,CAAC;IAED,IAAI,CAAC,GAAW;QACd,+CAA+C;QAC/C,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAM;QAC1B,IAAI,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IACrC,CAAC;IAED,UAAU,CAAC,GAAG,IAAc;QAC1B,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAM;QAC1B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1D,CAAC;CACF;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAA;AAC5C,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAA","sourcesContent":["import { isMobile } from '../utils/device'\nimport { audioLoader } from './loaders/audio'\n\nexport const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()\nwindow.addEventListener('mousedown', () => audioContext.resume())\nwindow.addEventListener('touchend', () => audioContext.resume())\n\nasync function getAvailableContext(): Promise<AudioContext> {\n if (audioContext.state === 'suspended') await audioContext.resume()\n return audioContext\n}\n\nlet isPageVisible = !document.hidden\nwindow.addEventListener('visibilitychange', () => {\n isPageVisible = !document.hidden\n})\n\ntype BasicStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>\n\ntype SafeStorage = BasicStorage & {\n persistent: boolean\n}\n\nfunction createSafeStorage(): SafeStorage {\n const memory = (() => {\n const m = new Map<string, string>()\n const api: SafeStorage = {\n persistent: false,\n getItem: (k) => (m.has(k) ? m.get(k)! : null),\n setItem: (k, v) => { m.set(k, String(v)) },\n removeItem: (k) => { m.delete(k) },\n }\n return api\n })()\n\n if (typeof window === 'undefined') return memory\n\n try {\n const ls = window.localStorage\n const probeKey = '__safe_ls_probe__' + Math.random().toString(36).slice(2)\n ls.setItem(probeKey, '1')\n ls.removeItem(probeKey)\n\n const safe: SafeStorage = {\n persistent: true,\n getItem: (k) => ls.getItem(k),\n setItem: (k, v) => ls.setItem(k, v),\n removeItem: (k) => ls.removeItem(k),\n }\n return safe\n } catch {\n return memory\n }\n}\n\nconst safeStorage = createSafeStorage()\n\nfunction clamp01(n: number) {\n return Math.max(0, Math.min(1, n))\n}\n\nfunction readBool(key: string, defaultValue: boolean) {\n const v = safeStorage.getItem(key)\n if (v == null) return defaultValue\n return v === '1' || v === 'true'\n}\n\nfunction writeBool(key: string, value: boolean) {\n safeStorage.setItem(key, value ? '1' : '0')\n}\n\nexport class Audio {\n src: string\n #volume: number\n #loop: boolean\n\n #audioBuffer?: AudioBuffer\n #audioContext?: AudioContext\n #gainNode?: GainNode\n #source?: AudioBufferSourceNode\n\n #isPlaying = false\n #isPaused = false\n #startTime = 0\n #pauseTime = 0\n #offset = 0\n\n constructor(src: string, volume: number, loop: boolean) {\n this.src = src\n this.#volume = clamp01(volume)\n this.#loop = loop\n this.play()\n }\n\n get volume() { return this.#volume }\n set volume(volume: number) {\n this.#volume = clamp01(volume)\n if (this.#gainNode) this.#gainNode.gain.value = this.#volume\n }\n\n async play() {\n // On mobile, avoid starting audio while the page is hidden (often blocked / unreliable)\n if (isMobile && !isPageVisible) return\n\n if (!this.#audioBuffer) {\n if (audioLoader.checkCached(this.src)) {\n this.#audioBuffer = audioLoader.getCached(this.src)\n } else {\n console.info(`Audio not preloaded. Loading now: ${this.src}`)\n this.#audioBuffer = await audioLoader.load(this.src)\n }\n }\n if (!this.#audioBuffer) return\n\n // If already playing, restart cleanly\n if (this.#isPlaying) this.stop()\n\n // If this is not a resume, reset offset to the beginning\n if (!this.#isPaused) this.#offset = 0\n\n this.#isPlaying = true\n this.#isPaused = false\n\n if (!this.#audioContext) this.#audioContext = await getAvailableContext()\n\n // If state changed while awaiting, bail out\n if (!this.#isPlaying) return\n\n if (!this.#gainNode) {\n this.#gainNode = this.#audioContext.createGain()\n this.#gainNode.gain.value = this.#volume\n this.#gainNode.connect(this.#audioContext.destination)\n } else {\n this.#gainNode.gain.value = this.#volume\n }\n\n this.#source = this.#audioContext.createBufferSource()\n this.#source.buffer = this.#audioBuffer\n this.#source.loop = this.#loop\n this.#source.connect(this.#gainNode)\n this.#source.start(0, this.#offset)\n this.#startTime = this.#audioContext.currentTime\n\n this.#source.onended = () => {\n // Only auto-stop for one-shot sounds that were not paused and are not looping\n if (!this.#isPaused && !this.#loop) this.stop()\n }\n }\n\n #clear(): void {\n if (this.#source) {\n // stop() can throw if already stopped; ignore safely\n try { this.#source.stop() } catch { /* noop */ }\n try { this.#source.disconnect() } catch { /* noop */ }\n this.#source = undefined\n }\n }\n\n pause() {\n if (this.#isPlaying && !this.#isPaused) {\n if (this.#audioContext) {\n // Track elapsed time so we can resume from the correct offset\n this.#pauseTime = this.#audioContext.currentTime\n this.#offset += this.#pauseTime - this.#startTime\n }\n this.#isPaused = true\n this.#isPlaying = false\n this.#clear()\n }\n }\n\n stop() {\n // Full stop resets the offset; next play starts from the beginning\n this.#isPlaying = false\n this.#isPaused = false\n this.#offset = 0\n this.#clear()\n }\n}\n\nclass MusicPlayer {\n #volume = 0.7\n #enabled = true\n #currentAudio?: Audio\n #pendingSrc?: string\n\n constructor() {\n const storedVol = parseFloat(safeStorage.getItem('musicVolume') || '')\n this.#volume = Number.isNaN(storedVol) ? this.#volume : clamp01(storedVol)\n\n // Separate from volume: true/false music enable state\n this.#enabled = readBool('musicEnabled', true)\n\n if (isMobile) {\n document.addEventListener('visibilitychange', () => {\n if (document.hidden) {\n // When hidden, pause to avoid mobile audio issues\n this.pause()\n } else {\n isPageVisible = true\n // Only resume if enabled\n if (this.#enabled) this.#currentAudio?.play()\n }\n })\n }\n }\n\n get enabled() { return this.#enabled }\n set enabled(v: boolean) {\n this.#enabled = v\n writeBool('musicEnabled', v)\n\n if (!v) {\n // \"Off\" means: do not output music. Keep the state by pausing (resume later).\n this.pause()\n return\n }\n\n // When turning on, play the last requested track if any; otherwise just resume\n if (this.#pendingSrc) {\n const src = this.#pendingSrc\n this.#pendingSrc = undefined\n this.play(src)\n } else {\n this.#currentAudio?.play()\n }\n }\n\n enable() { this.enabled = true }\n disable() { this.enabled = false }\n toggle() { this.enabled = !this.enabled }\n\n get volume() { return this.#volume }\n set volume(volume: number) {\n this.#volume = clamp01(volume)\n safeStorage.setItem('musicVolume', this.#volume.toString())\n if (this.#currentAudio) this.#currentAudio.volume = this.#volume\n }\n\n play(src: string) {\n // Remember the user's intent even if music is currently disabled\n this.#pendingSrc = src\n if (!this.#enabled) return\n\n // If it's the same track, resume instead of recreating the Audio\n if (this.#currentAudio?.src === src) {\n this.#currentAudio.play()\n return\n }\n\n this.#currentAudio?.stop()\n this.#currentAudio = new Audio(src, this.#volume, true)\n }\n\n pause() {\n this.#currentAudio?.pause()\n }\n\n stop() {\n // stop() is a hard reset (unlike pause)\n this.#currentAudio?.stop()\n this.#currentAudio = undefined\n this.#pendingSrc = undefined\n }\n}\n\nclass SfxPlayer {\n #volume = 1\n #enabled = true\n\n constructor() {\n const storedVol = parseFloat(safeStorage.getItem('sfxVolume') || '')\n this.#volume = Number.isNaN(storedVol) ? this.#volume : clamp01(storedVol)\n\n // Separate from volume: true/false SFX enable state\n this.#enabled = readBool('sfxEnabled', true)\n }\n\n get enabled() { return this.#enabled }\n set enabled(v: boolean) {\n this.#enabled = v\n writeBool('sfxEnabled', v)\n }\n\n enable() { this.enabled = true }\n disable() { this.enabled = false }\n toggle() { this.enabled = !this.enabled }\n\n get volume() { return this.#volume }\n set volume(volume: number) {\n this.#volume = clamp01(volume)\n safeStorage.setItem('sfxVolume', this.#volume.toString())\n }\n\n play(src: string) {\n // If disabled, do not play any one-shot sounds\n if (!this.#enabled) return\n new Audio(src, this.#volume, false)\n }\n\n playRandom(...srcs: string[]) {\n if (!this.#enabled) return\n this.play(srcs[Math.floor(Math.random() * srcs.length)])\n }\n}\n\nexport const musicPlayer = new MusicPlayer()\nexport const sfxPlayer = new SfxPlayer()\n"]}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
export declare const audioContext: AudioContext;
|
|
2
|
+
export declare class Audio {
|
|
3
|
+
#private;
|
|
4
|
+
src: string;
|
|
5
|
+
constructor(src: string, volume: number, loop: boolean);
|
|
6
|
+
get volume(): number;
|
|
7
|
+
set volume(volume: number);
|
|
8
|
+
play(): Promise<void>;
|
|
9
|
+
pause(): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
}
|
|
2
12
|
declare class MusicPlayer {
|
|
3
13
|
#private;
|
|
4
14
|
constructor();
|
|
15
|
+
get enabled(): boolean;
|
|
16
|
+
set enabled(v: boolean);
|
|
17
|
+
enable(): void;
|
|
18
|
+
disable(): void;
|
|
19
|
+
toggle(): void;
|
|
5
20
|
get volume(): number;
|
|
6
21
|
set volume(volume: number);
|
|
7
22
|
play(src: string): void;
|
|
@@ -11,6 +26,11 @@ declare class MusicPlayer {
|
|
|
11
26
|
declare class SfxPlayer {
|
|
12
27
|
#private;
|
|
13
28
|
constructor();
|
|
29
|
+
get enabled(): boolean;
|
|
30
|
+
set enabled(v: boolean);
|
|
31
|
+
enable(): void;
|
|
32
|
+
disable(): void;
|
|
33
|
+
toggle(): void;
|
|
14
34
|
get volume(): number;
|
|
15
35
|
set volume(volume: number);
|
|
16
36
|
play(src: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../../../src/asset/audio.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,cAAoE,CAAA;
|
|
1
|
+
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../../../src/asset/audio.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,cAAoE,CAAA;AAoE7F,qBAAa,KAAK;;IAChB,GAAG,EAAE,MAAM,CAAA;gBAeC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;IAOtD,IAAI,MAAM,IACS,MAAM,CADW;IACpC,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAGxB;IAEK,IAAI;IA0DV,KAAK;IAaL,IAAI;CAOL;AAED,cAAM,WAAW;;;IA2Bf,IAAI,OAAO,IACI,OAAO,CADgB;IACtC,IAAI,OAAO,CAAC,CAAC,EAAE,OAAO,EAkBrB;IAED,MAAM;IACN,OAAO;IACP,MAAM;IAEN,IAAI,MAAM,IACS,MAAM,CADW;IACpC,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAIxB;IAED,IAAI,CAAC,GAAG,EAAE,MAAM;IAehB,KAAK;IAIL,IAAI;CAML;AAED,cAAM,SAAS;;;IAYb,IAAI,OAAO,IACI,OAAO,CADgB;IACtC,IAAI,OAAO,CAAC,CAAC,EAAE,OAAO,EAGrB;IAED,MAAM;IACN,OAAO;IACP,MAAM;IAEN,IAAI,MAAM,IACS,MAAM,CADW;IACpC,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAGxB;IAED,IAAI,CAAC,GAAG,EAAE,MAAM;IAMhB,UAAU,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE;CAI7B;AAED,eAAO,MAAM,WAAW,aAAoB,CAAA;AAC5C,eAAO,MAAM,SAAS,WAAkB,CAAA"}
|
package/package.json
CHANGED
package/src/asset/audio.ts
CHANGED
|
@@ -11,7 +11,9 @@ async function getAvailableContext(): Promise<AudioContext> {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
let isPageVisible = !document.hidden
|
|
14
|
-
window.addEventListener('visibilitychange', () =>
|
|
14
|
+
window.addEventListener('visibilitychange', () => {
|
|
15
|
+
isPageVisible = !document.hidden
|
|
16
|
+
})
|
|
15
17
|
|
|
16
18
|
type BasicStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
|
|
17
19
|
|
|
@@ -53,7 +55,21 @@ function createSafeStorage(): SafeStorage {
|
|
|
53
55
|
|
|
54
56
|
const safeStorage = createSafeStorage()
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
function clamp01(n: number) {
|
|
59
|
+
return Math.max(0, Math.min(1, n))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readBool(key: string, defaultValue: boolean) {
|
|
63
|
+
const v = safeStorage.getItem(key)
|
|
64
|
+
if (v == null) return defaultValue
|
|
65
|
+
return v === '1' || v === 'true'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeBool(key: string, value: boolean) {
|
|
69
|
+
safeStorage.setItem(key, value ? '1' : '0')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class Audio {
|
|
57
73
|
src: string
|
|
58
74
|
#volume: number
|
|
59
75
|
#loop: boolean
|
|
@@ -71,18 +87,19 @@ class Audio {
|
|
|
71
87
|
|
|
72
88
|
constructor(src: string, volume: number, loop: boolean) {
|
|
73
89
|
this.src = src
|
|
74
|
-
this.#volume = volume
|
|
90
|
+
this.#volume = clamp01(volume)
|
|
75
91
|
this.#loop = loop
|
|
76
92
|
this.play()
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
get volume() { return this.#volume }
|
|
80
96
|
set volume(volume: number) {
|
|
81
|
-
this.#volume = volume
|
|
82
|
-
if (this.#gainNode) this.#gainNode.gain.value =
|
|
97
|
+
this.#volume = clamp01(volume)
|
|
98
|
+
if (this.#gainNode) this.#gainNode.gain.value = this.#volume
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
async play() {
|
|
102
|
+
// On mobile, avoid starting audio while the page is hidden (often blocked / unreliable)
|
|
86
103
|
if (isMobile && !isPageVisible) return
|
|
87
104
|
|
|
88
105
|
if (!this.#audioBuffer) {
|
|
@@ -95,17 +112,26 @@ class Audio {
|
|
|
95
112
|
}
|
|
96
113
|
if (!this.#audioBuffer) return
|
|
97
114
|
|
|
115
|
+
// If already playing, restart cleanly
|
|
98
116
|
if (this.#isPlaying) this.stop()
|
|
117
|
+
|
|
118
|
+
// If this is not a resume, reset offset to the beginning
|
|
99
119
|
if (!this.#isPaused) this.#offset = 0
|
|
120
|
+
|
|
100
121
|
this.#isPlaying = true
|
|
101
122
|
this.#isPaused = false
|
|
123
|
+
|
|
102
124
|
if (!this.#audioContext) this.#audioContext = await getAvailableContext()
|
|
125
|
+
|
|
126
|
+
// If state changed while awaiting, bail out
|
|
103
127
|
if (!this.#isPlaying) return
|
|
104
128
|
|
|
105
129
|
if (!this.#gainNode) {
|
|
106
130
|
this.#gainNode = this.#audioContext.createGain()
|
|
107
131
|
this.#gainNode.gain.value = this.#volume
|
|
108
132
|
this.#gainNode.connect(this.#audioContext.destination)
|
|
133
|
+
} else {
|
|
134
|
+
this.#gainNode.gain.value = this.#volume
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
this.#source = this.#audioContext.createBufferSource()
|
|
@@ -114,13 +140,18 @@ class Audio {
|
|
|
114
140
|
this.#source.connect(this.#gainNode)
|
|
115
141
|
this.#source.start(0, this.#offset)
|
|
116
142
|
this.#startTime = this.#audioContext.currentTime
|
|
117
|
-
|
|
143
|
+
|
|
144
|
+
this.#source.onended = () => {
|
|
145
|
+
// Only auto-stop for one-shot sounds that were not paused and are not looping
|
|
146
|
+
if (!this.#isPaused && !this.#loop) this.stop()
|
|
147
|
+
}
|
|
118
148
|
}
|
|
119
149
|
|
|
120
150
|
#clear(): void {
|
|
121
151
|
if (this.#source) {
|
|
122
|
-
|
|
123
|
-
this.#source.
|
|
152
|
+
// stop() can throw if already stopped; ignore safely
|
|
153
|
+
try { this.#source.stop() } catch { /* noop */ }
|
|
154
|
+
try { this.#source.disconnect() } catch { /* noop */ }
|
|
124
155
|
this.#source = undefined
|
|
125
156
|
}
|
|
126
157
|
}
|
|
@@ -128,6 +159,7 @@ class Audio {
|
|
|
128
159
|
pause() {
|
|
129
160
|
if (this.#isPlaying && !this.#isPaused) {
|
|
130
161
|
if (this.#audioContext) {
|
|
162
|
+
// Track elapsed time so we can resume from the correct offset
|
|
131
163
|
this.#pauseTime = this.#audioContext.currentTime
|
|
132
164
|
this.#offset += this.#pauseTime - this.#startTime
|
|
133
165
|
}
|
|
@@ -138,6 +170,7 @@ class Audio {
|
|
|
138
170
|
}
|
|
139
171
|
|
|
140
172
|
stop() {
|
|
173
|
+
// Full stop resets the offset; next play starts from the beginning
|
|
141
174
|
this.#isPlaying = false
|
|
142
175
|
this.#isPaused = false
|
|
143
176
|
this.#offset = 0
|
|
@@ -147,35 +180,74 @@ class Audio {
|
|
|
147
180
|
|
|
148
181
|
class MusicPlayer {
|
|
149
182
|
#volume = 0.7
|
|
183
|
+
#enabled = true
|
|
150
184
|
#currentAudio?: Audio
|
|
185
|
+
#pendingSrc?: string
|
|
151
186
|
|
|
152
187
|
constructor() {
|
|
153
|
-
const
|
|
154
|
-
this.#volume = Number.isNaN(
|
|
188
|
+
const storedVol = parseFloat(safeStorage.getItem('musicVolume') || '')
|
|
189
|
+
this.#volume = Number.isNaN(storedVol) ? this.#volume : clamp01(storedVol)
|
|
190
|
+
|
|
191
|
+
// Separate from volume: true/false music enable state
|
|
192
|
+
this.#enabled = readBool('musicEnabled', true)
|
|
155
193
|
|
|
156
194
|
if (isMobile) {
|
|
157
195
|
document.addEventListener('visibilitychange', () => {
|
|
158
|
-
if (document.hidden)
|
|
159
|
-
|
|
196
|
+
if (document.hidden) {
|
|
197
|
+
// When hidden, pause to avoid mobile audio issues
|
|
198
|
+
this.pause()
|
|
199
|
+
} else {
|
|
160
200
|
isPageVisible = true
|
|
161
|
-
|
|
201
|
+
// Only resume if enabled
|
|
202
|
+
if (this.#enabled) this.#currentAudio?.play()
|
|
162
203
|
}
|
|
163
204
|
})
|
|
164
205
|
}
|
|
165
206
|
}
|
|
166
207
|
|
|
167
|
-
get
|
|
168
|
-
|
|
208
|
+
get enabled() { return this.#enabled }
|
|
209
|
+
set enabled(v: boolean) {
|
|
210
|
+
this.#enabled = v
|
|
211
|
+
writeBool('musicEnabled', v)
|
|
212
|
+
|
|
213
|
+
if (!v) {
|
|
214
|
+
// "Off" means: do not output music. Keep the state by pausing (resume later).
|
|
215
|
+
this.pause()
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// When turning on, play the last requested track if any; otherwise just resume
|
|
220
|
+
if (this.#pendingSrc) {
|
|
221
|
+
const src = this.#pendingSrc
|
|
222
|
+
this.#pendingSrc = undefined
|
|
223
|
+
this.play(src)
|
|
224
|
+
} else {
|
|
225
|
+
this.#currentAudio?.play()
|
|
226
|
+
}
|
|
169
227
|
}
|
|
170
228
|
|
|
229
|
+
enable() { this.enabled = true }
|
|
230
|
+
disable() { this.enabled = false }
|
|
231
|
+
toggle() { this.enabled = !this.enabled }
|
|
232
|
+
|
|
233
|
+
get volume() { return this.#volume }
|
|
171
234
|
set volume(volume: number) {
|
|
172
|
-
this.#volume = volume
|
|
173
|
-
safeStorage.setItem('musicVolume', volume.toString())
|
|
174
|
-
if (this.#currentAudio) this.#currentAudio.volume = volume
|
|
235
|
+
this.#volume = clamp01(volume)
|
|
236
|
+
safeStorage.setItem('musicVolume', this.#volume.toString())
|
|
237
|
+
if (this.#currentAudio) this.#currentAudio.volume = this.#volume
|
|
175
238
|
}
|
|
176
239
|
|
|
177
240
|
play(src: string) {
|
|
178
|
-
if
|
|
241
|
+
// Remember the user's intent even if music is currently disabled
|
|
242
|
+
this.#pendingSrc = src
|
|
243
|
+
if (!this.#enabled) return
|
|
244
|
+
|
|
245
|
+
// If it's the same track, resume instead of recreating the Audio
|
|
246
|
+
if (this.#currentAudio?.src === src) {
|
|
247
|
+
this.#currentAudio.play()
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
179
251
|
this.#currentAudio?.stop()
|
|
180
252
|
this.#currentAudio = new Audio(src, this.#volume, true)
|
|
181
253
|
}
|
|
@@ -185,33 +257,49 @@ class MusicPlayer {
|
|
|
185
257
|
}
|
|
186
258
|
|
|
187
259
|
stop() {
|
|
260
|
+
// stop() is a hard reset (unlike pause)
|
|
188
261
|
this.#currentAudio?.stop()
|
|
189
262
|
this.#currentAudio = undefined
|
|
263
|
+
this.#pendingSrc = undefined
|
|
190
264
|
}
|
|
191
265
|
}
|
|
192
266
|
|
|
193
267
|
class SfxPlayer {
|
|
194
268
|
#volume = 1
|
|
269
|
+
#enabled = true
|
|
195
270
|
|
|
196
271
|
constructor() {
|
|
197
|
-
const
|
|
198
|
-
this.#volume = Number.isNaN(
|
|
272
|
+
const storedVol = parseFloat(safeStorage.getItem('sfxVolume') || '')
|
|
273
|
+
this.#volume = Number.isNaN(storedVol) ? this.#volume : clamp01(storedVol)
|
|
274
|
+
|
|
275
|
+
// Separate from volume: true/false SFX enable state
|
|
276
|
+
this.#enabled = readBool('sfxEnabled', true)
|
|
199
277
|
}
|
|
200
278
|
|
|
201
|
-
get
|
|
202
|
-
|
|
279
|
+
get enabled() { return this.#enabled }
|
|
280
|
+
set enabled(v: boolean) {
|
|
281
|
+
this.#enabled = v
|
|
282
|
+
writeBool('sfxEnabled', v)
|
|
203
283
|
}
|
|
204
284
|
|
|
285
|
+
enable() { this.enabled = true }
|
|
286
|
+
disable() { this.enabled = false }
|
|
287
|
+
toggle() { this.enabled = !this.enabled }
|
|
288
|
+
|
|
289
|
+
get volume() { return this.#volume }
|
|
205
290
|
set volume(volume: number) {
|
|
206
|
-
this.#volume = volume
|
|
207
|
-
safeStorage.setItem('sfxVolume', volume.toString())
|
|
291
|
+
this.#volume = clamp01(volume)
|
|
292
|
+
safeStorage.setItem('sfxVolume', this.#volume.toString())
|
|
208
293
|
}
|
|
209
294
|
|
|
210
295
|
play(src: string) {
|
|
296
|
+
// If disabled, do not play any one-shot sounds
|
|
297
|
+
if (!this.#enabled) return
|
|
211
298
|
new Audio(src, this.#volume, false)
|
|
212
299
|
}
|
|
213
300
|
|
|
214
301
|
playRandom(...srcs: string[]) {
|
|
302
|
+
if (!this.#enabled) return
|
|
215
303
|
this.play(srcs[Math.floor(Math.random() * srcs.length)])
|
|
216
304
|
}
|
|
217
305
|
}
|