@patterkit/model 0.13.1 → 0.15.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.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  DEFAULT_PAD_AFTER: () => DEFAULT_PAD_AFTER,
30
30
  DEFAULT_QUALIFIERS: () => DEFAULT_QUALIFIERS,
31
31
  DEFAULT_RECORDING_STATUSES: () => DEFAULT_RECORDING_STATUSES,
32
+ DEFAULT_SPEAKER_PATTERN: () => DEFAULT_SPEAKER_PATTERN,
32
33
  DEFAULT_WRITING_STATUSES: () => DEFAULT_WRITING_STATUSES,
33
34
  FLOW_SCHEMA: () => FLOW_SCHEMA,
34
35
  HANDOFF_SCHEMA: () => HANDOFF_SCHEMA,
@@ -41,10 +42,12 @@ __export(index_exports, {
41
42
  RESERVED_PROPERTY_NAMES: () => import_expr.RESERVED_PROPERTY_NAMES,
42
43
  SAVE_SCHEMA: () => SAVE_SCHEMA,
43
44
  SAVE_VERSION: () => SAVE_VERSION,
45
+ SPEAKER_PATTERN_KEY: () => SPEAKER_PATTERN_KEY,
44
46
  STRINGS_SCHEMA: () => STRINGS_SCHEMA,
45
47
  castStringKey: () => castStringKey,
46
48
  deriveRecordingFolders: () => deriveRecordingFolders,
47
49
  effectiveGameId: () => effectiveGameId,
50
+ formatSpeakerLabel: () => formatSpeakerLabel,
48
51
  gameIdify: () => gameIdify,
49
52
  isCaseOnlyPropertyName: () => import_expr.isCaseOnlyPropertyName,
50
53
  isContentlessBeat: () => isContentlessBeat,
@@ -54,7 +57,9 @@ __export(index_exports, {
54
57
  projectQualifiers: () => projectQualifiers,
55
58
  propertyNameify: () => import_expr.propertyNameify,
56
59
  qualifierStringKey: () => qualifierStringKey,
60
+ speakerLabelKey: () => speakerLabelKey,
57
61
  usedQualifiers: () => usedQualifiers,
62
+ usedSpeakerPairs: () => usedSpeakerPairs,
58
63
  walkNodes: () => walkNodes
59
64
  });
60
65
  module.exports = __toCommonJS(index_exports);
@@ -107,6 +112,29 @@ function usedQualifiers(project, scenes) {
107
112
  }
108
113
  return projectQualifiers(project).filter((q) => used.has(q.gameId));
109
114
  }
115
+ function usedSpeakerPairs(scenes) {
116
+ const seen = /* @__PURE__ */ new Set();
117
+ const out = [];
118
+ const add = (beat) => {
119
+ if (!beat.character || !beat.qualifier) return;
120
+ const key = `${beat.character}\0${beat.qualifier}`;
121
+ if (seen.has(key)) return;
122
+ seen.add(key);
123
+ out.push({ character: beat.character, qualifier: beat.qualifier });
124
+ };
125
+ for (const scene of scenes) {
126
+ for (const block of scene.blocks) {
127
+ walkNodes(block.children, (node) => {
128
+ if (node.type === "group") {
129
+ if (node.prompt?.kind === "line") add(node.prompt);
130
+ return;
131
+ }
132
+ for (const beat of node.beats ?? []) if (beat.kind === "line") add(beat);
133
+ });
134
+ }
135
+ }
136
+ return out;
137
+ }
110
138
  var DEFAULT_PAD_AFTER = 0.6;
111
139
  var PAD_AFTER_MIN = -10;
112
140
  var PAD_AFTER_MAX = 60;
@@ -158,6 +186,14 @@ function castStringKey(name) {
158
186
  function qualifierStringKey(gameId) {
159
187
  return `qualifier:${gameId}`;
160
188
  }
189
+ function speakerLabelKey(name, gameId) {
190
+ return `speaker:${name}:${gameId}`;
191
+ }
192
+ var SPEAKER_PATTERN_KEY = "speaker:pattern";
193
+ var DEFAULT_SPEAKER_PATTERN = "{name} ({qualifier})";
194
+ function formatSpeakerLabel(pattern, name, qualifier) {
195
+ return pattern.split("{name}").map((part) => part.split("{qualifier}").join(qualifier)).join(name);
196
+ }
161
197
  var HANDOFF_SCHEMA = "patter/handoff@0";
162
198
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
163
199
  var DEFAULT_CAPTION_CHARACTER = "SFX";
@@ -182,6 +218,7 @@ var AUTHORING_SCHEMA = "patter/authoring@0";
182
218
  DEFAULT_PAD_AFTER,
183
219
  DEFAULT_QUALIFIERS,
184
220
  DEFAULT_RECORDING_STATUSES,
221
+ DEFAULT_SPEAKER_PATTERN,
185
222
  DEFAULT_WRITING_STATUSES,
186
223
  FLOW_SCHEMA,
187
224
  HANDOFF_SCHEMA,
@@ -194,10 +231,12 @@ var AUTHORING_SCHEMA = "patter/authoring@0";
194
231
  RESERVED_PROPERTY_NAMES,
195
232
  SAVE_SCHEMA,
196
233
  SAVE_VERSION,
234
+ SPEAKER_PATTERN_KEY,
197
235
  STRINGS_SCHEMA,
198
236
  castStringKey,
199
237
  deriveRecordingFolders,
200
238
  effectiveGameId,
239
+ formatSpeakerLabel,
201
240
  gameIdify,
202
241
  isCaseOnlyPropertyName,
203
242
  isContentlessBeat,
@@ -207,6 +246,8 @@ var AUTHORING_SCHEMA = "patter/authoring@0";
207
246
  projectQualifiers,
208
247
  propertyNameify,
209
248
  qualifierStringKey,
249
+ speakerLabelKey,
210
250
  usedQualifiers,
251
+ usedSpeakerPairs,
211
252
  walkNodes
212
253
  });
package/dist/index.d.cts CHANGED
@@ -352,6 +352,10 @@ interface CastMember {
352
352
  /** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
353
353
  * Authoring-only (not shipped in the runtime bundle). */
354
354
  actor?: string;
355
+ /** This character's own label with a qualifier, by the qualifier's `gameId`, in the source language
356
+ * (`{ vo: "Your thoughts" }`), where the project's speaker pattern shouldn't apply. Localised as the
357
+ * `speaker:<name>:<gameId>` string (`speakerLabelKey`). Absent for every pair that follows the pattern. */
358
+ qualifierLabels?: Record<string, string>;
355
359
  gameData?: GameData;
356
360
  }
357
361
  /** A speaker qualifier (a screenplay's character extension): `V.O.`, `O.S.`, `RADIO`, or the project's own.
@@ -372,6 +376,12 @@ declare const DEFAULT_QUALIFIERS: SpeakerQualifier[];
372
376
  declare function projectQualifiers(project: Pick<ProjectFile, "qualifiers">): SpeakerQualifier[];
373
377
  /** The project's qualifiers that some line (or line prompt) in these scenes uses, in the project's order. */
374
378
  declare function usedQualifiers(project: Pick<ProjectFile, "qualifiers">, scenes: Scene[]): SpeakerQualifier[];
379
+ /** Every character and qualifier pair the lines use (a line's speaker with its qualifier, option prompts
380
+ * included), once each, in the order first met. A pair's label is shown only for these. */
381
+ declare function usedSpeakerPairs(scenes: Scene[]): {
382
+ character: string;
383
+ qualifier: string;
384
+ }[];
375
385
  /** The built-in `padAfter`, in seconds, for a project that sets no default of its own: the same on every runtime
376
386
  * and in Patterpad's Play window (design/proposals/line-padding.md section 4). */
377
387
  declare const DEFAULT_PAD_AFTER = 0.6;
@@ -392,19 +402,27 @@ type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
392
402
  /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
393
403
  * Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
394
404
  type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
395
- /** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
396
- * Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
397
- type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
405
+ /** The value type of a gameData field: the family's one field-type list, shared with Storylets' card and
406
+ * outcome fields (patterkit design/family-parity-2026-10.md item 9, patter-qualities.md section 1). Drives
407
+ * the inspector's editor widget (text / textarea / number / toggle / enum dropdown / flags checklist /
408
+ * quality ladder). A `quality` value is a stage of the field's own ladder (`stages`), compared by position by
409
+ * a host, never by the engine. */
410
+ type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum" | "flags" | "quality";
398
411
  /** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
399
412
  interface GameDataField {
400
413
  /** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
401
414
  name: string;
402
415
  type: GameDataFieldType;
403
416
  /** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
404
- * a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
417
+ * a reader falls back to this default (so changing it here propagates to every node that didn't set it).
418
+ * Optional in source: left out, the type's default holds (by the kernel's `defaultFor`), and the compiler
419
+ * writes it into the bundle, so a bundle's field always has one. An enum's may be `""`, none chosen. */
405
420
  default?: ScalarValue;
406
- /** Allowed values when `type` is "enum". */
421
+ /** Allowed values when `type` is "enum", or the flags a "flags" value may hold. */
407
422
  values?: string[];
423
+ /** A "quality" field's ordered ladder, by the family's one rule (at least two stages, none empty, none
424
+ * twice; `@wildwinter/expr`'s `qualityDeclarationProblems`). */
425
+ stages?: string[];
408
426
  /** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
409
427
  purpose?: string;
410
428
  }
@@ -623,6 +641,9 @@ interface ProjectFile {
623
641
  /** The speaker qualifiers a line can pick from, in display order; default `DEFAULT_QUALIFIERS`. An empty
624
642
  * list means the project uses none. */
625
643
  qualifiers?: SpeakerQualifier[];
644
+ /** How a character's label reads with a qualifier, in the source language: `{name}` and `{qualifier}` filled
645
+ * in. Absent = `DEFAULT_SPEAKER_PATTERN`. Localised as the `speaker:pattern` string. */
646
+ speakerPattern?: string;
626
647
  /** The project's default `padAfter` for every line and text beat that sets none and has no default above it.
627
648
  * Absent = `DEFAULT_PAD_AFTER`. */
628
649
  padAfterDefault?: number;
@@ -676,6 +697,19 @@ declare function castStringKey(name: string): string;
676
697
  /** The project-level loc-string key for a speaker qualifier's shown name, e.g. `qualifier:vo`. Resolved as a
677
698
  * cast display name is: the active locale, then the default locale, then the authored `name`. */
678
699
  declare function qualifierStringKey(gameId: string): string;
700
+ /** The project-level loc-string key for one character's own label with one qualifier, e.g. `speaker:PLAYER:vo`:
701
+ * only for a pair someone has overridden (`CastMember.qualifierLabels`). Every other pair's label is built from
702
+ * the speaker pattern. */
703
+ declare function speakerLabelKey(name: string, gameId: string): string;
704
+ /** The project-level loc-string key for the speaker pattern, the label a character shows with a qualifier. */
705
+ declare const SPEAKER_PATTERN_KEY = "speaker:pattern";
706
+ /** The speaker pattern a project starts with: `You (V.O.)`. `{name}` and `{qualifier}` are plain placeholders,
707
+ * never run through line interpolation. */
708
+ declare const DEFAULT_SPEAKER_PATTERN = "{name} ({qualifier})";
709
+ /** A qualified speaker's label: the pattern with its placeholders filled. Each placeholder is replaced
710
+ * wherever it appears; nothing else in the pattern is read. The one composition rule, on every runtime and
711
+ * in Patterpad. */
712
+ declare function formatSpeakerLabel(pattern: string, name: string, qualifier: string): string;
679
713
  /** Typed documentation annotation line (spec §18). */
680
714
  interface DocLine {
681
715
  /** The documentation CLASS (a `DocumentationClass.name`) - routes export
@@ -991,6 +1025,9 @@ interface Bundle {
991
1025
  /** The speaker qualifiers the content uses, in the project's order. Absent when no line has one, so a
992
1026
  * bundle without qualifiers is exactly as before. */
993
1027
  qualifiers?: BundleQualifier[];
1028
+ /** The project's speaker pattern, the unlocalised fallback for a qualified speaker's label. Absent when the
1029
+ * project keeps `DEFAULT_SPEAKER_PATTERN` or uses no qualifiers. */
1030
+ speakerPattern?: string;
994
1031
  /** The project's own `padAfterDefault`, when it sets one; absent = `DEFAULT_PAD_AFTER`. */
995
1032
  padAfterDefault?: number;
996
1033
  properties?: PropertyDecl[];
@@ -1076,6 +1113,8 @@ interface SavedChoicePrompt {
1076
1113
  direction?: string;
1077
1114
  qualifier?: string;
1078
1115
  qualifierName?: string;
1116
+ /** The speaker's label as the choice showed it. Absent in a save written before it existed. */
1117
+ speakerLabel?: string;
1079
1118
  /** The prompt's resolved pause (line padding). Absent in a save written before it existed. */
1080
1119
  padAfter?: number;
1081
1120
  }
@@ -1192,4 +1231,4 @@ declare const PROJECT_SCHEMA = "patter/project@0";
1192
1231
  declare const STRINGS_SCHEMA = "patter/strings@0";
1193
1232
  declare const AUTHORING_SCHEMA = "patter/authoring@0";
1194
1233
 
1195
- export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleQualifier, COMMON_GENDERS, type CaptionDelimiters, type CastMember, type Comment, type CommentMessage, type CommentRange, type CompiledBlock, type CompiledEffect, type CompiledGroup, type CompiledScene, type CompiledSnippet, type CoverageDriver, DEFAULT_CAPTION_CHARACTER, DEFAULT_CAPTION_DELIMITERS, DEFAULT_DOCUMENTATION_CLASSES, DEFAULT_LAYOUT, DEFAULT_PAD_AFTER, DEFAULT_QUALIFIERS, DEFAULT_RECORDING_STATUSES, DEFAULT_WRITING_STATUSES, type DocLine, type DocumentationClass, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, FLOW_SCHEMA, type FlowCursor, type FlowFile, type FlowNode, type FlowSnapshot, type FlowSnapshotV2, type FlowState, type FlowValues, type GameData, type GameDataField, type GameDataFieldType, type GameDataFields, type GameDataNodeKind, type GameEventBeat, type GrammaticalGender, type Group, HANDOFF_SCHEMA, type HandoffFile, type HandoffImport, type HandoffLine, type HandoffRow, type HostScopeDecl, type HostScopeRegistry, type HostScopeSpec, type Jump, type JumpTarget, type LineBeat, type LocaleFile, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectFile, type PromptBeat, type PropertyDecl, type PropertyType, RERECORD_STATUS, RERECORD_STATUS_DECL, type RecordingFolder, type RecordingStatusDecl, SAVE_SCHEMA, SAVE_VERSION, STRINGS_SCHEMA, type SaveEnvelope, type SaveGame, type SaveGameV2, type SavedChoice, type SavedChoiceOption, type SavedChoicePrompt, type ScalarValue, type Scene, type Selector, type SelectorExhaust, type SelectorOrder, type SelectorSnapshot, type SequenceOptions, type Snippet, type SpeakerQualifier, type StackFrame, type Suggestion, type SuggestionSource, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, gameIdify, isContentlessBeat, isValidGameId, projectLayout, projectQualifiers, qualifierStringKey, usedQualifiers, walkNodes };
1234
+ export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleQualifier, COMMON_GENDERS, type CaptionDelimiters, type CastMember, type Comment, type CommentMessage, type CommentRange, type CompiledBlock, type CompiledEffect, type CompiledGroup, type CompiledScene, type CompiledSnippet, type CoverageDriver, DEFAULT_CAPTION_CHARACTER, DEFAULT_CAPTION_DELIMITERS, DEFAULT_DOCUMENTATION_CLASSES, DEFAULT_LAYOUT, DEFAULT_PAD_AFTER, DEFAULT_QUALIFIERS, DEFAULT_RECORDING_STATUSES, DEFAULT_SPEAKER_PATTERN, DEFAULT_WRITING_STATUSES, type DocLine, type DocumentationClass, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, FLOW_SCHEMA, type FlowCursor, type FlowFile, type FlowNode, type FlowSnapshot, type FlowSnapshotV2, type FlowState, type FlowValues, type GameData, type GameDataField, type GameDataFieldType, type GameDataFields, type GameDataNodeKind, type GameEventBeat, type GrammaticalGender, type Group, HANDOFF_SCHEMA, type HandoffFile, type HandoffImport, type HandoffLine, type HandoffRow, type HostScopeDecl, type HostScopeRegistry, type HostScopeSpec, type Jump, type JumpTarget, type LineBeat, type LocaleFile, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectFile, type PromptBeat, type PropertyDecl, type PropertyType, RERECORD_STATUS, RERECORD_STATUS_DECL, type RecordingFolder, type RecordingStatusDecl, SAVE_SCHEMA, SAVE_VERSION, SPEAKER_PATTERN_KEY, STRINGS_SCHEMA, type SaveEnvelope, type SaveGame, type SaveGameV2, type SavedChoice, type SavedChoiceOption, type SavedChoicePrompt, type ScalarValue, type Scene, type Selector, type SelectorExhaust, type SelectorOrder, type SelectorSnapshot, type SequenceOptions, type Snippet, type SpeakerQualifier, type StackFrame, type Suggestion, type SuggestionSource, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, formatSpeakerLabel, gameIdify, isContentlessBeat, isValidGameId, projectLayout, projectQualifiers, qualifierStringKey, speakerLabelKey, usedQualifiers, usedSpeakerPairs, walkNodes };
package/dist/index.d.ts CHANGED
@@ -352,6 +352,10 @@ interface CastMember {
352
352
  /** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
353
353
  * Authoring-only (not shipped in the runtime bundle). */
354
354
  actor?: string;
355
+ /** This character's own label with a qualifier, by the qualifier's `gameId`, in the source language
356
+ * (`{ vo: "Your thoughts" }`), where the project's speaker pattern shouldn't apply. Localised as the
357
+ * `speaker:<name>:<gameId>` string (`speakerLabelKey`). Absent for every pair that follows the pattern. */
358
+ qualifierLabels?: Record<string, string>;
355
359
  gameData?: GameData;
356
360
  }
357
361
  /** A speaker qualifier (a screenplay's character extension): `V.O.`, `O.S.`, `RADIO`, or the project's own.
@@ -372,6 +376,12 @@ declare const DEFAULT_QUALIFIERS: SpeakerQualifier[];
372
376
  declare function projectQualifiers(project: Pick<ProjectFile, "qualifiers">): SpeakerQualifier[];
373
377
  /** The project's qualifiers that some line (or line prompt) in these scenes uses, in the project's order. */
374
378
  declare function usedQualifiers(project: Pick<ProjectFile, "qualifiers">, scenes: Scene[]): SpeakerQualifier[];
379
+ /** Every character and qualifier pair the lines use (a line's speaker with its qualifier, option prompts
380
+ * included), once each, in the order first met. A pair's label is shown only for these. */
381
+ declare function usedSpeakerPairs(scenes: Scene[]): {
382
+ character: string;
383
+ qualifier: string;
384
+ }[];
375
385
  /** The built-in `padAfter`, in seconds, for a project that sets no default of its own: the same on every runtime
376
386
  * and in Patterpad's Play window (design/proposals/line-padding.md section 4). */
377
387
  declare const DEFAULT_PAD_AFTER = 0.6;
@@ -392,19 +402,27 @@ type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
392
402
  /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
393
403
  * Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
394
404
  type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
395
- /** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
396
- * Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
397
- type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
405
+ /** The value type of a gameData field: the family's one field-type list, shared with Storylets' card and
406
+ * outcome fields (patterkit design/family-parity-2026-10.md item 9, patter-qualities.md section 1). Drives
407
+ * the inspector's editor widget (text / textarea / number / toggle / enum dropdown / flags checklist /
408
+ * quality ladder). A `quality` value is a stage of the field's own ladder (`stages`), compared by position by
409
+ * a host, never by the engine. */
410
+ type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum" | "flags" | "quality";
398
411
  /** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
399
412
  interface GameDataField {
400
413
  /** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
401
414
  name: string;
402
415
  type: GameDataFieldType;
403
416
  /** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
404
- * a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
417
+ * a reader falls back to this default (so changing it here propagates to every node that didn't set it).
418
+ * Optional in source: left out, the type's default holds (by the kernel's `defaultFor`), and the compiler
419
+ * writes it into the bundle, so a bundle's field always has one. An enum's may be `""`, none chosen. */
405
420
  default?: ScalarValue;
406
- /** Allowed values when `type` is "enum". */
421
+ /** Allowed values when `type` is "enum", or the flags a "flags" value may hold. */
407
422
  values?: string[];
423
+ /** A "quality" field's ordered ladder, by the family's one rule (at least two stages, none empty, none
424
+ * twice; `@wildwinter/expr`'s `qualityDeclarationProblems`). */
425
+ stages?: string[];
408
426
  /** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
409
427
  purpose?: string;
410
428
  }
@@ -623,6 +641,9 @@ interface ProjectFile {
623
641
  /** The speaker qualifiers a line can pick from, in display order; default `DEFAULT_QUALIFIERS`. An empty
624
642
  * list means the project uses none. */
625
643
  qualifiers?: SpeakerQualifier[];
644
+ /** How a character's label reads with a qualifier, in the source language: `{name}` and `{qualifier}` filled
645
+ * in. Absent = `DEFAULT_SPEAKER_PATTERN`. Localised as the `speaker:pattern` string. */
646
+ speakerPattern?: string;
626
647
  /** The project's default `padAfter` for every line and text beat that sets none and has no default above it.
627
648
  * Absent = `DEFAULT_PAD_AFTER`. */
628
649
  padAfterDefault?: number;
@@ -676,6 +697,19 @@ declare function castStringKey(name: string): string;
676
697
  /** The project-level loc-string key for a speaker qualifier's shown name, e.g. `qualifier:vo`. Resolved as a
677
698
  * cast display name is: the active locale, then the default locale, then the authored `name`. */
678
699
  declare function qualifierStringKey(gameId: string): string;
700
+ /** The project-level loc-string key for one character's own label with one qualifier, e.g. `speaker:PLAYER:vo`:
701
+ * only for a pair someone has overridden (`CastMember.qualifierLabels`). Every other pair's label is built from
702
+ * the speaker pattern. */
703
+ declare function speakerLabelKey(name: string, gameId: string): string;
704
+ /** The project-level loc-string key for the speaker pattern, the label a character shows with a qualifier. */
705
+ declare const SPEAKER_PATTERN_KEY = "speaker:pattern";
706
+ /** The speaker pattern a project starts with: `You (V.O.)`. `{name}` and `{qualifier}` are plain placeholders,
707
+ * never run through line interpolation. */
708
+ declare const DEFAULT_SPEAKER_PATTERN = "{name} ({qualifier})";
709
+ /** A qualified speaker's label: the pattern with its placeholders filled. Each placeholder is replaced
710
+ * wherever it appears; nothing else in the pattern is read. The one composition rule, on every runtime and
711
+ * in Patterpad. */
712
+ declare function formatSpeakerLabel(pattern: string, name: string, qualifier: string): string;
679
713
  /** Typed documentation annotation line (spec §18). */
680
714
  interface DocLine {
681
715
  /** The documentation CLASS (a `DocumentationClass.name`) - routes export
@@ -991,6 +1025,9 @@ interface Bundle {
991
1025
  /** The speaker qualifiers the content uses, in the project's order. Absent when no line has one, so a
992
1026
  * bundle without qualifiers is exactly as before. */
993
1027
  qualifiers?: BundleQualifier[];
1028
+ /** The project's speaker pattern, the unlocalised fallback for a qualified speaker's label. Absent when the
1029
+ * project keeps `DEFAULT_SPEAKER_PATTERN` or uses no qualifiers. */
1030
+ speakerPattern?: string;
994
1031
  /** The project's own `padAfterDefault`, when it sets one; absent = `DEFAULT_PAD_AFTER`. */
995
1032
  padAfterDefault?: number;
996
1033
  properties?: PropertyDecl[];
@@ -1076,6 +1113,8 @@ interface SavedChoicePrompt {
1076
1113
  direction?: string;
1077
1114
  qualifier?: string;
1078
1115
  qualifierName?: string;
1116
+ /** The speaker's label as the choice showed it. Absent in a save written before it existed. */
1117
+ speakerLabel?: string;
1079
1118
  /** The prompt's resolved pause (line padding). Absent in a save written before it existed. */
1080
1119
  padAfter?: number;
1081
1120
  }
@@ -1192,4 +1231,4 @@ declare const PROJECT_SCHEMA = "patter/project@0";
1192
1231
  declare const STRINGS_SCHEMA = "patter/strings@0";
1193
1232
  declare const AUTHORING_SCHEMA = "patter/authoring@0";
1194
1233
 
1195
- export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleQualifier, COMMON_GENDERS, type CaptionDelimiters, type CastMember, type Comment, type CommentMessage, type CommentRange, type CompiledBlock, type CompiledEffect, type CompiledGroup, type CompiledScene, type CompiledSnippet, type CoverageDriver, DEFAULT_CAPTION_CHARACTER, DEFAULT_CAPTION_DELIMITERS, DEFAULT_DOCUMENTATION_CLASSES, DEFAULT_LAYOUT, DEFAULT_PAD_AFTER, DEFAULT_QUALIFIERS, DEFAULT_RECORDING_STATUSES, DEFAULT_WRITING_STATUSES, type DocLine, type DocumentationClass, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, FLOW_SCHEMA, type FlowCursor, type FlowFile, type FlowNode, type FlowSnapshot, type FlowSnapshotV2, type FlowState, type FlowValues, type GameData, type GameDataField, type GameDataFieldType, type GameDataFields, type GameDataNodeKind, type GameEventBeat, type GrammaticalGender, type Group, HANDOFF_SCHEMA, type HandoffFile, type HandoffImport, type HandoffLine, type HandoffRow, type HostScopeDecl, type HostScopeRegistry, type HostScopeSpec, type Jump, type JumpTarget, type LineBeat, type LocaleFile, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectFile, type PromptBeat, type PropertyDecl, type PropertyType, RERECORD_STATUS, RERECORD_STATUS_DECL, type RecordingFolder, type RecordingStatusDecl, SAVE_SCHEMA, SAVE_VERSION, STRINGS_SCHEMA, type SaveEnvelope, type SaveGame, type SaveGameV2, type SavedChoice, type SavedChoiceOption, type SavedChoicePrompt, type ScalarValue, type Scene, type Selector, type SelectorExhaust, type SelectorOrder, type SelectorSnapshot, type SequenceOptions, type Snippet, type SpeakerQualifier, type StackFrame, type Suggestion, type SuggestionSource, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, gameIdify, isContentlessBeat, isValidGameId, projectLayout, projectQualifiers, qualifierStringKey, usedQualifiers, walkNodes };
1234
+ export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleQualifier, COMMON_GENDERS, type CaptionDelimiters, type CastMember, type Comment, type CommentMessage, type CommentRange, type CompiledBlock, type CompiledEffect, type CompiledGroup, type CompiledScene, type CompiledSnippet, type CoverageDriver, DEFAULT_CAPTION_CHARACTER, DEFAULT_CAPTION_DELIMITERS, DEFAULT_DOCUMENTATION_CLASSES, DEFAULT_LAYOUT, DEFAULT_PAD_AFTER, DEFAULT_QUALIFIERS, DEFAULT_RECORDING_STATUSES, DEFAULT_SPEAKER_PATTERN, DEFAULT_WRITING_STATUSES, type DocLine, type DocumentationClass, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, FLOW_SCHEMA, type FlowCursor, type FlowFile, type FlowNode, type FlowSnapshot, type FlowSnapshotV2, type FlowState, type FlowValues, type GameData, type GameDataField, type GameDataFieldType, type GameDataFields, type GameDataNodeKind, type GameEventBeat, type GrammaticalGender, type Group, HANDOFF_SCHEMA, type HandoffFile, type HandoffImport, type HandoffLine, type HandoffRow, type HostScopeDecl, type HostScopeRegistry, type HostScopeSpec, type Jump, type JumpTarget, type LineBeat, type LocaleFile, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectFile, type PromptBeat, type PropertyDecl, type PropertyType, RERECORD_STATUS, RERECORD_STATUS_DECL, type RecordingFolder, type RecordingStatusDecl, SAVE_SCHEMA, SAVE_VERSION, SPEAKER_PATTERN_KEY, STRINGS_SCHEMA, type SaveEnvelope, type SaveGame, type SaveGameV2, type SavedChoice, type SavedChoiceOption, type SavedChoicePrompt, type ScalarValue, type Scene, type Selector, type SelectorExhaust, type SelectorOrder, type SelectorSnapshot, type SequenceOptions, type Snippet, type SpeakerQualifier, type StackFrame, type Suggestion, type SuggestionSource, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, formatSpeakerLabel, gameIdify, isContentlessBeat, isValidGameId, projectLayout, projectQualifiers, qualifierStringKey, speakerLabelKey, usedQualifiers, usedSpeakerPairs, walkNodes };
package/dist/index.js CHANGED
@@ -53,6 +53,29 @@ function usedQualifiers(project, scenes) {
53
53
  }
54
54
  return projectQualifiers(project).filter((q) => used.has(q.gameId));
55
55
  }
56
+ function usedSpeakerPairs(scenes) {
57
+ const seen = /* @__PURE__ */ new Set();
58
+ const out = [];
59
+ const add = (beat) => {
60
+ if (!beat.character || !beat.qualifier) return;
61
+ const key = `${beat.character}\0${beat.qualifier}`;
62
+ if (seen.has(key)) return;
63
+ seen.add(key);
64
+ out.push({ character: beat.character, qualifier: beat.qualifier });
65
+ };
66
+ for (const scene of scenes) {
67
+ for (const block of scene.blocks) {
68
+ walkNodes(block.children, (node) => {
69
+ if (node.type === "group") {
70
+ if (node.prompt?.kind === "line") add(node.prompt);
71
+ return;
72
+ }
73
+ for (const beat of node.beats ?? []) if (beat.kind === "line") add(beat);
74
+ });
75
+ }
76
+ }
77
+ return out;
78
+ }
56
79
  var DEFAULT_PAD_AFTER = 0.6;
57
80
  var PAD_AFTER_MIN = -10;
58
81
  var PAD_AFTER_MAX = 60;
@@ -104,6 +127,14 @@ function castStringKey(name) {
104
127
  function qualifierStringKey(gameId) {
105
128
  return `qualifier:${gameId}`;
106
129
  }
130
+ function speakerLabelKey(name, gameId) {
131
+ return `speaker:${name}:${gameId}`;
132
+ }
133
+ var SPEAKER_PATTERN_KEY = "speaker:pattern";
134
+ var DEFAULT_SPEAKER_PATTERN = "{name} ({qualifier})";
135
+ function formatSpeakerLabel(pattern, name, qualifier) {
136
+ return pattern.split("{name}").map((part) => part.split("{qualifier}").join(qualifier)).join(name);
137
+ }
107
138
  var HANDOFF_SCHEMA = "patter/handoff@0";
108
139
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
109
140
  var DEFAULT_CAPTION_CHARACTER = "SFX";
@@ -127,6 +158,7 @@ export {
127
158
  DEFAULT_PAD_AFTER,
128
159
  DEFAULT_QUALIFIERS,
129
160
  DEFAULT_RECORDING_STATUSES,
161
+ DEFAULT_SPEAKER_PATTERN,
130
162
  DEFAULT_WRITING_STATUSES,
131
163
  FLOW_SCHEMA,
132
164
  HANDOFF_SCHEMA,
@@ -139,10 +171,12 @@ export {
139
171
  RESERVED_PROPERTY_NAMES,
140
172
  SAVE_SCHEMA,
141
173
  SAVE_VERSION,
174
+ SPEAKER_PATTERN_KEY,
142
175
  STRINGS_SCHEMA,
143
176
  castStringKey,
144
177
  deriveRecordingFolders,
145
178
  effectiveGameId,
179
+ formatSpeakerLabel,
146
180
  gameIdify,
147
181
  isCaseOnlyPropertyName,
148
182
  isContentlessBeat,
@@ -152,6 +186,8 @@ export {
152
186
  projectQualifiers,
153
187
  propertyNameify,
154
188
  qualifierStringKey,
189
+ speakerLabelKey,
155
190
  usedQualifiers,
191
+ usedSpeakerPairs,
156
192
  walkNodes
157
193
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/model",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Patter data-model types: source (scene/block/group/snippet/beat/jump), project, locale, authoring. The shape source-of-truth.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,6 @@
34
34
  "build": "tsup src/index.ts --format esm,cjs --dts --clean"
35
35
  },
36
36
  "dependencies": {
37
- "@wildwinter/expr": "^0.5.0"
37
+ "@wildwinter/expr": "^0.6.0"
38
38
  }
39
39
  }