@novely/renderer-toolkit
Version:
Toolkit for creating renderer for novely
1,563 lines (1,535 loc) • 42.8 kB
JavaScript
;
var Novely = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
STORE_UNMOUNT_DELAY: () => STORE_UNMOUNT_DELAY,
allTasks: () => allTasks,
atom: () => atom,
batched: () => batched,
clean: () => clean,
cleanStores: () => cleanStores,
cleanTasks: () => cleanTasks,
computed: () => computed,
createAudio: () => createAudio2,
createAudioMisc: () => createAudioMisc,
createContextStateRoot: () => createContextStateRoot,
createGetContext: () => createGetContext,
createRendererState: () => createRendererState,
createRootSetter: () => createRootSetter,
createShared: () => createShared,
createStartFunction: () => createStartFunction,
deepAtom: () => deepAtom,
deepMap: () => deepMap,
getPath: () => getPath,
handleBackgroundAction: () => handleBackgroundAction,
handleChoiceAction: () => handleChoiceAction,
handleClearAction: () => handleClearAction,
handleClearBlockingActions: () => handleClearBlockingActions,
handleCustomAction: () => handleCustomAction,
handleDialogAction: () => handleDialogAction,
handleInputAction: () => handleInputAction,
handleTextAction: () => handleTextAction,
handleVibrateAction: () => handleVibrateAction,
keepMount: () => keepMount,
listenKeys: () => listenKeys,
map: () => map,
mapCreator: () => mapCreator,
noop: () => noop,
onMount: () => onMount,
onNotify: () => onNotify,
onSet: () => onSet,
onStart: () => onStart,
onStop: () => onStop,
setByKey: () => setByKey,
setPath: () => setPath,
startTask: () => startTask,
subscribeKeys: () => subscribeKeys,
task: () => task
});
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/task/index.js
var tasks = 0;
var resolves = [];
function startTask() {
tasks += 1;
return () => {
tasks -= 1;
if (tasks === 0) {
let prevResolves = resolves;
resolves = [];
for (let i of prevResolves) i();
}
};
}
function task(cb) {
let endTask = startTask();
let promise = cb().finally(endTask);
promise.t = true;
return promise;
}
function allTasks() {
if (tasks === 0) {
return Promise.resolve();
} else {
return new Promise((resolve) => {
resolves.push(resolve);
});
}
}
function cleanTasks() {
tasks = 0;
}
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/clean-stores/index.js
var clean = Symbol("clean");
var cleanStores = (...stores) => {
if (process.env.NODE_ENV === "production") {
throw new Error(
"cleanStores() can be used only during development or tests"
);
}
cleanTasks();
for (let $store of stores) {
if ($store) {
if ($store.mocked) delete $store.mocked;
if ($store[clean]) $store[clean]();
}
}
};
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/atom/index.js
var listenerQueue = [];
var lqIndex = 0;
var QUEUE_ITEMS_PER_LISTENER = 4;
var epoch = 0;
var atom = (initialValue) => {
let listeners = [];
let $atom = {
get() {
if (!$atom.lc) {
$atom.listen(() => {
})();
}
return $atom.value;
},
lc: 0,
listen(listener) {
$atom.lc = listeners.push(listener);
return () => {
for (let i = lqIndex + QUEUE_ITEMS_PER_LISTENER; i < listenerQueue.length; ) {
if (listenerQueue[i] === listener) {
listenerQueue.splice(i, QUEUE_ITEMS_PER_LISTENER);
} else {
i += QUEUE_ITEMS_PER_LISTENER;
}
}
let index = listeners.indexOf(listener);
if (~index) {
listeners.splice(index, 1);
if (!--$atom.lc) $atom.off();
}
};
},
notify(oldValue, changedKey) {
epoch++;
let runListenerQueue = !listenerQueue.length;
for (let listener of listeners) {
listenerQueue.push(
listener,
$atom.value,
oldValue,
changedKey
);
}
if (runListenerQueue) {
for (lqIndex = 0; lqIndex < listenerQueue.length; lqIndex += QUEUE_ITEMS_PER_LISTENER) {
listenerQueue[lqIndex](
listenerQueue[lqIndex + 1],
listenerQueue[lqIndex + 2],
listenerQueue[lqIndex + 3]
);
}
listenerQueue.length = 0;
}
},
/* It will be called on last listener unsubscribing.
We will redefine it in onMount and onStop. */
off() {
},
set(newValue) {
let oldValue = $atom.value;
if (oldValue !== newValue) {
$atom.value = newValue;
$atom.notify(oldValue);
}
},
subscribe(listener) {
let unbind = $atom.listen(listener);
listener($atom.value);
return unbind;
},
value: initialValue
};
if (process.env.NODE_ENV !== "production") {
$atom[clean] = () => {
listeners = [];
$atom.lc = 0;
$atom.off();
};
}
return $atom;
};
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/lifecycle/index.js
var START = 0;
var STOP = 1;
var SET = 2;
var NOTIFY = 3;
var MOUNT = 5;
var UNMOUNT = 6;
var REVERT_MUTATION = 10;
var on = (object, listener, eventKey, mutateStore) => {
object.events = object.events || {};
if (!object.events[eventKey + REVERT_MUTATION]) {
object.events[eventKey + REVERT_MUTATION] = mutateStore((eventProps) => {
object.events[eventKey].reduceRight((event, l) => (l(event), event), {
shared: {},
...eventProps
});
});
}
object.events[eventKey] = object.events[eventKey] || [];
object.events[eventKey].push(listener);
return () => {
let currentListeners = object.events[eventKey];
let index = currentListeners.indexOf(listener);
currentListeners.splice(index, 1);
if (!currentListeners.length) {
delete object.events[eventKey];
object.events[eventKey + REVERT_MUTATION]();
delete object.events[eventKey + REVERT_MUTATION];
}
};
};
var onStart = ($store, listener) => on($store, listener, START, (runListeners) => {
let originListen = $store.listen;
$store.listen = (arg) => {
if (!$store.lc && !$store.starting) {
$store.starting = true;
runListeners();
delete $store.starting;
}
return originListen(arg);
};
return () => {
$store.listen = originListen;
};
});
var onStop = ($store, listener) => on($store, listener, STOP, (runListeners) => {
let originOff = $store.off;
$store.off = () => {
runListeners();
originOff();
};
return () => {
$store.off = originOff;
};
});
var onSet = ($store, listener) => on($store, listener, SET, (runListeners) => {
let originSet = $store.set;
let originSetKey = $store.setKey;
if ($store.setKey) {
$store.setKey = (changed, changedValue) => {
let isAborted;
let abort = () => {
isAborted = true;
};
runListeners({
abort,
changed,
newValue: { ...$store.value, [changed]: changedValue }
});
if (!isAborted) return originSetKey(changed, changedValue);
};
}
$store.set = (newValue) => {
let isAborted;
let abort = () => {
isAborted = true;
};
runListeners({ abort, newValue });
if (!isAborted) return originSet(newValue);
};
return () => {
$store.set = originSet;
$store.setKey = originSetKey;
};
});
var onNotify = ($store, listener) => on($store, listener, NOTIFY, (runListeners) => {
let originNotify = $store.notify;
$store.notify = (oldValue, changed) => {
let isAborted;
let abort = () => {
isAborted = true;
};
runListeners({ abort, changed, oldValue });
if (!isAborted) return originNotify(oldValue, changed);
};
return () => {
$store.notify = originNotify;
};
});
var STORE_UNMOUNT_DELAY = 1e3;
var onMount = ($store, initialize) => {
let listener = (payload) => {
let destroy = initialize(payload);
if (destroy) $store.events[UNMOUNT].push(destroy);
};
return on($store, listener, MOUNT, (runListeners) => {
let originListen = $store.listen;
$store.listen = (...args) => {
if (!$store.lc && !$store.active) {
$store.active = true;
runListeners();
}
return originListen(...args);
};
let originOff = $store.off;
$store.events[UNMOUNT] = [];
$store.off = () => {
originOff();
setTimeout(() => {
if ($store.active && !$store.lc) {
$store.active = false;
for (let destroy of $store.events[UNMOUNT]) destroy();
$store.events[UNMOUNT] = [];
}
}, STORE_UNMOUNT_DELAY);
};
if (process.env.NODE_ENV !== "production") {
let originClean = $store[clean];
$store[clean] = () => {
for (let destroy of $store.events[UNMOUNT]) destroy();
$store.events[UNMOUNT] = [];
$store.active = false;
originClean();
};
}
return () => {
$store.listen = originListen;
$store.off = originOff;
};
});
};
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/computed/index.js
var computedStore = (stores, cb, batched2) => {
if (!Array.isArray(stores)) stores = [stores];
let previousArgs;
let currentEpoch;
let set = () => {
if (currentEpoch === epoch) return;
currentEpoch = epoch;
let args = stores.map(($store) => $store.get());
if (!previousArgs || args.some((arg, i) => arg !== previousArgs[i])) {
previousArgs = args;
let value = cb(...args);
if (value && value.then && value.t) {
value.then((asyncValue) => {
if (previousArgs === args) {
$computed.set(asyncValue);
}
});
} else {
$computed.set(value);
currentEpoch = epoch;
}
}
};
let $computed = atom(void 0);
let get = $computed.get;
$computed.get = () => {
set();
return get();
};
let timer;
let run = batched2 ? () => {
clearTimeout(timer);
timer = setTimeout(set);
} : set;
onMount($computed, () => {
let unbinds = stores.map(($store) => $store.listen(run));
set();
return () => {
for (let unbind of unbinds) unbind();
};
});
return $computed;
};
var computed = (stores, fn) => computedStore(stores, fn);
var batched = (stores, fn) => computedStore(stores, fn, true);
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/deep-map/path.js
function getPath(obj, path) {
let allKeys = getAllKeysFromPath(path);
let res = obj;
for (let key of allKeys) {
if (res === void 0) {
break;
}
res = res[key];
}
return res;
}
function setPath(obj, path, value) {
return setByKey(obj != null ? obj : {}, getAllKeysFromPath(path), value);
}
function setByKey(obj, splittedKeys, value) {
let key = splittedKeys[0];
let copy = Array.isArray(obj) ? [...obj] : { ...obj };
if (splittedKeys.length === 1) {
if (value === void 0) {
if (Array.isArray(copy)) {
copy.splice(key, 1);
} else {
delete copy[key];
}
} else {
copy[key] = value;
}
return copy;
}
ensureKey(copy, key, splittedKeys[1]);
copy[key] = setByKey(copy[key], splittedKeys.slice(1), value);
return copy;
}
var ARRAY_INDEX = /(.*)\[(\d+)\]/;
function getAllKeysFromPath(path) {
return path.split(".").flatMap((key) => getKeyAndIndicesFromKey(key));
}
function getKeyAndIndicesFromKey(key) {
if (ARRAY_INDEX.test(key)) {
let [, keyPart, index] = key.match(ARRAY_INDEX);
return [...getKeyAndIndicesFromKey(keyPart), index];
}
return [key];
}
var IS_NUMBER = /^\d+$/;
function ensureKey(obj, key, nextKey) {
if (key in obj) {
return;
}
let isNum = IS_NUMBER.test(nextKey);
if (isNum) {
obj[key] = Array(parseInt(nextKey, 10) + 1);
} else {
obj[key] = {};
}
}
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/deep-map/index.js
function deepMap(initial = {}) {
let $deepMap = atom(initial);
$deepMap.setKey = (key, value) => {
if (getPath($deepMap.value, key) !== value) {
let oldValue = $deepMap.value;
$deepMap.value = setPath($deepMap.value, key, value);
$deepMap.notify(oldValue, key);
}
};
return $deepMap;
}
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/keep-mount/index.js
var keepMount = ($store) => {
$store.listen(() => {
});
};
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/listen-keys/index.js
function listenKeys($store, keys, listener) {
let keysSet = /* @__PURE__ */ new Set([...keys, void 0]);
return $store.listen((value, oldValue, changed) => {
if (keysSet.has(changed)) {
listener(value, oldValue, changed);
}
});
}
function subscribeKeys($store, keys, listener) {
let unbind = listenKeys($store, keys, listener);
listener($store.value);
return unbind;
}
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/map/index.js
var map = (initial = {}) => {
let $map = atom(initial);
$map.setKey = function(key, value) {
let oldMap = $map.value;
if (typeof value === "undefined" && key in $map.value) {
$map.value = { ...$map.value };
delete $map.value[key];
$map.notify(oldMap, key);
} else if ($map.value[key] !== value) {
$map.value = {
...$map.value,
[key]: value
};
$map.notify(oldMap, key);
}
};
return $map;
};
// ../../node_modules/.pnpm/nanostores@0.11.3/node_modules/nanostores/map-creator/index.js
function mapCreator(init) {
let Creator = (id, ...args) => {
if (!Creator.cache[id]) {
Creator.cache[id] = Creator.build(id, ...args);
}
return Creator.cache[id];
};
Creator.build = (id, ...args) => {
let store = map({ id });
onMount(store, () => {
let destroy;
if (init) destroy = init(store, id, ...args);
return () => {
delete Creator.cache[id];
if (destroy) destroy();
};
});
return store;
};
Creator.cache = {};
if (process.env.NODE_ENV !== "production") {
Creator[clean] = () => {
for (let id in Creator.cache) {
Creator.cache[id][clean]();
}
Creator.cache = {};
};
}
return Creator;
}
// src/atoms/deep-atom.ts
var usePath = (atomValue, getPath2) => {
const targets = /* @__PURE__ */ new Set();
const path = [];
let current;
const proxyHandler = {
get(target, prop, receiver) {
if (targets.has(target)) {
throw new ReferenceError(`Attempted to access property on the same target multiple times.`);
}
const value = Reflect.get(target, prop, receiver);
targets.add(target);
path.push(prop);
current = value;
if (value === void 0) {
return new Proxy({}, proxyHandler);
}
if (value && typeof value === "object") {
return new Proxy(value, proxyHandler);
}
return value;
}
};
getPath2(new Proxy(atomValue, proxyHandler));
if (path.length === 0) {
throw new Error("No valid path extracted from the provided getPath function.");
}
return {
path,
value: current
};
};
var deepAtom = (init) => {
const $atom = deepMap(init);
$atom.mutate = (getPath2, setter) => {
const { path, value } = usePath($atom.get(), getPath2);
const newValue = typeof setter === "function" ? setter(value) : setter;
if (newValue === value) {
return newValue;
}
const oldValue = $atom.value;
$atom.value = setByKey($atom.value, path, newValue);
$atom.notify(oldValue, path.join("."));
return newValue;
};
return $atom;
};
// src/state/context-state.ts
var getDefaultContextState = () => {
return {
background: {
background: "#0000"
},
characters: {},
choice: {
label: "",
visible: false,
choices: []
},
dialog: {
content: "",
name: "",
visible: false,
miniature: {}
},
input: {
element: null,
label: "",
error: "",
visible: false
},
text: {
content: ""
},
custom: {},
meta: {
restoring: false,
goingBack: false,
preview: false
},
loading: false
};
};
var createContextStateRoot = (getExtension = () => ({})) => {
const CACHE = /* @__PURE__ */ new Map();
const make = () => {
const contextState = deepAtom({
...getDefaultContextState(),
...getExtension()
});
return contextState;
};
const remove = (id) => {
const contextState = CACHE.get(id);
if (contextState) {
cleanStores(contextState);
}
CACHE.delete(id);
};
const use = (id) => {
const cached = CACHE.get(id);
if (cached) {
return cached;
}
const contextState = make();
CACHE.set(id, contextState);
onMount(contextState, () => {
return () => {
CACHE.delete(id);
};
});
return contextState;
};
return {
useContextState: use,
removeContextState: remove
};
};
// src/state/renderer-state.ts
var defaultEmpty = {};
var createRendererState = (extension = defaultEmpty) => {
const rendererState = deepAtom({
screen: "mainmenu",
loadingShown: false,
exitPromptShown: false,
...extension
});
return rendererState;
};
// src/utils/noop.ts
var noop = () => {
};
// src/utils/escape-html.ts
var escaped = {
'"': """,
"'": "'",
"&": "&",
"<": "<",
">": ">"
};
var escapeHTML = (str) => {
return String(str).replace(/["'&<>]/g, (match) => escaped[match]);
};
// src/renderer/start.ts
var createStartFunction = (fn) => {
let unmount = noop;
return () => {
unmount();
unmount = fn();
return {
unmount: () => {
unmount();
unmount = noop;
}
};
};
};
// ../../node_modules/.pnpm/yocto-queue@1.1.1/node_modules/yocto-queue/index.js
var Node = class {
value;
next;
constructor(value) {
this.value = value;
}
};
var Queue = class {
#head;
#tail;
#size;
constructor() {
this.clear();
}
enqueue(value) {
const node = new Node(value);
if (this.#head) {
this.#tail.next = node;
this.#tail = node;
} else {
this.#head = node;
this.#tail = node;
}
this.#size++;
}
dequeue() {
const current = this.#head;
if (!current) {
return;
}
this.#head = this.#head.next;
this.#size--;
return current.value;
}
peek() {
if (!this.#head) {
return;
}
return this.#head.value;
}
clear() {
this.#head = void 0;
this.#tail = void 0;
this.#size = 0;
}
get size() {
return this.#size;
}
*[Symbol.iterator]() {
let current = this.#head;
while (current) {
yield current.value;
current = current.next;
}
}
};
// ../../node_modules/.pnpm/p-limit@6.2.0/node_modules/p-limit/index.js
function pLimit(concurrency) {
validateConcurrency(concurrency);
const queue = new Queue();
let activeCount = 0;
const resumeNext = () => {
if (activeCount < concurrency && queue.size > 0) {
queue.dequeue()();
activeCount++;
}
};
const next = () => {
activeCount--;
resumeNext();
};
const run = async (function_, resolve, arguments_) => {
const result = (async () => function_(...arguments_))();
resolve(result);
try {
await result;
} catch {
}
next();
};
const enqueue = (function_, resolve, arguments_) => {
new Promise((internalResolve) => {
queue.enqueue(internalResolve);
}).then(
run.bind(void 0, function_, resolve, arguments_)
);
(async () => {
await Promise.resolve();
if (activeCount < concurrency) {
resumeNext();
}
})();
};
const generator = (function_, ...arguments_) => new Promise((resolve) => {
enqueue(function_, resolve, arguments_);
});
Object.defineProperties(generator, {
activeCount: {
get: () => activeCount
},
pendingCount: {
get: () => queue.size
},
clearQueue: {
value() {
queue.clear();
}
},
concurrency: {
get: () => concurrency,
set(newConcurrency) {
validateConcurrency(newConcurrency);
concurrency = newConcurrency;
queueMicrotask(() => {
while (activeCount < concurrency && queue.size > 0) {
resumeNext();
}
});
}
}
});
return generator;
}
function validateConcurrency(concurrency) {
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
throw new TypeError("Expected `concurrency` to be a number from 1 and up");
}
}
// ../../node_modules/.pnpm/simple-web-audio@0.9.0_postcss@8.5.1/node_modules/simple-web-audio/dist/index.mjs
var waitForInteraction = (() => {
const { promise, resolve } = Promise.withResolvers();
const onUserInteraction = () => {
resolve();
};
document.addEventListener("touchstart", onUserInteraction, { once: true });
document.addEventListener("touchend", onUserInteraction, { once: true });
document.addEventListener("click", onUserInteraction, { once: true });
document.addEventListener("keydown", onUserInteraction, { once: true });
return () => {
return promise;
};
})();
var createQueue = (queue, stopped = false) => {
const limit = pLimit(1);
const run = async () => {
const items = queue.slice();
for await (const item of items) {
if (stopped) break;
try {
await item();
} catch (error) {
console.error(error);
stopped = true;
}
}
queue = queue.filter((item) => !items.includes(item));
stopped = false;
};
return {
get queue() {
return queue;
},
set queue(value) {
queue = value;
},
stop() {
stopped = true;
},
execute: () => {
return limit(run);
}
};
};
var createMemo = () => {
const cache = /* @__PURE__ */ new Map();
return (key, fn) => {
return () => {
const preserved = cache.get(key);
if (preserved) {
return preserved;
}
const promise = fn();
cache.set(key, promise);
return promise;
};
};
};
var fetcherMemo = createMemo();
var decoderMemo = createMemo();
var onEndedOptions = {
once: true
};
var createAudio = (options) => {
let audioContext;
let gainNode;
let bufferSource;
let arrayBuffer;
let audioBuffer;
let pendingVolume = options.volume || 1;
let pendingLoop = options.loop || false;
const createAudioContext = () => {
audioContext = new AudioContext();
};
const getGainNode = () => {
return gainNode;
};
const createGainNode = () => {
gainNode = audioContext.createGain();
const node = (options.extendAudioGraph || getGainNode)({
context: audioContext,
node: gainNode
});
node.connect(audioContext.destination);
};
const createBufferSource = () => {
bufferSource = audioContext.createBufferSource();
};
const interruptQueueThenDestroy = (cause) => {
queue.stop();
instance.destroy();
return new Error("", { cause });
};
const fetchArrayBuffer = fetcherMemo(options.src, async () => {
try {
return await fetch(options.src).then((response) => response.arrayBuffer());
} catch (error) {
throw interruptQueueThenDestroy(error);
}
});
const setArrayBuffer = async () => {
arrayBuffer = await fetchArrayBuffer();
};
const decodeAudioData = decoderMemo(options.src, async () => {
try {
return await audioContext.decodeAudioData(arrayBuffer);
} catch (error) {
throw interruptQueueThenDestroy(error);
}
});
const setAudioData = async () => {
audioBuffer = await decodeAudioData();
};
const connectSources = () => {
if (bufferSource && bufferSource.buffer === null) {
bufferSource.buffer = audioBuffer;
bufferSource.connect(gainNode);
}
};
const setVolume = () => {
gainNode.gain.value = pendingVolume;
};
const setLoop = () => {
bufferSource.loop = pendingLoop;
};
const queue = createQueue([
waitForInteraction,
createAudioContext,
createGainNode,
setVolume,
createBufferSource,
setLoop,
fetchArrayBuffer,
setArrayBuffer,
decodeAudioData,
setAudioData,
connectSources
]);
const state = {
started: false,
playing: false,
destroyed: false
};
const playAudio = async () => {
if (state.destroyed) return;
if (audioContext.state === "suspended") {
await audioContext.resume();
if (state.started) {
state.playing = true;
}
}
if (!state.started) {
bufferSource.start();
state.started = true;
state.playing = true;
}
};
const pauseAudio = async () => {
if (state.destroyed) return;
if (audioContext.state === "suspended" && queue.queue.at(-1) === playAudio) {
queue.queue.pop();
}
if (audioContext.state === "running") {
await audioContext.suspend();
state.playing = false;
}
};
const disconnectAudio = async () => {
bufferSource && bufferSource.disconnect();
state.started = false;
};
const instance = {
async play() {
if (state.destroyed) return;
queue.queue.push(playAudio);
return queue.execute();
},
async pause() {
if (state.destroyed) return;
queue.queue.push(pauseAudio);
return queue.execute();
},
async reset() {
if (state.destroyed) return;
if (state.playing) {
queue.queue.push(pauseAudio);
}
queue.queue.push(
disconnectAudio,
createBufferSource,
setLoop,
connectSources
);
if (state.playing) {
queue.queue.push(playAudio);
}
return queue.execute();
},
async stop() {
if (state.destroyed) return;
queue.queue.push(
pauseAudio,
disconnectAudio,
createBufferSource,
setLoop,
connectSources
);
return queue.execute();
},
async destroy() {
if (state.destroyed) return;
queue.queue = [
pauseAudio,
disconnectAudio
];
await queue.execute();
state.destroyed = true;
audioContext = null;
gainNode = null;
bufferSource = null;
arrayBuffer = null;
audioBuffer = null;
},
async fetch() {
if (state.destroyed) return;
await fetchArrayBuffer();
},
onEnded: (callback) => {
if (state.destroyed || !bufferSource) return;
bufferSource.addEventListener("ended", callback, onEndedOptions);
},
get playing() {
return state.playing;
},
get destroyed() {
return state.destroyed;
},
get volume() {
return pendingVolume;
},
set volume(value) {
if (state.destroyed) return;
pendingVolume = value;
queue.queue.push(setVolume);
queue.execute();
},
get loop() {
return pendingLoop;
},
set loop(value) {
if (state.destroyed) return;
pendingLoop = value;
queue.queue.push(setLoop);
queue.execute();
}
};
if (options.autoplay) {
queue.queue.push(playAudio);
queue.execute();
}
return instance;
};
var prefetchAudio = (src) => {
const fetcher = fetcherMemo(src, () => fetch(src).then((res) => res.arrayBuffer()));
return fetcher();
};
// src/audio/audio.ts
var TYPE_META_MAP = {
music: 2,
sound: 3,
voice: 4
};
var createAudio2 = (storageData) => {
let started = false;
const store = {
music: {},
sound: {},
voices: {}
};
const getVolume = (type) => {
return storageData.get().meta[TYPE_META_MAP[type]];
};
const getAudio = (type, src) => {
const kind = type === "voice" ? "voices" : type;
const cached = store[kind][src];
if (cached) return cached;
const audio = createAudio({
src,
volume: getVolume(type)
});
store[kind][src] = audio;
return audio;
};
const cleanup = /* @__PURE__ */ new Set();
let voiceCleanup = noop;
const context = {
music(src, paused, method) {
const resource = getAudio(method, src);
this.start();
const unsubscribe = paused.subscribe((paused2) => {
if (paused2) {
resource.pause();
} else {
resource.play();
}
});
cleanup.add(unsubscribe);
return {
pause() {
resource.pause();
},
play(loop) {
if (resource.playing) {
resource.volume = getVolume(method);
resource.loop = loop;
return;
}
resource.reset().then(() => {
resource.volume = getVolume(method);
resource.loop = loop;
resource.play();
});
},
stop() {
resource.stop();
}
};
},
voice(source, paused) {
this.start();
this.voiceStop();
const resource = store.voice = getAudio("voice", source);
resource.volume = getVolume("voice");
resource.play();
voiceCleanup = paused.subscribe((paused2) => {
if (paused2) {
resource.pause();
} else {
resource.play();
}
});
},
voiceStop() {
if (!store.voice) return;
store.voice.stop();
voiceCleanup();
store.voice = void 0;
voiceCleanup = noop;
},
start() {
if (started) return;
started = true;
const unsubscribe = storageData.subscribe(() => {
for (const type of ["music", "sound", "voice"]) {
const volume = getVolume(type);
if (type === "music" || type === "sound") {
for (const audio of Object.values(store[type])) {
if (!audio) continue;
audio.volume = volume;
}
}
if (type === "voice" && store.voice) {
store.voice.volume = volume;
}
}
});
cleanup.add(unsubscribe);
},
clear() {
const musics = Object.values(store.music);
const sounds = Object.values(store.sound);
for (const music of [...musics, ...sounds]) {
if (!music) continue;
music.stop();
}
this.voiceStop();
},
destroy() {
cleanup.forEach((fn) => fn());
this.clear();
started = false;
}
};
const clear = (keepAudio) => {
context.voiceStop();
const entries = [
[store.music, keepAudio.music],
[store.sound, keepAudio.sounds]
];
const clearEntries = entries.flatMap(([incoming, keep]) => {
return Object.entries(incoming).filter(([name]) => !keep.has(name)).map(([_, a]) => a);
});
for (const music of clearEntries) {
if (!music) continue;
music.stop();
}
};
return {
context,
clear,
getVolume,
getAudio
};
};
var createAudioMisc = () => {
const misc = {
preloadAudioBlocking: async (src) => {
await prefetchAudio(src);
}
};
return misc;
};
// src/shared/create-shared.ts
var createShared = (get) => {
const CACHE = /* @__PURE__ */ new Map();
const use = (id) => {
const cached = CACHE.get(id);
if (cached) {
return cached;
}
const shared = get();
CACHE.set(id, shared);
return shared;
};
const remove = (id) => {
CACHE.delete(id);
};
return {
useShared: use,
removeShared: remove
};
};
// src/context/create-get-context.ts
var createGetContext = () => {
const CACHE = /* @__PURE__ */ new Map();
const getContextCached = (createContext) => {
return (key) => {
const cached = CACHE.get(key);
if (cached) {
return cached;
}
const context = createContext(key);
CACHE.set(key, context);
return context;
};
};
const removeContext = (key) => {
CACHE.delete(key);
};
return {
getContextCached,
removeContext
};
};
// src/root/root-setter.ts
var createRootSetter = (getContext) => {
let element;
return {
root() {
return element;
},
setRoot(root) {
element = root;
const context = getContext();
if (!context.root) {
context.root = root;
}
}
};
};
// src/context/background.ts
var useBackground = (backgrounds, set) => {
const mediaQueries = Object.keys(backgrounds).map((media) => matchMedia(media));
const allMedia = mediaQueries.find(({ media }) => media === "all");
const handle = () => {
const last = mediaQueries.findLast(({ matches, media }) => matches && media !== "all");
const bg = last ? backgrounds[last.media] : allMedia ? backgrounds["all"] : "";
set(bg);
};
for (const mq of mediaQueries) {
mq.onchange = handle;
}
let disposed = false;
Promise.resolve().then(() => {
if (disposed) return;
handle();
});
return {
/**
* Remove all listeners
*/
dispose() {
for (const mq of mediaQueries) {
mq.onchange = null;
}
disposed = true;
}
};
};
// src/context/vibrate.ts
var vibrationPossible = /* @__PURE__ */ (() => {
let possible = false;
const onPointerDown = () => {
possible = true;
};
const isPossible = () => {
return possible;
};
document.addEventListener("pointerdown", onPointerDown, { once: true });
return isPossible;
})();
var vibrate = (pattern) => {
if (vibrationPossible() && "vibrate" in navigator) {
try {
navigator.vibrate(pattern);
} catch {
}
}
};
// src/context/actions.ts
var allEmpty = (target) => {
if (typeof target === "string") {
return target == "";
}
if (typeof target === "number") {
return target == 0;
}
if (!target) {
return true;
}
if (Array.isArray(target) && target.length > 0) {
for (const inner of target) {
if (!allEmpty(inner)) {
return false;
}
}
}
for (const value of Object.values(target)) {
if (!allEmpty(value)) {
return false;
}
}
return true;
};
var handleBackgroundAction = ($contextState, background) => {
$contextState.get().background.clear?.();
const { dispose } = useBackground(background, (value) => {
$contextState.mutate((s) => s.background.background, value);
});
$contextState.mutate(
(s) => s.background.clear,
() => dispose
);
};
var handleDialogAction = ($contextState, content, name, character, emotion, resolve) => {
$contextState.mutate((s) => s.dialog, {
content,
name,
miniature: {
character,
emotion
},
visible: true,
resolve
});
};
var handleChoiceAction = ($contextState, label, choices, resolve) => {
$contextState.mutate((s) => s.choice, { choices, label, resolve, visible: true });
};
var handleClearAction = ($rendererState, $contextState, options, context, keep, keepCharacters) => {
$rendererState.mutate((s) => s.exitPromptShown, false);
if (!keep.has("showBackground")) {
$contextState.mutate((s) => s.background.background, "#000");
}
if (!keep.has("choice")) {
$contextState.mutate((s) => s.choice, {
choices: [],
visible: false,
label: ""
});
}
const inputCleanup = $contextState.get().input.cleanup;
if (inputCleanup) {
inputCleanup();
}
if (!keep.has("input")) {
$contextState.mutate((s) => s.input, {
element: null,
label: "",
visible: false,
error: ""
});
}
if (!keep.has("dialog")) {
$contextState.mutate((s) => s.dialog, {
visible: false,
content: "",
name: "",
miniature: {}
});
}
if (!keep.has("text")) {
$contextState.mutate((s) => s.text, { content: "" });
}
const { characters, custom } = $contextState.get();
for (const character of Object.keys(characters)) {
if (!keepCharacters.has(character)) {
$contextState.mutate((s) => s.characters[character], {
style: void 0,
visible: false
});
}
}
for (const [id, obj] of Object.entries(custom)) {
if (!obj) continue;
if (context.meta.goingBack && obj.fn.skipClearOnGoingBack) continue;
options.clearCustomAction(context, obj.fn);
$contextState.mutate((s) => s.custom[id], void 0);
}
};
var handleCustomAction = ($contextState, fn) => {
if (!$contextState.get().custom[fn.key]) {
$contextState.mutate((s) => s.custom[fn.key], {
fn,
node: null,
clear: noop
});
}
return {
setMountElement(node) {
$contextState.mutate(
(s) => s.custom[fn.key],
(state) => {
return {
...state,
node
};
}
);
},
setClear(clear) {
$contextState.mutate(
(s) => s.custom[fn.key],
(state) => {
return {
...state,
clear
};
}
);
},
remove() {
$contextState.mutate((s) => s.custom[fn.key], void 0);
}
};
};
var handleClearBlockingActions = ($contextState, preserve) => {
const current = $contextState.get();
if (preserve !== "choice" && !allEmpty(current.choice)) {
$contextState.mutate((s) => s.choice, {
choices: [],
visible: false,
label: ""
});
}
if (preserve !== "input" && !allEmpty(current.input)) {
$contextState.mutate((s) => s.input, {
element: null,
label: "",
visible: false,
error: ""
});
}
if (preserve !== "text" && !allEmpty(current.text)) {
$contextState.mutate((s) => s.text, { content: "" });
}
if (preserve !== "dialog" && !allEmpty(current.dialog)) {
$contextState.mutate((s) => s.dialog, {
visible: false,
content: "",
name: "",
miniature: {}
});
}
};
var handleTextAction = ($contextState, content, resolve) => {
$contextState.mutate((s) => s.text, { content, resolve });
};
var handleInputAction = ($contextState, options, context, label, onInput, setup, resolve) => {
const error = (value) => {
$contextState.mutate((s) => s.input.error, value);
};
const onInputHandler = (event) => {
let value;
onInput({
lang: options.storageData.get().meta[0],
input,
event,
error,
state: options.getStateFunction(context.id),
get value() {
if (value) return value;
return value = escapeHTML(input.value);
}
});
};
const input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("name", "novely-input");
input.setAttribute("id", "novely-input");
input.setAttribute("required", "true");
input.setAttribute("autocomplete", "off");
!context.meta.preview && input.addEventListener("input", onInputHandler);
$contextState.mutate((s) => s.input, {
element: input,
label,
error: "",
visible: true,
cleanup: setup(input) || noop,
resolve
});
!context.meta.preview && input.dispatchEvent(new InputEvent("input", { bubbles: true }));
};
var handleVibrateAction = vibrate;
return __toCommonJS(index_exports);
})();
//# sourceMappingURL=index.global.js.map