@patterkit/runtime 0.23.0 → 0.25.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,8 @@
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, DEFAULT_PAD_AFTER, 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
6
 
7
7
  /** A beat's own `padAfter` (when it sets one) and its resolved one (always). */
8
8
  interface BeatPad {
@@ -22,6 +22,7 @@ type StepResult = {
22
22
  direction?: string;
23
23
  qualifier?: string;
24
24
  qualifierName?: string;
25
+ speakerLabel?: string;
25
26
  padAfter: number;
26
27
  gameData?: GameData;
27
28
  tags?: string[];
@@ -58,6 +59,8 @@ interface BeatInfo {
58
59
  qualifier?: string;
59
60
  /** The qualifier's shown name (source locale), e.g. `V.O.`. */
60
61
  qualifierName?: string;
62
+ /** The speaker's label (source locale): `You (V.O.)`, a pair's own label, or with no qualifier the name. */
63
+ speakerLabel?: string;
61
64
  /** The pause after this beat, in seconds, resolved from its own value and the defaults above it (line and
62
65
  * text only; see line padding). What a delivered step carries. */
63
66
  padAfter?: number;
@@ -133,6 +136,8 @@ interface ChoicePrompt {
133
136
  /** The speaker qualifier's `gameId` (`vo`), and its resolved shown name (locale-aware). */
134
137
  qualifier?: string;
135
138
  qualifierName?: string;
139
+ /** The name to show for the speaker (locale-aware): see {@link speakerLabelOf}. */
140
+ speakerLabel?: string;
136
141
  /** The prompt's resolved pause, in seconds: when it is spoken (by `replayPromptOnChoose`, or by a game that
137
142
  * voices it), the wait before the reply, negative to cut in on the question (line padding). */
138
143
  padAfter: number;
@@ -379,6 +384,11 @@ interface FlowHost {
379
384
  castDisplay: Map<string, string>;
380
385
  /** Speaker qualifier gameId -> authored name (the unlocalised fallback when no loc string exists). */
381
386
  qualifierDisplay: Map<string, string>;
387
+ /** `<name>\0<gameId>` -> a character's own label with that qualifier, for the overridden pairs only (the
388
+ * unlocalised fallback). A pair not here follows the pattern, whatever a loc file holds for it. */
389
+ speakerLabels: Map<string, string>;
390
+ /** The project's speaker pattern (the unlocalised fallback for the `speaker:pattern` string). */
391
+ speakerPattern: string;
382
392
  nodeIndex: Map<string, SelectableNode>;
383
393
  blockIndex: Map<string, {
384
394
  sceneId: string;
@@ -686,6 +696,20 @@ declare class Engine {
686
696
  /** Let go of what a closed checkpoint recorded, so a handle the game keeps holds nothing. */
687
697
  private static forget;
688
698
  private refuseInCheckpoint;
699
+ /**
700
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
701
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
702
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
703
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
704
+ */
705
+ captureFlow(id: string): FlowState;
706
+ /**
707
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
708
+ * 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
709
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
710
+ * that id is open, or while a checkpoint is.
711
+ */
712
+ restoreFlow(id: string, state: FlowState): Flow;
689
713
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
690
714
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
691
715
  closeFlow(id: string): void;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
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, DEFAULT_PAD_AFTER, 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
6
 
7
7
  /** A beat's own `padAfter` (when it sets one) and its resolved one (always). */
8
8
  interface BeatPad {
@@ -22,6 +22,7 @@ type StepResult = {
22
22
  direction?: string;
23
23
  qualifier?: string;
24
24
  qualifierName?: string;
25
+ speakerLabel?: string;
25
26
  padAfter: number;
26
27
  gameData?: GameData;
27
28
  tags?: string[];
@@ -58,6 +59,8 @@ interface BeatInfo {
58
59
  qualifier?: string;
59
60
  /** The qualifier's shown name (source locale), e.g. `V.O.`. */
60
61
  qualifierName?: string;
62
+ /** The speaker's label (source locale): `You (V.O.)`, a pair's own label, or with no qualifier the name. */
63
+ speakerLabel?: string;
61
64
  /** The pause after this beat, in seconds, resolved from its own value and the defaults above it (line and
62
65
  * text only; see line padding). What a delivered step carries. */
63
66
  padAfter?: number;
@@ -133,6 +136,8 @@ interface ChoicePrompt {
133
136
  /** The speaker qualifier's `gameId` (`vo`), and its resolved shown name (locale-aware). */
134
137
  qualifier?: string;
135
138
  qualifierName?: string;
139
+ /** The name to show for the speaker (locale-aware): see {@link speakerLabelOf}. */
140
+ speakerLabel?: string;
136
141
  /** The prompt's resolved pause, in seconds: when it is spoken (by `replayPromptOnChoose`, or by a game that
137
142
  * voices it), the wait before the reply, negative to cut in on the question (line padding). */
138
143
  padAfter: number;
@@ -379,6 +384,11 @@ interface FlowHost {
379
384
  castDisplay: Map<string, string>;
380
385
  /** Speaker qualifier gameId -> authored name (the unlocalised fallback when no loc string exists). */
381
386
  qualifierDisplay: Map<string, string>;
387
+ /** `<name>\0<gameId>` -> a character's own label with that qualifier, for the overridden pairs only (the
388
+ * unlocalised fallback). A pair not here follows the pattern, whatever a loc file holds for it. */
389
+ speakerLabels: Map<string, string>;
390
+ /** The project's speaker pattern (the unlocalised fallback for the `speaker:pattern` string). */
391
+ speakerPattern: string;
382
392
  nodeIndex: Map<string, SelectableNode>;
383
393
  blockIndex: Map<string, {
384
394
  sceneId: string;
@@ -686,6 +696,20 @@ declare class Engine {
686
696
  /** Let go of what a closed checkpoint recorded, so a handle the game keeps holds nothing. */
687
697
  private static forget;
688
698
  private refuseInCheckpoint;
699
+ /**
700
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
701
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
702
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
703
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
704
+ */
705
+ captureFlow(id: string): FlowState;
706
+ /**
707
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
708
+ * 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
709
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
710
+ * that id is open, or while a checkpoint is.
711
+ */
712
+ restoreFlow(id: string, state: FlowState): Flow;
689
713
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
690
714
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
691
715
  closeFlow(id: string): void;
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_PAD_AFTER as DEFAULT_PAD_AFTER2, DEFAULT_CAPTION_DELIMITERS, DEFAULT_CAPTION_CHARACTER } from "@patterkit/model";
7
+ import { walkNodes, effectiveGameId, castStringKey, qualifierStringKey, speakerLabelKey, formatSpeakerLabel, SPEAKER_PATTERN_KEY, DEFAULT_SPEAKER_PATTERN, 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) {
@@ -105,6 +105,7 @@ function tagsOf(host, id) {
105
105
  }
106
106
  var OWNER = "Patter";
107
107
  var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
108
+ var unesc = (key) => key.replace(/%2F/g, "/").replace(/%25/g, "%");
108
109
  var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
109
110
  var keys = {
110
111
  /** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
@@ -167,6 +168,9 @@ var Engine = class _Engine {
167
168
  for (const c of bundle.cast ?? []) if (c.displayName) castDisplay.set(c.name, c.displayName);
168
169
  const qualifierDisplay = /* @__PURE__ */ new Map();
169
170
  for (const q of bundle.qualifiers ?? []) if (q?.gameId) qualifierDisplay.set(q.gameId, q.name);
171
+ const speakerLabels = /* @__PURE__ */ new Map();
172
+ for (const c of bundle.cast ?? []) for (const [q, label] of Object.entries(c.qualifierLabels ?? {})) if (typeof label === "string") speakerLabels.set(`${c.name}\0${q}`, label);
173
+ const speakerPattern = bundle.speakerPattern ?? DEFAULT_SPEAKER_PATTERN;
170
174
  this.defaultSeed = (options.seed ?? 2654435769) >>> 0;
171
175
  const nodeIndex = /* @__PURE__ */ new Map();
172
176
  const blockIndex = /* @__PURE__ */ new Map();
@@ -228,6 +232,8 @@ var Engine = class _Engine {
228
232
  defaultStrings,
229
233
  castDisplay,
230
234
  qualifierDisplay,
235
+ speakerLabels,
236
+ speakerPattern,
231
237
  nodeIndex,
232
238
  blockIndex,
233
239
  blockById,
@@ -638,6 +644,8 @@ var Engine = class _Engine {
638
644
  const name = this.host.defaultStrings[qualifierStringKey(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
639
645
  if (name !== void 0) info.qualifierName = name;
640
646
  }
647
+ const label = speakerLabelOf(this.host, beat.character, beat.qualifier, true);
648
+ if (label !== void 0) info.speakerLabel = label;
641
649
  }
642
650
  if (beat.kind === "line" || beat.kind === "text") {
643
651
  const pad = this.host.padIndex.get(beat.id);
@@ -733,6 +741,45 @@ var Engine = class _Engine {
733
741
  refuseInCheckpoint(what) {
734
742
  if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
735
743
  }
744
+ /**
745
+ * Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
746
+ * `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
747
+ * state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
748
+ * drops its values. Only reads, so it is allowed while a checkpoint is open.
749
+ */
750
+ captureFlow(id) {
751
+ const flow = this.flowsById.get(id);
752
+ if (!flow) throw new Error(`no open flow '${id}' to capture`);
753
+ return { version: SAVE_VERSION, ...flow.snapshot(), ...flowValuesFrom(id, this.host.registry.save()) };
754
+ }
755
+ /**
756
+ * Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
757
+ * 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
758
+ * captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
759
+ * that id is open, or while a checkpoint is.
760
+ */
761
+ restoreFlow(id, state) {
762
+ this.refuseInCheckpoint("restoreFlow");
763
+ const version = state.version;
764
+ if (version !== 2 && version !== SAVE_VERSION) throw new Error(`unsupported flow state version: ${String(version)}`);
765
+ const cursor = state.cursor;
766
+ if (typeof cursor !== "object" || cursor === null) throw new Error("malformed flow state: no cursor");
767
+ if (this.flowsById.has(id)) throw new Error(`a flow '${id}' is already open; close it before restoring another under that id`);
768
+ this.assertExternalScopes();
769
+ const parked = keys.flow(id);
770
+ this.host.registry.discardParked(parked);
771
+ this.host.registry.load(flowSections(id, state), { keepParked: true });
772
+ const flow = new Flow(id, this.host, this.defaultSeed);
773
+ try {
774
+ flow.restore(state);
775
+ } catch (e) {
776
+ flow.close();
777
+ this.host.registry.discardParked(parked);
778
+ throw e;
779
+ }
780
+ this.flowsById.set(id, flow);
781
+ return flow;
782
+ }
736
783
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
737
784
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
738
785
  closeFlow(id) {
@@ -1437,7 +1484,8 @@ var Flow = class {
1437
1484
  if (!node) continue;
1438
1485
  byId.set(o.id, node);
1439
1486
  const { prompt: saved, ...rest } = o;
1440
- options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node) } } : {} });
1487
+ const label = saved?.kind === "line" && saved.speakerLabel === void 0 ? speakerLabelOf(this.host, saved.character, saved.qualifier) : void 0;
1488
+ options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node), ...label !== void 0 ? { speakerLabel: label } : {} } } : {} });
1441
1489
  }
1442
1490
  if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
1443
1491
  }
@@ -1446,7 +1494,9 @@ var Flow = class {
1446
1494
  if (this.pendingPromptOwnerId) {
1447
1495
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
1448
1496
  const beat = owner ? this.authoredPromptOf(owner) : void 0;
1449
- if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt, padAfter: c.pendingPrompt.padAfter ?? this.padOf(beat.id) } } : {} };
1497
+ const saved = c.pendingPrompt;
1498
+ const label = saved?.kind === "line" && saved.speakerLabel === void 0 ? speakerLabelOf(this.host, saved.character, saved.qualifier) : void 0;
1499
+ if (beat) this.pendingPrompt = { beat, ...saved ? { shown: { ...saved, padAfter: saved.padAfter ?? this.padOf(beat.id), ...label !== void 0 ? { speakerLabel: label } : {} } } : {} };
1450
1500
  else this.pendingPromptOwnerId = null;
1451
1501
  }
1452
1502
  }
@@ -1832,6 +1882,7 @@ var Flow = class {
1832
1882
  direction: silent ? void 0 : beat.direction,
1833
1883
  qualifier: silent ? void 0 : beat.qualifier,
1834
1884
  qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
1885
+ speakerLabel: silent ? void 0 : speakerLabelOf(this.host, beat.character, beat.qualifier),
1835
1886
  padAfter: this.padOf(beat.id),
1836
1887
  // a silent line still fires, so it still carries its pause
1837
1888
  gameData: beat.gameData,
@@ -1881,6 +1932,7 @@ var Flow = class {
1881
1932
  direction: beat.direction,
1882
1933
  qualifier: beat.qualifier,
1883
1934
  qualifierName: this.resolveQualifierName(beat.qualifier),
1935
+ speakerLabel: speakerLabelOf(this.host, beat.character, beat.qualifier),
1884
1936
  padAfter
1885
1937
  } : { kind: "text", text, padAfter };
1886
1938
  }
@@ -1903,6 +1955,7 @@ var Flow = class {
1903
1955
  direction: shown.direction,
1904
1956
  qualifier: shown.qualifier,
1905
1957
  qualifierName: shown.qualifierName,
1958
+ speakerLabel: shown.speakerLabel,
1906
1959
  padAfter: this.padOf(beat.id),
1907
1960
  gameData: beat.gameData,
1908
1961
  ...withTags
@@ -2048,6 +2101,16 @@ function collectCast(nodes, out) {
2048
2101
  for (const beat of n.beats ?? []) if (beat.kind === "line" && beat.character) out.add(beat.character);
2049
2102
  });
2050
2103
  }
2104
+ function speakerLabelOf(host, character, qualifier, source = false) {
2105
+ if (!character || host.emitIds) return void 0;
2106
+ const lookup = (key) => (source ? void 0 : host.strings[key]) ?? host.defaultStrings[key];
2107
+ const name = lookup(castStringKey(character)) ?? host.castDisplay.get(character) ?? character;
2108
+ if (qualifier === void 0) return name;
2109
+ const own = host.speakerLabels.get(`${character}\0${qualifier}`);
2110
+ if (own !== void 0) return lookup(speakerLabelKey(character, qualifier)) ?? own;
2111
+ const shown = lookup(qualifierStringKey(qualifier)) ?? host.qualifierDisplay.get(qualifier) ?? qualifier;
2112
+ return formatSpeakerLabel(lookup(SPEAKER_PATTERN_KEY) ?? host.speakerPattern, name, shown);
2113
+ }
2051
2114
  function gameDataField(gameData) {
2052
2115
  return gameData && Object.keys(gameData).length ? { gameData } : {};
2053
2116
  }
@@ -2058,6 +2121,7 @@ function savedPrompt(p) {
2058
2121
  if (p.direction !== void 0) out.direction = p.direction;
2059
2122
  if (p.qualifier !== void 0) out.qualifier = p.qualifier;
2060
2123
  if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
2124
+ if (p.speakerLabel !== void 0) out.speakerLabel = p.speakerLabel;
2061
2125
  if (typeof p.padAfter === "number") out.padAfter = p.padAfter;
2062
2126
  return out;
2063
2127
  }
@@ -2136,9 +2200,23 @@ function sectionsFromV2(save) {
2136
2200
  const out = {};
2137
2201
  if (save.shared?.patter) out["patter"] = save.shared.patter;
2138
2202
  for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
2139
- for (const [id, f] of Object.entries(save.flows ?? {})) {
2140
- if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
2141
- for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
2203
+ for (const [id, f] of Object.entries(save.flows ?? {})) Object.assign(out, flowSections(id, f));
2204
+ return out;
2205
+ }
2206
+ function flowSections(id, f) {
2207
+ const out = {};
2208
+ if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
2209
+ for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
2210
+ return out;
2211
+ }
2212
+ function flowValuesFrom(id, saved) {
2213
+ const out = { scopes: {}, sceneBags: {} };
2214
+ const prefix = keys.flow(id);
2215
+ for (const [key, values] of Object.entries(saved)) {
2216
+ if (!key.startsWith(prefix)) continue;
2217
+ const rest = key.slice(prefix.length);
2218
+ if (rest === "patter") out.scopes.patter = values;
2219
+ else if (rest.startsWith("scene/")) out.sceneBags[unesc(rest.slice("scene/".length))] = values;
2142
2220
  }
2143
2221
  return out;
2144
2222
  }