The beat loop
Inkweaver moves through a story one beat at a time. Each beat runs through the same loop, and every stage of that loop is a command. That gives your plugin a place to hook into every stage.
The loop
Section titled “The loop”The loop runs once per beat. Between beats, Inkweaver waits for the reader.
init Once, when the app starts. │ ├─ startNovel ──┐ Once, when the story begins. ▼ │navigate ◄───────┘ Work out where the story goes next, and load it. ▼update Apply the beat's lines to plugin state. ▼commit Save a snapshot of every plugin's state. ▼draw Draw the scene. ▼wait for the readerinit runs once, when the app starts. It’s the first point where your plugin can change state or call commands and selectors.
decorateCommand("init", async (context, next) => { const inventory = await loadShopInventory(); setState({ inventory }); return next(context);});startNovel
Section titled “startNovel”startNovel runs once, when the story begins. It reads the start label from your project settings and goes there.
Use it for work that should happen once per story, not once per session.
navigate
Section titled “navigate”navigate moves the reader to a new place in the story. It works out where the story is going, updates the call stack, and loads that beat’s lines.
decorateCommand("navigate", async (context, next) => { await next(context); countBeat();});With no arguments, navigate moves to the next beat, following any silent links. With a label, it goes to that place in the story:
const { navigate } = getCommands();
await navigate(); // advanceawait navigate({ label: "the-docks" }); // jumpawait navigate({ label: "flashback", type: "call" }); // callawait navigate({ type: "return" }); // returnThe type decides what happens to the call stack:
- A jump replaces the top of the stack. The reader moves on, with no way back. This is the default.
- A call adds to the stack, so the story can play a passage and then return to where it branched. Use it for scenes that several parts of the story share.
- A return removes the top of the stack, taking the reader back to the beat the call was made from. If that beat offered choices, they’re still there.
update
Section titled “update”update applies a beat’s lines to state. Any plugin with state that depends on the screenplay does its work here, and this is also where label handlers run.
decorateCommand("update", async (context, next) => { const result = await next(context); const { lexiaRef } = context; countBeat(lexiaRef); return result;});update reports whether the beat has anything for the reader to see. If it doesn’t, navigate moves on to the next beat.
commit
Section titled “commit”commit saves a snapshot of every plugin’s state. The reader can rewind to that snapshot, or save it in a bookmark.
If something goes wrong during a beat before commit, navigate passes the error to abortNavigation. It discards the beat’s unsaved state changes, then throws the error again.
draw builds the scene. It reads the current state through selectors, and produces the instructions that turn into pixels on screen.
Inkweaver draws each PSD layer by layer, all the way down the layer tree. Each level is its own command, so you can change how a single layer is drawn without touching anything else. The Novel drawing reference covers every level.
For example, draw uses the resolveShaders command to decide which shaders apply to a layer. This decorator adds a crosshatch shader to any layer with the shading label:
decorateCommand("resolveShaders", async (context, next) => { const { labels } = context; const result = await next(context);
const hasShading = labels.includes("shading"); if (hasShading === false) { return result; }
return { shaders: [...result.shaders, crosshatch] };});