@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/dist/index.d.cts CHANGED
@@ -1,8 +1,14 @@
1
1
  import { ScalarValue } from '@wildwinter/expr';
2
2
  import { ScopeRegistry, PropertyBag, ScopeDeclaration, LogMount, ScopeResolver, PropertyRow } from '@wildwinter/scoperegistry';
3
3
  export { PropertyRow } from '@wildwinter/scoperegistry';
4
- import { GameData, Bundle, CompiledGroup, CompiledSnippet, CompiledBlock, FlowSnapshot, EngineSave, SaveGame, SaveGameV2, PropertyType, ScalarValue as ScalarValue$1, GameDataNodeKind, GameDataField } from '@patterkit/model';
5
- export { Bundle, EngineSave, FlowCursor, FlowSnapshot, SaveEnvelope, SaveGame, SavedChoice, SavedChoiceOption, SavedChoicePrompt, SelectorSnapshot, StackFrame } from '@patterkit/model';
4
+ import { GameData, Bundle, CompiledGroup, CompiledSnippet, CompiledBlock, FlowSnapshot, FlowState, EngineSave, SaveGame, SaveGameV2, PropertyType, ScalarValue as ScalarValue$1, GameDataNodeKind, GameDataField } from '@patterkit/model';
5
+ export { Bundle, DEFAULT_PAD_AFTER, EngineSave, FlowCursor, FlowSnapshot, FlowState, FlowValues, SaveEnvelope, SaveGame, SavedChoice, SavedChoiceOption, SavedChoicePrompt, SelectorSnapshot, StackFrame } from '@patterkit/model';
6
+
7
+ /** A beat's own `padAfter` (when it sets one) and its resolved one (always). */
8
+ interface BeatPad {
9
+ own?: number;
10
+ resolved: number;
11
+ }
6
12
 
7
13
  type SelectableNode = CompiledGroup | CompiledSnippet;
8
14
 
@@ -16,12 +22,14 @@ type StepResult = {
16
22
  direction?: string;
17
23
  qualifier?: string;
18
24
  qualifierName?: string;
25
+ padAfter: number;
19
26
  gameData?: GameData;
20
27
  tags?: string[];
21
28
  } | {
22
29
  type: "text";
23
30
  id: string;
24
31
  text: string;
32
+ padAfter: number;
25
33
  gameData?: GameData;
26
34
  tags?: string[];
27
35
  } | {
@@ -50,6 +58,11 @@ interface BeatInfo {
50
58
  qualifier?: string;
51
59
  /** The qualifier's shown name (source locale), e.g. `V.O.`. */
52
60
  qualifierName?: string;
61
+ /** The pause after this beat, in seconds, resolved from its own value and the defaults above it (line and
62
+ * text only; see line padding). What a delivered step carries. */
63
+ padAfter?: number;
64
+ /** The beat's own `padAfter`, when it sets one (line and text only). */
65
+ ownPadAfter?: number;
53
66
  /** Source text, un-interpolated (line / text). Omitted for gameEvent and IDs-only bundles. */
54
67
  text?: string;
55
68
  /** Author gameData overrides on this beat (raw, as the step carries them). Omitted when empty. */
@@ -120,6 +133,9 @@ interface ChoicePrompt {
120
133
  /** The speaker qualifier's `gameId` (`vo`), and its resolved shown name (locale-aware). */
121
134
  qualifier?: string;
122
135
  qualifierName?: string;
136
+ /** The prompt's resolved pause, in seconds: when it is spoken (by `replayPromptOnChoose`, or by a game that
137
+ * voices it), the wait before the reply, negative to cut in on the question (line padding). */
138
+ padAfter: number;
123
139
  }
124
140
  /** A single option of a pending `choice` group. */
125
141
  interface ChoiceOption {
@@ -374,6 +390,8 @@ interface FlowHost {
374
390
  blockGameIdToId: Map<string, Map<string, string>>;
375
391
  /** Author tags (#215): node id -> accumulated tags (own + every ancestor's, deduped). Built once. */
376
392
  tagIndex: Map<string, string[]>;
393
+ /** Every line and text beat's resolved pause (line padding), precomputed per bundle (pads.ts). */
394
+ padIndex: Map<string, BeatPad>;
377
395
  /** The game's one registry: `@patter` (the SHARED globals), host scopes, every instance bag. */
378
396
  registry: ScopeRegistry;
379
397
  /** True when the engine made the registry (a standalone game): `saveGame()` then carries its values. */
@@ -668,6 +686,20 @@ declare class Engine {
668
686
  /** Let go of what a closed checkpoint recorded, so a handle the game keeps holds nothing. */
669
687
  private static forget;
670
688
  private refuseInCheckpoint;
689
+ /**
690
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
691
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
692
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
693
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
694
+ */
695
+ captureFlow(id: string): FlowState;
696
+ /**
697
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
698
+ * 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
699
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
700
+ * that id is open, or while a checkpoint is.
701
+ */
702
+ restoreFlow(id: string, state: FlowState): Flow;
671
703
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
672
704
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
673
705
  closeFlow(id: string): void;
@@ -1005,6 +1037,12 @@ declare class Flow {
1005
1037
  * locale, else the authoring `displayName`. Undefined when the character has no display name at all
1006
1038
  * (the host falls back to the `character` token itself). */
1007
1039
  private resolveCharacterName;
1040
+ /** The resolved pause of the beat an option shows as its prompt (its authored prompt, else its first content
1041
+ * line), for a saved option that predates prompts carrying one. */
1042
+ private promptPadOf;
1043
+ /** A line or text beat's resolved pause (line padding): its own `padAfter`, else the nearest default above
1044
+ * it, else the project's, else the built-in one; clamped to zero on a snippet's last line. */
1045
+ private padOf;
1008
1046
  /** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
1009
1047
  * active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
1010
1048
  * maps the `qualifier` gameId itself). */
package/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  import { ScalarValue } from '@wildwinter/expr';
2
2
  import { ScopeRegistry, PropertyBag, ScopeDeclaration, LogMount, ScopeResolver, PropertyRow } from '@wildwinter/scoperegistry';
3
3
  export { PropertyRow } from '@wildwinter/scoperegistry';
4
- import { GameData, Bundle, CompiledGroup, CompiledSnippet, CompiledBlock, FlowSnapshot, EngineSave, SaveGame, SaveGameV2, PropertyType, ScalarValue as ScalarValue$1, GameDataNodeKind, GameDataField } from '@patterkit/model';
5
- export { Bundle, EngineSave, FlowCursor, FlowSnapshot, SaveEnvelope, SaveGame, SavedChoice, SavedChoiceOption, SavedChoicePrompt, SelectorSnapshot, StackFrame } from '@patterkit/model';
4
+ import { GameData, Bundle, CompiledGroup, CompiledSnippet, CompiledBlock, FlowSnapshot, FlowState, EngineSave, SaveGame, SaveGameV2, PropertyType, ScalarValue as ScalarValue$1, GameDataNodeKind, GameDataField } from '@patterkit/model';
5
+ export { Bundle, DEFAULT_PAD_AFTER, EngineSave, FlowCursor, FlowSnapshot, FlowState, FlowValues, SaveEnvelope, SaveGame, SavedChoice, SavedChoiceOption, SavedChoicePrompt, SelectorSnapshot, StackFrame } from '@patterkit/model';
6
+
7
+ /** A beat's own `padAfter` (when it sets one) and its resolved one (always). */
8
+ interface BeatPad {
9
+ own?: number;
10
+ resolved: number;
11
+ }
6
12
 
7
13
  type SelectableNode = CompiledGroup | CompiledSnippet;
8
14
 
@@ -16,12 +22,14 @@ type StepResult = {
16
22
  direction?: string;
17
23
  qualifier?: string;
18
24
  qualifierName?: string;
25
+ padAfter: number;
19
26
  gameData?: GameData;
20
27
  tags?: string[];
21
28
  } | {
22
29
  type: "text";
23
30
  id: string;
24
31
  text: string;
32
+ padAfter: number;
25
33
  gameData?: GameData;
26
34
  tags?: string[];
27
35
  } | {
@@ -50,6 +58,11 @@ interface BeatInfo {
50
58
  qualifier?: string;
51
59
  /** The qualifier's shown name (source locale), e.g. `V.O.`. */
52
60
  qualifierName?: string;
61
+ /** The pause after this beat, in seconds, resolved from its own value and the defaults above it (line and
62
+ * text only; see line padding). What a delivered step carries. */
63
+ padAfter?: number;
64
+ /** The beat's own `padAfter`, when it sets one (line and text only). */
65
+ ownPadAfter?: number;
53
66
  /** Source text, un-interpolated (line / text). Omitted for gameEvent and IDs-only bundles. */
54
67
  text?: string;
55
68
  /** Author gameData overrides on this beat (raw, as the step carries them). Omitted when empty. */
@@ -120,6 +133,9 @@ interface ChoicePrompt {
120
133
  /** The speaker qualifier's `gameId` (`vo`), and its resolved shown name (locale-aware). */
121
134
  qualifier?: string;
122
135
  qualifierName?: string;
136
+ /** The prompt's resolved pause, in seconds: when it is spoken (by `replayPromptOnChoose`, or by a game that
137
+ * voices it), the wait before the reply, negative to cut in on the question (line padding). */
138
+ padAfter: number;
123
139
  }
124
140
  /** A single option of a pending `choice` group. */
125
141
  interface ChoiceOption {
@@ -374,6 +390,8 @@ interface FlowHost {
374
390
  blockGameIdToId: Map<string, Map<string, string>>;
375
391
  /** Author tags (#215): node id -> accumulated tags (own + every ancestor's, deduped). Built once. */
376
392
  tagIndex: Map<string, string[]>;
393
+ /** Every line and text beat's resolved pause (line padding), precomputed per bundle (pads.ts). */
394
+ padIndex: Map<string, BeatPad>;
377
395
  /** The game's one registry: `@patter` (the SHARED globals), host scopes, every instance bag. */
378
396
  registry: ScopeRegistry;
379
397
  /** True when the engine made the registry (a standalone game): `saveGame()` then carries its values. */
@@ -668,6 +686,20 @@ declare class Engine {
668
686
  /** Let go of what a closed checkpoint recorded, so a handle the game keeps holds nothing. */
669
687
  private static forget;
670
688
  private refuseInCheckpoint;
689
+ /**
690
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
691
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
692
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
693
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
694
+ */
695
+ captureFlow(id: string): FlowState;
696
+ /**
697
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
698
+ * 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
699
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
700
+ * that id is open, or while a checkpoint is.
701
+ */
702
+ restoreFlow(id: string, state: FlowState): Flow;
671
703
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
672
704
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
673
705
  closeFlow(id: string): void;
@@ -1005,6 +1037,12 @@ declare class Flow {
1005
1037
  * locale, else the authoring `displayName`. Undefined when the character has no display name at all
1006
1038
  * (the host falls back to the `character` token itself). */
1007
1039
  private resolveCharacterName;
1040
+ /** The resolved pause of the beat an option shows as its prompt (its authored prompt, else its first content
1041
+ * line), for a saved option that predates prompts carrying one. */
1042
+ private promptPadOf;
1043
+ /** A line or text beat's resolved pause (line padding): its own `padAfter`, else the nearest default above
1044
+ * it, else the project's, else the built-in one; clamped to zero on a snippet's last line. */
1045
+ private padOf;
1008
1046
  /** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
1009
1047
  * active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
1010
1048
  * maps the `qualifier` gameId itself). */
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { matchedSpecificity as scoreSpecificity } from "@wildwinter/expr-specifi
4
4
  import { ScopeRegistry } from "@wildwinter/scoperegistry";
5
5
  import { defaultFor, PropertyBag } from "@wildwinter/scoperegistry";
6
6
  import { patterDialect, interpolate, splitRef, stripCaptions } from "@patterkit/dialect";
7
- import { walkNodes, effectiveGameId, castStringKey, qualifierStringKey, DEFAULT_CAPTION_DELIMITERS, DEFAULT_CAPTION_CHARACTER } from "@patterkit/model";
7
+ import { walkNodes, effectiveGameId, castStringKey, qualifierStringKey, DEFAULT_PAD_AFTER as DEFAULT_PAD_AFTER2, DEFAULT_CAPTION_DELIMITERS, DEFAULT_CAPTION_CHARACTER } from "@patterkit/model";
8
8
 
9
9
  // src/tags.ts
10
10
  function dedupe(tags) {
@@ -40,6 +40,45 @@ function buildTagIndex(bundle) {
40
40
  return index;
41
41
  }
42
42
 
43
+ // src/pads.ts
44
+ import { DEFAULT_PAD_AFTER } from "@patterkit/model";
45
+ var spoken = (b) => b.kind === "line" || b.kind === "text";
46
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
47
+ function buildPadIndex(bundle) {
48
+ const index = /* @__PURE__ */ new Map();
49
+ const set = (beat, inherited, clamp) => {
50
+ const own = beat.kind === "gameEvent" ? void 0 : num(beat.padAfter);
51
+ let resolved = own ?? inherited;
52
+ if (clamp && resolved < 0) resolved = 0;
53
+ index.set(beat.id, own !== void 0 ? { own, resolved } : { resolved });
54
+ };
55
+ const visit = (node, inherited) => {
56
+ const def = num(node.padAfterDefault) ?? inherited;
57
+ if (node.type === "group") {
58
+ if (node.prompt) set(node.prompt, def, false);
59
+ for (const child of node.children) visit(child, def);
60
+ return;
61
+ }
62
+ const beats = node.beats ?? [];
63
+ let last = -1;
64
+ beats.forEach((b, i) => {
65
+ if (spoken(b)) last = i;
66
+ });
67
+ beats.forEach((b, i) => {
68
+ if (spoken(b)) set(b, def, i === last);
69
+ });
70
+ };
71
+ const projectDefault = num(bundle.padAfterDefault) ?? DEFAULT_PAD_AFTER;
72
+ for (const scene of Object.values(bundle.scenes)) {
73
+ const sceneDefault = num(scene.padAfterDefault) ?? projectDefault;
74
+ for (const block of scene.blocks) {
75
+ const blockDefault = num(block.padAfterDefault) ?? sceneDefault;
76
+ for (const child of block.children) visit(child, blockDefault);
77
+ }
78
+ }
79
+ return index;
80
+ }
81
+
43
82
  // src/engine.ts
44
83
  import { SAVE_VERSION } from "@patterkit/model";
45
84
  var astCache = /* @__PURE__ */ new WeakMap();
@@ -52,11 +91,21 @@ function tagIndexFor(bundle) {
52
91
  }
53
92
  return index;
54
93
  }
94
+ var padIndexCache = /* @__PURE__ */ new WeakMap();
95
+ function padIndexFor(bundle) {
96
+ let index = padIndexCache.get(bundle);
97
+ if (!index) {
98
+ index = buildPadIndex(bundle);
99
+ padIndexCache.set(bundle, index);
100
+ }
101
+ return index;
102
+ }
55
103
  function tagsOf(host, id) {
56
104
  return host.tagIndex.get(id)?.slice();
57
105
  }
58
106
  var OWNER = "Patter";
59
107
  var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
108
+ var unesc = (key) => key.replace(/%2F/g, "/").replace(/%25/g, "%");
60
109
  var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
61
110
  var keys = {
62
111
  /** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
@@ -187,6 +236,7 @@ var Engine = class _Engine {
187
236
  blockGameIdToId: this.blockGameIdToId,
188
237
  // same instances the engine resolves with
189
238
  tagIndex: tagIndexFor(bundle),
239
+ padIndex: padIndexFor(bundle),
190
240
  registry,
191
241
  ownsRegistry,
192
242
  patterBag,
@@ -591,6 +641,9 @@ var Engine = class _Engine {
591
641
  }
592
642
  }
593
643
  if (beat.kind === "line" || beat.kind === "text") {
644
+ const pad = this.host.padIndex.get(beat.id);
645
+ info.padAfter = pad?.resolved ?? DEFAULT_PAD_AFTER2;
646
+ if (pad?.own !== void 0) info.ownPadAfter = pad.own;
594
647
  const source = this.host.defaultStrings[beat.id];
595
648
  if (source !== void 0) info.text = source;
596
649
  }
@@ -681,6 +734,45 @@ var Engine = class _Engine {
681
734
  refuseInCheckpoint(what) {
682
735
  if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
683
736
  }
737
+ /**
738
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
739
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
740
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
741
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
742
+ */
743
+ captureFlow(id) {
744
+ const flow = this.flowsById.get(id);
745
+ if (!flow) throw new Error(`no open flow '${id}' to capture`);
746
+ return { version: SAVE_VERSION, ...flow.snapshot(), ...flowValuesFrom(id, this.host.registry.save()) };
747
+ }
748
+ /**
749
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
750
+ * 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
751
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
752
+ * that id is open, or while a checkpoint is.
753
+ */
754
+ restoreFlow(id, state) {
755
+ this.refuseInCheckpoint("restoreFlow");
756
+ const version = state.version;
757
+ if (version !== 2 && version !== SAVE_VERSION) throw new Error(`unsupported flow state version: ${String(version)}`);
758
+ const cursor = state.cursor;
759
+ if (typeof cursor !== "object" || cursor === null) throw new Error("malformed flow state: no cursor");
760
+ if (this.flowsById.has(id)) throw new Error(`a flow '${id}' is already open; close it before restoring another under that id`);
761
+ this.assertExternalScopes();
762
+ const parked = keys.flow(id);
763
+ this.host.registry.discardParked(parked);
764
+ this.host.registry.load(flowSections(id, state), { keepParked: true });
765
+ const flow = new Flow(id, this.host, this.defaultSeed);
766
+ try {
767
+ flow.restore(state);
768
+ } catch (e) {
769
+ flow.close();
770
+ this.host.registry.discardParked(parked);
771
+ throw e;
772
+ }
773
+ this.flowsById.set(id, flow);
774
+ return flow;
775
+ }
684
776
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
685
777
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
686
778
  closeFlow(id) {
@@ -1384,7 +1476,8 @@ var Flow = class {
1384
1476
  const node = this.host.nodeIndex.get(o.id);
1385
1477
  if (!node) continue;
1386
1478
  byId.set(o.id, node);
1387
- options.push({ ...o });
1479
+ const { prompt: saved, ...rest } = o;
1480
+ options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node) } } : {} });
1388
1481
  }
1389
1482
  if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
1390
1483
  }
@@ -1393,7 +1486,7 @@ var Flow = class {
1393
1486
  if (this.pendingPromptOwnerId) {
1394
1487
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
1395
1488
  const beat = owner ? this.authoredPromptOf(owner) : void 0;
1396
- if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
1489
+ if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt, padAfter: c.pendingPrompt.padAfter ?? this.padOf(beat.id) } } : {} };
1397
1490
  else this.pendingPromptOwnerId = null;
1398
1491
  }
1399
1492
  }
@@ -1763,7 +1856,7 @@ var Flow = class {
1763
1856
  case "gameEvent":
1764
1857
  return { type: "gameEvent", id: beat.id, gameData: beat.gameData, ...withTags };
1765
1858
  case "text":
1766
- return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), gameData: beat.gameData, ...withTags };
1859
+ return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
1767
1860
  case "line": {
1768
1861
  const raw = this.resolveString(beat.id);
1769
1862
  const off = !this.host.captionsOn;
@@ -1779,6 +1872,8 @@ var Flow = class {
1779
1872
  direction: silent ? void 0 : beat.direction,
1780
1873
  qualifier: silent ? void 0 : beat.qualifier,
1781
1874
  qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
1875
+ padAfter: this.padOf(beat.id),
1876
+ // a silent line still fires, so it still carries its pause
1782
1877
  gameData: beat.gameData,
1783
1878
  ...withTags
1784
1879
  };
@@ -1817,6 +1912,7 @@ var Flow = class {
1817
1912
  const beat = this.promptBeatOf(node);
1818
1913
  if (!beat) return void 0;
1819
1914
  const text = this.interpolate(this.resolveString(beat.id));
1915
+ const padAfter = this.padOf(beat.id);
1820
1916
  return beat.kind === "line" ? {
1821
1917
  kind: "line",
1822
1918
  text: this.captionLine(text),
@@ -1824,8 +1920,9 @@ var Flow = class {
1824
1920
  characterName: this.resolveCharacterName(beat.character),
1825
1921
  direction: beat.direction,
1826
1922
  qualifier: beat.qualifier,
1827
- qualifierName: this.resolveQualifierName(beat.qualifier)
1828
- } : { kind: "text", text };
1923
+ qualifierName: this.resolveQualifierName(beat.qualifier),
1924
+ padAfter
1925
+ } : { kind: "text", text, padAfter };
1829
1926
  }
1830
1927
  /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
1831
1928
  authoredPromptOf(node) {
@@ -1836,7 +1933,7 @@ var Flow = class {
1836
1933
  promptResult(beat, shown) {
1837
1934
  const tags = tagsOf(this.host, beat.id);
1838
1935
  const withTags = tags && tags.length ? { tags } : {};
1839
- if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
1936
+ if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
1840
1937
  return {
1841
1938
  type: "line",
1842
1939
  id: beat.id,
@@ -1846,6 +1943,7 @@ var Flow = class {
1846
1943
  direction: shown.direction,
1847
1944
  qualifier: shown.qualifier,
1848
1945
  qualifierName: shown.qualifierName,
1946
+ padAfter: this.padOf(beat.id),
1849
1947
  gameData: beat.gameData,
1850
1948
  ...withTags
1851
1949
  };
@@ -1882,6 +1980,17 @@ var Flow = class {
1882
1980
  const key = castStringKey(character);
1883
1981
  return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
1884
1982
  }
1983
+ /** The resolved pause of the beat an option shows as its prompt (its authored prompt, else its first content
1984
+ * line), for a saved option that predates prompts carrying one. */
1985
+ promptPadOf(node) {
1986
+ const beat = this.promptBeatOf(node);
1987
+ return beat ? this.padOf(beat.id) : DEFAULT_PAD_AFTER2;
1988
+ }
1989
+ /** A line or text beat's resolved pause (line padding): its own `padAfter`, else the nearest default above
1990
+ * it, else the project's, else the built-in one; clamped to zero on a snippet's last line. */
1991
+ padOf(beatId) {
1992
+ return this.host.padIndex.get(beatId)?.resolved ?? DEFAULT_PAD_AFTER2;
1993
+ }
1885
1994
  /** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
1886
1995
  * active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
1887
1996
  * maps the `qualifier` gameId itself). */
@@ -1989,6 +2098,7 @@ function savedPrompt(p) {
1989
2098
  if (p.direction !== void 0) out.direction = p.direction;
1990
2099
  if (p.qualifier !== void 0) out.qualifier = p.qualifier;
1991
2100
  if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
2101
+ if (typeof p.padAfter === "number") out.padAfter = p.padAfter;
1992
2102
  return out;
1993
2103
  }
1994
2104
  function serialiseSelectors(map) {
@@ -2066,9 +2176,23 @@ function sectionsFromV2(save) {
2066
2176
  const out = {};
2067
2177
  if (save.shared?.patter) out["patter"] = save.shared.patter;
2068
2178
  for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
2069
- for (const [id, f] of Object.entries(save.flows ?? {})) {
2070
- if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
2071
- for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
2179
+ for (const [id, f] of Object.entries(save.flows ?? {})) Object.assign(out, flowSections(id, f));
2180
+ return out;
2181
+ }
2182
+ function flowSections(id, f) {
2183
+ const out = {};
2184
+ if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
2185
+ for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
2186
+ return out;
2187
+ }
2188
+ function flowValuesFrom(id, saved) {
2189
+ const out = { scopes: {}, sceneBags: {} };
2190
+ const prefix = keys.flow(id);
2191
+ for (const [key, values] of Object.entries(saved)) {
2192
+ if (!key.startsWith(prefix)) continue;
2193
+ const rest = key.slice(prefix.length);
2194
+ if (rest === "patter") out.scopes.patter = values;
2195
+ else if (rest.startsWith("scene/")) out.sceneBags[unesc(rest.slice("scene/".length))] = values;
2072
2196
  }
2073
2197
  return out;
2074
2198
  }
@@ -2079,6 +2203,9 @@ function truthy(v) {
2079
2203
  return v.length > 0;
2080
2204
  }
2081
2205
 
2206
+ // src/index.ts
2207
+ import { DEFAULT_PAD_AFTER as DEFAULT_PAD_AFTER3 } from "@patterkit/model";
2208
+
2082
2209
  // src/describe.ts
2083
2210
  import { effectiveGameId as effectiveGameId2 } from "@patterkit/model";
2084
2211
  var isShared = (d, scopeDefault) => d.shared ?? scopeDefault;
@@ -2201,6 +2328,7 @@ function effectiveGameData(fields, node) {
2201
2328
  }
2202
2329
  export {
2203
2330
  Checkpoint,
2331
+ DEFAULT_PAD_AFTER3 as DEFAULT_PAD_AFTER,
2204
2332
  Engine,
2205
2333
  Flow,
2206
2334
  buildTagIndex,