@patterkit/model 0.10.0 → 0.12.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
@@ -26,10 +26,14 @@ __export(index_exports, {
26
26
  DEFAULT_CAPTION_DELIMITERS: () => DEFAULT_CAPTION_DELIMITERS,
27
27
  DEFAULT_DOCUMENTATION_CLASSES: () => DEFAULT_DOCUMENTATION_CLASSES,
28
28
  DEFAULT_LAYOUT: () => DEFAULT_LAYOUT,
29
+ DEFAULT_PAD_AFTER: () => DEFAULT_PAD_AFTER,
30
+ DEFAULT_QUALIFIERS: () => DEFAULT_QUALIFIERS,
29
31
  DEFAULT_RECORDING_STATUSES: () => DEFAULT_RECORDING_STATUSES,
30
32
  DEFAULT_WRITING_STATUSES: () => DEFAULT_WRITING_STATUSES,
31
33
  FLOW_SCHEMA: () => FLOW_SCHEMA,
32
34
  HANDOFF_SCHEMA: () => HANDOFF_SCHEMA,
35
+ PAD_AFTER_MAX: () => PAD_AFTER_MAX,
36
+ PAD_AFTER_MIN: () => PAD_AFTER_MIN,
33
37
  PROJECT_LOCALE_SCENE: () => PROJECT_LOCALE_SCENE,
34
38
  PROJECT_SCHEMA: () => PROJECT_SCHEMA,
35
39
  RERECORD_STATUS: () => RERECORD_STATUS,
@@ -47,7 +51,10 @@ __export(index_exports, {
47
51
  isValidGameId: () => isValidGameId,
48
52
  isValidPropertyName: () => import_expr.isValidPropertyName,
49
53
  projectLayout: () => projectLayout,
54
+ projectQualifiers: () => projectQualifiers,
50
55
  propertyNameify: () => import_expr.propertyNameify,
56
+ qualifierStringKey: () => qualifierStringKey,
57
+ usedQualifiers: () => usedQualifiers,
51
58
  walkNodes: () => walkNodes
52
59
  });
53
60
  module.exports = __toCommonJS(index_exports);
@@ -77,6 +84,32 @@ function effectiveGameId(entity) {
77
84
  return g ? g : gameIdify(entity.name);
78
85
  }
79
86
  var COMMON_GENDERS = ["male", "female", "neuter"];
87
+ var DEFAULT_QUALIFIERS = [
88
+ { gameId: "vo", name: "V.O.", description: "Voice-over: the character isn't in the scene (a thought, narration, a voice in the ear)." },
89
+ { gameId: "os", name: "O.S.", description: "Off-screen: the character is there, but not seen (the next room, behind a door)." },
90
+ { gameId: "radio", name: "RADIO", description: "Heard through a radio, a phone, or a loudspeaker." }
91
+ ];
92
+ function projectQualifiers(project) {
93
+ return project.qualifiers ?? DEFAULT_QUALIFIERS;
94
+ }
95
+ function usedQualifiers(project, scenes) {
96
+ const used = /* @__PURE__ */ new Set();
97
+ for (const scene of scenes) {
98
+ for (const block of scene.blocks) {
99
+ walkNodes(block.children, (node) => {
100
+ if (node.type === "group") {
101
+ if (node.prompt?.kind === "line" && node.prompt.qualifier) used.add(node.prompt.qualifier);
102
+ return;
103
+ }
104
+ for (const beat of node.beats ?? []) if (beat.kind === "line" && beat.qualifier) used.add(beat.qualifier);
105
+ });
106
+ }
107
+ }
108
+ return projectQualifiers(project).filter((q) => used.has(q.gameId));
109
+ }
110
+ var DEFAULT_PAD_AFTER = 0.6;
111
+ var PAD_AFTER_MIN = -10;
112
+ var PAD_AFTER_MAX = 60;
80
113
  var DEFAULT_WRITING_STATUSES = [
81
114
  { name: "stub", colour: 0 },
82
115
  // red
@@ -122,6 +155,9 @@ var PROJECT_LOCALE_SCENE = "@project";
122
155
  function castStringKey(name) {
123
156
  return `cast:${name}`;
124
157
  }
158
+ function qualifierStringKey(gameId) {
159
+ return `qualifier:${gameId}`;
160
+ }
125
161
  var HANDOFF_SCHEMA = "patter/handoff@0";
126
162
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
127
163
  var DEFAULT_CAPTION_CHARACTER = "SFX";
@@ -143,10 +179,14 @@ var AUTHORING_SCHEMA = "patter/authoring@0";
143
179
  DEFAULT_CAPTION_DELIMITERS,
144
180
  DEFAULT_DOCUMENTATION_CLASSES,
145
181
  DEFAULT_LAYOUT,
182
+ DEFAULT_PAD_AFTER,
183
+ DEFAULT_QUALIFIERS,
146
184
  DEFAULT_RECORDING_STATUSES,
147
185
  DEFAULT_WRITING_STATUSES,
148
186
  FLOW_SCHEMA,
149
187
  HANDOFF_SCHEMA,
188
+ PAD_AFTER_MAX,
189
+ PAD_AFTER_MIN,
150
190
  PROJECT_LOCALE_SCENE,
151
191
  PROJECT_SCHEMA,
152
192
  RERECORD_STATUS,
@@ -164,6 +204,9 @@ var AUTHORING_SCHEMA = "patter/authoring@0";
164
204
  isValidGameId,
165
205
  isValidPropertyName,
166
206
  projectLayout,
207
+ projectQualifiers,
167
208
  propertyNameify,
209
+ qualifierStringKey,
210
+ usedQualifiers,
168
211
  walkNodes
169
212
  });
package/dist/index.d.cts CHANGED
@@ -23,6 +23,15 @@ interface LineBeat {
23
23
  character?: string;
24
24
  /** Performance direction, language-neutral (never localised). */
25
25
  direction?: string;
26
+ /** Speaker qualifier: the `gameId` of one of the project's qualifiers (`vo`, `os`, `radio`), which says
27
+ * how the line is delivered (`TAM (O.S.)`), not who says it. Absent = none. The speaker is still
28
+ * `character` for every authoring and reporting purpose. See `SpeakerQualifier`. */
29
+ qualifier?: string;
30
+ /** The pause after this beat, before the next, in seconds (`padAfter`, design/proposals/line-padding.md):
31
+ * positive waits, zero follows at once, negative starts the next line that long before this one ends (a
32
+ * cut-in). Absent = the nearest `padAfterDefault` above it, else the project's, else `DEFAULT_PAD_AFTER`.
33
+ * Never negative on a snippet's last line or text beat: the runtime clamps it to zero there. */
34
+ padAfter?: number;
26
35
  gameData?: GameData;
27
36
  /** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
28
37
  * At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
@@ -38,6 +47,11 @@ interface LineBeat {
38
47
  interface TextBeat {
39
48
  id: string;
40
49
  kind: "text";
50
+ /** The pause after this beat, before the next, in seconds (`padAfter`, design/proposals/line-padding.md):
51
+ * positive waits, zero follows at once, negative starts the next line that long before this one ends (a
52
+ * cut-in). Absent = the nearest `padAfterDefault` above it, else the project's, else `DEFAULT_PAD_AFTER`.
53
+ * Never negative on a snippet's last line or text beat: the runtime clamps it to zero there. */
54
+ padAfter?: number;
41
55
  gameData?: GameData;
42
56
  /** Author tags (#215). See LineBeat.tags. */
43
57
  tags?: string[];
@@ -85,6 +99,9 @@ interface Snippet {
85
99
  beats?: Beat[];
86
100
  onEnter?: Effect[];
87
101
  onExit?: Effect[];
102
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
103
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
104
+ padAfterDefault?: number;
88
105
  gameData?: GameData;
89
106
  /** Author tags (#215). See LineBeat.tags. */
90
107
  tags?: string[];
@@ -119,6 +136,9 @@ interface Group {
119
136
  /** `sequence` config (order × exhaust, spec §4). */
120
137
  options?: SequenceOptions;
121
138
  children: Array<Group | Snippet>;
139
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
140
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
141
+ padAfterDefault?: number;
122
142
  gameData?: GameData;
123
143
  /** Author tags (#215). See LineBeat.tags. */
124
144
  tags?: string[];
@@ -149,6 +169,9 @@ interface Block {
149
169
  */
150
170
  gameId?: string;
151
171
  children: Array<Group | Snippet>;
172
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
173
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
174
+ padAfterDefault?: number;
152
175
  gameData?: GameData;
153
176
  /** Author tags (#215). See LineBeat.tags. */
154
177
  tags?: string[];
@@ -165,6 +188,9 @@ interface Scene {
165
188
  gameId?: string;
166
189
  /** Host metadata - e.g. `location` lives here, not as a core field. */
167
190
  gameData?: GameData;
191
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
192
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
193
+ padAfterDefault?: number;
168
194
  /** Author tags (#215). See LineBeat.tags. */
169
195
  tags?: string[];
170
196
  /** Entry behaviour / setup (spec §15). */
@@ -328,6 +354,35 @@ interface CastMember {
328
354
  actor?: string;
329
355
  gameData?: GameData;
330
356
  }
357
+ /** A speaker qualifier (a screenplay's character extension): `V.O.`, `O.S.`, `RADIO`, or the project's own.
358
+ * A line stores the `gameId`; the `name` is what the cue, Play, and the scripts show, and is localised as a
359
+ * cast display name is (`qualifierStringKey`). */
360
+ interface SpeakerQualifier {
361
+ /** What code switches on, and what a line stores. Unique in the project; hyphen-slug form (`gameIdify`). */
362
+ gameId: string;
363
+ /** What the script shows (`V.O.`). Free to change: lines hold the `gameId`. */
364
+ name: string;
365
+ /** What the qualifier means, for writers. Authoring only. */
366
+ description?: string;
367
+ }
368
+ /** The qualifiers a project starts with. Their `gameId`s are pinned (`gameIdify` would make `V.O.` into
369
+ * `v-o`); a qualifier a writer adds takes its `gameId` from `gameIdify(name)`. */
370
+ declare const DEFAULT_QUALIFIERS: SpeakerQualifier[];
371
+ /** The project's qualifiers: its own list, else the defaults. */
372
+ declare function projectQualifiers(project: Pick<ProjectFile, "qualifiers">): SpeakerQualifier[];
373
+ /** The project's qualifiers that some line (or line prompt) in these scenes uses, in the project's order. */
374
+ declare function usedQualifiers(project: Pick<ProjectFile, "qualifiers">, scenes: Scene[]): SpeakerQualifier[];
375
+ /** The built-in `padAfter`, in seconds, for a project that sets no default of its own: the same on every runtime
376
+ * and in Patterpad's Play window (design/proposals/line-padding.md section 4). */
377
+ declare const DEFAULT_PAD_AFTER = 0.6;
378
+ /** The range validation keeps a `padAfter` or `padAfterDefault` within, in seconds. */
379
+ declare const PAD_AFTER_MIN = -10;
380
+ declare const PAD_AFTER_MAX = 60;
381
+ /** A qualifier as it reaches a compiled bundle: its `gameId` and authored name (the unlocalised fallback). */
382
+ interface BundleQualifier {
383
+ gameId: string;
384
+ name: string;
385
+ }
331
386
  /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
332
387
  * `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
333
388
  * game never carries a real person's name or a writer's private notes. The compiler copies the shipping
@@ -565,6 +620,12 @@ interface ProjectFile {
565
620
  * externally-gated branches get exercised. Authoring-only (never reaches the runtime bundle). */
566
621
  coverageDrivers?: CoverageDriver[];
567
622
  cast?: CastMember[];
623
+ /** The speaker qualifiers a line can pick from, in display order; default `DEFAULT_QUALIFIERS`. An empty
624
+ * list means the project uses none. */
625
+ qualifiers?: SpeakerQualifier[];
626
+ /** The project's default `padAfter` for every line and text beat that sets none and has no default above it.
627
+ * Absent = `DEFAULT_PAD_AFTER`. */
628
+ padAfterDefault?: number;
568
629
  gameDataFields?: GameDataFields;
569
630
  /** Ordered writing-status ladder (not-done -> done); default `DEFAULT_WRITING_STATUSES`. */
570
631
  writingStatuses?: WritingStatusDecl[];
@@ -605,12 +666,16 @@ interface LocaleFile {
605
666
  strings: Record<string, string>;
606
667
  }
607
668
  /** The `scene` marker for a project-level loc shard (`loc/<locale>/_project.patterloc`): strings that
608
- * aren't tied to a scene beat - currently cast display names, later project title / UI strings. */
669
+ * aren't tied to a scene beat - currently cast display names and speaker qualifier names, later project
670
+ * title / UI strings. */
609
671
  declare const PROJECT_LOCALE_SCENE = "@project";
610
672
  /** The project-level loc-string key for a cast member's player-facing name, e.g. `cast:BARKEEP`.
611
673
  * Namespaced so it can't collide with opaque beat ids. The default-locale value is seeded from the
612
674
  * CastMember's `displayName`; the runtime resolves a character's shown name through this key. */
613
675
  declare function castStringKey(name: string): string;
676
+ /** The project-level loc-string key for a speaker qualifier's shown name, e.g. `qualifier:vo`. Resolved as a
677
+ * cast display name is: the active locale, then the default locale, then the authored `name`. */
678
+ declare function qualifierStringKey(gameId: string): string;
614
679
  /** Typed documentation annotation line (spec §18). */
615
680
  interface DocLine {
616
681
  /** The documentation CLASS (a `DocumentationClass.name`) - routes export
@@ -690,6 +755,10 @@ interface Suggestion {
690
755
  /** A proposed new direction (the parenthetical), and the direction when suggested ("" for none). */
691
756
  proposedDirection?: string;
692
757
  baselineDirection?: string;
758
+ /** A proposed new speaker qualifier (a `gameId` from the project's list, "" for none), and the qualifier
759
+ * when suggested ("" for none). */
760
+ proposedQualifier?: string;
761
+ baselineQualifier?: string;
693
762
  /** A proposal to cut the beat (an editor emptied its line). Accept marks it cut. */
694
763
  proposedCut?: boolean;
695
764
  /** Where the suggestion came from, when it arrived in an editable-script handoff: the handoff and the
@@ -795,8 +864,13 @@ interface HandoffLine {
795
864
  /** The beat id (a line, a narration beat, or an option prompt). */
796
865
  id: string;
797
866
  kind: "line" | "narration" | "option";
798
- /** The speaker and direction as sent (spoken lines only). */
867
+ /** The speaker, speaker qualifier (`gameId`), and direction as sent (spoken lines only). */
799
868
  character?: string;
869
+ qualifier?: string;
870
+ /** The qualifier's name as the cue printed it (`O.S.` in `TAM (O.S.)`), so a returned cue is read against
871
+ * what the editor saw even after the name changes in settings. Absent in handoffs written before it was
872
+ * recorded, which compare against the qualifier's current name. */
873
+ qualifierName?: string;
800
874
  direction?: string;
801
875
  /** The text as sent: the baseline for every comparison on reimport. */
802
876
  baseline: string;
@@ -841,6 +915,7 @@ interface CompiledSnippet {
841
915
  beats?: Beat[];
842
916
  onEnter?: CompiledEffect[];
843
917
  onExit?: CompiledEffect[];
918
+ padAfterDefault?: number;
844
919
  gameData?: GameData;
845
920
  tags?: string[];
846
921
  jump?: Jump;
@@ -860,6 +935,7 @@ interface CompiledGroup {
860
935
  shared?: boolean;
861
936
  options?: SequenceOptions;
862
937
  children: Array<CompiledGroup | CompiledSnippet>;
938
+ padAfterDefault?: number;
863
939
  gameData?: GameData;
864
940
  tags?: string[];
865
941
  /** Option-position fields (spec §5) - only when a direct child of a `choice`. */
@@ -877,6 +953,7 @@ interface CompiledBlock {
877
953
  /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
878
954
  gameId?: string;
879
955
  children: Array<CompiledGroup | CompiledSnippet>;
956
+ padAfterDefault?: number;
880
957
  gameData?: GameData;
881
958
  tags?: string[];
882
959
  }
@@ -886,6 +963,7 @@ interface CompiledScene {
886
963
  name: string;
887
964
  /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
888
965
  gameId?: string;
966
+ padAfterDefault?: number;
889
967
  gameData?: GameData;
890
968
  tags?: string[];
891
969
  onEntry?: CompiledEffect[];
@@ -910,6 +988,11 @@ interface Bundle {
910
988
  };
911
989
  /** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
912
990
  cast?: BundleCastMember[];
991
+ /** The speaker qualifiers the content uses, in the project's order. Absent when no line has one, so a
992
+ * bundle without qualifiers is exactly as before. */
993
+ qualifiers?: BundleQualifier[];
994
+ /** The project's own `padAfterDefault`, when it sets one; absent = `DEFAULT_PAD_AFTER`. */
995
+ padAfterDefault?: number;
913
996
  properties?: PropertyDecl[];
914
997
  /** Host / world scope declarations, baked from the project so the runtime can self-back a declared
915
998
  * scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
@@ -991,6 +1074,10 @@ interface SavedChoicePrompt {
991
1074
  character?: string;
992
1075
  characterName?: string;
993
1076
  direction?: string;
1077
+ qualifier?: string;
1078
+ qualifierName?: string;
1079
+ /** The prompt's resolved pause (line padding). Absent in a save written before it existed. */
1080
+ padAfter?: number;
994
1081
  }
995
1082
  /** One option of a pending choice, saved VERBATIM: re-deriving on load would re-evaluate conditions. */
996
1083
  interface SavedChoiceOption {
@@ -1092,4 +1179,4 @@ declare const PROJECT_SCHEMA = "patter/project@0";
1092
1179
  declare const STRINGS_SCHEMA = "patter/strings@0";
1093
1180
  declare const AUTHORING_SCHEMA = "patter/authoring@0";
1094
1181
 
1095
- export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, 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_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 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, 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 StackFrame, type Suggestion, type SuggestionSource, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, gameIdify, isContentlessBeat, isValidGameId, projectLayout, walkNodes };
1182
+ 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 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 };
package/dist/index.d.ts CHANGED
@@ -23,6 +23,15 @@ interface LineBeat {
23
23
  character?: string;
24
24
  /** Performance direction, language-neutral (never localised). */
25
25
  direction?: string;
26
+ /** Speaker qualifier: the `gameId` of one of the project's qualifiers (`vo`, `os`, `radio`), which says
27
+ * how the line is delivered (`TAM (O.S.)`), not who says it. Absent = none. The speaker is still
28
+ * `character` for every authoring and reporting purpose. See `SpeakerQualifier`. */
29
+ qualifier?: string;
30
+ /** The pause after this beat, before the next, in seconds (`padAfter`, design/proposals/line-padding.md):
31
+ * positive waits, zero follows at once, negative starts the next line that long before this one ends (a
32
+ * cut-in). Absent = the nearest `padAfterDefault` above it, else the project's, else `DEFAULT_PAD_AFTER`.
33
+ * Never negative on a snippet's last line or text beat: the runtime clamps it to zero there. */
34
+ padAfter?: number;
26
35
  gameData?: GameData;
27
36
  /** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
28
37
  * At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
@@ -38,6 +47,11 @@ interface LineBeat {
38
47
  interface TextBeat {
39
48
  id: string;
40
49
  kind: "text";
50
+ /** The pause after this beat, before the next, in seconds (`padAfter`, design/proposals/line-padding.md):
51
+ * positive waits, zero follows at once, negative starts the next line that long before this one ends (a
52
+ * cut-in). Absent = the nearest `padAfterDefault` above it, else the project's, else `DEFAULT_PAD_AFTER`.
53
+ * Never negative on a snippet's last line or text beat: the runtime clamps it to zero there. */
54
+ padAfter?: number;
41
55
  gameData?: GameData;
42
56
  /** Author tags (#215). See LineBeat.tags. */
43
57
  tags?: string[];
@@ -85,6 +99,9 @@ interface Snippet {
85
99
  beats?: Beat[];
86
100
  onEnter?: Effect[];
87
101
  onExit?: Effect[];
102
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
103
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
104
+ padAfterDefault?: number;
88
105
  gameData?: GameData;
89
106
  /** Author tags (#215). See LineBeat.tags. */
90
107
  tags?: string[];
@@ -119,6 +136,9 @@ interface Group {
119
136
  /** `sequence` config (order × exhaust, spec §4). */
120
137
  options?: SequenceOptions;
121
138
  children: Array<Group | Snippet>;
139
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
140
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
141
+ padAfterDefault?: number;
122
142
  gameData?: GameData;
123
143
  /** Author tags (#215). See LineBeat.tags. */
124
144
  tags?: string[];
@@ -149,6 +169,9 @@ interface Block {
149
169
  */
150
170
  gameId?: string;
151
171
  children: Array<Group | Snippet>;
172
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
173
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
174
+ padAfterDefault?: number;
152
175
  gameData?: GameData;
153
176
  /** Author tags (#215). See LineBeat.tags. */
154
177
  tags?: string[];
@@ -165,6 +188,9 @@ interface Scene {
165
188
  gameId?: string;
166
189
  /** Host metadata - e.g. `location` lives here, not as a core field. */
167
190
  gameData?: GameData;
191
+ /** The default `padAfter` for the line and text beats inside it that set none (the nearest default wins:
192
+ * snippet, then each group, innermost first, then block, then scene, then the project). */
193
+ padAfterDefault?: number;
168
194
  /** Author tags (#215). See LineBeat.tags. */
169
195
  tags?: string[];
170
196
  /** Entry behaviour / setup (spec §15). */
@@ -328,6 +354,35 @@ interface CastMember {
328
354
  actor?: string;
329
355
  gameData?: GameData;
330
356
  }
357
+ /** A speaker qualifier (a screenplay's character extension): `V.O.`, `O.S.`, `RADIO`, or the project's own.
358
+ * A line stores the `gameId`; the `name` is what the cue, Play, and the scripts show, and is localised as a
359
+ * cast display name is (`qualifierStringKey`). */
360
+ interface SpeakerQualifier {
361
+ /** What code switches on, and what a line stores. Unique in the project; hyphen-slug form (`gameIdify`). */
362
+ gameId: string;
363
+ /** What the script shows (`V.O.`). Free to change: lines hold the `gameId`. */
364
+ name: string;
365
+ /** What the qualifier means, for writers. Authoring only. */
366
+ description?: string;
367
+ }
368
+ /** The qualifiers a project starts with. Their `gameId`s are pinned (`gameIdify` would make `V.O.` into
369
+ * `v-o`); a qualifier a writer adds takes its `gameId` from `gameIdify(name)`. */
370
+ declare const DEFAULT_QUALIFIERS: SpeakerQualifier[];
371
+ /** The project's qualifiers: its own list, else the defaults. */
372
+ declare function projectQualifiers(project: Pick<ProjectFile, "qualifiers">): SpeakerQualifier[];
373
+ /** The project's qualifiers that some line (or line prompt) in these scenes uses, in the project's order. */
374
+ declare function usedQualifiers(project: Pick<ProjectFile, "qualifiers">, scenes: Scene[]): SpeakerQualifier[];
375
+ /** The built-in `padAfter`, in seconds, for a project that sets no default of its own: the same on every runtime
376
+ * and in Patterpad's Play window (design/proposals/line-padding.md section 4). */
377
+ declare const DEFAULT_PAD_AFTER = 0.6;
378
+ /** The range validation keeps a `padAfter` or `padAfterDefault` within, in seconds. */
379
+ declare const PAD_AFTER_MIN = -10;
380
+ declare const PAD_AFTER_MAX = 60;
381
+ /** A qualifier as it reaches a compiled bundle: its `gameId` and authored name (the unlocalised fallback). */
382
+ interface BundleQualifier {
383
+ gameId: string;
384
+ name: string;
385
+ }
331
386
  /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
332
387
  * `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
333
388
  * game never carries a real person's name or a writer's private notes. The compiler copies the shipping
@@ -565,6 +620,12 @@ interface ProjectFile {
565
620
  * externally-gated branches get exercised. Authoring-only (never reaches the runtime bundle). */
566
621
  coverageDrivers?: CoverageDriver[];
567
622
  cast?: CastMember[];
623
+ /** The speaker qualifiers a line can pick from, in display order; default `DEFAULT_QUALIFIERS`. An empty
624
+ * list means the project uses none. */
625
+ qualifiers?: SpeakerQualifier[];
626
+ /** The project's default `padAfter` for every line and text beat that sets none and has no default above it.
627
+ * Absent = `DEFAULT_PAD_AFTER`. */
628
+ padAfterDefault?: number;
568
629
  gameDataFields?: GameDataFields;
569
630
  /** Ordered writing-status ladder (not-done -> done); default `DEFAULT_WRITING_STATUSES`. */
570
631
  writingStatuses?: WritingStatusDecl[];
@@ -605,12 +666,16 @@ interface LocaleFile {
605
666
  strings: Record<string, string>;
606
667
  }
607
668
  /** The `scene` marker for a project-level loc shard (`loc/<locale>/_project.patterloc`): strings that
608
- * aren't tied to a scene beat - currently cast display names, later project title / UI strings. */
669
+ * aren't tied to a scene beat - currently cast display names and speaker qualifier names, later project
670
+ * title / UI strings. */
609
671
  declare const PROJECT_LOCALE_SCENE = "@project";
610
672
  /** The project-level loc-string key for a cast member's player-facing name, e.g. `cast:BARKEEP`.
611
673
  * Namespaced so it can't collide with opaque beat ids. The default-locale value is seeded from the
612
674
  * CastMember's `displayName`; the runtime resolves a character's shown name through this key. */
613
675
  declare function castStringKey(name: string): string;
676
+ /** The project-level loc-string key for a speaker qualifier's shown name, e.g. `qualifier:vo`. Resolved as a
677
+ * cast display name is: the active locale, then the default locale, then the authored `name`. */
678
+ declare function qualifierStringKey(gameId: string): string;
614
679
  /** Typed documentation annotation line (spec §18). */
615
680
  interface DocLine {
616
681
  /** The documentation CLASS (a `DocumentationClass.name`) - routes export
@@ -690,6 +755,10 @@ interface Suggestion {
690
755
  /** A proposed new direction (the parenthetical), and the direction when suggested ("" for none). */
691
756
  proposedDirection?: string;
692
757
  baselineDirection?: string;
758
+ /** A proposed new speaker qualifier (a `gameId` from the project's list, "" for none), and the qualifier
759
+ * when suggested ("" for none). */
760
+ proposedQualifier?: string;
761
+ baselineQualifier?: string;
693
762
  /** A proposal to cut the beat (an editor emptied its line). Accept marks it cut. */
694
763
  proposedCut?: boolean;
695
764
  /** Where the suggestion came from, when it arrived in an editable-script handoff: the handoff and the
@@ -795,8 +864,13 @@ interface HandoffLine {
795
864
  /** The beat id (a line, a narration beat, or an option prompt). */
796
865
  id: string;
797
866
  kind: "line" | "narration" | "option";
798
- /** The speaker and direction as sent (spoken lines only). */
867
+ /** The speaker, speaker qualifier (`gameId`), and direction as sent (spoken lines only). */
799
868
  character?: string;
869
+ qualifier?: string;
870
+ /** The qualifier's name as the cue printed it (`O.S.` in `TAM (O.S.)`), so a returned cue is read against
871
+ * what the editor saw even after the name changes in settings. Absent in handoffs written before it was
872
+ * recorded, which compare against the qualifier's current name. */
873
+ qualifierName?: string;
800
874
  direction?: string;
801
875
  /** The text as sent: the baseline for every comparison on reimport. */
802
876
  baseline: string;
@@ -841,6 +915,7 @@ interface CompiledSnippet {
841
915
  beats?: Beat[];
842
916
  onEnter?: CompiledEffect[];
843
917
  onExit?: CompiledEffect[];
918
+ padAfterDefault?: number;
844
919
  gameData?: GameData;
845
920
  tags?: string[];
846
921
  jump?: Jump;
@@ -860,6 +935,7 @@ interface CompiledGroup {
860
935
  shared?: boolean;
861
936
  options?: SequenceOptions;
862
937
  children: Array<CompiledGroup | CompiledSnippet>;
938
+ padAfterDefault?: number;
863
939
  gameData?: GameData;
864
940
  tags?: string[];
865
941
  /** Option-position fields (spec §5) - only when a direct child of a `choice`. */
@@ -877,6 +953,7 @@ interface CompiledBlock {
877
953
  /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
878
954
  gameId?: string;
879
955
  children: Array<CompiledGroup | CompiledSnippet>;
956
+ padAfterDefault?: number;
880
957
  gameData?: GameData;
881
958
  tags?: string[];
882
959
  }
@@ -886,6 +963,7 @@ interface CompiledScene {
886
963
  name: string;
887
964
  /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
888
965
  gameId?: string;
966
+ padAfterDefault?: number;
889
967
  gameData?: GameData;
890
968
  tags?: string[];
891
969
  onEntry?: CompiledEffect[];
@@ -910,6 +988,11 @@ interface Bundle {
910
988
  };
911
989
  /** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
912
990
  cast?: BundleCastMember[];
991
+ /** The speaker qualifiers the content uses, in the project's order. Absent when no line has one, so a
992
+ * bundle without qualifiers is exactly as before. */
993
+ qualifiers?: BundleQualifier[];
994
+ /** The project's own `padAfterDefault`, when it sets one; absent = `DEFAULT_PAD_AFTER`. */
995
+ padAfterDefault?: number;
913
996
  properties?: PropertyDecl[];
914
997
  /** Host / world scope declarations, baked from the project so the runtime can self-back a declared
915
998
  * scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
@@ -991,6 +1074,10 @@ interface SavedChoicePrompt {
991
1074
  character?: string;
992
1075
  characterName?: string;
993
1076
  direction?: string;
1077
+ qualifier?: string;
1078
+ qualifierName?: string;
1079
+ /** The prompt's resolved pause (line padding). Absent in a save written before it existed. */
1080
+ padAfter?: number;
994
1081
  }
995
1082
  /** One option of a pending choice, saved VERBATIM: re-deriving on load would re-evaluate conditions. */
996
1083
  interface SavedChoiceOption {
@@ -1092,4 +1179,4 @@ declare const PROJECT_SCHEMA = "patter/project@0";
1092
1179
  declare const STRINGS_SCHEMA = "patter/strings@0";
1093
1180
  declare const AUTHORING_SCHEMA = "patter/authoring@0";
1094
1181
 
1095
- export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, 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_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 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, 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 StackFrame, type Suggestion, type SuggestionSource, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, gameIdify, isContentlessBeat, isValidGameId, projectLayout, walkNodes };
1182
+ 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 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 };
package/dist/index.js CHANGED
@@ -30,6 +30,32 @@ function effectiveGameId(entity) {
30
30
  return g ? g : gameIdify(entity.name);
31
31
  }
32
32
  var COMMON_GENDERS = ["male", "female", "neuter"];
33
+ var DEFAULT_QUALIFIERS = [
34
+ { gameId: "vo", name: "V.O.", description: "Voice-over: the character isn't in the scene (a thought, narration, a voice in the ear)." },
35
+ { gameId: "os", name: "O.S.", description: "Off-screen: the character is there, but not seen (the next room, behind a door)." },
36
+ { gameId: "radio", name: "RADIO", description: "Heard through a radio, a phone, or a loudspeaker." }
37
+ ];
38
+ function projectQualifiers(project) {
39
+ return project.qualifiers ?? DEFAULT_QUALIFIERS;
40
+ }
41
+ function usedQualifiers(project, scenes) {
42
+ const used = /* @__PURE__ */ new Set();
43
+ for (const scene of scenes) {
44
+ for (const block of scene.blocks) {
45
+ walkNodes(block.children, (node) => {
46
+ if (node.type === "group") {
47
+ if (node.prompt?.kind === "line" && node.prompt.qualifier) used.add(node.prompt.qualifier);
48
+ return;
49
+ }
50
+ for (const beat of node.beats ?? []) if (beat.kind === "line" && beat.qualifier) used.add(beat.qualifier);
51
+ });
52
+ }
53
+ }
54
+ return projectQualifiers(project).filter((q) => used.has(q.gameId));
55
+ }
56
+ var DEFAULT_PAD_AFTER = 0.6;
57
+ var PAD_AFTER_MIN = -10;
58
+ var PAD_AFTER_MAX = 60;
33
59
  var DEFAULT_WRITING_STATUSES = [
34
60
  { name: "stub", colour: 0 },
35
61
  // red
@@ -75,6 +101,9 @@ var PROJECT_LOCALE_SCENE = "@project";
75
101
  function castStringKey(name) {
76
102
  return `cast:${name}`;
77
103
  }
104
+ function qualifierStringKey(gameId) {
105
+ return `qualifier:${gameId}`;
106
+ }
78
107
  var HANDOFF_SCHEMA = "patter/handoff@0";
79
108
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
80
109
  var DEFAULT_CAPTION_CHARACTER = "SFX";
@@ -95,10 +124,14 @@ export {
95
124
  DEFAULT_CAPTION_DELIMITERS,
96
125
  DEFAULT_DOCUMENTATION_CLASSES,
97
126
  DEFAULT_LAYOUT,
127
+ DEFAULT_PAD_AFTER,
128
+ DEFAULT_QUALIFIERS,
98
129
  DEFAULT_RECORDING_STATUSES,
99
130
  DEFAULT_WRITING_STATUSES,
100
131
  FLOW_SCHEMA,
101
132
  HANDOFF_SCHEMA,
133
+ PAD_AFTER_MAX,
134
+ PAD_AFTER_MIN,
102
135
  PROJECT_LOCALE_SCENE,
103
136
  PROJECT_SCHEMA,
104
137
  RERECORD_STATUS,
@@ -116,6 +149,9 @@ export {
116
149
  isValidGameId,
117
150
  isValidPropertyName,
118
151
  projectLayout,
152
+ projectQualifiers,
119
153
  propertyNameify,
154
+ qualifierStringKey,
155
+ usedQualifiers,
120
156
  walkNodes
121
157
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/model",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",