Skip to content

Menu schema

A menu schema describes a menu and each of its fields. Inkweaver uses it to build the menu’s interface, and to store and act on the values the reader chooses. For a walkthrough, see Menus and input.

Describes a menu.

interface MenuSchema {
fields: Array<MenuField>;
labels?: Array<string>;
layer?: string;
name: MenuName;
scope?: Scope;
type: string;
}
FieldDescription
fieldsThe menu’s fields, in the order they’re shown.
labelsLabels that choose between variations in the menu’s artwork, the same way labels choose a scene’s variations.
layerA selector for the layer of the menu’s artwork that the interface is drawn over.
nameThe menu’s name.
scopeA scope to push when the menu opens, and release when it closes.
typeThe menu’s type.

A menu with no artwork of its own is drawn on the artwork of the menu beneath it on the stack, along with the labels of every menu drawn on that artwork. So an artist can make one PSD for every menu, and give each menu its own look with variation sets.

  • A menu’s labels replace any labels beneath it in the same variation set.
  • Labels that aren’t in any variation set don’t carry up to the next menu.
  • A variation set that no label chooses shows its first variation.

layer is matched against the layers in the menu’s artwork, and the interface is drawn over the bounds of the layer it finds. Layer names match as classes, so a layer named UI matches .ui.

The layer has to be visible to be measured. To keep it from showing, leave it empty or set its opacity to zero. Don’t hide it.

name is keyof Menus. To declare a new menu, add its name to the Menus interface, then return a schema for that name from getMenuSchema.

Example
declare module "@inkweaverdev/inkweaver-sdk" {
interface Menus {
journal: true;
}
}
decorateSelector("getMenuSchema", (context, next) => {
const result = next(context);
switch (context.name) {
case "journal": {
return {
schema: {
...result.schema,
fields: [...result.schema.fields, journalOpacityField],
},
};
}
default: {
return result;
}
}
});

Every field in fields has these properties.

Identifies the field. Must be unique within the menu.

name: string;

The field’s type, which decides how it’s shown and which other properties it has.

type: "button" | "separator" | "boolean" | "number" | "string" | "enum" | "binding";

The field’s text, one version per locale. The field is shown in the reader’s text language. If there’s no version for that language, it falls back to any version it has, then to the field’s name.

locales: Record<string, string>;
Example
locales: {
"en-US": "Show puzzle hints",
"ja-JP": "パズルのヒントを表示",
}

boolean, number, string, enum, and binding fields have a value function and a setValue function. The menu calls value after every change to state, settings, or view state, and shows the result right away. Keep value fast, and have it read from one of those three.

A button that runs an action.

{
name: string;
type: "button";
locales: Record<string, string>;
action: () => Promise<void>;
}

A visual break between groups of fields.

{
name: string;
type: "separator";
locales: Record<string, string>;
}

A toggle.

{
name: string;
type: "boolean";
locales: Record<string, string>;
value: () => boolean;
setValue: (newValue: boolean) => void;
}

A slider.

{
name: string;
type: "number";
locales: Record<string, string>;
minimum?: number;
maximum?: number;
step?: number;
value: () => number;
setValue: (newValue: number) => void;
}
Example
{
name: "journalOpacity",
type: "number",
minimum: 0.2,
maximum: 1,
step: 0.05,
locales: { "en-US": "Background opacity" },
value: () => getSettings().journalOpacity ?? 0.9,
setValue: (newValue) => updateSettings({ journalOpacity: newValue }),
}

A text box.

{
name: string;
type: "string";
locales: Record<string, string>;
value: () => string;
setValue: (newValue: string) => void;
}

A drop-down list. Each option has a value, and the text shown to the reader in locales.

{
name: string;
type: "enum";
locales: Record<string, string>;
options: Array<{
value: string;
locales: Record<string, string>;
}>;
value: () => string;
setValue: (newValue: string) => void;
}
Example
{
name: "textSpeed",
type: "enum",
locales: { "en-US": "Text speed" },
options: [
{ value: "slow", locales: { "en-US": "Slow" } },
{ value: "normal", locales: { "en-US": "Normal" } },
{ value: "instant", locales: { "en-US": "Instant" } },
],
value: () => getSettings().textSpeed ?? "normal",
setValue: (newValue) => updateSettings({ textSpeed: newValue }),
}

A shortcut that runs an action when the reader presses one of its inputs.

{
name: string;
type: "binding";
locales: Record<string, string>;
scope: Scope;
action: () => Promise<void>;
value: () => Array<string>;
setValue: (newValue: Array<string>) => void;
}
PropertyDescription
scopeWhere the binding can run. See Scopes.
actionRuns when the reader presses one of the binding’s inputs.
valueReturns the binding’s inputs, in the input binding format.
setValueStores the reader’s new list of inputs when they change it.

The list of inputs goes by position. The first two entries are keyboard or mouse inputs, and the third is a gamepad button. An empty string leaves a position unused. The menu shows the first three entries, and any entries after them still work.

When two bindings in the same scope share an input, neither one runs, and the menu flags both.

Example
{
name: "openJournal",
type: "binding",
scope: "navigate",
locales: { "en-US": "Open journal" },
action: () => getCommands().openJournal(),
value: () => getSettings().openJournalInputs ?? ["j", "", "gamepad:3"],
setValue: (newValue) => updateSettings({ openJournalInputs: newValue }),
}

An input is one of:

  • One or more keys joined with +. The main key is a lowercase KeyboardEvent.key value, and the space bar is space. Modifiers can come in any order.
  • click, for a mouse click or a tap.
  • A gamepad button.

When the reader records an input, it’s stored with its modifiers in alt, control, meta, shift order, then the key, joined with +.

Clicking an on-screen control, like a link or button, uses that control and doesn’t trigger any binding.

Examples
"escape";
"meta + l";
"shift + arrowright";
"click";

A gamepad input is written as gamepad:<index>, where <index> is the button’s number in the standard gamepad layout. Buttons are numbered by position, so gamepad:0 is the bottom face button on every controller: A on Xbox, Cross on PlayStation, and B on Nintendo. The menu labels each button the way the reader’s controller does.

A gamepad binding runs once per press, so holding the button doesn’t repeat it. Every connected controller works.

Examples
"gamepad:0"; // bottom face button
"gamepad:5"; // right bumper: RB, R1, or R
"gamepad:9"; // start: Menu, Options, or Plus