@patterkit/runtime 0.24.0 → 0.26.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
@@ -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;
@@ -899,6 +909,15 @@ declare class Flow {
899
909
  private emit;
900
910
  /** How many times this flow has entered each node, by node id. */
901
911
  getVisitCounts(): Record<string, number>;
912
+ /**
913
+ * The step this flow would deliver for beat `beatId` right now, as `advance` would give it: its text in the
914
+ * active locale, interpolated against this flow's values (unless the project is voiced), closed captions
915
+ * applied, and the speaker's names and label, its pause, Game Data, and tags. Built by the same function that
916
+ * builds a delivered step, so the two can't differ. It doesn't move the flow, count a visit, or run an effect.
917
+ * Undefined for an id that is neither a beat nor an option's prompt. For a game that keeps lines by id and
918
+ * shows them again (after a load, or a language switch).
919
+ */
920
+ stepFor(beatId: string): StepResult | undefined;
902
921
  /** The options of a pending choice (empty when not at a choice point). */
903
922
  getChoices(): ChoiceOption[];
904
923
  /** Pick an eligible option by id; the next `advance()` runs it. */
package/dist/index.d.ts CHANGED
@@ -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;
@@ -899,6 +909,15 @@ declare class Flow {
899
909
  private emit;
900
910
  /** How many times this flow has entered each node, by node id. */
901
911
  getVisitCounts(): Record<string, number>;
912
+ /**
913
+ * The step this flow would deliver for beat `beatId` right now, as `advance` would give it: its text in the
914
+ * active locale, interpolated against this flow's values (unless the project is voiced), closed captions
915
+ * applied, and the speaker's names and label, its pause, Game Data, and tags. Built by the same function that
916
+ * builds a delivered step, so the two can't differ. It doesn't move the flow, count a visit, or run an effect.
917
+ * Undefined for an id that is neither a beat nor an option's prompt. For a game that keeps lines by id and
918
+ * shows them again (after a load, or a language switch).
919
+ */
920
+ stepFor(beatId: string): StepResult | undefined;
902
921
  /** The options of a pending choice (empty when not at a choice point). */
903
922
  getChoices(): ChoiceOption[];
904
923
  /** Pick an eligible option by id; the next `advance()` runs it. */
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) {
@@ -100,6 +100,25 @@ function padIndexFor(bundle) {
100
100
  }
101
101
  return index;
102
102
  }
103
+ var beatIndexCache = /* @__PURE__ */ new WeakMap();
104
+ function beatIndexFor(bundle) {
105
+ let index = beatIndexCache.get(bundle);
106
+ if (index) return index;
107
+ index = /* @__PURE__ */ new Map();
108
+ for (const scene of Object.values(bundle.scenes)) {
109
+ for (const block of scene.blocks) {
110
+ walkNodes(block.children, (n) => {
111
+ if (n.type === "group") {
112
+ if (n.prompt) index.set(n.prompt.id, n.prompt);
113
+ return;
114
+ }
115
+ for (const beat of n.beats ?? []) index.set(beat.id, beat);
116
+ });
117
+ }
118
+ }
119
+ beatIndexCache.set(bundle, index);
120
+ return index;
121
+ }
103
122
  function tagsOf(host, id) {
104
123
  return host.tagIndex.get(id)?.slice();
105
124
  }
@@ -168,6 +187,9 @@ var Engine = class _Engine {
168
187
  for (const c of bundle.cast ?? []) if (c.displayName) castDisplay.set(c.name, c.displayName);
169
188
  const qualifierDisplay = /* @__PURE__ */ new Map();
170
189
  for (const q of bundle.qualifiers ?? []) if (q?.gameId) qualifierDisplay.set(q.gameId, q.name);
190
+ const speakerLabels = /* @__PURE__ */ new Map();
191
+ 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);
192
+ const speakerPattern = bundle.speakerPattern ?? DEFAULT_SPEAKER_PATTERN;
171
193
  this.defaultSeed = (options.seed ?? 2654435769) >>> 0;
172
194
  const nodeIndex = /* @__PURE__ */ new Map();
173
195
  const blockIndex = /* @__PURE__ */ new Map();
@@ -229,6 +251,8 @@ var Engine = class _Engine {
229
251
  defaultStrings,
230
252
  castDisplay,
231
253
  qualifierDisplay,
254
+ speakerLabels,
255
+ speakerPattern,
232
256
  nodeIndex,
233
257
  blockIndex,
234
258
  blockById,
@@ -639,6 +663,8 @@ var Engine = class _Engine {
639
663
  const name = this.host.defaultStrings[qualifierStringKey(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
640
664
  if (name !== void 0) info.qualifierName = name;
641
665
  }
666
+ const label = speakerLabelOf(this.host, beat.character, beat.qualifier, true);
667
+ if (label !== void 0) info.speakerLabel = label;
642
668
  }
643
669
  if (beat.kind === "line" || beat.kind === "text") {
644
670
  const pad = this.host.padIndex.get(beat.id);
@@ -1345,6 +1371,18 @@ var Flow = class {
1345
1371
  getVisitCounts() {
1346
1372
  return Object.fromEntries(this.visitCounts);
1347
1373
  }
1374
+ /**
1375
+ * The step this flow would deliver for beat `beatId` right now, as `advance` would give it: its text in the
1376
+ * active locale, interpolated against this flow's values (unless the project is voiced), closed captions
1377
+ * applied, and the speaker's names and label, its pause, Game Data, and tags. Built by the same function that
1378
+ * builds a delivered step, so the two can't differ. It doesn't move the flow, count a visit, or run an effect.
1379
+ * Undefined for an id that is neither a beat nor an option's prompt. For a game that keeps lines by id and
1380
+ * shows them again (after a load, or a language switch).
1381
+ */
1382
+ stepFor(beatId) {
1383
+ const beat = beatIndexFor(this.host.bundle).get(beatId);
1384
+ return beat ? this.beatResult(beat) : void 0;
1385
+ }
1348
1386
  /** The options of a pending choice (empty when not at a choice point). */
1349
1387
  getChoices() {
1350
1388
  return this.pendingChoice?.options ?? [];
@@ -1477,7 +1515,8 @@ var Flow = class {
1477
1515
  if (!node) continue;
1478
1516
  byId.set(o.id, node);
1479
1517
  const { prompt: saved, ...rest } = o;
1480
- options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node) } } : {} });
1518
+ const label = saved?.kind === "line" && saved.speakerLabel === void 0 ? speakerLabelOf(this.host, saved.character, saved.qualifier) : void 0;
1519
+ options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node), ...label !== void 0 ? { speakerLabel: label } : {} } } : {} });
1481
1520
  }
1482
1521
  if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
1483
1522
  }
@@ -1486,7 +1525,9 @@ var Flow = class {
1486
1525
  if (this.pendingPromptOwnerId) {
1487
1526
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
1488
1527
  const beat = owner ? this.authoredPromptOf(owner) : void 0;
1489
- if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt, padAfter: c.pendingPrompt.padAfter ?? this.padOf(beat.id) } } : {} };
1528
+ const saved = c.pendingPrompt;
1529
+ const label = saved?.kind === "line" && saved.speakerLabel === void 0 ? speakerLabelOf(this.host, saved.character, saved.qualifier) : void 0;
1530
+ if (beat) this.pendingPrompt = { beat, ...saved ? { shown: { ...saved, padAfter: saved.padAfter ?? this.padOf(beat.id), ...label !== void 0 ? { speakerLabel: label } : {} } } : {} };
1490
1531
  else this.pendingPromptOwnerId = null;
1491
1532
  }
1492
1533
  }
@@ -1872,6 +1913,7 @@ var Flow = class {
1872
1913
  direction: silent ? void 0 : beat.direction,
1873
1914
  qualifier: silent ? void 0 : beat.qualifier,
1874
1915
  qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
1916
+ speakerLabel: silent ? void 0 : speakerLabelOf(this.host, beat.character, beat.qualifier),
1875
1917
  padAfter: this.padOf(beat.id),
1876
1918
  // a silent line still fires, so it still carries its pause
1877
1919
  gameData: beat.gameData,
@@ -1921,6 +1963,7 @@ var Flow = class {
1921
1963
  direction: beat.direction,
1922
1964
  qualifier: beat.qualifier,
1923
1965
  qualifierName: this.resolveQualifierName(beat.qualifier),
1966
+ speakerLabel: speakerLabelOf(this.host, beat.character, beat.qualifier),
1924
1967
  padAfter
1925
1968
  } : { kind: "text", text, padAfter };
1926
1969
  }
@@ -1943,6 +1986,7 @@ var Flow = class {
1943
1986
  direction: shown.direction,
1944
1987
  qualifier: shown.qualifier,
1945
1988
  qualifierName: shown.qualifierName,
1989
+ speakerLabel: shown.speakerLabel,
1946
1990
  padAfter: this.padOf(beat.id),
1947
1991
  gameData: beat.gameData,
1948
1992
  ...withTags
@@ -2088,6 +2132,16 @@ function collectCast(nodes, out) {
2088
2132
  for (const beat of n.beats ?? []) if (beat.kind === "line" && beat.character) out.add(beat.character);
2089
2133
  });
2090
2134
  }
2135
+ function speakerLabelOf(host, character, qualifier, source = false) {
2136
+ if (!character || host.emitIds) return void 0;
2137
+ const lookup = (key) => (source ? void 0 : host.strings[key]) ?? host.defaultStrings[key];
2138
+ const name = lookup(castStringKey(character)) ?? host.castDisplay.get(character) ?? character;
2139
+ if (qualifier === void 0) return name;
2140
+ const own = host.speakerLabels.get(`${character}\0${qualifier}`);
2141
+ if (own !== void 0) return lookup(speakerLabelKey(character, qualifier)) ?? own;
2142
+ const shown = lookup(qualifierStringKey(qualifier)) ?? host.qualifierDisplay.get(qualifier) ?? qualifier;
2143
+ return formatSpeakerLabel(lookup(SPEAKER_PATTERN_KEY) ?? host.speakerPattern, name, shown);
2144
+ }
2091
2145
  function gameDataField(gameData) {
2092
2146
  return gameData && Object.keys(gameData).length ? { gameData } : {};
2093
2147
  }
@@ -2098,6 +2152,7 @@ function savedPrompt(p) {
2098
2152
  if (p.direction !== void 0) out.direction = p.direction;
2099
2153
  if (p.qualifier !== void 0) out.qualifier = p.qualifier;
2100
2154
  if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
2155
+ if (p.speakerLabel !== void 0) out.speakerLabel = p.speakerLabel;
2101
2156
  if (typeof p.padAfter === "number") out.padAfter = p.padAfter;
2102
2157
  return out;
2103
2158
  }