/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { ExtensionFactory, hookstate, State, useHookstate } from "@hookstate/core" import type { InkStorySaveState, VisualInkAllLineTypes, VisualInkState, } from "@a-morphous/frontispiece-ink-processor/types" import { getStoryStateObject, InkParserConfigParams, InkProcessorEngine, InkStory } from "@a-morphous/frontispiece-ink-processor" // the ink story has to be stored outside of hookstate. export type InkHookStateType = { visualState: VisualInkState inkState: Record<string, any> } /* Used to retrieve all the function-only types */ type PickMatching<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] } type ExtractMethods<T> = PickMatching<T, Function> /** * Spins up the entirety of hookstate, with the result being both a hook that can be put into React / Preact functional components, * and a global object that can be used to update the state outside a component. * @param story - the Ink story, already compiled, * @param config * @param extension * @returns */ export const createInkHookState = ( story: InkStory, config?: InkParserConfigParams, extension?: ExtensionFactory<InkHookStateType, {}, {}> ) => { const inkProcessor: InkProcessorEngine = new InkProcessorEngine(story, config) const globalInkHookState = hookstate<InkHookStateType>( { visualState: inkProcessor.visualState, inkState: {}, }, extension ) inkProcessor.events.on('updatedVisualInkState', (evt) => { globalInkHookState.inkState.set(getStoryStateObject(evt.data.story)) globalInkHookState.visualState.set(evt.data.visualInkState) }) // we pass a single lines object around in `getVisibleLines` so we don't redraw every // single line when a new one is added. const visibleLines: VisualInkAllLineTypes[] = [] const getInkHookStateController = (state: State<InkHookStateType>) => { const returnObj: ExtractMethods<InkProcessorEngine> = Reflect.ownKeys( Object.getPrototypeOf(inkProcessor) ).reduce((accumulator, value) => { if (value !== "constructor") { const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inkProcessor), value) if (!desc) { return { ...accumulator } } let fn = desc.value ?? desc.get if (!fn) { return { ...accumulator } } return { ...accumulator, [value]: fn.bind(inkProcessor) } } }, {}) as ExtractMethods<InkProcessorEngine> return { ...returnObj, _state: state, /** * Most functions are mirrored to top level. However, if you need anything specific, * best to get it directly from the engine. */ _engine: inkProcessor, import: (save: InkStorySaveState) => { inkProcessor.import(save) state.visualState.set(save.visualInkState) state.inkState.set(save.storyState) }, /** * Returns an array of lines to be rendered by the frontend. Ensures that only the lines that are returned * are the ones that 'should' be rendered, in order from oldest to newest. * * This does not render the currently available choices. * @param options List of options that determine which lines get returned. * @param options.numberOfSectionsToFetch Limit the number of sections to get lines from, starting from the most recent. By default, there are no limits. * @param options.showHidden Whether to show lines that have been previously cleared. By default, is false. Set to true to produce a backlog of lines. * @param options.showChosenChoice Whether to display the choices that were made previously. By default, is false. Generally unneeded because Ink duplicates the choice in the response. * @param options.showCommands Whether to show commands in the display. Defaults to false, this is useful only if there's some visual parsing to work with commands, or for debugging. * @returns - array of lines to be rendered on screen. */ getVisibleLines: (options) => { const mergedOptions = { ...options, arrayReference: visibleLines, } return inkProcessor.getVisibleLines( mergedOptions, state.visualState.get({ noproxy: true }) as VisualInkState ) }, shouldShowChoices: () => { return !inkProcessor.canAdvance && !inkProcessor.locked }, } } const createUseHookState = () => { const state = useHookstate(globalInkHookState) return getInkHookStateController(state) } return { InkHookStateController: getInkHookStateController(globalInkHookState), useInkHookState: createUseHookState, } } export type InkHookStateControllerType = ReturnType< typeof createInkHookState >["InkHookStateController"] export type UseInkHookStateType = ReturnType<typeof createInkHookState>["useInkHookState"]