@patterkit/runtime 0.22.0 → 0.24.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,32 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.24.0] - 2026-10-11
37
+
38
+ ### Added
39
+
40
+ - **Flow capture and restore**: `engine.captureFlow(id)` takes one flow out as a `FlowState`, plain data in the
41
+ save's own spelling (its snapshot, a `version`, and its own values as a version 2 save's per-flow `scopes`
42
+ and `sceneBags`, without its id), and `engine.restoreFlow(id, state)` opens it again under any id, as
43
+ `loadGame` restores each flow. Shared state isn't in it, so a restore rewinds nothing else, and anything a
44
+ load left waiting under the id is dropped first, as when a flow starts. Refused over an open id, while a
45
+ checkpoint is open, and for a state with no cursor, changing nothing; capturing is allowed in a checkpoint.
46
+ The conformance corpus holds every runtime to it.
47
+
48
+ ## [0.23.0] - 2026-10-10
49
+
50
+ ### Added
51
+
52
+ - **Line padding**: the writer's pause after a line, in seconds. A `line` or `text` step carries `padAfter`,
53
+ resolved from the beat's own value, else the nearest `padAfterDefault` above it (its snippet, each group
54
+ around it, innermost first, its block, its scene), else the project's, else the built-in 0.6. Negative is a
55
+ cut-in, the next line (or game event) starting that long before this one ends; it is clamped to zero only on
56
+ a snippet's last line or text beat, where nothing certain follows. A choice option's `prompt` carries its
57
+ `padAfter` too, resolved through its option and never clamped, for a game that voices the prompts itself.
58
+ The outline and beat sequence carry `padAfter` and, when the beat sets its own, `ownPadAfter`.
59
+ `DEFAULT_PAD_AFTER` (0.6) is exported. What to do with it is the game's: the docs give the timing
60
+ rules a writer expects.
61
+
36
62
  ## [0.22.0] - 2026-10-10
37
63
 
38
64
  ### Added
package/README.md CHANGED
@@ -44,6 +44,13 @@ for (;;) {
44
44
  }
45
45
  ```
46
46
 
47
+ A `line` step carries `id`, `text`, `character`, `characterName`, `direction`, and, when the line has a
48
+ speaker qualifier, `qualifier` (its `gameId`, `os`) and `qualifierName` (its shown name, `O.S.`). Line and
49
+ text steps both carry `padAfter`, the writer's pause after the beat in seconds, already resolved; a negative
50
+ one means the next line cuts in. Line, text, and game event steps can carry `gameData` and `tags`. The
51
+ [step shapes](https://patterkit.dev/play/engine/#step-shapes) give the full list and the timing rules a game
52
+ should follow.
53
+
47
54
  `new Engine(bundle, options)` takes `{ rng?, seed?, locale?, hostScopes?, registry?, replayPromptOnChoose?, closedCaptions? }`:
48
55
  - `hostScopes` binds the scopes your game owns, by token, each `{ get(name), set?(name, value) }`: for World
49
56
  Properties, `hostScopes: { world: { get, set } }`. Leave `world` unbound and the runtime self-backs `@world`
@@ -60,8 +67,8 @@ state, visit counts, and PRNG are untouched - so a game's "language" setting can
60
67
  rebuilding the engine or losing the player's place.
61
68
 
62
69
  The above is the **Embedded** build (strings ship inside the `.patterc`). An **IDs-only** build ships no
63
- strings: `step.text` is the beat **ID** and `step.characterName` is omitted - your game localises the IDs in
64
- its own system, then applies `{@ref}` replacement with **`flow.interpolate(yourString)`**. See the
70
+ strings: `step.text` is the beat **ID**, and `step.characterName` and `step.qualifierName` are omitted, so
71
+ your game localises the IDs in its own system, then applies `{@ref}` replacement with **`flow.interpolate(yourString)`**. See the
65
72
  [Localisation guide](https://patterkit.dev/play/localisation/) for both modes across all four runtimes.
66
73
 
67
74
  ## Drop-in (`<script>`)
@@ -103,6 +110,10 @@ value); `engine.loadGame(blob)` restores it. Saves from before property values m
103
110
  registry (version 2) still load. [@patterkit/play-helpers](../play-helpers) wraps these as
104
111
  `serializeState` / `deserializeState` with a tagged envelope.
105
112
 
113
+ To set one flow aside instead, `engine.captureFlow(id)` returns its part of a save as plain data (its
114
+ position, PRNG, visits, selectors, and its own property values, without its id); close it after capturing,
115
+ and `engine.restoreFlow(newId, state)` opens it again later, under any id, without rewinding shared state.
116
+
106
117
  ## One registry per game
107
118
 
108
119
  A game has one `ScopeRegistry` holding every property from every engine, except values the game
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Checkpoint: () => Checkpoint,
24
+ DEFAULT_PAD_AFTER: () => import_model5.DEFAULT_PAD_AFTER,
24
25
  Engine: () => Engine,
25
26
  Flow: () => Flow,
26
27
  buildTagIndex: () => buildTagIndex,
@@ -37,7 +38,7 @@ var import_expr_specificity = require("@wildwinter/expr-specificity");
37
38
  var import_scoperegistry = require("@wildwinter/scoperegistry");
38
39
  var import_scoperegistry2 = require("@wildwinter/scoperegistry");
39
40
  var import_dialect = require("@patterkit/dialect");
40
- var import_model = require("@patterkit/model");
41
+ var import_model2 = require("@patterkit/model");
41
42
 
42
43
  // src/tags.ts
43
44
  function dedupe(tags) {
@@ -73,8 +74,47 @@ function buildTagIndex(bundle) {
73
74
  return index;
74
75
  }
75
76
 
77
+ // src/pads.ts
78
+ var import_model = require("@patterkit/model");
79
+ var spoken = (b) => b.kind === "line" || b.kind === "text";
80
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
81
+ function buildPadIndex(bundle) {
82
+ const index = /* @__PURE__ */ new Map();
83
+ const set = (beat, inherited, clamp) => {
84
+ const own = beat.kind === "gameEvent" ? void 0 : num(beat.padAfter);
85
+ let resolved = own ?? inherited;
86
+ if (clamp && resolved < 0) resolved = 0;
87
+ index.set(beat.id, own !== void 0 ? { own, resolved } : { resolved });
88
+ };
89
+ const visit = (node, inherited) => {
90
+ const def = num(node.padAfterDefault) ?? inherited;
91
+ if (node.type === "group") {
92
+ if (node.prompt) set(node.prompt, def, false);
93
+ for (const child of node.children) visit(child, def);
94
+ return;
95
+ }
96
+ const beats = node.beats ?? [];
97
+ let last = -1;
98
+ beats.forEach((b, i) => {
99
+ if (spoken(b)) last = i;
100
+ });
101
+ beats.forEach((b, i) => {
102
+ if (spoken(b)) set(b, def, i === last);
103
+ });
104
+ };
105
+ const projectDefault = num(bundle.padAfterDefault) ?? import_model.DEFAULT_PAD_AFTER;
106
+ for (const scene of Object.values(bundle.scenes)) {
107
+ const sceneDefault = num(scene.padAfterDefault) ?? projectDefault;
108
+ for (const block of scene.blocks) {
109
+ const blockDefault = num(block.padAfterDefault) ?? sceneDefault;
110
+ for (const child of block.children) visit(child, blockDefault);
111
+ }
112
+ }
113
+ return index;
114
+ }
115
+
76
116
  // src/engine.ts
77
- var import_model2 = require("@patterkit/model");
117
+ var import_model3 = require("@patterkit/model");
78
118
  var astCache = /* @__PURE__ */ new WeakMap();
79
119
  var tagIndexCache = /* @__PURE__ */ new WeakMap();
80
120
  function tagIndexFor(bundle) {
@@ -85,11 +125,21 @@ function tagIndexFor(bundle) {
85
125
  }
86
126
  return index;
87
127
  }
128
+ var padIndexCache = /* @__PURE__ */ new WeakMap();
129
+ function padIndexFor(bundle) {
130
+ let index = padIndexCache.get(bundle);
131
+ if (!index) {
132
+ index = buildPadIndex(bundle);
133
+ padIndexCache.set(bundle, index);
134
+ }
135
+ return index;
136
+ }
88
137
  function tagsOf(host, id) {
89
138
  return host.tagIndex.get(id)?.slice();
90
139
  }
91
140
  var OWNER = "Patter";
92
141
  var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
142
+ var unesc = (key) => key.replace(/%2F/g, "/").replace(/%25/g, "%");
93
143
  var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
94
144
  var keys = {
95
145
  /** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
@@ -157,13 +207,13 @@ var Engine = class _Engine {
157
207
  const blockIndex = /* @__PURE__ */ new Map();
158
208
  const blockById = /* @__PURE__ */ new Map();
159
209
  for (const [sceneId, scene] of Object.entries(bundle.scenes)) {
160
- this.sceneGameIdToId.set((0, import_model.effectiveGameId)(scene), sceneId);
210
+ this.sceneGameIdToId.set((0, import_model2.effectiveGameId)(scene), sceneId);
161
211
  const blockAddrs = /* @__PURE__ */ new Map();
162
212
  for (const block of scene.blocks) {
163
213
  blockIndex.set(block.id, { sceneId });
164
214
  blockById.set(block.id, block);
165
- blockAddrs.set((0, import_model.effectiveGameId)(block), block.id);
166
- (0, import_model.walkNodes)(block.children, (n) => nodeIndex.set(n.id, n));
215
+ blockAddrs.set((0, import_model2.effectiveGameId)(block), block.id);
216
+ (0, import_model2.walkNodes)(block.children, (n) => nodeIndex.set(n.id, n));
167
217
  }
168
218
  this.blockGameIdToId.set(sceneId, blockAddrs);
169
219
  }
@@ -220,6 +270,7 @@ var Engine = class _Engine {
220
270
  blockGameIdToId: this.blockGameIdToId,
221
271
  // same instances the engine resolves with
222
272
  tagIndex: tagIndexFor(bundle),
273
+ padIndex: padIndexFor(bundle),
223
274
  registry,
224
275
  ownsRegistry,
225
276
  patterBag,
@@ -239,9 +290,9 @@ var Engine = class _Engine {
239
290
  replayPromptOnChoose: options.replayPromptOnChoose ?? false,
240
291
  captionsOn: options.closedCaptions ?? true,
241
292
  // captions shown by default (full text)
242
- captionOpen: (bundle.closedCaptions ?? import_model.DEFAULT_CAPTION_DELIMITERS).open,
243
- captionClose: (bundle.closedCaptions ?? import_model.DEFAULT_CAPTION_DELIMITERS).close,
244
- captionCharacter: bundle.closedCaptions?.character || import_model.DEFAULT_CAPTION_CHARACTER,
293
+ captionOpen: (bundle.closedCaptions ?? import_model2.DEFAULT_CAPTION_DELIMITERS).open,
294
+ captionClose: (bundle.closedCaptions ?? import_model2.DEFAULT_CAPTION_DELIMITERS).close,
295
+ captionCharacter: bundle.closedCaptions?.character || import_model2.DEFAULT_CAPTION_CHARACTER,
245
296
  // absent/empty -> SFX
246
297
  refSplitCache: /* @__PURE__ */ new Map(),
247
298
  refSplitRevision: registry.revision
@@ -466,11 +517,11 @@ var Engine = class _Engine {
466
517
  * The inverse of the resolve helpers - for a host that wants to display / log the address. */
467
518
  sceneAddress(sceneId) {
468
519
  const scene = this.host.bundle.scenes[sceneId];
469
- return scene ? (0, import_model.effectiveGameId)(scene) : void 0;
520
+ return scene ? (0, import_model2.effectiveGameId)(scene) : void 0;
470
521
  }
471
522
  blockAddress(blockId) {
472
523
  const block = this.host.blockById.get(blockId);
473
- return block ? (0, import_model.effectiveGameId)(block) : void 0;
524
+ return block ? (0, import_model2.effectiveGameId)(block) : void 0;
474
525
  }
475
526
  /**
476
527
  * Author tags (#215) accumulated for a beat by id: its own tags unioned with every ancestor's
@@ -553,13 +604,13 @@ var Engine = class _Engine {
553
604
  getOutline() {
554
605
  return Object.values(this.host.bundle.scenes).map((scene) => ({
555
606
  id: scene.id,
556
- ...(0, import_model.effectiveGameId)(scene) ? { gameId: (0, import_model.effectiveGameId)(scene) } : {},
607
+ ...(0, import_model2.effectiveGameId)(scene) ? { gameId: (0, import_model2.effectiveGameId)(scene) } : {},
557
608
  name: scene.name,
558
609
  ...gameDataField(scene.gameData),
559
610
  ...this.tagsField(scene.id),
560
611
  blocks: scene.blocks.map((block) => ({
561
612
  id: block.id,
562
- ...(0, import_model.effectiveGameId)(block) ? { gameId: (0, import_model.effectiveGameId)(block) } : {},
613
+ ...(0, import_model2.effectiveGameId)(block) ? { gameId: (0, import_model2.effectiveGameId)(block) } : {},
563
614
  name: block.name,
564
615
  ...gameDataField(block.gameData),
565
616
  ...this.tagsField(block.id),
@@ -576,7 +627,7 @@ var Engine = class _Engine {
576
627
  const out = [];
577
628
  for (const scene of Object.values(this.host.bundle.scenes)) {
578
629
  for (const block of scene.blocks) {
579
- (0, import_model.walkNodes)(block.children, (n) => {
630
+ (0, import_model2.walkNodes)(block.children, (n) => {
580
631
  if (n.type !== "snippet") return;
581
632
  for (const beat of n.beats ?? []) {
582
633
  out.push({ sceneId: scene.id, blockId: block.id, snippetId: n.id, beat: this.beatInfo(beat) });
@@ -613,17 +664,20 @@ var Engine = class _Engine {
613
664
  if (beat.kind === "line") {
614
665
  if (beat.character !== void 0) {
615
666
  info.character = beat.character;
616
- const name = this.host.defaultStrings[(0, import_model.castStringKey)(beat.character)] ?? this.host.castDisplay.get(beat.character);
667
+ const name = this.host.defaultStrings[(0, import_model2.castStringKey)(beat.character)] ?? this.host.castDisplay.get(beat.character);
617
668
  if (name !== void 0) info.characterName = name;
618
669
  }
619
670
  if (beat.direction !== void 0) info.direction = beat.direction;
620
671
  if (beat.qualifier !== void 0) {
621
672
  info.qualifier = beat.qualifier;
622
- const name = this.host.defaultStrings[(0, import_model.qualifierStringKey)(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
673
+ const name = this.host.defaultStrings[(0, import_model2.qualifierStringKey)(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
623
674
  if (name !== void 0) info.qualifierName = name;
624
675
  }
625
676
  }
626
677
  if (beat.kind === "line" || beat.kind === "text") {
678
+ const pad = this.host.padIndex.get(beat.id);
679
+ info.padAfter = pad?.resolved ?? import_model2.DEFAULT_PAD_AFTER;
680
+ if (pad?.own !== void 0) info.ownPadAfter = pad.own;
627
681
  const source = this.host.defaultStrings[beat.id];
628
682
  if (source !== void 0) info.text = source;
629
683
  }
@@ -714,6 +768,45 @@ var Engine = class _Engine {
714
768
  refuseInCheckpoint(what) {
715
769
  if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
716
770
  }
771
+ /**
772
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
773
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
774
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
775
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
776
+ */
777
+ captureFlow(id) {
778
+ const flow = this.flowsById.get(id);
779
+ if (!flow) throw new Error(`no open flow '${id}' to capture`);
780
+ return { version: import_model3.SAVE_VERSION, ...flow.snapshot(), ...flowValuesFrom(id, this.host.registry.save()) };
781
+ }
782
+ /**
783
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
784
+ * registry as a load's do, and the flow is restored as a load restores each flow. `id` needn't be the id it was
785
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
786
+ * that id is open, or while a checkpoint is.
787
+ */
788
+ restoreFlow(id, state) {
789
+ this.refuseInCheckpoint("restoreFlow");
790
+ const version = state.version;
791
+ if (version !== 2 && version !== import_model3.SAVE_VERSION) throw new Error(`unsupported flow state version: ${String(version)}`);
792
+ const cursor = state.cursor;
793
+ if (typeof cursor !== "object" || cursor === null) throw new Error("malformed flow state: no cursor");
794
+ if (this.flowsById.has(id)) throw new Error(`a flow '${id}' is already open; close it before restoring another under that id`);
795
+ this.assertExternalScopes();
796
+ const parked = keys.flow(id);
797
+ this.host.registry.discardParked(parked);
798
+ this.host.registry.load(flowSections(id, state), { keepParked: true });
799
+ const flow = new Flow(id, this.host, this.defaultSeed);
800
+ try {
801
+ flow.restore(state);
802
+ } catch (e) {
803
+ flow.close();
804
+ this.host.registry.discardParked(parked);
805
+ throw e;
806
+ }
807
+ this.flowsById.set(id, flow);
808
+ return flow;
809
+ }
717
810
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
718
811
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
719
812
  closeFlow(id) {
@@ -849,7 +942,7 @@ var Engine = class _Engine {
849
942
  const flows = {};
850
943
  for (const [id, flow] of this.flowsById) flows[id] = flow.snapshot();
851
944
  return {
852
- version: import_model2.SAVE_VERSION,
945
+ version: import_model3.SAVE_VERSION,
853
946
  ...this.host.ownsRegistry ? { registry: this.host.registry.save() } : {},
854
947
  sharedVisits: Object.fromEntries(this.host.sharedVisits),
855
948
  sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
@@ -866,7 +959,7 @@ var Engine = class _Engine {
866
959
  loadGame(save) {
867
960
  this.refuseInCheckpoint("loadGame");
868
961
  const version = save.version;
869
- if (version !== 2 && version !== import_model2.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
962
+ if (version !== 2 && version !== import_model3.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
870
963
  const flows = save.flows;
871
964
  if (typeof flows !== "object" || flows === null || Array.isArray(flows)) throw new Error("malformed save: no flows");
872
965
  this.assertExternalScopes();
@@ -1417,7 +1510,8 @@ var Flow = class {
1417
1510
  const node = this.host.nodeIndex.get(o.id);
1418
1511
  if (!node) continue;
1419
1512
  byId.set(o.id, node);
1420
- options.push({ ...o });
1513
+ const { prompt: saved, ...rest } = o;
1514
+ options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node) } } : {} });
1421
1515
  }
1422
1516
  if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
1423
1517
  }
@@ -1426,7 +1520,7 @@ var Flow = class {
1426
1520
  if (this.pendingPromptOwnerId) {
1427
1521
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
1428
1522
  const beat = owner ? this.authoredPromptOf(owner) : void 0;
1429
- if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
1523
+ if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt, padAfter: c.pendingPrompt.padAfter ?? this.padOf(beat.id) } } : {} };
1430
1524
  else this.pendingPromptOwnerId = null;
1431
1525
  }
1432
1526
  }
@@ -1796,7 +1890,7 @@ var Flow = class {
1796
1890
  case "gameEvent":
1797
1891
  return { type: "gameEvent", id: beat.id, gameData: beat.gameData, ...withTags };
1798
1892
  case "text":
1799
- return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), gameData: beat.gameData, ...withTags };
1893
+ return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
1800
1894
  case "line": {
1801
1895
  const raw = this.resolveString(beat.id);
1802
1896
  const off = !this.host.captionsOn;
@@ -1812,6 +1906,8 @@ var Flow = class {
1812
1906
  direction: silent ? void 0 : beat.direction,
1813
1907
  qualifier: silent ? void 0 : beat.qualifier,
1814
1908
  qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
1909
+ padAfter: this.padOf(beat.id),
1910
+ // a silent line still fires, so it still carries its pause
1815
1911
  gameData: beat.gameData,
1816
1912
  ...withTags
1817
1913
  };
@@ -1850,6 +1946,7 @@ var Flow = class {
1850
1946
  const beat = this.promptBeatOf(node);
1851
1947
  if (!beat) return void 0;
1852
1948
  const text = this.interpolate(this.resolveString(beat.id));
1949
+ const padAfter = this.padOf(beat.id);
1853
1950
  return beat.kind === "line" ? {
1854
1951
  kind: "line",
1855
1952
  text: this.captionLine(text),
@@ -1857,8 +1954,9 @@ var Flow = class {
1857
1954
  characterName: this.resolveCharacterName(beat.character),
1858
1955
  direction: beat.direction,
1859
1956
  qualifier: beat.qualifier,
1860
- qualifierName: this.resolveQualifierName(beat.qualifier)
1861
- } : { kind: "text", text };
1957
+ qualifierName: this.resolveQualifierName(beat.qualifier),
1958
+ padAfter
1959
+ } : { kind: "text", text, padAfter };
1862
1960
  }
1863
1961
  /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
1864
1962
  authoredPromptOf(node) {
@@ -1869,7 +1967,7 @@ var Flow = class {
1869
1967
  promptResult(beat, shown) {
1870
1968
  const tags = tagsOf(this.host, beat.id);
1871
1969
  const withTags = tags && tags.length ? { tags } : {};
1872
- if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
1970
+ if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
1873
1971
  return {
1874
1972
  type: "line",
1875
1973
  id: beat.id,
@@ -1879,6 +1977,7 @@ var Flow = class {
1879
1977
  direction: shown.direction,
1880
1978
  qualifier: shown.qualifier,
1881
1979
  qualifierName: shown.qualifierName,
1980
+ padAfter: this.padOf(beat.id),
1882
1981
  gameData: beat.gameData,
1883
1982
  ...withTags
1884
1983
  };
@@ -1892,7 +1991,7 @@ var Flow = class {
1892
1991
  /** The first snippet with a line/text beat within a child list, depth-first in authored order. */
1893
1992
  firstTextSnippetIn(children) {
1894
1993
  let found;
1895
- (0, import_model.walkNodes)(children, (n) => {
1994
+ (0, import_model2.walkNodes)(children, (n) => {
1896
1995
  if (!found && n.type === "snippet" && (n.beats ?? []).some((b) => b.kind === "line" || b.kind === "text")) {
1897
1996
  found = n;
1898
1997
  }
@@ -1912,16 +2011,27 @@ var Flow = class {
1912
2011
  resolveCharacterName(character) {
1913
2012
  if (character === void 0) return void 0;
1914
2013
  if (this.host.emitIds) return void 0;
1915
- const key = (0, import_model.castStringKey)(character);
2014
+ const key = (0, import_model2.castStringKey)(character);
1916
2015
  return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
1917
2016
  }
2017
+ /** The resolved pause of the beat an option shows as its prompt (its authored prompt, else its first content
2018
+ * line), for a saved option that predates prompts carrying one. */
2019
+ promptPadOf(node) {
2020
+ const beat = this.promptBeatOf(node);
2021
+ return beat ? this.padOf(beat.id) : import_model2.DEFAULT_PAD_AFTER;
2022
+ }
2023
+ /** A line or text beat's resolved pause (line padding): its own `padAfter`, else the nearest default above
2024
+ * it, else the project's, else the built-in one; clamped to zero on a snippet's last line. */
2025
+ padOf(beatId) {
2026
+ return this.host.padIndex.get(beatId)?.resolved ?? import_model2.DEFAULT_PAD_AFTER;
2027
+ }
1918
2028
  /** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
1919
2029
  * active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
1920
2030
  * maps the `qualifier` gameId itself). */
1921
2031
  resolveQualifierName(qualifier) {
1922
2032
  if (qualifier === void 0) return void 0;
1923
2033
  if (this.host.emitIds) return void 0;
1924
- const key = (0, import_model.qualifierStringKey)(qualifier);
2034
+ const key = (0, import_model2.qualifierStringKey)(qualifier);
1925
2035
  return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.qualifierDisplay.get(qualifier);
1926
2036
  }
1927
2037
  /** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
@@ -2004,7 +2114,7 @@ var Flow = class {
2004
2114
  }
2005
2115
  };
2006
2116
  function collectCast(nodes, out) {
2007
- (0, import_model.walkNodes)(nodes, (n) => {
2117
+ (0, import_model2.walkNodes)(nodes, (n) => {
2008
2118
  if (n.type === "group") {
2009
2119
  if (n.prompt?.kind === "line" && n.prompt.character) out.add(n.prompt.character);
2010
2120
  return;
@@ -2022,6 +2132,7 @@ function savedPrompt(p) {
2022
2132
  if (p.direction !== void 0) out.direction = p.direction;
2023
2133
  if (p.qualifier !== void 0) out.qualifier = p.qualifier;
2024
2134
  if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
2135
+ if (typeof p.padAfter === "number") out.padAfter = p.padAfter;
2025
2136
  return out;
2026
2137
  }
2027
2138
  function serialiseSelectors(map) {
@@ -2099,9 +2210,23 @@ function sectionsFromV2(save) {
2099
2210
  const out = {};
2100
2211
  if (save.shared?.patter) out["patter"] = save.shared.patter;
2101
2212
  for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
2102
- for (const [id, f] of Object.entries(save.flows ?? {})) {
2103
- if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
2104
- for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
2213
+ for (const [id, f] of Object.entries(save.flows ?? {})) Object.assign(out, flowSections(id, f));
2214
+ return out;
2215
+ }
2216
+ function flowSections(id, f) {
2217
+ const out = {};
2218
+ if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
2219
+ for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
2220
+ return out;
2221
+ }
2222
+ function flowValuesFrom(id, saved) {
2223
+ const out = { scopes: {}, sceneBags: {} };
2224
+ const prefix = keys.flow(id);
2225
+ for (const [key, values] of Object.entries(saved)) {
2226
+ if (!key.startsWith(prefix)) continue;
2227
+ const rest = key.slice(prefix.length);
2228
+ if (rest === "patter") out.scopes.patter = values;
2229
+ else if (rest.startsWith("scene/")) out.sceneBags[unesc(rest.slice("scene/".length))] = values;
2105
2230
  }
2106
2231
  return out;
2107
2232
  }
@@ -2112,8 +2237,11 @@ function truthy(v) {
2112
2237
  return v.length > 0;
2113
2238
  }
2114
2239
 
2240
+ // src/index.ts
2241
+ var import_model5 = require("@patterkit/model");
2242
+
2115
2243
  // src/describe.ts
2116
- var import_model3 = require("@patterkit/model");
2244
+ var import_model4 = require("@patterkit/model");
2117
2245
  var isShared = (d, scopeDefault) => d.shared ?? scopeDefault;
2118
2246
  function summariseProperty(d, scopeDefault) {
2119
2247
  return {
@@ -2166,11 +2294,11 @@ function describeBundle(bundle) {
2166
2294
  const sceneProps = [];
2167
2295
  for (const scene of Object.values(bundle.scenes)) {
2168
2296
  counts.scenes++;
2169
- const gameId = (0, import_model3.effectiveGameId)(scene);
2297
+ const gameId = (0, import_model4.effectiveGameId)(scene);
2170
2298
  addresses.push({
2171
2299
  gameId,
2172
2300
  name: scene.name,
2173
- blocks: scene.blocks.map((b) => ({ gameId: (0, import_model3.effectiveGameId)(b), name: b.name }))
2301
+ blocks: scene.blocks.map((b) => ({ gameId: (0, import_model4.effectiveGameId)(b), name: b.name }))
2174
2302
  });
2175
2303
  for (const block of scene.blocks) countBlock(block, counts);
2176
2304
  if (scene.sceneProps?.length) {
@@ -2235,6 +2363,7 @@ function effectiveGameData(fields, node) {
2235
2363
  // Annotate the CommonJS export names for ESM import in node:
2236
2364
  0 && (module.exports = {
2237
2365
  Checkpoint,
2366
+ DEFAULT_PAD_AFTER,
2238
2367
  Engine,
2239
2368
  Flow,
2240
2369
  buildTagIndex,