@patterkit/runtime 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -459,6 +459,10 @@ declare class Engine {
459
459
  *
460
460
  * Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
461
461
  * call to reach for when you want a speaker's variation state to carry on.
462
+ *
463
+ * THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
464
+ * scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
465
+ * exactly as it was.
462
466
  */
463
467
  openFlow(id: string, opts?: OpenFlowOptions): Flow;
464
468
  /**
@@ -490,6 +494,13 @@ declare class Engine {
490
494
  * it opens (or the save as it loads), before anything changes. By then a game has built all of its
491
495
  * engines, whatever order it built them in. The same message on every runtime. */
492
496
  private assertExternalScopes;
497
+ /**
498
+ * `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
499
+ * scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
500
+ * id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
501
+ * is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
502
+ */
503
+ private resolveOpenAddress;
493
504
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
494
505
  private resolveSceneRef;
495
506
  /** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
@@ -673,10 +684,13 @@ declare class Flow {
673
684
  private activeSnippet;
674
685
  private beatIndex;
675
686
  private pendingChoice;
676
- /** When `replayPromptOnChoose`, the chosen option's prompt beat to deliver before its content. */
677
- private pendingPromptBeat;
678
- /** The chosen option that owns `pendingPromptBeat`, so a save taken between choose() and the next
679
- * advance() can re-derive the prompt on load (the beat isn't otherwise reachable by id). */
687
+ /** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
688
+ * beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
689
+ * `shown` is missing only after loading a save written before saves carried it: the beat is then
690
+ * resolved when it is delivered, as it always was. */
691
+ private pendingPrompt;
692
+ /** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
693
+ * advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
680
694
  private pendingPromptOwnerId;
681
695
  private selectors;
682
696
  /** Per-node entry counts for this flow (node id -> times entered). */
@@ -896,6 +910,11 @@ declare class Flow {
896
910
  * option's first content line. NO look-ahead. Undefined only when even that is absent.
897
911
  */
898
912
  private promptFor;
913
+ /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
914
+ private authoredPromptOf;
915
+ /** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
916
+ * choice showed. */
917
+ private promptResult;
899
918
  /** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
900
919
  private promptBeatOf;
901
920
  /** The first snippet with a line/text beat within a child list, depth-first in authored order. */
package/dist/index.d.ts CHANGED
@@ -459,6 +459,10 @@ declare class Engine {
459
459
  *
460
460
  * Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
461
461
  * call to reach for when you want a speaker's variation state to carry on.
462
+ *
463
+ * THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
464
+ * scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
465
+ * exactly as it was.
462
466
  */
463
467
  openFlow(id: string, opts?: OpenFlowOptions): Flow;
464
468
  /**
@@ -490,6 +494,13 @@ declare class Engine {
490
494
  * it opens (or the save as it loads), before anything changes. By then a game has built all of its
491
495
  * engines, whatever order it built them in. The same message on every runtime. */
492
496
  private assertExternalScopes;
497
+ /**
498
+ * `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
499
+ * scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
500
+ * id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
501
+ * is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
502
+ */
503
+ private resolveOpenAddress;
493
504
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
494
505
  private resolveSceneRef;
495
506
  /** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
@@ -673,10 +684,13 @@ declare class Flow {
673
684
  private activeSnippet;
674
685
  private beatIndex;
675
686
  private pendingChoice;
676
- /** When `replayPromptOnChoose`, the chosen option's prompt beat to deliver before its content. */
677
- private pendingPromptBeat;
678
- /** The chosen option that owns `pendingPromptBeat`, so a save taken between choose() and the next
679
- * advance() can re-derive the prompt on load (the beat isn't otherwise reachable by id). */
687
+ /** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
688
+ * beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
689
+ * `shown` is missing only after loading a save written before saves carried it: the beat is then
690
+ * resolved when it is delivered, as it always was. */
691
+ private pendingPrompt;
692
+ /** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
693
+ * advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
680
694
  private pendingPromptOwnerId;
681
695
  private selectors;
682
696
  /** Per-node entry counts for this flow (node id -> times entered). */
@@ -896,6 +910,11 @@ declare class Flow {
896
910
  * option's first content line. NO look-ahead. Undefined only when even that is absent.
897
911
  */
898
912
  private promptFor;
913
+ /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
914
+ private authoredPromptOf;
915
+ /** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
916
+ * choice showed. */
917
+ private promptResult;
899
918
  /** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
900
919
  private promptBeatOf;
901
920
  /** The first snippet with a line/text beat within a child list, depth-first in authored order. */
package/dist/index.js CHANGED
@@ -299,11 +299,14 @@ var Engine = class _Engine {
299
299
  *
300
300
  * Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
301
301
  * call to reach for when you want a speaker's variation state to carry on.
302
+ *
303
+ * THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
304
+ * scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
305
+ * exactly as it was.
302
306
  */
303
307
  openFlow(id, opts = {}) {
304
308
  this.assertExternalScopes();
305
- const sceneId = this.resolveSceneRef(opts.scene);
306
- const blockId = this.resolveBlockRef(sceneId, opts.block);
309
+ const { sceneId, blockId } = this.resolveOpenAddress(opts.scene, opts.block);
307
310
  const prior = this.flowsById.get(id);
308
311
  const journal = this.host.journal;
309
312
  if (journal && prior && !prior.isClosed) throw new Error(`openFlow would replace the open flow '${id}', which can't be undone while a checkpoint is open`);
@@ -364,6 +367,28 @@ var Engine = class _Engine {
364
367
  }
365
368
  }
366
369
  }
370
+ /**
371
+ * `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
372
+ * scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
373
+ * id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
374
+ * is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
375
+ */
376
+ resolveOpenAddress(scene, block) {
377
+ if (scene !== void 0) {
378
+ const sceneId = this.resolveSceneRef(scene);
379
+ if (!this.host.bundle.scenes[sceneId]) throw new Error(`unknown scene: ${scene}`);
380
+ if (block === void 0) return { sceneId };
381
+ const blockId = this.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
382
+ if (blockId === void 0) throw new Error(`unknown block: ${block}`);
383
+ return { sceneId, blockId };
384
+ }
385
+ if (block !== void 0) {
386
+ if (!this.host.blockIndex.has(block)) throw new Error(`unknown block: ${block}`);
387
+ return { blockId: block };
388
+ }
389
+ if (Object.keys(this.host.bundle.scenes).length === 0) throw new Error("no scenes in bundle");
390
+ return {};
391
+ }
367
392
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
368
393
  resolveSceneRef(ref) {
369
394
  if (ref == null) return void 0;
@@ -818,10 +843,13 @@ var Flow = class {
818
843
  activeSnippet = null;
819
844
  beatIndex = 0;
820
845
  pendingChoice = null;
821
- /** When `replayPromptOnChoose`, the chosen option's prompt beat to deliver before its content. */
822
- pendingPromptBeat = null;
823
- /** The chosen option that owns `pendingPromptBeat`, so a save taken between choose() and the next
824
- * advance() can re-derive the prompt on load (the beat isn't otherwise reachable by id). */
846
+ /** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
847
+ * beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
848
+ * `shown` is missing only after loading a save written before saves carried it: the beat is then
849
+ * resolved when it is delivered, as it always was. */
850
+ pendingPrompt = null;
851
+ /** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
852
+ * advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
825
853
  pendingPromptOwnerId = null;
826
854
  selectors = /* @__PURE__ */ new Map();
827
855
  /** Per-node entry counts for this flow (node id -> times entered). */
@@ -983,7 +1011,7 @@ var Flow = class {
983
1011
  if (scene === "END") {
984
1012
  this.started = true;
985
1013
  this.pendingChoice = null;
986
- this.pendingPromptBeat = null;
1014
+ this.pendingPrompt = null;
987
1015
  this.pendingPromptOwnerId = null;
988
1016
  this.activeSnippet = null;
989
1017
  this.beatIndex = 0;
@@ -1003,7 +1031,7 @@ var Flow = class {
1003
1031
  return true;
1004
1032
  }
1005
1033
  this.pendingChoice = null;
1006
- this.pendingPromptBeat = null;
1034
+ this.pendingPrompt = null;
1007
1035
  this.pendingPromptOwnerId = null;
1008
1036
  this.activeSnippet = null;
1009
1037
  this.beatIndex = 0;
@@ -1030,7 +1058,7 @@ var Flow = class {
1030
1058
  this.activeSnippet = null;
1031
1059
  this.beatIndex = 0;
1032
1060
  this.pendingChoice = null;
1033
- this.pendingPromptBeat = null;
1061
+ this.pendingPrompt = null;
1034
1062
  this.pendingPromptOwnerId = null;
1035
1063
  }
1036
1064
  /** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
@@ -1040,10 +1068,10 @@ var Flow = class {
1040
1068
  const journal = this.host.journal;
1041
1069
  if (!journal || journal.flows.has(this)) return;
1042
1070
  journal.flows.add(this);
1043
- const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId } = this;
1071
+ const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId } = this;
1044
1072
  const stack = this.stack.map((f) => ({ ...f }));
1045
1073
  journal.undo.push(() => {
1046
- Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId, stack });
1074
+ Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId, stack });
1047
1075
  });
1048
1076
  }
1049
1077
  /** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
@@ -1061,11 +1089,11 @@ var Flow = class {
1061
1089
  if (this.closed) return { type: "end" };
1062
1090
  if (!this.started) throw new Error("flow has not been started");
1063
1091
  this.touch();
1064
- if (this.pendingPromptBeat) {
1065
- const b = this.pendingPromptBeat;
1066
- this.pendingPromptBeat = null;
1092
+ if (this.pendingPrompt) {
1093
+ const p = this.pendingPrompt;
1094
+ this.pendingPrompt = null;
1067
1095
  this.pendingPromptOwnerId = null;
1068
- return this.beatResult(b);
1096
+ return p.shown ? this.promptResult(p.beat, p.shown) : this.beatResult(p.beat);
1069
1097
  }
1070
1098
  this.settle();
1071
1099
  if (this.flowEnded) return { type: "end" };
@@ -1184,8 +1212,9 @@ var Flow = class {
1184
1212
  const node = choice.byId.get(id);
1185
1213
  this.emit({ type: "chose", group: choice.groupId, option: id });
1186
1214
  this.pendingChoice = null;
1187
- this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
1188
- this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
1215
+ const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
1216
+ this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
1217
+ this.pendingPromptOwnerId = this.pendingPrompt ? node.id : null;
1189
1218
  this.enterChild(node);
1190
1219
  }
1191
1220
  isEnded() {
@@ -1251,6 +1280,8 @@ var Flow = class {
1251
1280
  beatIndex: this.beatIndex,
1252
1281
  pendingChoice: this.pendingChoice ? { groupId: this.pendingChoice.groupId, options: this.pendingChoice.options.map((o) => ({ ...o })) } : null,
1253
1282
  pendingPromptOwnerId: this.pendingPromptOwnerId,
1283
+ // The prompt still to be replayed, as the choice showed it: present only in the choose -> advance window.
1284
+ ...this.pendingPrompt?.shown ? { pendingPrompt: savedPrompt(this.pendingPrompt.shown) } : {},
1254
1285
  selectors: serialiseSelectors(this.selectors)
1255
1286
  }
1256
1287
  };
@@ -1301,12 +1332,13 @@ var Flow = class {
1301
1332
  }
1302
1333
  if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
1303
1334
  }
1304
- this.pendingPromptBeat = null;
1335
+ this.pendingPrompt = null;
1305
1336
  this.pendingPromptOwnerId = c.pendingPromptOwnerId ?? null;
1306
1337
  if (this.pendingPromptOwnerId) {
1307
1338
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
1308
- this.pendingPromptBeat = owner ? this.promptBeatOf(owner) ?? null : null;
1309
- if (!this.pendingPromptBeat) this.pendingPromptOwnerId = null;
1339
+ const beat = owner ? this.authoredPromptOf(owner) : void 0;
1340
+ if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
1341
+ else this.pendingPromptOwnerId = null;
1310
1342
  }
1311
1343
  }
1312
1344
  // -- Scene / block / node entry ------------------------------------------
@@ -1701,6 +1733,27 @@ var Flow = class {
1701
1733
  const text = this.interpolate(this.resolveString(beat.id));
1702
1734
  return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
1703
1735
  }
1736
+ /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
1737
+ authoredPromptOf(node) {
1738
+ return node.type === "group" ? node.prompt : void 0;
1739
+ }
1740
+ /** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
1741
+ * choice showed. */
1742
+ promptResult(beat, shown) {
1743
+ const tags = this.host.tagIndex.get(beat.id);
1744
+ const withTags = tags && tags.length ? { tags } : {};
1745
+ if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
1746
+ return {
1747
+ type: "line",
1748
+ id: beat.id,
1749
+ text: shown.text,
1750
+ character: shown.character,
1751
+ characterName: shown.characterName,
1752
+ direction: shown.direction,
1753
+ gameData: beat.gameData,
1754
+ ...withTags
1755
+ };
1756
+ }
1704
1757
  /** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
1705
1758
  promptBeatOf(node) {
1706
1759
  if (node.type === "group" && node.prompt) return node.prompt;
@@ -1824,6 +1877,13 @@ function collectCast(nodes, out) {
1824
1877
  function gameDataField(gameData) {
1825
1878
  return gameData && Object.keys(gameData).length ? { gameData } : {};
1826
1879
  }
1880
+ function savedPrompt(p) {
1881
+ const out = { kind: p.kind, text: p.text };
1882
+ if (p.character !== void 0) out.character = p.character;
1883
+ if (p.characterName !== void 0) out.characterName = p.characterName;
1884
+ if (p.direction !== void 0) out.direction = p.direction;
1885
+ return out;
1886
+ }
1827
1887
  function serialiseSelectors(map) {
1828
1888
  const out = {};
1829
1889
  for (const [id, st] of map) {