Skip to content

Menus and input

These commands and selectors build menus, manage the menu stack and the scope stack, and run input bindings. For a walkthrough, see Menus and input.

Your novel opens on the main menu when your project.json sets startup.bootstrapCommand to openMainMenu.

MenuFields
mainNew Story, Continue, Load, Options, Language, and Back
pauseSave, Load, Options, Main Menu, and Back
optionsText and Voice languages, Controls, and Back
controlsBack
saveThree save slots, and Back
loadThree save slots, and Back

The save slots are bookmarks with the IDs slot-1, slot-2, and slot-3.

Back returns to the menu underneath. From the main menu, it closes every menu and returns to the story.

Each menu’s name is also one of its labels, so artists can give each menu its own look. The controls menu also has a label for the reader’s controller: generic, nintendo, playstation, or xbox. It uses generic when no controller is connected.

Novel’s menus draw their interface over the .ui layer by default. If a menu doesn’t name a layer, or its artwork has no matching layer, the menu covers the whole canvas.

buildMenuSchema returns a menu’s schema.

buildMenuSchema(ctx: { name: MenuName }): Promise<{ schema: MenuSchema }>

To add fields to a menu, decorate this command and check ctx.name.

Example
const { buildMenuSchema } = getCommands();
const { schema } = await buildMenuSchema({ name: "options" });

openMainMenu closes any open menus, and opens the main menu.

openMainMenu(): Promise<void>
Example
const { openMainMenu } = getCommands();
await openMainMenu();

pushMenu adds a menu to the menu stack. If the menu’s schema has a scope, it pushes that scope too.

pushMenu(ctx: PushMenuContext): Promise<PushMenuResult>
interface PushMenuContext {
name: MenuName;
cleanupCallback?: () => void;
}
interface PushMenuResult {
release: () => Promise<void>;
}

name is keyof Menus. release removes the menu, and its scope if it pushed one, and then redraws. cleanupCallback runs when you call release.

Example
const { pushMenu } = getCommands();
const { release } = await pushMenu({ name: "options" });

pushScope adds a scope to the scope stack. Only bindings for the scope on top of the stack, or for "global", can run. See Scopes.

pushScope(ctx: PushScopeContext): Promise<PushScopeResult>
interface PushScopeContext {
name: Scope;
cleanupCallback?: () => void;
}
interface PushScopeResult {
release: () => void;
}

name is keyof Scopes. Call release to remove the scope. cleanupCallback runs when you do.

Example
const { pushScope } = getCommands();
const { release } = await pushScope({ name: "journal" });

toggleMenu opens or closes menus.

toggleMenu(): Promise<void>

If a menu is open, toggleMenu closes the menu on top, as long as there’s something to go back to: another menu underneath, or an open story. Closing the main menu closes every menu. If no menu is open, it opens the "pause" menu over an open story, or the "main" menu otherwise.

Example
const { toggleMenu } = getCommands();
await toggleMenu();

getInputBindings returns every input binding.

getInputBindings(): { bindings: Array<InputBinding> }
interface InputBinding {
action: () => Promise<void>;
inputs: Array<string>;
name: string;
scope: Scope;
}
FieldDescription
actionRuns when the reader presses one of inputs.
inputsThe binding’s inputs, in the input binding format.
nameIdentifies the binding.
scopeWhere the binding can run. See Scopes.

Novel comes with these bindings:

NameScopeInputs
toggleMenuglobalescape, Start (gamepad:9)
advancenavigatespace, click, bottom face button (gamepad:0)
stepBacknavigatearrowleft, left bumper (gamepad:4)
stepForwardnavigatearrowright, right bumper (gamepad:5)
quickSavenavigatemeta + s
quickLoadnavigatemeta + l

Decorate it to add bindings of your own.

Example
decorateSelector("getInputBindings", (context, next) => {
const result = next(context);
return { bindings: [...result.bindings, openJournal] };
});

getInputAction returns the action bound to an input. It checks the scope on top of the scope stack first, then "global".

getInputAction(ctx: { inputs: string }): { action?: () => Promise<void> }

inputs is a single input, in the input binding format. Bindings come from getInputBindings. If more than one binding in a scope uses the input, the first one in the list wins.

Example
const { getInputAction } = getSelectors();
const { action } = getInputAction({ inputs: "space" });
action?.();