Novel
The novel plugin is the storyteller. It takes your screenplay and artwork and turns them into a playable visual novel. Everything between “the reader pressed a key” and “the next beat is on screen” happens here: navigating the call stack, fetching beats and PSDs, resolving cues and cameras, composing the draw instructions for a scene, and managing save state.
Novel keeps its own private state, holding the current beat, the active cue, and the screenplay call stack. From there it drives the story forward one beat at a time. Where core defines the application’s lifecycle, novel defines the story’s.
Novel provides both commands and selectors. It also decorates core’s suspendSession and resumeSession at startup, so that a reader’s exact position survives a hot reload during development.
Every command below can be decorated. The commands are grouped by what they do, moving through navigation, state, fetching, resolving, and drawing, followed by the plugin’s selectors.
Navigation
Section titled “Navigation”These commands move the reader through the story. Under the hood they manipulate the call stack: a list of LexiaRefs that records where the reader is and how they got there.
startNovel
Section titled “startNovel”Starts the novel from the beginning, jumping to the start label defined in your project settings.
startNovel(): Promise<void>Reads startup.startLabel from your project settings, resolves it to a LexiaRef, and draws the opening scene.
Example
const { startNovel } = getCommands();await startNovel();advance
Section titled “advance”Advances to the next beat from the reader’s current position.
advance(): Promise<void>Looks at the top of the call stack, resolves the next beat that follows it, and updates the scene. If there’s nowhere left to go, it does nothing.
Example
const { advance } = getCommands();await advance();followLink
Section titled “followLink”Follows a link in the screenplay, jumping to its target label. If the link has no target, it simply advances.
followLink(ctx: FollowLinkContext): Promise<void>
interface FollowLinkContext { label: undefined | string;}Example
const { followLink } = getCommands();await followLink({ label: "the-docks" });navigate
Section titled “navigate”Moves the call stack to a new lexia. This is the low-level primitive that advance and followLink build on.
navigate(ctx: NavigateContext): Promise<void>
interface NavigateContext { lexiaRef: LexiaRef; navigationType?: "call" | "jump";}The navigationType controls how the call stack changes. A "jump" (the default) replaces the top of the stack, so the reader moves on without a way back. A "call" pushes onto the stack, so the story can later return to where it branched.
Example
const { navigate } = getCommands();await navigate({ lexiaRef, navigationType: "call" });stepForward
Section titled “stepForward”Steps the reader forward through their history and redraws the scene.
stepForward(): Promise<void>Novel wraps the SDK’s stepForward so that stepping through state history also updates what’s drawn on screen.
Example
const { stepForward } = getCommands();await stepForward();stepBack
Section titled “stepBack”Steps the reader backward through their history and redraws the scene.
stepBack(): Promise<void>The counterpart to stepForward, wrapping the SDK’s stepBack with a redraw.
Example
const { stepBack } = getCommands();await stepBack();These commands manage the beat that’s currently on screen and the reader’s save state.
update
Section titled “update”Builds the current beat from a set of screenplay lines and commits it to state.
update(ctx: UpdateContext): Promise<void>
interface UpdateContext { lines: Array<BlockLexia | TransitionLexia>;}For each line, update processes any labels, resolves the characters and scene resources involved, evaluates inline expressions, and folds the result into the beat. It then resolves the active cue. If the resulting beat has no visible content, it advances automatically.
Example
const { update } = getCommands();await update({ lines });commit
Section titled “commit”Flushes the current state to the engine’s history, creating a checkpoint the reader can step back to.
commit(ctx?: CommitContext): Promise<CommitResult>A thin wrapper over the SDK’s commitState.
Example
const { commit } = getCommands();await commit();canDiscardState
Section titled “canDiscardState”Reports whether the current state can be safely discarded, for instance before loading a bookmark over unsaved progress.
canDiscardState(ctx?: CanDiscardStateContext): Promise<CanDiscardStateResult>
interface CanDiscardStateResult { canDiscard: boolean;}Novel’s implementation always returns true. Decorate it if your plugin needs to guard against losing progress, for example to prompt the reader before discarding an unsaved beat.
Example
decorateCommand("canDiscardState", async (context, next) => { if (hasUnsavedProgress()) { return { canDiscard: false }; } return next(context);});quickSave
Section titled “quickSave”Saves a quicksave bookmark capturing the current state.
quickSave(): Promise<void>Example
const { quickSave } = getCommands();await quickSave();quickLoad
Section titled “quickLoad”Loads the most recent quicksave bookmark and redraws the scene.
quickLoad(): Promise<void>If no quicksave exists, it does nothing.
Example
const { quickLoad } = getCommands();await quickLoad();Fetching
Section titled “Fetching”These commands read your novel’s built files, resolving logical paths to their content-addressed filenames along the way. They cover manifests, screenplays, and PSDs, and act as the bridge between the story’s logical structure and the files on disk.
fetchManifest
Section titled “fetchManifest”Loads the asset manifest, which maps logical asset names to their built files and metadata.
fetchManifest(ctx?: FetchManifestContext): Promise<FetchManifestResult>
interface FetchManifestResult { manifest: Manifest;}Example
const { fetchManifest } = getCommands();const { manifest } = await fetchManifest();fetchLabelManifest
Section titled “fetchLabelManifest”Loads the label manifest and re-indexes it by label, so any label can be looked up to find the screenplay and line it lives on.
fetchLabelManifest( ctx?: FetchLabelManifestContext,): Promise<FetchLabelManifestResult>
interface FetchLabelManifestResult { labelManifest: LabelManifest;}Example
const { fetchLabelManifest } = getCommands();const { labelManifest } = await fetchLabelManifest();fetchProjectSettings
Section titled “fetchProjectSettings”Loads your novel’s project settings, including the start label and other startup configuration.
fetchProjectSettings( ctx?: FetchProjectSettingsContext,): Promise<FetchProjectSettingsResult>
interface FetchProjectSettingsResult { settings: ProjectSettings;}Example
const { fetchProjectSettings } = getCommands();const { settings } = await fetchProjectSettings();fetchScreenplay
Section titled “fetchScreenplay”Loads a single screenplay file by its logical URL, resolving it for the current text locale.
fetchScreenplay(ctx: FetchScreenplayContext): Promise<FetchScreenplayResult>
interface FetchScreenplayContext { logicalUrl: string;}interface FetchScreenplayResult { screenplay: Screenplay;}Example
const { fetchScreenplay } = getCommands();const { screenplay } = await fetchScreenplay({ logicalUrl });fetchBeat
Section titled “fetchBeat”Reads the lines of a single beat from a screenplay, starting at the given lexia reference.
fetchBeat(ctx: FetchBeatContext): Promise<FetchBeatResult>
interface FetchBeatContext { lexiaRef: LexiaRef;}interface FetchBeatResult { lines: Array<BlockLexia | TransitionLexia>;}Walks the screenplay from the reference until it reaches the boundary of the beat, collecting the lines that make it up.
Example
const { fetchBeat } = getCommands();const { lines } = await fetchBeat({ lexiaRef });fetchPsd
Section titled “fetchPsd”Loads a PSD as an InkRoot, resolving it for the current image locale.
fetchPsd(ctx: FetchPsdContext): Promise<FetchPsdResult>
interface FetchPsdContext { logicalUrl: string;}interface FetchPsdResult { root: InkRoot;}Example
const { fetchPsd } = getCommands();const { root } = await fetchPsd({ logicalUrl });fetchResource
Section titled “fetchResource”Resolves the artwork resource behind a lexia, the PSD for a character or scene. Returns its logical URL along with the labels declared inside it.
fetchResource(ctx: FetchResourceContext): Promise<null | FetchResourceResult>
interface FetchResourceContext { lexia: BlockLexia;}interface FetchResourceResult { logicalUrl: string; resourceLabels: Array<ResourceLabel>;}Returns null if the lexia has no matching resource.
Example
const { fetchResource } = getCommands();const resource = await fetchResource({ lexia });Resolving
Section titled “Resolving”Resolvers turn one piece of information into another: a label into a reference, a beat into its cue, a layer into the actions needed to draw it. Several of them are deliberately simple and exist to be decorated. resolveLocale and resolveShaders, for instance, are where localization and shader plugins hook in.
resolveLabel
Section titled “resolveLabel”Resolves a screenplay label to the LexiaRef it points to.
resolveLabel(ctx: ResolveLabelContext): Promise<ResolveLabelResult>
interface ResolveLabelContext { label: string;}interface ResolveLabelResult { lexiaRef: LexiaRef;}Throws if the label can’t be found in the label manifest.
Example
const { resolveLabel } = getCommands();const { lexiaRef } = await resolveLabel({ label: "chapter-two" });resolveNextBeat
Section titled “resolveNextBeat”Determines which beat follows the current one, taking screenplay links into account.
resolveNextBeat( ctx: ResolveNextBeatContext,): Promise<null | ResolveNextBeatResult>
interface ResolveNextBeatContext { lexiaRef: LexiaRef;}interface ResolveNextBeatResult { lexiaRef: LexiaRef; navigationType?: "call" | "jump";}Inspects the current beat for links. A blind link redirects the flow automatically; otherwise the story falls through to the next beat in the screenplay. Returns null when the story has reached its end.
Example
const { resolveNextBeat } = getCommands();const next = await resolveNextBeat({ lexiaRef });resolveCue
Section titled “resolveCue”Resolves the active audio cue for a set of lines, carrying the current cue forward if none of the lines change it.
resolveCue(ctx: ResolveCueContext): Promise<ResolveCueResult>
interface ResolveCueContext { lines: Array<BlockLexia | TransitionLexia>;}interface ResolveCueResult { activeCue: undefined | ActiveCue;}Example
const { resolveCue } = getCommands();const { activeCue } = await resolveCue({ lines });resolveLocale
Section titled “resolveLocale”Resolves the locale to use for a given role, such as "text" or "image".
resolveLocale(ctx: ResolveLocaleContext): Promise<ResolveLocaleResult>
interface ResolveLocaleContext { role: string;}interface ResolveLocaleResult { locale: string;}The default implementation always returns "en-US". This is the seam localization plugins decorate to switch languages per role. See the localization guide.
Example
decorateCommand("resolveLocale", (context, next) => { if (context.role === "text") { return { locale: getState().language }; } return next(context);});resolveCamera
Section titled “resolveCamera”Resolves the camera framing for a drawn root, defaulting to the bounds of the artwork.
resolveCamera(ctx: ResolveCameraContext): Promise<ResolveCameraResult>
interface ResolveCameraContext { root: InkRoot;}interface ResolveCameraResult { camera: CameraInstruction;}Decorate this to implement pans, zooms, or shots that frame something other than the full scene.
Example
const { resolveCamera } = getCommands();const { camera } = await resolveCamera({ root });resolveShaders
Section titled “resolveShaders”Resolves the shaders that should be applied to a layer, scene, or transition.
resolveShaders(ctx: ResolveShadersContext): Promise<ResolveShadersResult>
interface ResolveShadersContext { labels?: Array<string>; layer?: AnyLayer; lexia?: Lexia; resourceType: "character" | "scene" | "transition";}interface ResolveShadersResult { shaders: Array<ShaderInstruction>;}Novel returns no shaders by default. This is the extension point for visual effects. Web UI decorates it to apply character and transition shaders.
Example
const { resolveShaders } = getCommands();const { shaders } = await resolveShaders({ resourceType: "character", layer });resolveDrawActions
Section titled “resolveDrawActions”Decides what should happen to a single layer during drawing: whether to draw it, draw a placeholder in its place, or both.
resolveDrawActions( ctx: ResolveDrawActionsContext,): Promise<ResolveDrawActionsResult>
interface ResolveDrawActionsContext { labels: Array<string>; layer: AnyLayer; resourceType: "character" | "scene" | "transition";}interface ResolveDrawActionsResult { actions: Array<"draw" | "draw-placeholder">; match: undefined | string; placeholder?: null | Placeholder;}Matches the beat’s active labels against the layer’s own labels to decide whether the layer is shown, and resolves any placeholder it should host.
Example
const { resolveDrawActions } = getCommands();const { actions, placeholder } = await resolveDrawActions({ labels, layer, resourceType: "scene",});resolvePlaceholder
Section titled “resolvePlaceholder”Resolves the placeholder content that fills a slot in a layer, for example the character PSD that drops into a scene’s character placeholder.
resolvePlaceholder( ctx: ResolvePlaceholderContext,): Promise<ResolvePlaceholderResult>
interface ResolvePlaceholderContext { layer: AnyLayer; resourceType: null | "character" | "scene" | "transition";}interface ResolvePlaceholderResult { placeholder: null | Placeholder;}Returns null when the layer has no placeholder to fill.
Example
const { resolvePlaceholder } = getCommands();const { placeholder } = await resolvePlaceholder({ layer, resourceType: "character" });Drawing
Section titled “Drawing”The draw pipeline turns the current beat into a flat list of FrameInstructions, the low-level draw commands a renderer like Web UI consumes. Drawing is recursive: a scene draws its root, a root draws its batches, a batch draws its layers, and so on down the layer tree.
drawScene
Section titled “drawScene”Draws the current scene, producing the beat instructions and active cue for what’s on screen.
drawScene(ctx?: DrawSceneContext): Promise<null | DrawSceneResult>
interface DrawSceneResult { activeCue: undefined | null | ActiveCue; beats: { new: null | BeatInstructions; old: null | BeatInstructions; };}The top of the draw pipeline. It reads the current beat from state, draws it, and returns both the new beat instructions and the previous ones so a renderer can transition between them. Returns null if there’s nothing to draw. Web UI decorates this command to push the result into its rendering store.
Example
const { drawScene } = getCommands();const result = await drawScene();Draws a single resource, a character or scene PSD, identified by its logical URL.
draw(ctx: DrawContext): Promise<DrawResult>
interface DrawContext { labels: Array<string>; logicalUrl: string; resourceType: "character" | "scene" | "transition";}interface DrawResult { imageInstructions: Array<FrameInstruction>; root: InkRoot;}Fetches the PSD, draws its root against the active labels, and groups the resulting instructions for rendering.
Example
const { draw } = getCommands();const { imageInstructions } = await draw({ labels, logicalUrl, resourceType: "scene" });drawRoot
Section titled “drawRoot”Draws the root of a PSD, wrapping its layer batches in a layer group.
drawRoot(ctx: DrawRootContext): Promise<DrawRootResult>
interface DrawRootContext { labels: Array<string>; resourceType: "character" | "scene" | "transition"; root: InkRoot;}interface DrawRootResult { frameInstructions: Array<FrameInstruction>;}Example
const { drawRoot } = getCommands();const { frameInstructions } = await drawRoot({ labels, resourceType: "scene", root });drawLayer
Section titled “drawLayer”Draws a single layer, honoring the draw actions resolved for it.
drawLayer(ctx: DrawLayerContext): Promise<DrawLayerResult>
interface DrawLayerContext { labels: Array<string>; layer: InkLayer; resourceType: "character" | "scene" | "transition";}interface DrawLayerResult { frameInstructions: Array<FrameInstruction>;}Consults resolveDrawActions to decide whether to draw the layer, a placeholder, or both.
Example
const { drawLayer } = getCommands();const { frameInstructions } = await drawLayer({ labels, layer, resourceType: "character" });drawLayerBatch
Section titled “drawLayerBatch”Draws a batch of layers that share a clipping context.
drawLayerBatch(ctx: DrawLayerBatchContext): Promise<DrawLayerBatchResult>
interface DrawLayerBatchContext { batch: InkLayerBatch; labels: Array<string>; resourceType: "character" | "scene" | "transition";}interface DrawLayerBatchResult { frameInstructions: Array<FrameInstruction>;}Wraps its children in a clipping context so that clipped layers mask correctly.
Example
const { drawLayerBatch } = getCommands();const { frameInstructions } = await drawLayerBatch({ batch, labels, resourceType: "scene" });drawLayerGroup
Section titled “drawLayerGroup”Draws a group of layers, applying the group’s blend mode and opacity.
drawLayerGroup(ctx: DrawLayerGroupContext): Promise<DrawLayerGroupResult>
interface DrawLayerGroupContext { labels: Array<string>; layerGroup: InkLayerGroup; resourceType: "character" | "scene" | "transition";}interface DrawLayerGroupResult { frameInstructions: Array<FrameInstruction>;}Like drawLayer, it consults the resolved draw actions to decide whether the group is drawn or replaced with a placeholder.
Example
const { drawLayerGroup } = getCommands();const { frameInstructions } = await drawLayerGroup({ labels, layerGroup, resourceType: "scene" });drawPlaceholder
Section titled “drawPlaceholder”Draws the content that fills a placeholder, fitting it to the bounds of its parent layer.
drawPlaceholder(ctx: DrawPlaceholderContext): Promise<DrawPlaceholderResult>
interface DrawPlaceholderContext { parentLayer: AnyLayer; placeholder: Placeholder;}interface DrawPlaceholderResult { frameInstructions: Array<FrameInstruction>;}Scales the placeholder’s root into the parent’s bounds, draws it, and applies any resolved shaders.
Example
const { drawPlaceholder } = getCommands();const { frameInstructions } = await drawPlaceholder({ parentLayer, placeholder });Selectors
Section titled “Selectors”Novel exposes two selectors that let your scripts read into the current beat.
getCallStackIndex
Section titled “getCallStackIndex”Returns the index of the top of the screenplay call stack.
getCallStackIndex(ctx: GetCallStackContext): { index: number }Example
const { getCallStackIndex } = getSelectors();const { index } = getCallStackIndex();getExpressionValues
Section titled “getExpressionValues”Returns the values of the inline expressions evaluated in the current beat, keyed by expression id.
getExpressionValues(): { expressionValues: Record<string, any> }Useful with onChange to react when a computed value in the current beat changes.
Example
const { getExpressionValues } = getSelectors();const { expressionValues } = getExpressionValues();