@patterkit/runtime
v0.26.0
Published
Patter reference runtime: load a bundle and play dialogue - flows, selectors, choices, jumps, effects, properties. Built on @wildwinter/expr + scoperegistry.
Readme
@patterkit/runtime
Patterplay JS - play Patter branching
dialogue in JavaScript or TypeScript. Load a compiled bundle (a .patterc, the
artifact Patterpad's Build Bundle writes) and play the dialogue: flows, selectors,
choices, jumps, effects, properties, save/load. One of the four Patterplay runtimes
(JS, Unity, Unreal, Godot): every runtime plays the same bundle with the same
behaviour, and the whole set is versioned in lockstep (see
CHANGELOG.md).
Get it
Three equivalent channels - pick whichever fits how you work; no npm is required:
- Release zip:
patterplay-js-<version>.zipon everyplay-js-v*GitHub Release - this runtime as a plain download, symmetric with the Unity / Unreal / Godot plugin zips. It carriespatterplay.min.js(the<script>drop-in: this runtime plus@patterkit/play-helpersunder one global), the module builds of both packages under@patterkit/runtime/dist/and@patterkit/play-helpers/dist/(ESM + CJS + types, with their dependencies inside them, so they need nothing installed), this README, the CHANGELOG, and twodemos/(the zero-build drop-in page and the interactive tour). The zip's builds carry their own registry; to share one registry between engines, install from npm. - npm:
npm install @patterkit/runtime(same version as the zip).@wildwinter/scoperegistryis a peer dependency, so a game holds one registry that every engine shares; npm installs it for you, and a package manager that doesn't install peers needsnpm install @wildwinter/scoperegistry. - CDN:
patterplay.min.jsvia unpkg/jsDelivr from@patterkit/play-helpers(the package that builds it), or loose on the same Release.
Use it (npm / bundler)
import { Engine } from "@patterkit/runtime";
const engine = new Engine(bundle); // bundle = parsed .patterc JSON
const flow = engine.openFlow("main", { scene: "square" });
for (;;) {
const step = flow.advance();
if (step.type === "line") console.log(`${step.characterName ?? step.character}: ${step.text}`);
else if (step.type === "text") console.log(step.text);
else if (step.type === "choice") { flow.choose(step.options.find((o) => o.eligible)!.id); }
else if (step.type === "end") break;
}A line step carries id, text, character, characterName, direction, and, when the line has a
speaker qualifier, qualifier (its gameId, os) and qualifierName (its shown name, O.S.). Line and
text steps both carry padAfter, the writer's pause after the beat in seconds, already resolved; a negative
one means the next line cuts in. Line, text, and game event steps can carry gameData and tags. The
step shapes give the full list and the timing rules a game
should follow.
new Engine(bundle, options) takes { rng?, seed?, locale?, hostScopes?, registry?, replayPromptOnChoose?, closedCaptions? }:
hostScopesbinds the scopes your game owns, by token, each{ get(name), set?(name, value) }: for World Properties,hostScopes: { world: { get, set } }. Leaveworldunbound and the runtime self-backs@worldfrom the declared defaults, as a property it saves.registryis the game's oneScopeRegistry(from@wildwinter/scoperegistry), holding every engine's properties. Omit it and the engine makes its own. See One registry per game below.localeplays a non-default language (embedded localisation; an IDs-only bundle ignores it).- A string the active locale is missing falls back to the default-locale source,
flagged
<Untranslated: {id}> {source}so a partial translation is impossible to miss.
Switch language mid-game with engine.setLocale("fr") (read it back via engine.locale):
subsequent beats / character names / {@ref} render in the new locale while the flow's position,
state, visit counts, and PRNG are untouched - so a game's "language" setting can change live without
rebuilding the engine or losing the player's place.
The above is the Embedded build (strings ship inside the .patterc). An IDs-only build ships no
strings: step.text is the beat ID, and step.characterName and step.qualifierName are omitted, so
your game localises the IDs in its own system, then applies {@ref} replacement with flow.interpolate(yourString). See the
Localisation guide for both modes across all four runtimes.
Drop-in (<script>)
patterplay.min.js is a single self-contained, minified IIFE - every dependency
inlined - for plain HTML pages with no bundler. It carries this runtime AND
@patterkit/play-helpers on the one Patterplay global, so a plain
page can also write and read the family's save text:
<script src="https://unpkg.com/@patterkit/play-helpers/dist/patterplay.min.js"></script>
<script>
const { Engine, serializeState, deserializeState } = window.Patterplay; // also: Flow, gameDataFields, createStateLogger, ...
const engine = new Engine(BUNDLE);
const flow = engine.openFlow("main", { scene: "square" });
// ...advance() / choose() exactly as above; localStorage.setItem("slot1", serializeState(engine)) to save.
</script>It is built by the play-helpers package, the one that depends on both: npm run build -w
@patterkit/play-helpers (emits packages/play-helpers/dist/patterplay.min.js).
Demos
The release zip's demos/ folder holds two working references (in the repo:
examples/drop-in and examples/tour-web):
- drop-in - the smallest possible host: a plain HTML page playing a compiled bundle
via
patterplay.min.js. Openindex.htmlstraight from the unzipped folder. - tour-web - the full interactive Patter tour in a browser (
node serve.mjs, open the page). The JS counterpart of the demos the Unity, Godot, and Unreal plugins bundle (audio-less: the resolver wiring is there, playback is your call).
Save / load
engine.saveGame() returns a JSON-serialisable snapshot of the whole game (visit counts,
selector cursors, every live flow, and, for an engine built without a registry, every property
value); engine.loadGame(blob) restores it. Saves from before property values moved into the
registry (version 2) still load. @patterkit/play-helpers wraps these as
serializeState / deserializeState with a tagged envelope.
To set one flow aside instead, engine.captureFlow(id) returns its part of a save as plain data (its
position, PRNG, visits, selectors, and its own property values, without its id); close it after capturing,
and engine.restoreFlow(newId, state) opens it again later, under any id, without rewinding shared state.
One registry per game
A game has one ScopeRegistry holding every property from every engine, except values the game
keeps itself and lends through a resolver, and saves it once. Pass it as registry:
import { ScopeRegistry } from "@wildwinter/scoperegistry";
const registry = new ScopeRegistry().defineOwned("world", worldDeclarations, { owner: "Game" });
const engine = new Engine(bundle, { registry });
const save = { registry: registry.save(), patter: engine.saveGame() }; // saveGame() holds no values nowThe engine registers @patter under patter and every per-flow and per-scene bag under a key
starting patter/; @world is the game's to register. Loading works in either order.
