Skip to content

Plugins

Inkweaver’s engine isn’t a monolith. Everything it does is provided by a plugin, whether that’s advancing the story, drawing a scene, or rendering to the screen. The engine you run is really just a handful of these plugins working together.

This section documents the plugins that ship with Inkweaver. Each one exposes its behavior through the same SDK that your own plugins use, which means there’s nothing they can do that you can’t. If you want to change how the engine behaves, these are the pieces you’ll decorate.

Every plugin exports a createPlugin function. When the engine loads the plugin, it calls this function and collects whatever the plugin returns:

export function createPlugin(key: ContextKey) {
bindContext(key);
return { commands, selectors };
}
  • Commands are the verbs of the engine. They’re how work gets done: fetching a beat, drawing a scene, saving a bookmark. Any command can be wrapped with decorateCommand, which is how plugins layer behavior on top of one another.
  • Selectors are the windows into a plugin’s private state. Because each plugin keeps its own state, selectors are the safe, read-only way for your scripts to see inside. They can be wrapped with decorateSelector.

The pages that follow document the commands and selectors each plugin returns from its createPlugin export.

Plugins extend one another through a middleware pattern. If you’ve used Express or a similar framework, it will feel familiar.

When you decorate a command, you don’t overwrite it. You wrap it. Your function runs first, and it receives a next function that calls the implementation you’re wrapping. From there you can run code before or after calling next, change the arguments on the way in, change the result on the way out, or skip next entirely to replace the behavior outright.

Run code before the wrapped command by doing your work first, then calling next:

decorateCommand("advance", async (context, next) => {
console.log("About to advance");
return next(context);
});

Run code after it by calling next first, then acting on what it returns. Here we let novel frame the shot, then nudge the camera up a little:

decorateCommand("resolveCamera", async (context, next) => {
const result = await next(context);
result.camera.to.top -= 20;
return result;
});

Replace the behavior entirely by never calling next and returning your own result:

decorateCommand("canDiscardState", async () => {
return { canDiscard: false };
});

Because each decorator wraps the one beneath it, several plugins can layer onto the same command without any of them knowing about the others. Novel wraps a command that Core defined; Web UI wraps the same command again on top of Novel. Each layer adds its own behavior, and the call flows down through them like a stack.

This is what makes the engine composable, and it’s why you have the same reach as the plugins that ship with Inkweaver. See decorateCommand and decorateSelector for the mechanics.

Three plugins make up the standard Inkweaver runtime. They build on one another, so the order matters:

  • Core is the foundation. It bootstraps the application and defines the lifecycle commands (init, resumeSession, suspendSession) that everything else hangs off of.
  • Novel is the storyteller. It turns your screenplay and artwork into a playable novel: navigation, scene composition, the draw pipeline, and save/load all live here.
  • Web UI is the frontend. It mounts the React application and renders the novel to a WebGL canvas in the browser.

Each plugin depends on the ones above it. Novel decorates Core’s lifecycle commands; Web UI decorates Novel’s draw pipeline. It’s plugins all the way down.

  • Manhwa is an alternative way to author a novel. It delivers your story incrementally from PSDs rather than a screenplay.