@patterkit/runtime 0.17.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/CHANGELOG.md CHANGED
@@ -33,6 +33,26 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.18.0] - 2026-10-05
37
+
38
+ ### Changed
39
+
40
+ - **`replayPromptOnChoose` speaks back only an authored prompt, and exactly what the choice showed.** A game that
41
+ turns the option on will see the difference, which is why this is a minor release rather than a patch:
42
+ - An option whose prompt the choice borrowed from its own first content line (a bare-snippet option,
43
+ or an Option group with no prompt of its own) is no longer spoken back. That line used to play twice
44
+ in a row, once as the replay and once as content. It now plays once.
45
+ - The replayed prompt carries the text, `character`, `characterName`, and `direction` the choice
46
+ displayed, instead of resolving the prompt again when it is delivered. So if choosing runs effects
47
+ first (an Option group that picks a snippet runs its onEnter at once), or the language changes
48
+ between `choose()` and the next `advance()`, the replay still says what the player saw. In a voiced
49
+ project a replayed line prompt is now interpolated, as the choice showed it.
50
+ - A save taken between `choose()` and the next `advance()` carries that prompt as shown, in a new
51
+ optional `pendingPrompt` on the flow's cursor, so the replay after a load is the same. Every
52
+ Patterplay runtime writes and reads it. A save from an earlier version, without it, still loads and
53
+ resolves the prompt when it is delivered, as before.
54
+ The save shape's `FlowCursor.pendingPrompt` is typed in `@patterkit/model`.
55
+
36
56
  ## [0.17.0] - 2026-10-05
37
57
 
38
58
  ### Fixed
package/dist/index.cjs CHANGED
@@ -876,10 +876,13 @@ var Flow = class {
876
876
  activeSnippet = null;
877
877
  beatIndex = 0;
878
878
  pendingChoice = null;
879
- /** When `replayPromptOnChoose`, the chosen option's prompt beat to deliver before its content. */
880
- pendingPromptBeat = null;
881
- /** The chosen option that owns `pendingPromptBeat`, so a save taken between choose() and the next
882
- * advance() can re-derive the prompt on load (the beat isn't otherwise reachable by id). */
879
+ /** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
880
+ * beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
881
+ * `shown` is missing only after loading a save written before saves carried it: the beat is then
882
+ * resolved when it is delivered, as it always was. */
883
+ pendingPrompt = null;
884
+ /** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
885
+ * advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
883
886
  pendingPromptOwnerId = null;
884
887
  selectors = /* @__PURE__ */ new Map();
885
888
  /** Per-node entry counts for this flow (node id -> times entered). */
@@ -1041,7 +1044,7 @@ var Flow = class {
1041
1044
  if (scene === "END") {
1042
1045
  this.started = true;
1043
1046
  this.pendingChoice = null;
1044
- this.pendingPromptBeat = null;
1047
+ this.pendingPrompt = null;
1045
1048
  this.pendingPromptOwnerId = null;
1046
1049
  this.activeSnippet = null;
1047
1050
  this.beatIndex = 0;
@@ -1061,7 +1064,7 @@ var Flow = class {
1061
1064
  return true;
1062
1065
  }
1063
1066
  this.pendingChoice = null;
1064
- this.pendingPromptBeat = null;
1067
+ this.pendingPrompt = null;
1065
1068
  this.pendingPromptOwnerId = null;
1066
1069
  this.activeSnippet = null;
1067
1070
  this.beatIndex = 0;
@@ -1088,7 +1091,7 @@ var Flow = class {
1088
1091
  this.activeSnippet = null;
1089
1092
  this.beatIndex = 0;
1090
1093
  this.pendingChoice = null;
1091
- this.pendingPromptBeat = null;
1094
+ this.pendingPrompt = null;
1092
1095
  this.pendingPromptOwnerId = null;
1093
1096
  }
1094
1097
  /** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
@@ -1098,10 +1101,10 @@ var Flow = class {
1098
1101
  const journal = this.host.journal;
1099
1102
  if (!journal || journal.flows.has(this)) return;
1100
1103
  journal.flows.add(this);
1101
- const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId } = this;
1104
+ const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId } = this;
1102
1105
  const stack = this.stack.map((f) => ({ ...f }));
1103
1106
  journal.undo.push(() => {
1104
- Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId, stack });
1107
+ Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId, stack });
1105
1108
  });
1106
1109
  }
1107
1110
  /** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
@@ -1119,11 +1122,11 @@ var Flow = class {
1119
1122
  if (this.closed) return { type: "end" };
1120
1123
  if (!this.started) throw new Error("flow has not been started");
1121
1124
  this.touch();
1122
- if (this.pendingPromptBeat) {
1123
- const b = this.pendingPromptBeat;
1124
- this.pendingPromptBeat = null;
1125
+ if (this.pendingPrompt) {
1126
+ const p = this.pendingPrompt;
1127
+ this.pendingPrompt = null;
1125
1128
  this.pendingPromptOwnerId = null;
1126
- return this.beatResult(b);
1129
+ return p.shown ? this.promptResult(p.beat, p.shown) : this.beatResult(p.beat);
1127
1130
  }
1128
1131
  this.settle();
1129
1132
  if (this.flowEnded) return { type: "end" };
@@ -1242,8 +1245,9 @@ var Flow = class {
1242
1245
  const node = choice.byId.get(id);
1243
1246
  this.emit({ type: "chose", group: choice.groupId, option: id });
1244
1247
  this.pendingChoice = null;
1245
- this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
1246
- this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
1248
+ const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
1249
+ this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
1250
+ this.pendingPromptOwnerId = this.pendingPrompt ? node.id : null;
1247
1251
  this.enterChild(node);
1248
1252
  }
1249
1253
  isEnded() {
@@ -1309,6 +1313,8 @@ var Flow = class {
1309
1313
  beatIndex: this.beatIndex,
1310
1314
  pendingChoice: this.pendingChoice ? { groupId: this.pendingChoice.groupId, options: this.pendingChoice.options.map((o) => ({ ...o })) } : null,
1311
1315
  pendingPromptOwnerId: this.pendingPromptOwnerId,
1316
+ // The prompt still to be replayed, as the choice showed it: present only in the choose -> advance window.
1317
+ ...this.pendingPrompt?.shown ? { pendingPrompt: savedPrompt(this.pendingPrompt.shown) } : {},
1312
1318
  selectors: serialiseSelectors(this.selectors)
1313
1319
  }
1314
1320
  };
@@ -1359,12 +1365,13 @@ var Flow = class {
1359
1365
  }
1360
1366
  if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
1361
1367
  }
1362
- this.pendingPromptBeat = null;
1368
+ this.pendingPrompt = null;
1363
1369
  this.pendingPromptOwnerId = c.pendingPromptOwnerId ?? null;
1364
1370
  if (this.pendingPromptOwnerId) {
1365
1371
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
1366
- this.pendingPromptBeat = owner ? this.promptBeatOf(owner) ?? null : null;
1367
- if (!this.pendingPromptBeat) this.pendingPromptOwnerId = null;
1372
+ const beat = owner ? this.authoredPromptOf(owner) : void 0;
1373
+ if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
1374
+ else this.pendingPromptOwnerId = null;
1368
1375
  }
1369
1376
  }
1370
1377
  // -- Scene / block / node entry ------------------------------------------
@@ -1759,6 +1766,27 @@ var Flow = class {
1759
1766
  const text = this.interpolate(this.resolveString(beat.id));
1760
1767
  return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
1761
1768
  }
1769
+ /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
1770
+ authoredPromptOf(node) {
1771
+ return node.type === "group" ? node.prompt : void 0;
1772
+ }
1773
+ /** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
1774
+ * choice showed. */
1775
+ promptResult(beat, shown) {
1776
+ const tags = this.host.tagIndex.get(beat.id);
1777
+ const withTags = tags && tags.length ? { tags } : {};
1778
+ if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
1779
+ return {
1780
+ type: "line",
1781
+ id: beat.id,
1782
+ text: shown.text,
1783
+ character: shown.character,
1784
+ characterName: shown.characterName,
1785
+ direction: shown.direction,
1786
+ gameData: beat.gameData,
1787
+ ...withTags
1788
+ };
1789
+ }
1762
1790
  /** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
1763
1791
  promptBeatOf(node) {
1764
1792
  if (node.type === "group" && node.prompt) return node.prompt;
@@ -1882,6 +1910,13 @@ function collectCast(nodes, out) {
1882
1910
  function gameDataField(gameData) {
1883
1911
  return gameData && Object.keys(gameData).length ? { gameData } : {};
1884
1912
  }
1913
+ function savedPrompt(p) {
1914
+ const out = { kind: p.kind, text: p.text };
1915
+ if (p.character !== void 0) out.character = p.character;
1916
+ if (p.characterName !== void 0) out.characterName = p.characterName;
1917
+ if (p.direction !== void 0) out.direction = p.direction;
1918
+ return out;
1919
+ }
1885
1920
  function serialiseSelectors(map) {
1886
1921
  const out = {};
1887
1922
  for (const [id, st] of map) {