Skip to content

Add features

There are five ways a plugin can shape your novel. It can:

  • Provide commands that do work
  • Provide selectors that share data
  • Respond to labels in the screenplay
  • Respond to changes in state
  • Decorate commands and selectors from other plugins

Inkweaver’s bundled plugins are built the same way, so there’s nothing they can do that yours can’t.

A command is an action any plugin can call, like fetching a beat or drawing a scene. When your plugin provides a command, other plugins can call it and extend it too.

A command is an async function. It takes an object of arguments, and returns a promise that resolves to a result object:

import { getState, setState } from "@inkweaverdev/inkweaver-sdk";
import { BuyItemContext, BuyItemResult } from "../plugin-types";
import { State } from "../types";
export async function buyItem(ctx: BuyItemContext): Promise<BuyItemResult> {
const { itemId, price } = ctx;
const current = getState();
const inventory = [...current.inventory, itemId];
const coins = current.coins - price;
if (coins < 0) {
return { didBuy: false };
}
const newState: State = { ...current, coins, inventory };
setState(newState);
return { didBuy: true };
}

A selector shares data from inside your plugin in a safe, predictable way. Like a command, it takes an object of arguments and returns a result object:

import { getState } from "@inkweaverdev/inkweaver-sdk";
import { GetInventoryResult } from "../plugin-types";
import { State } from "../types";
import { getItemName } from "utils";
export function getInventory(): GetInventoryResult {
const current = getState<State>();
const inventory = current.inventory.map((itemId) => getItemName(itemId));
return { inventory };
}

Labels are how writers call your code from the screenplay. If you know HTML, a label is a bit like a class name: it says what a line is, and your code decides what that means.

Register a handler with onLabel, and it runs whenever the reader reaches a line with a matching label:

onLabel("has-coins", () => {
const { coins } = getState();
return coins >= 0;
});

Your handler’s return value decides what happens to the line:

  • Return true, or nothing, and the line is shown as normal.
  • Return false or null, and the line is skipped.

In this example, if the reader has no coins, the shopkeeper’s dialogue is skipped:

SHOPKEEPER <has-coins>
What are you buying?

You can also return a new lexia, which replaces the line:

onLabel<DialogLexia>("shouting", (lexia) => ({
...lexia,
content: toUpperCase(lexia.content),
}));

Handlers can be synchronous or asynchronous.

A handler’s second argument holds any named capture groups from a regular expression pattern. That’s how you give a label parameters. See onLabel for an example.

onChange watches a selector, and runs your callback whenever its value changes:

const { getCoins } = getSelectors();
onChange(getCoins, (coins) => console.log(`Now holding ${coins} coins`));

To change how an existing command or selector behaves, decorate it. Decorators follow the middleware pattern, so if you’ve used a web framework like Express, this will feel familiar.

Wrap a command with decorateCommand, or a selector with decorateSelector. Your function receives the original arguments and a next function that calls the implementation beneath yours. You can do your own work before next, after it, or instead of it.

decorateCommand(
"buyItem",
async (context, next) => {
const result = await next(context);
if (result.didBuy) {
console.log(`Reader bought item: ${context.itemId}`);
}
return result;
},
PRIORITY,
);

This decorator calls the original buyItem through next, and logs a message if the purchase went through.

Several plugins can decorate the same command, each adding its own behavior. The optional priority argument sets the order. Decorators with a higher priority run first, and wrap those with a lower priority. The priority defaults to 0, and decorators with the same priority run in the order their plugins were loaded.

A common use for decorating is changing the interface that the Web UI plugin draws. Its getComponents selector returns all of the React components it renders with. Decorate it to replace a component, or to add your own for other plugins to use:

decorateSelector("getComponents", (context, next) => {
const components = next(context);
return {
...components,
Scene: MyCustomScene,
QuickSaveButton: MyQuickSaveButton,
};
});

Replacing an existing key, like Scene, swaps that component everywhere it’s used through getComponents. Adding a new key, like QuickSaveButton, makes your component available to other plugins.

To decorate another plugin’s commands with type checking, your project needs that plugin’s types. Add the plugin as an npm dependency, then import it for its side effects at the top of your index.ts:

import "shop-plugin";

This brings in the plugin’s declarations, so its commands and selectors become available to your code. They come from the other plugin’s plugin-types.ts, described in Create a plugin.