NarraLeaf

DevTools

DevTools is a static utility class for editor hosts — a live preview, an inspector, a stage editor, a thumbnail renderer. It exposes the few engine internals such a host needs and that the ordinary story API deliberately does not: action identity, direct registration of a displayable, the current dialog line, and a layered image's per-layer sources.

You never need it to write a game.

import {DevTools} from "narraleaf-react/built-in";

Every member is static — there is nothing to construct. DevTools is also re-exported from the package root (narraleaf-react), but the narraleaf-react/built-in entry is the one to prefer.

Action Identity

Every action in a story receives an id when the story is constructed: a generated one (a-0, a-1, ...) unless a static id was assigned to it, in which case that id is used instead. A static id is what makes an action addressable across rebuilds of the story — for example as the target of fastForward. Duplicate static ids are rejected at story construction.

getActionId

Read the resolved id of an action.

  • action: LogicAction.Actions - The action
  • Returns string - The id assigned at story construction
const id = DevTools.getActionId(action);

setActionId

Overwrite the resolved id of an action.

  • action: LogicAction.Actions - The action
  • id: string - The new id
  • Returns LogicAction.Actions - The same action
DevTools.setActionId(action, "chapter1:intro");

getStaticId

Read the static id of an action, or null when it has none and therefore takes a generated one.

  • action: LogicAction.Actions - The action
  • Returns string | null
const staticId = DevTools.getStaticId(action);

setStaticId

Assign the static id an action keeps across story construction. Pass null to drop it and fall back to a generated id.

  • action: LogicAction.Actions - The action
  • id: string | null - The static id
  • Returns LogicAction.Actions - The same action
DevTools.setStaticId(action, "chapter1:intro");

Chains and Actions

chainToActions

Unwrap a chainable expression into the flat list of actions it produced.

  • chain: Proxied<LogicAction.GameElement, Chained<LogicAction.Actions>> - A chained expression, see ChainedActions
  • Returns LogicAction.Actions[]
const actions = DevTools.chainToActions(
    image.char(["sad"]).darken(0.5, 300)
);

wrapAction

Wrap actions — or a chain — into a single Control action, so a whole sequence can be handled as one statement.

  • action: LogicAction.Actions[] | Proxied<LogicAction.GameElement, Chained<LogicAction.Actions>> - The actions or chain to wrap
  • Returns ControlAction
const wrapped = DevTools.wrapAction(image.char(["sad"]).darken(0.5, 300));

Stage Introspection

getCurrentScene

The scene currently mounted, or null when none is.

  • gameState: GameState - See GameState
  • Returns Scene | null - See Scene
const scene = DevTools.getCurrentScene(gameState);

getLayerSrcs

The src of each of a layered image's layers, bottom to top. A null entry is a layer that draws nothing for the given tags. A non-layered image yields an empty array.

A layered image has no single src to read — it is a stack — so a host rendering its own thumbnail of an on-stage element has to composite these itself, in order.

  • image: Image - See Image
  • tags?: string[] - The tags to resolve against. Defaults to the image's current tags.
  • Returns (string | null)[]
// what the image is showing right now
const srcs = DevTools.getLayerSrcs(yuko);
// => ["yuko/body.png", "yuko/casual.png", "yuko/jacket.png", null, "yuko/mouth_sad.png"]

// what it would show for another set of tags, without touching the stage
const preview = DevTools.getLayerSrcs(yuko, ["uniform", "happy"]);

for (const src of preview) {
    if (src === null) continue; // this layer draws nothing
    // draw src onto your own canvas, in order
}

getDisplayableTransformProps

Read a displayable's current transform-state props — position, opacity, zoom, rotation, scale, effects. Returns a shallow copy, so mutating it changes nothing on stage.

Intended for capturing a live pose, for example to prefill a motion editor with an element's current stage state.

  • displayable: LogicAction.DisplayableElements - See Displayable
  • Returns Record<string, unknown>
const pose = DevTools.getDisplayableTransformProps(image);

setDisplayableTransformProps

Overwrite a displayable's transform-state props with no animation, and push the result to the DOM immediately when the element is mounted.

By default the given props are merged over the current state; with merge: false the previous state is discarded entirely.

  • gameState: GameState - See GameState
  • displayable: LogicAction.DisplayableElements - The element to update
  • props: Record<string, unknown> - The transform props to write
  • options?: { merge?: boolean } - merge defaults to true
DevTools.setDisplayableTransformProps(gameState, image, {opacity: 0.5});

// replace the whole state instead of merging into it
DevTools.setDisplayableTransformProps(gameState, image, pose, {merge: false});

Throws if the transform state is locked by an in-flight transform. Only inject while the stage is idle.

registerDisplayable

Register a displayable into a scene's render tree without emitting a displayable:init action. The element renders immediately at its constructor-config transform state; visibility is governed by opacity > 0.

Intended for hosts that pre-pose a stage: construct elements with their computed state as constructor config — config state survives element.reset() and newGame() — then register them from a Script action or after mount.

The call is idempotent: an element the scene root's auto-init actions already registered is left alone.

  • gameState: GameState - See GameState
  • displayable: LogicAction.DisplayableElements - The element to register
  • scene?: Scene | null - Defaults to null, see Scene
  • layer?: Layer | null - Defaults to null, see Layer
const ghost = new Image({src: "yuko/body.png", opacity: 0.4});

DevTools.setElementId(ghost, "preview:ghost");
DevTools.registerDisplayable(gameState, ghost, scene, layer);

setElementId

Assign an explicit element id.

Elements reachable from a scene's action tree receive generated ids (e-0, e-1, ...) at story construction. An element registered directly via registerDisplayable is outside that tree and would otherwise keep the default id, colliding with others as a React key — so give it a unique id, with a distinct prefix so it never collides with a generated one.

  • element: LogicAction.GameElement - The element
  • id: string - The id to assign
DevTools.setElementId(ghost, "preview:ghost");

setElementStaticId

Give an element a name that story construction keeps.

Without one, an element's id is generated at construction and changes when the story around it changes. A save restores element state by id, so name every element whose state has to survive edits to the script.

setElementId assigns an id directly, and construction replaces it for every element the scene's action tree reaches. Pass null here to drop a name and use the generated id.

  • element: LogicAction.GameElement - The element
  • id: string | null - The name to keep, or null to use the generated id
const classroom = new Image({src: "bg/classroom.png"});

DevTools.setElementStaticId(classroom, "bg:classroom");

Dialog

getCurrentDialog

Read the dialog line currently presented to the player, or null when no dialog is on screen. Covers both ADV and NVL presentation.

const dialog = DevTools.getCurrentDialog(gameState);

if (dialog?.ended) {
    console.log("waiting for the player to advance", dialog.actionId);
}

onDialogStateChange

Subscribe to changes of the currently presented dialog line — creation, typing completion, advance and settle — across both ADV and NVL modes.

The listener carries no payload; call getCurrentDialog to read the new state.

  • gameState: GameState - See GameState
  • listener: () => void - Called on every change
  • Returns LiveGameEventToken - See LiveGameEventToken
const token = DevTools.onDialogStateChange(gameState, () => {
    const dialog = DevTools.getCurrentDialog(gameState);
    highlightRow(dialog?.actionId ?? null);
});

return function cleanup() {
    token.cancel();
};

DevToolsCurrentDialog

Snapshot of the dialog line currently presented to the player.

type DevToolsCurrentDialog = {
    /** Id of the say action that produced the line (static id when assigned). */
    actionId: string | null;
    /** True once the line finished displaying and awaits advance. */
    ended: boolean;
    mode: "adv" | "nvl";
};

Persistent

getNamespaceName

The name a Persistent is registered under in the store — the same string Storable.getNamespace takes.

  • persistent: Persistent<any> - See Persistent
  • Returns string
const name = DevTools.getNamespaceName(playerPersistent);

DynamicPersistent

The DynamicPersistent class, which is otherwise not exported from the package. It is the internal Persistent variant whose namespace is prefixed and created at runtime — a scene's locals are one.

const Dynamic = DevTools.DynamicPersistent;

Internal by design. It is reachable here for hosts that have to mirror the engine's own namespaces, not for story code.

On this page