@novely/renderer-toolkit
Version:
Toolkit for creating renderer for novely
1 lines • 98.2 kB
Source Map (JSON)
{"version":3,"sources":["../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/task/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/clean-stores/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/atom/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/lifecycle/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/computed/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/deep-map/path.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/deep-map/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/keep-mount/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/listen-keys/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/map/index.js","../../../node_modules/.pnpm/nanostores@0.10.3/node_modules/nanostores/map-creator/index.js","../../../node_modules/.pnpm/dequal@2.0.3/node_modules/dequal/dist/index.mjs","../src/atoms/memo.ts","../src/atoms/deep-atom.ts","../src/state/context-state.ts","../src/state/renderer-state.ts","../src/utils/noop.ts","../src/utils/findLast.ts","../src/utils/escape-html.ts","../src/renderer/start.ts","../../../node_modules/.pnpm/yocto-queue@1.1.1/node_modules/yocto-queue/index.js","../../../node_modules/.pnpm/p-limit@6.1.0/node_modules/p-limit/index.js","../../../node_modules/.pnpm/simple-web-audio@0.7.0_postcss@8.4.32/node_modules/simple-web-audio/src/wait_for_interaction.ts","../../../node_modules/.pnpm/simple-web-audio@0.7.0_postcss@8.4.32/node_modules/simple-web-audio/src/browser-events.ts","../../../node_modules/.pnpm/simple-web-audio@0.7.0_postcss@8.4.32/node_modules/simple-web-audio/src/queue.ts","../../../node_modules/.pnpm/simple-web-audio@0.7.0_postcss@8.4.32/node_modules/simple-web-audio/src/memo.ts","../../../node_modules/.pnpm/simple-web-audio@0.7.0_postcss@8.4.32/node_modules/simple-web-audio/src/audio.ts","../src/audio/audio.ts","../src/shared/create-shared.ts","../src/context/create-get-context.ts","../src/root/root-setter.ts","../src/context/vibrate.ts","../src/context/background.ts","../src/context/actions.ts"],"sourcesContent":["let tasks = 0\nlet resolves = []\n\nexport function startTask() {\n tasks += 1\n return () => {\n tasks -= 1\n if (tasks === 0) {\n let prevResolves = resolves\n resolves = []\n for (let i of prevResolves) i()\n }\n }\n}\n\nexport function task(cb) {\n let endTask = startTask()\n let promise = cb().finally(endTask)\n promise.t = true\n return promise\n}\n\nexport function allTasks() {\n if (tasks === 0) {\n return Promise.resolve()\n } else {\n return new Promise(resolve => {\n resolves.push(resolve)\n })\n }\n}\n\nexport function cleanTasks() {\n tasks = 0\n}\n","import { cleanTasks } from '../task/index.js'\n\nexport let clean = Symbol('clean')\n\nexport let cleanStores = (...stores) => {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'cleanStores() can be used only during development or tests'\n )\n }\n cleanTasks()\n for (let $store of stores) {\n if ($store) {\n if ($store.mocked) delete $store.mocked\n if ($store[clean]) $store[clean]()\n }\n }\n}\n","import { clean } from '../clean-stores/index.js'\n\nlet listenerQueue = []\n\nexport let atom = (initialValue, level) => {\n let listeners = []\n let $atom = {\n get() {\n if (!$atom.lc) {\n $atom.listen(() => {})()\n }\n return $atom.value\n },\n l: level || 0,\n lc: 0,\n listen(listener, listenerLevel) {\n $atom.lc = listeners.push(listener, listenerLevel || $atom.l) / 2\n\n return () => {\n let index = listeners.indexOf(listener)\n if (~index) {\n listeners.splice(index, 2)\n if (!--$atom.lc) $atom.off()\n }\n }\n },\n notify(oldValue, changedKey) {\n let runListenerQueue = !listenerQueue.length\n for (let i = 0; i < listeners.length; i += 2) {\n listenerQueue.push(\n listeners[i],\n listeners[i + 1],\n $atom.value,\n oldValue,\n changedKey\n )\n }\n\n if (runListenerQueue) {\n for (let i = 0; i < listenerQueue.length; i += 5) {\n let skip\n for (let j = i + 1; !skip && (j += 5) < listenerQueue.length; ) {\n if (listenerQueue[j] < listenerQueue[i + 1]) {\n skip = listenerQueue.push(\n listenerQueue[i],\n listenerQueue[i + 1],\n listenerQueue[i + 2],\n listenerQueue[i + 3],\n listenerQueue[i + 4]\n )\n }\n }\n\n if (!skip) {\n listenerQueue[i](\n listenerQueue[i + 2],\n listenerQueue[i + 3],\n listenerQueue[i + 4]\n )\n }\n }\n listenerQueue.length = 0\n }\n },\n /* It will be called on last listener unsubscribing.\n We will redefine it in onMount and onStop. */\n off() {},\n set(newValue) {\n let oldValue = $atom.value\n if (oldValue !== newValue) {\n $atom.value = newValue\n $atom.notify(oldValue)\n }\n },\n subscribe(listener, listenerLevel) {\n let unbind = $atom.listen(listener, listenerLevel)\n listener($atom.value)\n return unbind\n },\n value: initialValue\n }\n\n if (process.env.NODE_ENV !== 'production') {\n $atom[clean] = () => {\n listeners = []\n $atom.lc = 0\n $atom.off()\n }\n }\n\n return $atom\n}\n","import { clean } from '../clean-stores/index.js'\n\nconst START = 0\nconst STOP = 1\nconst SET = 2\nconst NOTIFY = 3\nconst MOUNT = 5\nconst UNMOUNT = 6\nconst REVERT_MUTATION = 10\n\nexport let on = (object, listener, eventKey, mutateStore) => {\n object.events = object.events || {}\n if (!object.events[eventKey + REVERT_MUTATION]) {\n object.events[eventKey + REVERT_MUTATION] = mutateStore(eventProps => {\n // eslint-disable-next-line no-sequences\n object.events[eventKey].reduceRight((event, l) => (l(event), event), {\n shared: {},\n ...eventProps\n })\n })\n }\n object.events[eventKey] = object.events[eventKey] || []\n object.events[eventKey].push(listener)\n return () => {\n let currentListeners = object.events[eventKey]\n let index = currentListeners.indexOf(listener)\n currentListeners.splice(index, 1)\n if (!currentListeners.length) {\n delete object.events[eventKey]\n object.events[eventKey + REVERT_MUTATION]()\n delete object.events[eventKey + REVERT_MUTATION]\n }\n }\n}\n\nexport let onStart = ($store, listener) =>\n on($store, listener, START, runListeners => {\n let originListen = $store.listen\n $store.listen = arg => {\n if (!$store.lc && !$store.starting) {\n $store.starting = true\n runListeners()\n delete $store.starting\n }\n return originListen(arg)\n }\n return () => {\n $store.listen = originListen\n }\n })\n\nexport let onStop = ($store, listener) =>\n on($store, listener, STOP, runListeners => {\n let originOff = $store.off\n $store.off = () => {\n runListeners()\n originOff()\n }\n return () => {\n $store.off = originOff\n }\n })\n\nexport let onSet = ($store, listener) =>\n on($store, listener, SET, runListeners => {\n let originSet = $store.set\n let originSetKey = $store.setKey\n if ($store.setKey) {\n $store.setKey = (changed, changedValue) => {\n let isAborted\n let abort = () => {\n isAborted = true\n }\n\n runListeners({\n abort,\n changed,\n newValue: { ...$store.value, [changed]: changedValue }\n })\n if (!isAborted) return originSetKey(changed, changedValue)\n }\n }\n $store.set = newValue => {\n let isAborted\n let abort = () => {\n isAborted = true\n }\n\n runListeners({ abort, newValue })\n if (!isAborted) return originSet(newValue)\n }\n return () => {\n $store.set = originSet\n $store.setKey = originSetKey\n }\n })\n\nexport let onNotify = ($store, listener) =>\n on($store, listener, NOTIFY, runListeners => {\n let originNotify = $store.notify\n $store.notify = (oldValue, changed) => {\n let isAborted\n let abort = () => {\n isAborted = true\n }\n\n runListeners({ abort, changed, oldValue })\n if (!isAborted) return originNotify(oldValue, changed)\n }\n return () => {\n $store.notify = originNotify\n }\n })\n\nexport let STORE_UNMOUNT_DELAY = 1000\n\nexport let onMount = ($store, initialize) => {\n let listener = payload => {\n let destroy = initialize(payload)\n if (destroy) $store.events[UNMOUNT].push(destroy)\n }\n return on($store, listener, MOUNT, runListeners => {\n let originListen = $store.listen\n $store.listen = (...args) => {\n if (!$store.lc && !$store.active) {\n $store.active = true\n runListeners()\n }\n return originListen(...args)\n }\n\n let originOff = $store.off\n $store.events[UNMOUNT] = []\n $store.off = () => {\n originOff()\n setTimeout(() => {\n if ($store.active && !$store.lc) {\n $store.active = false\n for (let destroy of $store.events[UNMOUNT]) destroy()\n $store.events[UNMOUNT] = []\n }\n }, STORE_UNMOUNT_DELAY)\n }\n\n if (process.env.NODE_ENV !== 'production') {\n let originClean = $store[clean]\n $store[clean] = () => {\n for (let destroy of $store.events[UNMOUNT]) destroy()\n $store.events[UNMOUNT] = []\n $store.active = false\n originClean()\n }\n }\n\n return () => {\n $store.listen = originListen\n $store.off = originOff\n }\n })\n}\n","import { atom } from '../atom/index.js'\nimport { onMount } from '../lifecycle/index.js'\n\nlet computedStore = (stores, cb, batched) => {\n if (!Array.isArray(stores)) stores = [stores]\n\n let previousArgs\n let currentRunId = 0\n let set = () => {\n let args = stores.map($store => $store.get())\n if (\n previousArgs === undefined ||\n args.some((arg, i) => arg !== previousArgs[i])\n ) {\n let runId = ++currentRunId\n previousArgs = args\n let value = cb(...args)\n if (value && value.then && value.t) {\n value.then(asyncValue => {\n if (runId === currentRunId) {\n // Prevent a stale set\n $computed.set(asyncValue)\n }\n })\n } else {\n $computed.set(value)\n }\n }\n }\n let $computed = atom(undefined, Math.max(...stores.map($s => $s.l)) + 1)\n\n let timer\n let run = batched\n ? () => {\n clearTimeout(timer)\n timer = setTimeout(set)\n }\n : set\n\n onMount($computed, () => {\n let unbinds = stores.map($store => $store.listen(run, -1 / $computed.l))\n set()\n return () => {\n for (let unbind of unbinds) unbind()\n }\n })\n\n return $computed\n}\n\nexport let computed = (stores, fn) => computedStore(stores, fn)\nexport let batched = (stores, fn) => computedStore(stores, fn, true)\n","export function getPath(obj, path) {\n let allKeys = getAllKeysFromPath(path)\n let res = obj\n for (let key of allKeys) {\n if (res === undefined) {\n break\n }\n res = res[key]\n }\n return res\n}\n\nexport function setPath(obj, path, value) {\n return setByKey(obj != null ? obj : {}, getAllKeysFromPath(path), value)\n}\n\nfunction setByKey(obj, splittedKeys, value) {\n let key = splittedKeys[0]\n ensureKey(obj, key, splittedKeys[1])\n let copy = Array.isArray(obj) ? [...obj] : { ...obj }\n if (splittedKeys.length === 1) {\n if (value === undefined) {\n if (Array.isArray(obj)) {\n copy.splice(key, 1)\n } else {\n delete copy[key]\n }\n } else {\n copy[key] = value\n }\n return copy\n }\n let newVal = setByKey(obj[key], splittedKeys.slice(1), value)\n obj[key] = newVal\n return obj\n}\n\nconst ARRAY_INDEX = /(.*)\\[(\\d+)\\]/\n\nfunction getAllKeysFromPath(path) {\n return path.split('.').flatMap(key => getKeyAndIndicesFromKey(key))\n}\n\nfunction getKeyAndIndicesFromKey(key) {\n if (ARRAY_INDEX.test(key)) {\n let [, keyPart, index] = key.match(ARRAY_INDEX)\n return [...getKeyAndIndicesFromKey(keyPart), index]\n }\n return [key]\n}\n\nconst IS_NUMBER = /^\\d+$/\nfunction ensureKey(obj, key, nextKey) {\n if (key in obj) {\n return\n }\n\n let isNum = IS_NUMBER.test(nextKey)\n\n if (isNum) {\n obj[key] = Array(parseInt(nextKey, 10) + 1).fill(undefined)\n } else {\n obj[key] = {}\n }\n}\n","import { atom } from '../atom/index.js'\nimport { getPath, setPath } from './path.js'\n\nexport { getPath, setPath } from './path.js'\n\nexport function deepMap(initial = {}) {\n let $deepMap = atom(initial)\n $deepMap.setKey = (key, value) => {\n let oldValue\n try {\n oldValue = structuredClone($deepMap.value)\n } catch {\n oldValue = { ...$deepMap.value }\n }\n if (getPath($deepMap.value, key) !== value) {\n $deepMap.value = { ...setPath($deepMap.value, key, value) }\n $deepMap.notify(oldValue, key)\n }\n }\n return $deepMap\n}\n","export let keepMount = $store => {\n $store.listen(() => {})\n}\n","export function listenKeys($store, keys, listener) {\n let keysSet = new Set([...keys, undefined])\n return $store.listen((value, oldValue, changed) => {\n if (keysSet.has(changed)) {\n listener(value, oldValue, changed)\n }\n })\n}\n","import { atom } from '../atom/index.js'\n\nexport let map = (initial = {}) => {\n let $map = atom(initial)\n\n $map.setKey = function (key, value) {\n let oldMap = $map.value\n if (typeof value === 'undefined' && key in $map.value) {\n $map.value = { ...$map.value }\n delete $map.value[key]\n $map.notify(oldMap, key)\n } else if ($map.value[key] !== value) {\n $map.value = {\n ...$map.value,\n [key]: value\n }\n $map.notify(oldMap, key)\n }\n }\n\n return $map\n}\n","import { clean } from '../clean-stores/index.js'\nimport { onMount } from '../lifecycle/index.js'\nimport { map } from '../map/index.js'\n\nexport function mapCreator(init) {\n let Creator = (id, ...args) => {\n if (!Creator.cache[id]) {\n Creator.cache[id] = Creator.build(id, ...args)\n }\n return Creator.cache[id]\n }\n\n Creator.build = (id, ...args) => {\n let store = map({ id })\n onMount(store, () => {\n let destroy\n if (init) destroy = init(store, id, ...args)\n return () => {\n delete Creator.cache[id]\n if (destroy) destroy()\n }\n })\n return store\n }\n\n Creator.cache = {}\n\n if (process.env.NODE_ENV !== 'production') {\n Creator[clean] = () => {\n for (let id in Creator.cache) {\n Creator.cache[id][clean]()\n }\n Creator.cache = {}\n }\n }\n\n return Creator\n}\n","var has = Object.prototype.hasOwnProperty;\n\nfunction find(iter, tar, key) {\n\tfor (key of iter.keys()) {\n\t\tif (dequal(key, tar)) return key;\n\t}\n}\n\nexport function dequal(foo, bar) {\n\tvar ctor, len, tmp;\n\tif (foo === bar) return true;\n\n\tif (foo && bar && (ctor=foo.constructor) === bar.constructor) {\n\t\tif (ctor === Date) return foo.getTime() === bar.getTime();\n\t\tif (ctor === RegExp) return foo.toString() === bar.toString();\n\n\t\tif (ctor === Array) {\n\t\t\tif ((len=foo.length) === bar.length) {\n\t\t\t\twhile (len-- && dequal(foo[len], bar[len]));\n\t\t\t}\n\t\t\treturn len === -1;\n\t\t}\n\n\t\tif (ctor === Set) {\n\t\t\tif (foo.size !== bar.size) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfor (len of foo) {\n\t\t\t\ttmp = len;\n\t\t\t\tif (tmp && typeof tmp === 'object') {\n\t\t\t\t\ttmp = find(bar, tmp);\n\t\t\t\t\tif (!tmp) return false;\n\t\t\t\t}\n\t\t\t\tif (!bar.has(tmp)) return false;\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tif (ctor === Map) {\n\t\t\tif (foo.size !== bar.size) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tfor (len of foo) {\n\t\t\t\ttmp = len[0];\n\t\t\t\tif (tmp && typeof tmp === 'object') {\n\t\t\t\t\ttmp = find(bar, tmp);\n\t\t\t\t\tif (!tmp) return false;\n\t\t\t\t}\n\t\t\t\tif (!dequal(len[1], bar.get(tmp))) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tif (ctor === ArrayBuffer) {\n\t\t\tfoo = new Uint8Array(foo);\n\t\t\tbar = new Uint8Array(bar);\n\t\t} else if (ctor === DataView) {\n\t\t\tif ((len=foo.byteLength) === bar.byteLength) {\n\t\t\t\twhile (len-- && foo.getInt8(len) === bar.getInt8(len));\n\t\t\t}\n\t\t\treturn len === -1;\n\t\t}\n\n\t\tif (ArrayBuffer.isView(foo)) {\n\t\t\tif ((len=foo.byteLength) === bar.byteLength) {\n\t\t\t\twhile (len-- && foo[len] === bar[len]);\n\t\t\t}\n\t\t\treturn len === -1;\n\t\t}\n\n\t\tif (!ctor || typeof foo === 'object') {\n\t\t\tlen = 0;\n\t\t\tfor (ctor in foo) {\n\t\t\t\tif (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;\n\t\t\t\tif (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false;\n\t\t\t}\n\t\t\treturn Object.keys(bar).length === len;\n\t\t}\n\t}\n\n\treturn foo !== foo && bar !== bar;\n}\n","import type { Atom, ReadableAtom } from 'nanostores';\nimport { onMount, atom } from 'nanostores';\nimport { dequal } from 'dequal'\n\nconst memo = <T, K>(input: Atom<T>, cb: (value: T) => K) => {\n const $memoized = atom<K>(cb(input.get()));\n\n const unsubscribe = input.subscribe((value) => {\n const comparable = cb(value);\n\n if (!dequal($memoized.get(), comparable)) {\n // @ts-ignore\n $memoized.set(typeof comparable === 'object' ? { ...comparable } : Array.isArray(comparable) ? [...comparable] : comparable)\n }\n })\n\n onMount($memoized, () => {\n return unsubscribe;\n })\n\n return $memoized as ReadableAtom<K>;\n}\n\nexport { memo }\n","import type { BaseDeepMap, DeepMapStore } from 'nanostores'\nimport { deepMap, setPath } from 'nanostores'\n\ntype AnyFunction = (...args: any[]) => any;\n\n/**\n * @deprecated\n * @todo remove it\n */\ntype NoInfer<T> = [T][T extends any ? 0 : never];\n\ntype GetPath<$AtomValue extends object, $MutateValue> = (object: $AtomValue) => $MutateValue;\ntype Setter<T> = T extends AnyFunction ? () => T : (T | ((prev: T) => T))\n\ntype DeepAtom<T extends BaseDeepMap> = DeepMapStore<T> & {\n mutate: <$MutateValue>(getPath: ((object: T) => $MutateValue), setter: Setter<NoInfer<$MutateValue>>) => NoInfer<$MutateValue>;\n}\n\nconst usePath = <$AtomValue extends BaseDeepMap, $MutateValue>(atomValue: $AtomValue, getPath: GetPath<$AtomValue, $MutateValue>) => {\n const targets = new Set();\n const path: PropertyKey[] = [];\n\n let current;\n\n const proxyHandler: ProxyHandler<any> = {\n get(target, prop, receiver) {\n if (targets.has(target)) {\n /**\n * Same property was accessed twice\n */\n throw new ReferenceError(`Attempted to access property on the same target multiple times.`)\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n targets.add(target);\n path.push(prop);\n\n current = value;\n\n if (value === undefined) {\n return new Proxy({}, proxyHandler);\n }\n\n if (value && typeof value === 'object') {\n return new Proxy(value, proxyHandler);\n }\n\n return value;\n },\n }\n\n getPath(new Proxy(atomValue, proxyHandler));\n\n if (path.length === 0) {\n throw new Error('No valid path extracted from the provided getPath function.')\n }\n\n return {\n path,\n value: current as $MutateValue\n }\n}\n\n/**\n * Creates a `deepMap` extended with `mutate` method\n *\n * @example\n * ```ts\n * const $user = deepAtom({ age: 16 });\n *\n * $user.mutate((s) => s.age, (age) => age + 1);\n * ```\n */\nconst deepAtom = <$AtomValue extends BaseDeepMap>(init: $AtomValue): DeepAtom<$AtomValue> => {\n const $atom = deepMap(init) as unknown as DeepAtom<$AtomValue>;\n\n $atom.mutate = (getPath, setter) => {\n const { path, value } = usePath($atom.get(), getPath);\n\n const newValue = typeof setter === 'function' ? setter(value) : setter;\n\n if (newValue === value) {\n return newValue;\n }\n\n const oldValue = $atom.value;\n\n /**\n * They split string and then call flatMap, but we are going to pass an array directly\n * @link https://github.com/nanostores/nanostores/blob/56b0fbc7f51d94073191309376b9cf63948b2c91/deep-map/path.js#L41\n */\n const fakedPath = {\n split: () => {\n return {\n flatMap: () => {\n return path;\n }\n }\n }\n }\n\n // @ts-expect-error Value is actually is not read-only\n $atom.value = setPath($atom.value, fakedPath, newValue)\n // @ts-expect-error There is a hidden notify method\n $atom.notify(oldValue, path.join('.'))\n\n return newValue;\n }\n\n return $atom;\n}\n\nexport { deepAtom }\nexport type { DeepAtom }\n","import type { CustomHandler, CustomHandlerGetResult } from '@novely/core';\nimport type { BaseDeepMap } from 'nanostores';\nimport type { DeepAtom } from '../atoms/deep-atom';\nimport { onMount, cleanStores } from 'nanostores';\nimport { deepAtom } from '../atoms/deep-atom';\n\ntype Disposable = {\n /**\n * Function that is called after action is completed and game should move forward\n *\n * @example\n * ```ts\n * function handleTextActionClick() {\n * const { resolve } = contextState.get().text;\n *\n * // as user clicked on text we will hide text by updating this store\n * contextState.setKey('text', { content: '' });\n *\n * // now proceed to go to next action\n * resolve()\n * }\n * ```\n */\n resolve?: () => void;\n}\n\ntype WithActionVisibility = {\n /**\n * Used to check if something should be rendered\n */\n visible: boolean;\n}\n\ntype Labelled = {\n /**\n * Label for the action.\n *\n * In example for Input action it might be \"Enter youʼ age\", and for Choice action it might be \"Select youʼr next move\"\n */\n label: string;\n}\n\ntype ContextStateBackground = {\n /**\n * In-game background image\n */\n background: string;\n /**\n * Function that is NOT provided by core. You can set it yourself.\n */\n clear?: () => void;\n}\n\ntype ContextStateCharacter = WithActionVisibility & {\n /**\n * Basically `element.style`\n */\n style: string | undefined;\n /**\n * Character removal can be delayed so it could be removed with animation.\n *\n * Storing timeout id is needed to cancel it if in example ShowCharacter was called before time for removal came to the end to prevent character unexpectedly be removed\n */\n hideTimeoutId?: ReturnType<typeof setTimeout>;\n}\n\ntype ContextStateCharacters = {\n [key: string]: ContextStateCharacter | undefined;\n}\n\ntype ContextStateCustomHandler = {\n /**\n * Node in which custom action is rendered\n */\n node: null | HTMLDivElement;\n /**\n * Custom Handler function itself\n */\n fn: CustomHandler;\n /**\n * Clear Function. Removes the action.\n */\n clear: () => void;\n}\n\ntype ContextStateCustomHandlers = {\n [key: string]: ContextStateCustomHandler | undefined\n}\n\ntype ContextStateText = Disposable & {\n /**\n * Text to be rendered\n */\n content: string;\n}\n\ntype ContextStateDialog = Disposable & WithActionVisibility & {\n /**\n * Character lyrics\n */\n content: string;\n /**\n * Character lyrics. It might be also empty\n */\n name: string;\n /**\n * Miniature character rendered along with text\n */\n miniature: {\n /**\n * Character\n */\n character?: string;\n /**\n * Character's emotion\n */\n emotion?: string;\n }\n}\n\ntype ContextStateInput = Disposable & WithActionVisibility & Labelled & {\n /**\n * Input Element. Input action very dependent on DOM so this is needed\n */\n element: null | HTMLInputElement;\n /**\n * When input validation failed this error message should be shown near input element.\n * When error is present, going to next action should be restricted.\n */\n error: string;\n /**\n * Function that should be called before input action should be removed\n */\n cleanup?: () => void;\n}\n\ntype ContextStateChoice = WithActionVisibility & Labelled & {\n /**\n * It is an array of choices.\n *\n * First item of choice is a choice text and second one is active it or not.\n * When choice is not action it should be impossible to select that choice.\n */\n choices: [string, boolean][];\n /**\n * Function that is called after choice was made and game should move forward\n * @param selected index\n * @example\n * ```ts\n * function handleChoiceActionSelection() {\n * const index = document.querySelector('select.choice').selectedIndex;\n *\n * const { resolve } = contextState.get().choice;\n *\n * // pass index\n * resolve(index);\n *\n * contextState.setKey('choice', { choices: [] });\n * }\n * ```\n */\n resolve?: (selected: number) => void;\n}\n\ntype ContextStateMeta = {\n /**\n * Is it currently in restoring phase\n */\n restoring: boolean\n /**\n * Is it in preview mode\n *\n * In this mode game should be un-playable\n */\n preview: boolean\n /**\n * Is Novely in goingBack state\n */\n goingBack: boolean\n}\n\n/**\n * State which is related to game contexts and contains data about it\n */\ntype ContextState = {\n /**\n * ShowBackground action.\n */\n background: ContextStateBackground;\n /**\n * Character information.\n */\n characters: ContextStateCharacters;\n /**\n * Text action. Basically shown over other action\n */\n text: ContextStateText;\n /**\n * Dialog action.\n */\n dialog: ContextStateDialog;\n /**\n * Input action.\n */\n input: ContextStateInput;\n /**\n * Choice action.\n */\n choice: ContextStateChoice;\n /**\n * Meta information about current context\n */\n meta: ContextStateMeta;\n /**\n * Custom Action store\n */\n custom: ContextStateCustomHandlers;\n}\n\nconst defaultEmpty = {} satisfies BaseDeepMap;\n\ntype ContextStateStore<Extension extends BaseDeepMap = typeof defaultEmpty> = ContextState & Extension\n\nconst getDefaultContextState = (): ContextState => {\n return {\n background: {\n background: '#000',\n },\n characters: {},\n choice: {\n label: '',\n visible: false,\n choices: [],\n },\n dialog: {\n content: '',\n name: '',\n visible: false,\n miniature: {},\n },\n input: {\n element: null,\n label: '',\n error: '',\n visible: false\n },\n text: {\n content: ''\n },\n custom: {},\n meta: {\n restoring: false,\n goingBack: false,\n preview: false\n }\n }\n}\n\n/**\n * Creates typed context state root\n *\n * @example\n * ```ts\n * const { useContextState, removeContextState } = createContextStateRoot<{ additionalContextProp: number }>(() => {\n * return {\n * additionalContextProp: 123\n * }\n * });\n *\n * // when you want to create or get context state\n * useContextState('id here')\n *\n * // when context state should be removed\n * removeContextState('id here')\n * ```\n */\nconst createContextStateRoot = <Extension extends BaseDeepMap = typeof defaultEmpty>(getExtension: () => Extension = () => ({}) as Extension) => {\n const CACHE = new Map<string, DeepAtom<ContextStateStore<Extension>>>();\n\n const make = () => {\n const contextState = deepAtom<ContextStateStore<Extension>>({\n ...getDefaultContextState(),\n ...getExtension()\n } as ContextStateStore<Extension>);\n\n return contextState;\n }\n\n const remove = (id: string) => {\n const contextState = CACHE.get(id);\n\n if (contextState) {\n cleanStores(contextState)\n }\n\n CACHE.delete(id);\n }\n\n const use = (id: string) => {\n const cached = CACHE.get(id);\n\n if (cached) {\n return cached;\n }\n\n const contextState = make();\n\n CACHE.set(id, contextState);\n\n onMount(contextState, () => {\n return () => {\n CACHE.delete(id);\n }\n })\n\n return contextState;\n }\n\n return {\n useContextState: use,\n removeContextState: remove\n }\n}\n\nexport { createContextStateRoot }\nexport type {\n ContextStateStore,\n ContextState,\n ContextStateCharacter,\n ContextStateCustomHandler\n}\n","import type { NovelyScreen } from '@novely/core';\nimport type { BaseDeepMap } from 'nanostores';\nimport { deepAtom } from '../atoms/deep-atom';\n\n/**\n * State which is related to whole renderer\n */\ntype RendererState = {\n /**\n * Current screen that should be rendered\n */\n screen: NovelyScreen;\n /**\n * Is loading shown. Unlike screen 'loading', it does not change screen and shown above all layers\n */\n loadingShown: boolean;\n /**\n * Is exit prompt should be shown\n */\n exitPromptShown: boolean;\n}\n\nconst defaultEmpty = {} satisfies BaseDeepMap;\n\ntype RendererStateStore<Extension extends BaseDeepMap = typeof defaultEmpty> = RendererState & Extension;\n\n/**\n * Helper to make renderer state with default recommended values\n * @param extension Additional object to be merged with default values\n * @returns Store\n * @example\n * ```ts\n * createRenderer(() => {\n * const rendererState = createRendererState();\n *\n * return {\n * ui: {\n * showScreen(name) {\n * rendererState.setKey('screen', name)\n * },\n * getScreen() {\n * return rendererState.get().screen;\n * }\n * }\n * }\n * })\n * ```\n */\nconst createRendererState = <Extension extends BaseDeepMap = typeof defaultEmpty>(extension = defaultEmpty as Extension) => {\n const rendererState = deepAtom<RendererStateStore<Extension>>({\n screen: 'mainmenu',\n loadingShown: false,\n exitPromptShown: false,\n ...extension\n } as RendererStateStore<Extension>)\n\n return rendererState;\n}\n\nexport { createRendererState }\nexport type { RendererState, RendererStateStore }\n","const noop = () => {};\n\nexport { noop }\n","const findLastIndex = <T>(array: T[], fn: (this: T[], item: T, index: number, array: T[]) => boolean) => {\n\tfor (let i = array.length - 1; i >= 0; i--) {\n\t\tif (fn.call(array, array[i], i, array)) {\n\t\t\treturn i;\n\t\t}\n\t}\n\n\treturn -1;\n};\n\n/**\n * Using this because `Array.prototype.findLast` has not enough support\n * @see https://caniuse.com/?search=findLast\n */\nconst findLast = <T>(array: T[], fn: (this: T[], item: T, index: number, array: T[]) => boolean) => {\n\treturn array[findLastIndex(array, fn)];\n}\n\nexport { findLastIndex, findLast }\n","const escaped: Record<string, string> = {\n\t'\"': '"',\n\t\"'\": ''',\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n};\n\nconst escapeHTML = (str: string) => {\n\treturn String(str).replace(/[\"'&<>]/g, (match) => escaped[match]);\n};\n\nexport { escapeHTML }\n","import { noop } from \"../utils\";\n\n/**\n * Unmounts app\n *\n * @example\n * ```ts\n * import { createRoot } from 'react-dom/client';\n *\n * const root = createRoot(document.body);\n *\n * root.render(<App />);\n *\n * // this is we want\n * root.unmount();\n * ```\n */\ntype StartFunctionUnMountFn = () => void;\n\n/**\n * Mounts app, returns unmount function\n *\n * @example\n * ```ts\n * import { createRoot } from 'react-dom/client';\n *\n * const root = createRoot(document.body);\n *\n * function start() {\n * root.render(<App />);\n *\n * return () => {\n * root.unmount();\n * }\n * }\n * ```\n */\ntype StartFunctionMountFn = () => StartFunctionUnMountFn;\n\n/**\n * @example\n * ```ts\n * import { createRoot } from 'react-dom/client';\n *\n * const root = createRoot(document.body);\n *\n * createStartFunction(() => {\n * root.render(<App />);\n *\n * return () => {\n * root.unmount();\n * }\n * })\n * ```\n */\nconst createStartFunction = (fn: StartFunctionMountFn) => {\n let unmount: StartFunctionUnMountFn = noop;\n\n return () => {\n unmount();\n unmount = fn();\n\n return {\n unmount: () => {\n unmount();\n unmount = noop;\n }\n }\n }\n}\n\nexport { createStartFunction }\nexport type { StartFunctionMountFn, StartFunctionUnMountFn }\n","/*\nHow it works:\n`this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.\n*/\n\nclass Node {\n\tvalue;\n\tnext;\n\n\tconstructor(value) {\n\t\tthis.value = value;\n\t}\n}\n\nexport default class Queue {\n\t#head;\n\t#tail;\n\t#size;\n\n\tconstructor() {\n\t\tthis.clear();\n\t}\n\n\tenqueue(value) {\n\t\tconst node = new Node(value);\n\n\t\tif (this.#head) {\n\t\t\tthis.#tail.next = node;\n\t\t\tthis.#tail = node;\n\t\t} else {\n\t\t\tthis.#head = node;\n\t\t\tthis.#tail = node;\n\t\t}\n\n\t\tthis.#size++;\n\t}\n\n\tdequeue() {\n\t\tconst current = this.#head;\n\t\tif (!current) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#head = this.#head.next;\n\t\tthis.#size--;\n\t\treturn current.value;\n\t}\n\n\tpeek() {\n\t\tif (!this.#head) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn this.#head.value;\n\n\t\t// TODO: Node.js 18.\n\t\t// return this.#head?.value;\n\t}\n\n\tclear() {\n\t\tthis.#head = undefined;\n\t\tthis.#tail = undefined;\n\t\tthis.#size = 0;\n\t}\n\n\tget size() {\n\t\treturn this.#size;\n\t}\n\n\t* [Symbol.iterator]() {\n\t\tlet current = this.#head;\n\n\t\twhile (current) {\n\t\t\tyield current.value;\n\t\t\tcurrent = current.next;\n\t\t}\n\t}\n}\n","import Queue from 'yocto-queue';\n\nexport default function pLimit(concurrency) {\n\tvalidateConcurrency(concurrency);\n\n\tconst queue = new Queue();\n\tlet activeCount = 0;\n\n\tconst resumeNext = () => {\n\t\tif (activeCount < concurrency && queue.size > 0) {\n\t\t\tqueue.dequeue()();\n\t\t\t// Since `pendingCount` has been decreased by one, increase `activeCount` by one.\n\t\t\tactiveCount++;\n\t\t}\n\t};\n\n\tconst next = () => {\n\t\tactiveCount--;\n\n\t\tresumeNext();\n\t};\n\n\tconst run = async (function_, resolve, arguments_) => {\n\t\tconst result = (async () => function_(...arguments_))();\n\n\t\tresolve(result);\n\n\t\ttry {\n\t\t\tawait result;\n\t\t} catch {}\n\n\t\tnext();\n\t};\n\n\tconst enqueue = (function_, resolve, arguments_) => {\n\t\t// Queue `internalResolve` instead of the `run` function\n\t\t// to preserve asynchronous context.\n\t\tnew Promise(internalResolve => {\n\t\t\tqueue.enqueue(internalResolve);\n\t\t}).then(\n\t\t\trun.bind(undefined, function_, resolve, arguments_),\n\t\t);\n\n\t\t(async () => {\n\t\t\t// This function needs to wait until the next microtask before comparing\n\t\t\t// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously\n\t\t\t// after the `internalResolve` function is dequeued and called. The comparison in the if-statement\n\t\t\t// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.\n\t\t\tawait Promise.resolve();\n\n\t\t\tif (activeCount < concurrency) {\n\t\t\t\tresumeNext();\n\t\t\t}\n\t\t})();\n\t};\n\n\tconst generator = (function_, ...arguments_) => new Promise(resolve => {\n\t\tenqueue(function_, resolve, arguments_);\n\t});\n\n\tObject.defineProperties(generator, {\n\t\tactiveCount: {\n\t\t\tget: () => activeCount,\n\t\t},\n\t\tpendingCount: {\n\t\t\tget: () => queue.size,\n\t\t},\n\t\tclearQueue: {\n\t\t\tvalue() {\n\t\t\t\tqueue.clear();\n\t\t\t},\n\t\t},\n\t\tconcurrency: {\n\t\t\tget: () => concurrency,\n\n\t\t\tset(newConcurrency) {\n\t\t\t\tvalidateConcurrency(newConcurrency);\n\t\t\t\tconcurrency = newConcurrency;\n\n\t\t\t\tqueueMicrotask(() => {\n\t\t\t\t\t// eslint-disable-next-line no-unmodified-loop-condition\n\t\t\t\t\twhile (activeCount < concurrency && queue.size > 0) {\n\t\t\t\t\t\tresumeNext();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t});\n\n\treturn generator;\n}\n\nfunction validateConcurrency(concurrency) {\n\tif (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {\n\t\tthrow new TypeError('Expected `concurrency` to be a number from 1 and up');\n\t}\n}\n","const waitForInteraction = (() => {\n const { promise, resolve } = Promise.withResolvers<void>();\n\t\n\tconst onUserInteraction = () => {\n\t\tresolve();\n\t};\n\n\tdocument.addEventListener('touchstart', onUserInteraction, { once: true });\n\tdocument.addEventListener('touchend', onUserInteraction, { once: true });\n\tdocument.addEventListener('click', onUserInteraction, { once: true });\n\tdocument.addEventListener('keydown', onUserInteraction, { once: true });\n\n\treturn () => {\n return promise;\n };\n})();\n\nexport { waitForInteraction }","type EventHandler<T> = (this: Document, event: T) => void;\n\ntype BlurEventHandler = EventHandler<Event>;\ntype FocusEventHandler = EventHandler<FocusEvent>;\n\nconst BLUR_HANDLERS = new Set<BlurEventHandler>();\nconst FOCUS_HANDLERS = new Set<FocusEventHandler>();\n\ntype EventListeners = {\n focus: FocusEventHandler;\n blur: BlurEventHandler;\n}\n\nconst registerEventListeners = (listeners: EventListeners) => {\n BLUR_HANDLERS.add(listeners.blur);\n FOCUS_HANDLERS.add(listeners.focus);\n\n return () => {\n BLUR_HANDLERS.delete(listeners.blur);\n FOCUS_HANDLERS.delete(listeners.focus);\n }\n}\n\naddEventListener('focus', function (event) {\n for (const handler of FOCUS_HANDLERS) {\n try {\n handler.call(this.document, event);\n } catch {}\n }\n});\n\naddEventListener('blur', function (event) {\n for (const handler of BLUR_HANDLERS) {\n try {\n handler.call(this.document, event);\n } catch {}\n }\n});\n\nexport { registerEventListeners, BLUR_HANDLERS as _BLUR_HANDLERS, FOCUS_HANDLERS as _FOCUS_HANDLERS }\nexport type { EventHandler, BlurEventHandler, FocusEventHandler, EventListeners }","import pLimit from 'p-limit';\n\ntype Thenable<T> = T | Promise<T>;\ntype Queue = (() => Thenable<void>)[]\n\nconst createQueue = (queue: Queue, stopped = false) => {\n const limit = pLimit(1);\n\n const run = async () => {\n const items = queue.slice();\n\n for await (const item of items) {\n if (stopped) break;\n\n try {\n await item();\n } catch (error) {\n console.error(error);\n \n /**\n * In case that exception is handled then stopped will be set manually in catch block\n * But in other cases stop it here\n */\n stopped = true;\n }\n }\n\n queue = queue.filter(item => !items.includes(item));\n stopped = false;\n };\n\n return {\n get queue() {\n return queue;\n },\n set queue(value) {\n queue = value;\n },\n stop() {\n stopped = true;\n },\n execute: () => {\n return limit(run);\n }\n }\n}\n\nexport { createQueue }\nexport type { Queue }","/**\n * Primitive memo key-based wrapper.\n * \n * This memoization function wraps an asynchronous function and caches its result\n * based on a unique key. If the function has already been called with the same\n * key, the cached result (a promise) will be returned instead of invoking the\n * function again.\n * \n * @warning\n * This cache implementation does not handle promise rejections. If the provided\n * function's promise is rejected, the rejection will be cached, and any subsequent\n * calls with the same key will return the same rejected promise.\n */\nconst createMemo = <T>() => {\n const cache = new Map<string, Promise<T>>()\n\n return (key: string, fn: () => Promise<T>) => {\n return () => {\n const preserved = cache.get(key);\n\n if (preserved) {\n return preserved;\n }\n\n const promise = fn();\n\n cache.set(key, promise);\n\n return promise;\n }\n }\n}\n\nexport { createMemo }","import type { AudioInstance } from \"./types\";\nimport { waitForInteraction } from \"./wait_for_interaction\";\nimport { registerEventListeners } from './browser-events';\nimport { createQueue } from './queue';\nimport { createMemo } from './memo';\n\nconst fetcherMemo = createMemo<ArrayBuffer>();\nconst decoderMemo = createMemo<AudioBuffer>();\n\ntype ExtendAudioGraphOptions = {\n context: AudioContext;\n node: GainNode;\n}\n\ntype AudioNodeLike = {\n connect: ((destinationNode: AudioNode) => void);\n}\n\ntype ExtendAudioGraph = (options: ExtendAudioGraphOptions) => AudioNode | AudioNodeLike;\n\ntype AudioOptions = {\n /**\n * Source\n */\n src: string;\n /**\n * Loop\n * @default false\n */\n loop?: boolean;\n /**\n * Volume\n * @default 1\n */\n volume?: number;\n /**\n * Will pause playing on blur event, play on focus.\n * @default false\n */\n pauseOnBlur?: boolean;\n /**\n * @default false\n */\n autoplay?: boolean;\n /**\n * Function to extend audio \"graph\"\n */\n extendAudioGraph?: ExtendAudioGraph;\n};\n\nconst createAudio = (options: AudioOptions) => {\n let audioContext: AudioContext;\n let gainNode: GainNode;\n let bufferSource: AudioBufferSourceNode;\n let arrayBuffer: ArrayBuffer;\n let audioBuffer: AudioBuffer;\n\n /**\n * Values that pending it's queue to be set\n */\n let pendingVolume = options.volume || 1;\n let pendingLoop = options.loop || false;\n\n const createAudioContext = () => {\n audioContext = new AudioContext()\n }\n\n const getGainNode = () => {\n return gainNode;\n }\n\n const createGainNode = () => {\n gainNode = audioContext.createGain();\n\n const node = (options.extendAudioGraph || getGainNode)({\n context: audioContext,\n node: gainNode\n });\n\n node.connect(audioContext.destination);\n }\n\n const createBufferSource = () => {\n bufferSource = audioContext.createBufferSource();\n }\n\n const interruptQueueThenDestroy = (cause: Error | unknown) => {\n /**\n * Firstly prevent next queue items from running because they depend on previous items\n * Then destroy audio because there is no reason to try run it over and over again\n */\n queue.stop();\n instance.destroy();\n\n return new Error('', { cause });\n }\n\n const fetchArrayBuffer = fetcherMemo(options.src, async () => {\n try {\n return await fetch(options.src).then(response => response.arrayBuffer());\n } catch (error) {\n throw interruptQueueThenDestroy(error);\n }\n });\n\n const setArrayBuffer = async () => {\n arrayBuffer = await fetchArrayBuffer();\n }\n\n const decodeAudioData = decoderMemo(options.src, async () => {\n try {\n return await audioContext.decodeAudioData(arrayBuffer);\n } catch (error) {\n throw interruptQueueThenDestroy(error);\n }\n })\n\n const setAudioData = async () => {\n audioBuffer = await decodeAudioData();\n }\n\n const connectSources = () => {\n if (bufferSource && bufferSource.buffer === null) {\n bufferSource.buffer = audioBuffer;\n bufferSource.connect(gainNode);\n }\n }\n\n const setVolume = () => {\n gainNode.gain.value = pendingVolume;\n }\n\n const setLoop = () => {\n bufferSource.loop = pendingLoop;\n }\n\n const queue = createQueue([\n waitForInteraction,\n createAudioContext,\n createGainNode,\n setVolume,\n createBufferSource,\n setLoop,\n fetchArrayBuffer,\n setArrayBuffer,\n decodeAudioData,\n setAudioData,\n connectSources,\n ]);\n\n /**\n * Will resume when focus or not\n */\n let resume = false;\n\n const unregister = registerEventListeners({\n focus: () => {\n if (!options.pauseOnBlur || !resume || state.destroyed) return;\n\n resume = false;\n\n queue.queue.push(playAudio);\n queue.execute()\n },\n blur: () => {\n if (!options.pauseOnBlur || !state.playing || state.destroyed) return;\n\n resume = true;\n\n queue.queue.push(pauseAudio);\n queue.execute()\n }\n });\n\n const state = {\n started: false,\n playing: false,\n destroyed: false\n };\n\n const playAudio = async () => {\n if (state.destroyed) return;\n\n if (audioContext.state === \"suspended\") {\n await audioContext.resume();\n\n if (state.started) {\n state.playing = true;\n }\n }\n\n if (!state.started) {\n bufferSource.start();\n\n state.started = true;\n state.playing = true;\n }\n }\n\n const pauseAudio = async () => {\n if (state.destroyed) return;\n \n if (audioContext.state === \"suspended\" && queue.queue.at(-1) === playAudio) {\n queue.queue.pop();\n }\n\n if (audioContext.state === \"running\") {\n await audioContext.suspend();\n\n state.playing = false;\n }\n }\n\n const disconnectAudio = async () => {\n bufferSource && bufferSource.disconnect();\n\n /**\n * Reset `started` value\n * That will make `source.start()` call when `play()` will be called\n */\n state.started = false;\n }\n\n const instance = {\n async play() {\n if (state.destroyed) return;\n \n queue.queue.push(playAudio);\n\n return queue.execute();\n },\n async pause() {\n if (state.destroyed) return;\n \n queue.queue.push(pauseAudio);\n\n return queue.execute();\n },\n async reset() {\n if (state.destroyed) return;\n\n if (state.playing) {\n queue.queue.push(pauseAudio)\n }\n\n queue.queue.push(\n disconnectAudio,\n createBufferSource,\n setLoop,\n connectSources\n );\n\n if (state.playing) {\n queue.queue.push(playAudio)\n }\n\n return queue.execute();\n },\n async stop() {\n if (state.destroyed) return;\n\n queue.queue.push(\n pauseAudio,\n disconnectAudio,\n createBufferSource,\n setLoop,\n connectSources\n );\n\n return queue.execute();\n },\n async destroy() {\n if (state.destroyed) return;\n\n unregister();\n\n queue.queue = [\n pauseAudio,\n disconnectAudio\n ];\n\n await queue.execute();\n\n state.destroyed = true;\n\n // @ts-expect-error\n audioContext = null;\n // @ts-expect-error\n gainNode = null;\n // @ts-expect-error\n bufferSource = null;\n // @ts-expect-error\n arrayBuffer = null;\n // @ts-expect-error\n audioBuffer = null;\n },\n async fetch() {\n if (state.destroyed) return;\n\n await fetchArrayBuffer();\n },\n get playing() {\n return state.playing;\n },\n get destroyed() {\n return state.destroyed;\n },\n get volume() {\n return pendingVolume;\n },\n set volume(value) {\n if (state.destroyed) return;\n\n pendingVolume = value;\n queue.queue.push(setVolume);\n queue.execute()\n },\n get loop() {\n return pendingLoop;\n },\n set loop(value) {\n if (state.destroyed) return;\n \n pendingLoop = value;\n queue.queue.push(setLoop);\n queue.execute()\n }\n } satisfies AudioInstance;\n\n if (options.autoplay) {\n queue.queue.push(playAudio)\n queue.execute()\n }\n\n return instance;\n};\n\nconst prefetchAudio = (src: string) => {\n const fetcher = fetcherMemo(src, () => fetch(src).then(res => res.arrayBuffer()));\n\n return fetcher();\n}\n\nexport { prefetchAudio, createAudio };\nexport type { AudioOptions }","import type { AudioStore } from './types';\nimport type { AudioHandle, Context, Stored, StorageData, Data, Renderer } from '@novely/core';\nimport { createAudio as createWebAudio, prefetchAudio } from 'simple-web-audio';\nimport { noop } from '../utils';\n\ntype AudioContext = Context['audio'];\ntype AudioMisc = Pick<Renderer['misc'], 'preloadAudioBlocking'>;\ntype StorageDataStore = Stored<StorageData<string, Data>>;\n\ntype KeepAudio = {\n music: Set<string>;\n sounds: Set<string>;\n}\n\nconst TYPE_META_MAP = {\n 'music': 2,\n 'sound': 3,\n 'voice': 4\n} as const;\n\n/**\n * Audio easy! This implementation uses `simple-web-audio` package under the hood.\n *\n * @example\n * ```ts\n * const audio = createAudio(options.storageData);\n * ```\n */\nconst createAudio = (storageData: StorageDataStore) => {\n const store: AudioStore = {\n music: {},\n sound: {},\n voices: {},\n }\n\n const getVolume = (type: 'music' | 'sound' | 'voice') => {\n return storageData.get().meta[TYPE_META_MAP[type]];\n }\n\n const getAudio = (type: 'music' | 'sound' | 'voice', src: string) => {\n const kind = type === 'voice' ? 'voices' : type;\n const cached = store[kind][src];\n\n if (cached) return cached;\n\n const audio = createWebAudio({\n src,\n volume: getVolume(type),\n pauseOnBlur: true\n })\n\n store[kind][src] = audio;\n\n return audio;\n }\n\n let unsubscribe = noop;\n\n const context: AudioContext = {\n music(src, method) {\n const resource = getAudio(method, src);\n\n this.start();\n\n return {\n pause() {\n resource.pause();\n },\n play(loop) {\n try {\n resource.loop = loop;\n } catch {}\n\n resource.play();\n },\n stop() {\n resource.stop();\n },\n } satisfies AudioHandle;\n },\n voice(source) {\n this.start();\n