JavaScript and web
The JavaScript runtime plays a .patterc bundle in any browser or
Node app. Two ways in: a zero-build drop-in for a plain HTML page, or the
@patterkit/runtime npm package for a bundled app. Both expose the same
play loop.
Install
Section titled “Install”Drop-in (no build step):
<script src="patterplay.min.js"></script> <!-- exposes window.Patterplay -->Grab patterplay.min.js from the downloads page or a CDN. It’s a single
self-contained file: no module loader, no bundler. It carries the runtime and
@patterkit/play-helpers under the one Patterplay global, so Patterplay.serializeState(engine)
writes the family’s save text from a plain page (see Helpers). The same release also carries
patterplay-js-<version>.zip, the whole JS runtime as a plain download, symmetric with
the Unity / Unreal / Godot plugin zips: the drop-in, the module builds of the runtime and the
helpers (under @patterkit/runtime/dist/ and @patterkit/play-helpers/dist/, to copy into your
project and import from), the changelog, and two bundled demos (a zero-build drop-in page and the
interactive Patter tour). The zip’s builds carry everything they need inside them, so no npm is
needed anywhere.
npm (for an app you bundle):
npm install @patterkit/runtimeimport { Engine } from "@patterkit/runtime";The runtime shares one property store with every other engine in your game, the registry from
@wildwinter/scoperegistry (see
One registry per game). It’s a peer dependency, so your
install holds exactly one copy for every engine to share. npm installs it for you. With a package
manager that doesn’t install peer dependencies, add it yourself:
npm install @wildwinter/scoperegistryIf two packages in your game need registry versions that can’t be the same copy, the install stops and says so, rather than quietly installing two.
npm or the zip. The two are built differently for exactly this reason. From npm, the packages share the one registry in your install. The zip’s builds and the drop-in carry their own registry inside them, because there is no install to share one from. A game with only Patter won’t notice. A game that also runs another engine, such as the Storylet Engine, from its zip or its drop-in has one registry per engine, so for engines sharing one store, install them all from npm.
A complete drop-in player
Section titled “A complete drop-in player”This is a full, runnable HTML page: load a bundle, render each beat, handle choices, and
save/load to localStorage. (A working copy ships in
examples/drop-in/.)
<div id="stage"></div><div id="controls"></div><button id="save">Save</button><button id="load">Load</button>
<script src="patterplay.min.js"></script> <!-- window.Patterplay --><script src="bundle.js"></script> <!-- your compiled story as window.PATTER_BUNDLE --><script> const { Engine } = window.Patterplay; const BUNDLE = window.PATTER_BUNDLE; const startScene = Object.keys(BUNDLE.scenes)[0]; const stage = document.getElementById("stage"); const controls = document.getElementById("controls");
let engine, flow;
function newGame() { engine = new Engine(BUNDLE); flow = engine.openFlow("main", { scene: startScene }); stage.innerHTML = ""; run(); }
// Advance until a choice or the end, rendering each beat. function run() { controls.innerHTML = ""; for (;;) { const step = flow.advance(); if (step.type === "line") add(`${step.characterName ?? step.character ?? ""}: ${step.text}`); else if (step.type === "text") add(step.text); else if (step.type === "gameEvent") doHostCue(step); // your game's cue else if (step.type === "choice") return renderChoice(step); else if (step.type === "end") return add("The End"); } }
function renderChoice(step) { for (const opt of step.options) { const b = document.createElement("button"); b.textContent = opt.prompt?.text ?? "(continue)"; b.disabled = !opt.eligible; // failed-condition options grey out b.onclick = () => { flow.choose(opt.id); run(); }; controls.appendChild(b); } }
const add = (t) => { const d = document.createElement("div"); d.textContent = t; stage.appendChild(d); }; function doHostCue(step) { /* play step.gameData?.sfx, etc. */ }
document.getElementById("save").onclick = () => localStorage.setItem("save", JSON.stringify(engine.saveGame())); document.getElementById("load").onclick = () => { const blob = localStorage.getItem("save"); if (!blob) return; engine = new Engine(BUNDLE); engine.loadGame(JSON.parse(blob)); flow = engine.getFlow("main"); stage.innerHTML = ""; run(); };
newGame();</script>That’s the whole integration: engine.openFlow to start, flow.advance() to pull the next
beat, flow.choose(id) on a pick, engine.saveGame() / loadGame() for persistence.
Getting the bundle into the page
Section titled “Getting the bundle into the page”Build your story to a .patterc (Patterpad’s Publish Bundle, or patter export). Because it’s
plain JSON, load it however suits you: fetch() it, import it, or (as above) generate a tiny
bundle.js that sets window.PATTER_BUNDLE. The drop-in example includes a gen.mjs that does
the last.
Helpers, state, and localisation
Section titled “Helpers, state, and localisation”@patterkit/play-helpers wraps the common chores. It gives you serializeState /
deserializeState (exactly what saveGame() does), typed property setters, a state logger, a live
state inspector (below), and the live refresh & debug link, including
applyLiveBundle so editor saves land in the running game. It’s optional, but it saves boilerplate.
The drop-in carries all of it on window.Patterplay; the npm package is the module form.
Save/load & Game Data has the detail.
Read your Game Data and tags off each step to drive audio and visuals, supply your @world values,
and emit host events, as Save/load & Game Data describes.
For localisation, read resolved text in Embedded mode, or get ids in IDs-only mode and resolve them yourself, and switch language live. Localisation covers both modes.
Live state inspector
Section titled “Live state inspector”@patterkit/play-helpers ships a drop-in state inspector: a small DOM panel that watches and edits a
running engine’s @patter properties live, with type-aware editors (toggle / number / text / enum /
flags) and a reset-to-default on each row. It’s the browser parity of Unity’s Runtime State window and
Godot’s PatterStatePanel; because the JS game runs in-process, you pass the engine directly instead of
going through a registry.
import { createPropertyInspector } from "@patterkit/play-helpers";
const inspector = createPropertyInspector(engine, { container: document.body });// It refreshes a few times a second and never clobbers a field you're editing.// inspector.destroy(); // remove the panel when you're doneEdits write through engine.setProperty, so a change takes effect on the next beat, handy for poking at
values while you playtest. Leave it out of your shipping build.
Going deeper
Section titled “Going deeper”This page gets you playing. For the full method-by-method reference: advance,
advanceToStop, getChoices, property access, multiple flows: see
The Engine API. To send a flow somewhere from the game (goto, and runFlow
for barks), see Host navigation.
Open source under the MIT licence, made by Ian Thomas.