UNPKG

@novely/renderer-toolkit

Version:
655 lines (642 loc) 15.7 kB
// src/index.ts export * from "nanostores"; // src/atoms/deep-atom.ts import { deepMap, setByKey } from "nanostores"; var usePath = (atomValue, getPath) => { 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; } }; getPath(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 = (getPath, setter) => { const { path, value } = usePath($atom.get(), getPath); 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 import { cleanStores, onMount } from "nanostores"; 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 = { '"': "&quot;", "'": "&#39;", "&": "&amp;", "<": "&lt;", ">": "&gt;" }; 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; } }; }; }; // src/audio/audio.ts import { createAudio as createWebAudio, prefetchAudio } from "simple-web-audio"; var TYPE_META_MAP = { music: 2, sound: 3, voice: 4 }; var createAudio = (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 = createWebAudio({ 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; export { createAudio, createAudioMisc, createContextStateRoot, createGetContext, createRendererState, createRootSetter, createShared, createStartFunction, deepAtom, handleBackgroundAction, handleChoiceAction, handleClearAction, handleClearBlockingActions, handleCustomAction, handleDialogAction, handleInputAction, handleTextAction, handleVibrateAction, noop }; //# sourceMappingURL=index.js.map