- commit
- 6e3be105a3be4960e332fdae2036b3ed040aa93c
- parent
- 0616901233bf0bf0885d92d6e2f7a741817e2040
- author
- Dami <dami.etoile@gmail.com>
@@ -1,22 +1,33 @@
1-
2-
3- UI Pack (2.0)
4-
5- Created/distributed by Kenney (www.kenney.nl)
6- Creation date: 12-06-2024
7-
8- ------------------------------
9-
10- License: (Creative Commons Zero, CC0)
11- http://creativecommons.org/publicdomain/zero/1.0/
12-
13- This content is free to use in personal, educational and commercial projects.
14- Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
15-
16- ------------------------------
17-
18- Donate: http://support.kenney.nl
19- Patreon: http://patreon.com/kenney/
20-
21- Follow on Twitter for updates:
22- http://twitter.com/KenneyNL\ No newline at end of file1+2+3+ UI Pack (2.0)4+5+ Created/distributed by Kenney (www.kenney.nl)6+ Creation date: 12-06-20247+8+ ------------------------------9+10+ License: (Creative Commons Zero, CC0)11+ http://creativecommons.org/publicdomain/zero/1.0/12+13+ This content is free to use in personal, educational and commercial projects.14+ Support us by crediting Kenney or www.kenney.nl (this is not mandatory)15+16+ ------------------------------17+18+ Donate: http://support.kenney.nl19+ Patreon: http://patreon.com/kenney/20+21+ Follow on Twitter for updates:22+ http://twitter.com/KenneyNL23+24+ Additional clips used by Miau Is True25+26+ The move, push, blocked, and defeat clips are from Kenney's Impact Sounds27+ (1.0) pack. The rule, transform, sink, undo, restart, menu, and scene28+ clips are from Kenney's Interface Sounds (1.0) pack. Both packs are CC0:29+ https://creativecommons.org/publicdomain/zero/1.0/30+31+ Source packs:32+ https://kenney.nl/assets/impact-sounds33+ https://kenney.nl/assets/interface-soundsBinary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
@@ -0,0 +1,225 @@
1+/**2+ * Small Web Audio facade for the game shell.3+ *4+ * Audio files are fetched as soon as the module is created, but the5+ * AudioContext is created only from a user gesture. This keeps browsers from6+ * rejecting the context while still having decoded buffers ready for play.7+ */8+9+const DEFAULT_CLIPS = Object.freeze({10+ move: ['move-0.ogg', 'move-1.ogg', 'move-2.ogg', 'move-3.ogg', 'move-4.ogg'],11+ push: ['push-0.ogg', 'push-1.ogg', 'push-2.ogg', 'push-3.ogg', 'push-4.ogg'],12+ blockedStop: ['blocked-stop-0.ogg', 'blocked-stop-1.ogg', 'blocked-stop-2.ogg', 'blocked-stop-3.ogg', 'blocked-stop-4.ogg'],13+ blockedBounds: ['blocked-bounds-0.ogg', 'blocked-bounds-1.ogg', 'blocked-bounds-2.ogg', 'blocked-bounds-3.ogg', 'blocked-bounds-4.ogg'],14+ ruleFormed: ['rule-formed-0.ogg', 'rule-formed-1.ogg', 'rule-formed-2.ogg', 'rule-formed-3.ogg'],15+ ruleBroken: ['rule-broken-0.ogg', 'rule-broken-1.ogg', 'rule-broken-2.ogg', 'rule-broken-3.ogg'],16+ transform: ['transform-0.ogg', 'transform-1.ogg'],17+ sink: ['sink-0.ogg', 'sink-1.ogg', 'sink-2.ogg', 'sink-3.ogg'],18+ defeat: ['defeat-0.ogg', 'defeat-1.ogg', 'defeat-2.ogg', 'defeat-3.ogg', 'defeat-4.ogg'],19+ win: ['win.ogg'],20+ undo: ['undo-0.ogg', 'undo-1.ogg', 'undo-2.ogg'],21+ undoEmpty: ['undo-empty-0.ogg', 'undo-empty-1.ogg', 'undo-empty-2.ogg', 'undo-empty-3.ogg'],22+ restart: ['restart-0.ogg', 'restart-1.ogg', 'restart-2.ogg', 'restart-3.ogg', 'restart-4.ogg'],23+ select: ['select.ogg'],24+ confirm: ['confirm.ogg'],25+ back: ['back.ogg'],26+ scene: ['scene.ogg'],27+});28+29+const ALIASES = Object.freeze({30+ rule: 'ruleFormed',31+ blocked: 'blockedStop',32+ bounds: 'blockedBounds',33+ undoEmpty: 'undoEmpty',34+});35+36+const DEFAULT_VOLUMES = Object.freeze({37+ move: 0.18,38+ push: 0.22,39+ blockedStop: 0.2,40+ blockedBounds: 0.2,41+ ruleFormed: 0.24,42+ ruleBroken: 0.2,43+ transform: 0.24,44+ sink: 0.22,45+ defeat: 0.24,46+ win: 0.3,47+ undo: 0.16,48+ undoEmpty: 0.12,49+ restart: 0.18,50+ select: 0.16,51+ confirm: 0.22,52+ back: 0.16,53+ scene: 0.2,54+});55+56+function readEnabled(storage, key) {57+ try {58+ return storage?.getItem(key) !== '0';59+ } catch {60+ return true;61+ }62+}63+64+function writeEnabled(storage, key, enabled) {65+ try {66+ if (enabled) storage?.removeItem(key);67+ else storage?.setItem(key, '0');68+ } catch {69+ // Storage may be unavailable in private browsing or embedded previews.70+ }71+}72+73+function choose(list, random = Math.random) {74+ return list[Math.floor(random() * list.length)] || list[0];75+}76+77+function availableStorage() {78+ try { return globalThis.localStorage; } catch { return null; }79+}80+81+export { DEFAULT_CLIPS };82+83+export function createAudioSystem({84+ basePath = '/audio/',85+ storage = availableStorage(),86+ storageKey = 'miau.sound.enabled',87+ clips = DEFAULT_CLIPS,88+ AudioContextCtor = globalThis.AudioContext || globalThis.webkitAudioContext,89+ fetchImpl = globalThis.fetch,90+ random = Math.random,91+} = {}) {92+ let enabled = readEnabled(storage, storageKey);93+ let musicEnabled = false;94+ let context = null;95+ let masterGain = null;96+ let unlocking = null;97+ let listenersTarget = null;98+ let removeGestureListeners = null;99+ let disposed = false;100+ const rawBuffers = new Map();101+ const buffers = new Map();102+ const loadErrors = new Map();103+104+ const preload = fetchImpl105+ ? Promise.all(Object.entries(clips).flatMap(([name, files]) => files.map(async file => {106+ try {107+ const response = await fetchImpl(`${basePath.replace(/\/$/, '')}/${file}`);108+ if (!response.ok) throw new Error(`Audio request failed (${response.status})`);109+ rawBuffers.set(file, await response.arrayBuffer());110+ } catch (error) {111+ loadErrors.set(name, error);112+ }113+ }))).then(() => rawBuffers)114+ : Promise.resolve(rawBuffers);115+116+ function setMasterGain() {117+ if (masterGain && context) masterGain.gain.setTargetAtTime(enabled ? 1 : 0, context.currentTime, 0.015);118+ }119+120+ async function unlock() {121+ if (disposed || !AudioContextCtor) return false;122+ if (unlocking) return unlocking;123+ unlocking = (async () => {124+ context ||= new AudioContextCtor();125+ masterGain ||= context.createGain();126+ masterGain.connect(context.destination);127+ setMasterGain();128+ if (context.state === 'suspended') await context.resume();129+ await preload;130+ for (const [name, files] of Object.entries(clips)) {131+ const decoded = [];132+ for (const file of files) {133+ if (buffers.has(file)) {134+ decoded.push(buffers.get(file));135+ continue;136+ }137+ const source = rawBuffers.get(file);138+ if (!source) continue;139+ try {140+ const buffer = await context.decodeAudioData(source.slice(0));141+ buffers.set(file, buffer);142+ decoded.push(buffer);143+ } catch (error) {144+ loadErrors.set(name, error);145+ }146+ }147+ if (decoded.length) buffers.set(name, decoded);148+ }149+ return true;150+ })().catch(() => false).finally(() => { unlocking = null; });151+ return unlocking;152+ }153+154+ function installGestureListeners(target = globalThis.document || globalThis) {155+ if (listenersTarget || !target?.addEventListener) return () => {};156+ listenersTarget = target;157+ const onGesture = () => {158+ void unlock().then(() => {159+ if (!listenersTarget) return;160+ for (const event of ['pointerdown', 'touchstart', 'keydown']) listenersTarget.removeEventListener(event, onGesture, true);161+ listenersTarget = null;162+ removeGestureListeners = null;163+ });164+ };165+ for (const event of ['pointerdown', 'touchstart', 'keydown']) target.addEventListener(event, onGesture, { capture: true, passive: true });166+ removeGestureListeners = () => {167+ if (listenersTarget !== target) return;168+ for (const event of ['pointerdown', 'touchstart', 'keydown']) target.removeEventListener(event, onGesture, true);169+ listenersTarget = null;170+ removeGestureListeners = null;171+ };172+ return removeGestureListeners;173+ }174+175+ function play(name, { volume = DEFAULT_VOLUMES[name] ?? 0.2, rate = 1, loop = false, offset = 0 } = {}) {176+ const resolved = ALIASES[name] || name;177+ if (!enabled || disposed) return false;178+ if (!context || !masterGain) {179+ void unlock().then(() => play(name, { volume, rate, loop, offset }));180+ return false;181+ }182+ const available = buffers.get(resolved);183+ if (!available?.length) return false;184+ const source = context.createBufferSource();185+ const gain = context.createGain();186+ source.buffer = choose(available, random);187+ source.playbackRate.value = rate;188+ source.loop = loop;189+ gain.gain.value = volume;190+ source.connect(gain).connect(masterGain);191+ source.start(0, offset);192+ return true;193+ }194+195+ function mute(value = true) {196+ enabled = !value;197+ writeEnabled(storage, storageKey, enabled);198+ setMasterGain();199+ return !enabled;200+ }201+202+ function toggleMute() {203+ return mute(enabled);204+ }205+206+ function dispose() {207+ disposed = true;208+ removeGestureListeners?.();209+ return context?.close?.();210+ }211+212+ return {213+ preload,214+ unlock,215+ installGestureListeners,216+ play,217+ mute,218+ toggleMute,219+ isMuted: () => !enabled,220+ isMusicEnabled: () => musicEnabled,221+ setMusicEnabled: value => { musicEnabled = Boolean(value); return musicEnabled; },222+ getStatus: () => ({ enabled, muted: !enabled, unlocked: Boolean(context), ready: buffers.size > 0, errors: [...loadErrors.keys()] }),223+ dispose,224+ };225+}