@patterkit/model 0.20.0 → 0.21.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
@@ -54,11 +54,11 @@ __export(index_exports, {
54
54
  deriveRecordingFolders: () => deriveRecordingFolders,
55
55
  effectiveGameId: () => effectiveGameId,
56
56
  formatSpeakerLabel: () => formatSpeakerLabel,
57
- gameIdify: () => gameIdify,
57
+ gameIdify: () => import_toolkit.gameIdify,
58
58
  isCaseOnlyPropertyName: () => import_expr.isCaseOnlyPropertyName,
59
59
  isContentlessBeat: () => isContentlessBeat,
60
60
  isDialogueScene: () => isDialogueScene,
61
- isValidGameId: () => isValidGameId,
61
+ isValidGameId: () => import_toolkit.isValidGameId,
62
62
  isValidPropertyName: () => import_expr.isValidPropertyName,
63
63
  projectLayout: () => projectLayout,
64
64
  projectQualifiers: () => projectQualifiers,
@@ -73,6 +73,7 @@ __export(index_exports, {
73
73
  walkNodes: () => walkNodes
74
74
  });
75
75
  module.exports = __toCommonJS(index_exports);
76
+ var import_toolkit = require("@wildwinter/toolkit");
76
77
  var import_expr = require("@wildwinter/expr");
77
78
  function walkNodes(nodes, visit) {
78
79
  for (const node of nodes) {
@@ -88,15 +89,9 @@ function isContentlessBeat(beat, hasText) {
88
89
  if (beat.tags && beat.tags.length > 0) return false;
89
90
  return beat.kind === "text" || !beat.character && !beat.direction;
90
91
  }
91
- function gameIdify(text) {
92
- return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
93
- }
94
- function isValidGameId(gameId) {
95
- return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);
96
- }
97
92
  function effectiveGameId(entity) {
98
93
  const g = entity.gameId?.trim();
99
- return g ? g : gameIdify(entity.name);
94
+ return g ? g : (0, import_toolkit.gameIdify)(entity.name);
100
95
  }
101
96
  function sceneAddressIndex(scenes) {
102
97
  const byAddress = /* @__PURE__ */ new Map();
@@ -205,7 +200,7 @@ function deriveRecordingFolders(audioRoot, statuses) {
205
200
  const root = audioRoot?.trim();
206
201
  return statuses.map((s, i) => {
207
202
  if (i === 0 || !root) return { name: s.name };
208
- return { name: s.name, folder: `${root}/${gameIdify(s.name)}` };
203
+ return { name: s.name, folder: `${root}/${(0, import_toolkit.gameIdify)(s.name)}` };
209
204
  });
210
205
  }
211
206
  var DEFAULT_DOCUMENTATION_CLASSES = [
@@ -237,7 +232,7 @@ var HANDOFF_SCHEMA = "patter/handoff@0";
237
232
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
238
233
  var DEFAULT_CAPTION_CHARACTER = "SFX";
239
234
  var SAVE_SCHEMA = "patter/save@0";
240
- var SAVE_VERSION = 3;
235
+ var SAVE_VERSION = 4;
241
236
  var DEFAULT_LAYOUT = { flow: "scenes/", strings: "loc/", authoring: "authoring/", extensions: "extensions/" };
242
237
  function projectLayout(project) {
243
238
  return { ...DEFAULT_LAYOUT, ...project.layout };
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AstNode } from '@wildwinter/expr';
2
2
  export { RESERVED_PROPERTY_NAMES, isCaseOnlyPropertyName, isValidPropertyName, propertyNameify } from '@wildwinter/expr';
3
+ export { gameIdify, isValidGameId } from '@wildwinter/toolkit';
3
4
 
4
5
  type ScalarValue = boolean | number | string | string[];
5
6
  /** Developer-defined host metadata (spec §17). Opaque to Patter. */
@@ -246,11 +247,6 @@ declare function walkNodes<N extends {
246
247
  * just `{ jump }` with zero beats - which is valid.
247
248
  */
248
249
  declare function isContentlessBeat(beat: Beat, hasText: boolean): boolean;
249
- /** Slugify a name into a hyphen-form game id: lowercase, drop apostrophes, runs of
250
- * other punctuation -> a single hyphen, collapse repeats, no leading / trailing hyphen. */
251
- declare function gameIdify(text: string): string;
252
- /** A valid authored game id: lowercase alphanumerics + hyphens, no leading / trailing hyphen. */
253
- declare function isValidGameId(gameId: string): boolean;
254
250
 
255
251
  /** The effective address: the explicit `gameId` if pinned, else derived from `name`. */
256
252
  declare function effectiveGameId(entity: {
@@ -392,6 +388,11 @@ interface CastMember {
392
388
  * (`{ vo: "Your thoughts" }`), where the project's speaker pattern shouldn't apply. Localised as the
393
389
  * `speaker:<name>:<gameId>` string (`speakerLabelKey`). Absent for every pair that follows the pattern. */
394
390
  qualifierLabels?: Record<string, string>;
391
+ /** The character's colour in the editors (their cue, the play window, the readable script): a slot of the
392
+ * family's curated palette, 0 to 11. Absent, the colour comes from a hash of the name, so a cast needs no
393
+ * setup; renaming a character stamps the old name's slot here, so they keep their colour. Authoring only
394
+ * (not shipped in the runtime bundle). */
395
+ colour?: number;
395
396
  gameData?: GameData;
396
397
  }
397
398
  /** A speaker qualifier (a screenplay's character extension): `V.O.`, `O.S.`, `RADIO`, or the project's own.
@@ -435,12 +436,12 @@ interface BundleQualifier {
435
436
  gameId: string;
436
437
  name: string;
437
438
  }
438
- /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
439
- * `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
439
+ /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor`,
440
+ * `gender` and `colour` are authoring / production / translation context, and the compiler drops them, so a shipped
440
441
  * game never carries a real person's name or a writer's private notes. The compiler copies the shipping
441
442
  * fields across explicitly (an allow-list), which is what actually keeps a new authoring field out of
442
443
  * the bundle; this type states the resulting contract for anyone reading a bundle. */
443
- type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
444
+ type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender" | "colour">;
444
445
  /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
445
446
  * Beat kinds map straight through: dialogue = `line`, text = `text`, game event = `gameEvent`.
446
447
  * `option` is a choice's option: a group that is a direct child of a `choice` group, whose gameData the
@@ -1091,6 +1092,13 @@ interface Bundle {
1091
1092
  default: string;
1092
1093
  included: string[];
1093
1094
  };
1095
+ /** Where a flow opened without a scene starts: the project's authored start (scene id, and block id when
1096
+ * it names one). Absent when the project sets none, or names one that has gone: every runtime then starts
1097
+ * at the first scene. */
1098
+ start?: {
1099
+ scene: string;
1100
+ block?: string;
1101
+ };
1094
1102
  /** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
1095
1103
  cast?: BundleCastMember[];
1096
1104
  /** The speaker qualifiers the content uses, in the project's order. Absent when no line has one, so a
@@ -1154,7 +1162,7 @@ declare const DEFAULT_CAPTION_DELIMITERS: CaptionDelimiters;
1154
1162
  declare const DEFAULT_CAPTION_CHARACTER = "SFX";
1155
1163
  declare const SAVE_SCHEMA = "patter/save@0";
1156
1164
  /** The `save.version` inside the envelope. Bumped only when a reader would MISREAD an older save. */
1157
- declare const SAVE_VERSION = 3;
1165
+ declare const SAVE_VERSION = 4;
1158
1166
  /** The tagged envelope a host stores: `serializeState` writes it, `deserializeState` refuses anything else. */
1159
1167
  interface SaveEnvelope {
1160
1168
  schema: typeof SAVE_SCHEMA;
@@ -1181,8 +1189,11 @@ interface StackFrame {
1181
1189
  * restore against an EDITED bundle re-finds the position by id. Absent when the frame sat at its
1182
1190
  * container's end (no next child), or in an older save. */
1183
1191
  nextId?: string;
1192
+ /** SNAPSHOT-ONLY: on a frame whose container is an option of a choice, that choice group's id, so a load
1193
+ * whose option has gone offers the choice again. Absent on every other frame, and in a version 3 save. */
1194
+ choiceId?: string;
1184
1195
  }
1185
- /** The prompt of a saved choice option: the choice text as a structured line/text beat. */
1196
+ /** A prompt as the choice showed it: a replayed prompt still to be spoken (`FlowCursor.pendingPrompt`). */
1186
1197
  interface SavedChoicePrompt {
1187
1198
  kind: "line" | "text";
1188
1199
  text: string;
@@ -1196,10 +1207,12 @@ interface SavedChoicePrompt {
1196
1207
  /** The prompt's resolved pause (line padding). Absent in a save written before it existed. */
1197
1208
  padAfter?: number;
1198
1209
  }
1199
- /** One option of a pending choice, saved VERBATIM: re-deriving on load would re-evaluate conditions. */
1210
+ /** One option of a pending choice: which option, and whether it could be taken, saved VERBATIM, since
1211
+ * re-deriving on load would re-evaluate conditions. Its prompt is not saved: it is built again in the
1212
+ * language in use each time the choice is delivered. (A version 3 save's option also has a `prompt`,
1213
+ * which a load discards.) */
1200
1214
  interface SavedChoiceOption {
1201
1215
  id: string;
1202
- prompt?: SavedChoicePrompt;
1203
1216
  eligible: boolean;
1204
1217
  gameData?: GameData;
1205
1218
  }
@@ -1215,17 +1228,21 @@ interface FlowCursor {
1215
1228
  /** The continuation stack (call frames + the active block run), root first. */
1216
1229
  stack: StackFrame[];
1217
1230
  activeSnippetId: string | null;
1218
- beatIndex: number;
1231
+ /** The id of the next beat the active snippet delivers, so a load against an edited bundle finds its
1232
+ * place by id. Absent when no snippet is active or the active one has delivered every beat. */
1233
+ beatId?: string;
1234
+ /** Version 3 only, read and never written: the index of the next beat in the active snippet. */
1235
+ beatIndex?: number;
1219
1236
  /** Present (non-null) iff the flow is stopped at a choice. */
1220
1237
  pendingChoice: SavedChoice | null;
1221
1238
  /** The chosen option owning a prompt still to be replayed (a save taken between choose + advance).
1222
1239
  * Optional / null in older saves -> no pending prompt. */
1223
1240
  pendingPromptOwnerId?: string | null;
1224
- /** The prompt still to be replayed, exactly as the choice SHOWED it (text and speaker fields, the same
1225
- * shape as a saved option's `prompt`), so the replay after a load says what the player saw. Written only
1226
- * alongside a `pendingPromptOwnerId` (the choose -> advance window, with `replayPromptOnChoose`), and
1227
- * absent otherwise. A save without it (written before Patterplay 0.18.0) resolves the owner's prompt beat
1228
- * when it is delivered, as before. */
1241
+ /** The prompt still to be replayed, exactly as the choice SHOWED it (text and speaker fields), so the
1242
+ * replay after a load says what the player saw, in the language they saw it in: the one prompt a save
1243
+ * keeps as text. Written only alongside a `pendingPromptOwnerId` (the choose -> advance window, with
1244
+ * `replayPromptOnChoose`), and absent otherwise. A save without it (written before Patterplay 0.18.0)
1245
+ * resolves the owner's prompt beat when it is delivered, as before. */
1229
1246
  pendingPrompt?: SavedChoicePrompt;
1230
1247
  /** This flow's (non-shared) selector cursors, keyed by group id. */
1231
1248
  selectors: Record<string, SelectorSnapshot>;
@@ -1238,10 +1255,10 @@ interface FlowSnapshot {
1238
1255
  visits: Record<string, number>;
1239
1256
  cursor: FlowCursor;
1240
1257
  }
1241
- /** A full resumable save-game (version 3): everything that is not a property, plus the registry's
1258
+ /** A full resumable save-game (version 4): everything that is not a property, plus the registry's
1242
1259
  * values when the engine made its own registry. */
1243
1260
  interface SaveGame {
1244
- version: 3;
1261
+ version: typeof SAVE_VERSION;
1245
1262
  /** The engine's own registry's values, keyed by registry key: present only when the engine made the
1246
1263
  * registry itself (a standalone game). A game that passed a registry saves it once, beside this. */
1247
1264
  registry?: EngineSave;
@@ -1252,17 +1269,18 @@ interface SaveGame {
1252
1269
  /** Each live flow's snapshot, keyed by flow id. */
1253
1270
  flows: Record<string, FlowSnapshot>;
1254
1271
  }
1255
- /** One flow's own property values, without its id: the sections a version 2 save carried per flow, and a
1256
- * `FlowState` carries now. */
1272
+ /** A version 3 save, still read: the same shape apart from the cursor (`beatIndex` for `beatId`, no
1273
+ * `choiceId` on its frames) and a pending choice's options (each with its prompt text, discarded). */
1274
+ interface SaveGameV3 extends Omit<SaveGame, "version"> {
1275
+ version: 3;
1276
+ }
1277
+ /** One flow's own property values, without its id: what a `FlowState` carries beside its snapshot. */
1257
1278
  interface FlowValues {
1258
1279
  /** The NOT-shared `@patter` globals, under token "patter". */
1259
1280
  scopes: EngineSave;
1260
1281
  /** Per-scene NOT-shared `@scene` bags (scene id -> name -> value). */
1261
1282
  sceneBags: Record<string, Record<string, ScalarValue>>;
1262
1283
  }
1263
- /** A version 2 flow snapshot, which carried the flow's property values itself. Still read. */
1264
- interface FlowSnapshotV2 extends FlowSnapshot, FlowValues {
1265
- }
1266
1284
  /**
1267
1285
  * One flow taken out of an engine (`engine.captureFlow`) to be put back later (`engine.restoreFlow`), under the
1268
1286
  * same id or another: its snapshot as a save writes it, and its own property values without its id. Plain data
@@ -1270,19 +1288,8 @@ interface FlowSnapshotV2 extends FlowSnapshot, FlowValues {
1270
1288
  * visit counts, shared selectors) is never in it, so restoring one rewinds nothing else.
1271
1289
  */
1272
1290
  interface FlowState extends FlowSnapshot, FlowValues {
1273
- version: typeof SAVE_VERSION;
1274
- }
1275
- /** A version 2 save, from before the registry held the properties. Every runtime still reads it: its
1276
- * property sections move into the registry (under the keys in the header above) as it loads. */
1277
- interface SaveGameV2 {
1278
- version: 2;
1279
- /** Shared `@patter` globals (owned scope "patter"). */
1280
- shared: EngineSave;
1281
- sharedVisits: Record<string, number>;
1282
- sharedSelectors: Record<string, SelectorSnapshot>;
1283
- /** Shared, scene-namespaced `@scene` bags (scene id -> name -> value). */
1284
- stageBags: Record<string, Record<string, ScalarValue>>;
1285
- flows: Record<string, FlowSnapshotV2>;
1291
+ /** `SAVE_VERSION` when written; a version 3 state still restores. */
1292
+ version: typeof SAVE_VERSION | 3;
1286
1293
  }
1287
1294
  /** Where a project keeps its shards unless its `layout` says otherwise, relative to the project root. */
1288
1295
  declare const DEFAULT_LAYOUT: {
@@ -1394,4 +1401,4 @@ interface ExtensionFieldDecl {
1394
1401
  /** A node's extension values in a bundle: extension id -> field -> value, raw. */
1395
1402
  type NodeExtensions = Record<string, Record<string, unknown>>;
1396
1403
 
1397
- export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleExtension, 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, EXTENSIONS_DIR, EXTENSION_SCHEMA, EXTENSION_SHARD, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, type ExtensionFieldDecl, type ExtensionShard, 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, MANIFEST_EXTENSION, type NodeExtensions, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_EXTENSION_STEM, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectExtension, type ProjectFile, type ProjectLayout, 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, UNRESOLVED_KEY, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, formatSpeakerLabel, gameIdify, isContentlessBeat, isDialogueScene, isValidGameId, projectLayout, projectQualifiers, qualifierStringKey, resolveSceneRef, sceneAddressIndex, speakerLabelKey, textIsAction, usedQualifiers, usedSpeakerPairs, walkNodes };
1404
+ export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleExtension, 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, EXTENSIONS_DIR, EXTENSION_SCHEMA, EXTENSION_SHARD, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, type ExtensionFieldDecl, type ExtensionShard, FLOW_SCHEMA, type FlowCursor, type FlowFile, type FlowNode, type FlowSnapshot, 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, MANIFEST_EXTENSION, type NodeExtensions, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_EXTENSION_STEM, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectExtension, type ProjectFile, type ProjectLayout, 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 SaveGameV3, 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, UNRESOLVED_KEY, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, formatSpeakerLabel, isContentlessBeat, isDialogueScene, projectLayout, projectQualifiers, qualifierStringKey, resolveSceneRef, sceneAddressIndex, speakerLabelKey, textIsAction, usedQualifiers, usedSpeakerPairs, walkNodes };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AstNode } from '@wildwinter/expr';
2
2
  export { RESERVED_PROPERTY_NAMES, isCaseOnlyPropertyName, isValidPropertyName, propertyNameify } from '@wildwinter/expr';
3
+ export { gameIdify, isValidGameId } from '@wildwinter/toolkit';
3
4
 
4
5
  type ScalarValue = boolean | number | string | string[];
5
6
  /** Developer-defined host metadata (spec §17). Opaque to Patter. */
@@ -246,11 +247,6 @@ declare function walkNodes<N extends {
246
247
  * just `{ jump }` with zero beats - which is valid.
247
248
  */
248
249
  declare function isContentlessBeat(beat: Beat, hasText: boolean): boolean;
249
- /** Slugify a name into a hyphen-form game id: lowercase, drop apostrophes, runs of
250
- * other punctuation -> a single hyphen, collapse repeats, no leading / trailing hyphen. */
251
- declare function gameIdify(text: string): string;
252
- /** A valid authored game id: lowercase alphanumerics + hyphens, no leading / trailing hyphen. */
253
- declare function isValidGameId(gameId: string): boolean;
254
250
 
255
251
  /** The effective address: the explicit `gameId` if pinned, else derived from `name`. */
256
252
  declare function effectiveGameId(entity: {
@@ -392,6 +388,11 @@ interface CastMember {
392
388
  * (`{ vo: "Your thoughts" }`), where the project's speaker pattern shouldn't apply. Localised as the
393
389
  * `speaker:<name>:<gameId>` string (`speakerLabelKey`). Absent for every pair that follows the pattern. */
394
390
  qualifierLabels?: Record<string, string>;
391
+ /** The character's colour in the editors (their cue, the play window, the readable script): a slot of the
392
+ * family's curated palette, 0 to 11. Absent, the colour comes from a hash of the name, so a cast needs no
393
+ * setup; renaming a character stamps the old name's slot here, so they keep their colour. Authoring only
394
+ * (not shipped in the runtime bundle). */
395
+ colour?: number;
395
396
  gameData?: GameData;
396
397
  }
397
398
  /** A speaker qualifier (a screenplay's character extension): `V.O.`, `O.S.`, `RADIO`, or the project's own.
@@ -435,12 +436,12 @@ interface BundleQualifier {
435
436
  gameId: string;
436
437
  name: string;
437
438
  }
438
- /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
439
- * `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
439
+ /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor`,
440
+ * `gender` and `colour` are authoring / production / translation context, and the compiler drops them, so a shipped
440
441
  * game never carries a real person's name or a writer's private notes. The compiler copies the shipping
441
442
  * fields across explicitly (an allow-list), which is what actually keeps a new authoring field out of
442
443
  * the bundle; this type states the resulting contract for anyone reading a bundle. */
443
- type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
444
+ type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender" | "colour">;
444
445
  /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
445
446
  * Beat kinds map straight through: dialogue = `line`, text = `text`, game event = `gameEvent`.
446
447
  * `option` is a choice's option: a group that is a direct child of a `choice` group, whose gameData the
@@ -1091,6 +1092,13 @@ interface Bundle {
1091
1092
  default: string;
1092
1093
  included: string[];
1093
1094
  };
1095
+ /** Where a flow opened without a scene starts: the project's authored start (scene id, and block id when
1096
+ * it names one). Absent when the project sets none, or names one that has gone: every runtime then starts
1097
+ * at the first scene. */
1098
+ start?: {
1099
+ scene: string;
1100
+ block?: string;
1101
+ };
1094
1102
  /** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
1095
1103
  cast?: BundleCastMember[];
1096
1104
  /** The speaker qualifiers the content uses, in the project's order. Absent when no line has one, so a
@@ -1154,7 +1162,7 @@ declare const DEFAULT_CAPTION_DELIMITERS: CaptionDelimiters;
1154
1162
  declare const DEFAULT_CAPTION_CHARACTER = "SFX";
1155
1163
  declare const SAVE_SCHEMA = "patter/save@0";
1156
1164
  /** The `save.version` inside the envelope. Bumped only when a reader would MISREAD an older save. */
1157
- declare const SAVE_VERSION = 3;
1165
+ declare const SAVE_VERSION = 4;
1158
1166
  /** The tagged envelope a host stores: `serializeState` writes it, `deserializeState` refuses anything else. */
1159
1167
  interface SaveEnvelope {
1160
1168
  schema: typeof SAVE_SCHEMA;
@@ -1181,8 +1189,11 @@ interface StackFrame {
1181
1189
  * restore against an EDITED bundle re-finds the position by id. Absent when the frame sat at its
1182
1190
  * container's end (no next child), or in an older save. */
1183
1191
  nextId?: string;
1192
+ /** SNAPSHOT-ONLY: on a frame whose container is an option of a choice, that choice group's id, so a load
1193
+ * whose option has gone offers the choice again. Absent on every other frame, and in a version 3 save. */
1194
+ choiceId?: string;
1184
1195
  }
1185
- /** The prompt of a saved choice option: the choice text as a structured line/text beat. */
1196
+ /** A prompt as the choice showed it: a replayed prompt still to be spoken (`FlowCursor.pendingPrompt`). */
1186
1197
  interface SavedChoicePrompt {
1187
1198
  kind: "line" | "text";
1188
1199
  text: string;
@@ -1196,10 +1207,12 @@ interface SavedChoicePrompt {
1196
1207
  /** The prompt's resolved pause (line padding). Absent in a save written before it existed. */
1197
1208
  padAfter?: number;
1198
1209
  }
1199
- /** One option of a pending choice, saved VERBATIM: re-deriving on load would re-evaluate conditions. */
1210
+ /** One option of a pending choice: which option, and whether it could be taken, saved VERBATIM, since
1211
+ * re-deriving on load would re-evaluate conditions. Its prompt is not saved: it is built again in the
1212
+ * language in use each time the choice is delivered. (A version 3 save's option also has a `prompt`,
1213
+ * which a load discards.) */
1200
1214
  interface SavedChoiceOption {
1201
1215
  id: string;
1202
- prompt?: SavedChoicePrompt;
1203
1216
  eligible: boolean;
1204
1217
  gameData?: GameData;
1205
1218
  }
@@ -1215,17 +1228,21 @@ interface FlowCursor {
1215
1228
  /** The continuation stack (call frames + the active block run), root first. */
1216
1229
  stack: StackFrame[];
1217
1230
  activeSnippetId: string | null;
1218
- beatIndex: number;
1231
+ /** The id of the next beat the active snippet delivers, so a load against an edited bundle finds its
1232
+ * place by id. Absent when no snippet is active or the active one has delivered every beat. */
1233
+ beatId?: string;
1234
+ /** Version 3 only, read and never written: the index of the next beat in the active snippet. */
1235
+ beatIndex?: number;
1219
1236
  /** Present (non-null) iff the flow is stopped at a choice. */
1220
1237
  pendingChoice: SavedChoice | null;
1221
1238
  /** The chosen option owning a prompt still to be replayed (a save taken between choose + advance).
1222
1239
  * Optional / null in older saves -> no pending prompt. */
1223
1240
  pendingPromptOwnerId?: string | null;
1224
- /** The prompt still to be replayed, exactly as the choice SHOWED it (text and speaker fields, the same
1225
- * shape as a saved option's `prompt`), so the replay after a load says what the player saw. Written only
1226
- * alongside a `pendingPromptOwnerId` (the choose -> advance window, with `replayPromptOnChoose`), and
1227
- * absent otherwise. A save without it (written before Patterplay 0.18.0) resolves the owner's prompt beat
1228
- * when it is delivered, as before. */
1241
+ /** The prompt still to be replayed, exactly as the choice SHOWED it (text and speaker fields), so the
1242
+ * replay after a load says what the player saw, in the language they saw it in: the one prompt a save
1243
+ * keeps as text. Written only alongside a `pendingPromptOwnerId` (the choose -> advance window, with
1244
+ * `replayPromptOnChoose`), and absent otherwise. A save without it (written before Patterplay 0.18.0)
1245
+ * resolves the owner's prompt beat when it is delivered, as before. */
1229
1246
  pendingPrompt?: SavedChoicePrompt;
1230
1247
  /** This flow's (non-shared) selector cursors, keyed by group id. */
1231
1248
  selectors: Record<string, SelectorSnapshot>;
@@ -1238,10 +1255,10 @@ interface FlowSnapshot {
1238
1255
  visits: Record<string, number>;
1239
1256
  cursor: FlowCursor;
1240
1257
  }
1241
- /** A full resumable save-game (version 3): everything that is not a property, plus the registry's
1258
+ /** A full resumable save-game (version 4): everything that is not a property, plus the registry's
1242
1259
  * values when the engine made its own registry. */
1243
1260
  interface SaveGame {
1244
- version: 3;
1261
+ version: typeof SAVE_VERSION;
1245
1262
  /** The engine's own registry's values, keyed by registry key: present only when the engine made the
1246
1263
  * registry itself (a standalone game). A game that passed a registry saves it once, beside this. */
1247
1264
  registry?: EngineSave;
@@ -1252,17 +1269,18 @@ interface SaveGame {
1252
1269
  /** Each live flow's snapshot, keyed by flow id. */
1253
1270
  flows: Record<string, FlowSnapshot>;
1254
1271
  }
1255
- /** One flow's own property values, without its id: the sections a version 2 save carried per flow, and a
1256
- * `FlowState` carries now. */
1272
+ /** A version 3 save, still read: the same shape apart from the cursor (`beatIndex` for `beatId`, no
1273
+ * `choiceId` on its frames) and a pending choice's options (each with its prompt text, discarded). */
1274
+ interface SaveGameV3 extends Omit<SaveGame, "version"> {
1275
+ version: 3;
1276
+ }
1277
+ /** One flow's own property values, without its id: what a `FlowState` carries beside its snapshot. */
1257
1278
  interface FlowValues {
1258
1279
  /** The NOT-shared `@patter` globals, under token "patter". */
1259
1280
  scopes: EngineSave;
1260
1281
  /** Per-scene NOT-shared `@scene` bags (scene id -> name -> value). */
1261
1282
  sceneBags: Record<string, Record<string, ScalarValue>>;
1262
1283
  }
1263
- /** A version 2 flow snapshot, which carried the flow's property values itself. Still read. */
1264
- interface FlowSnapshotV2 extends FlowSnapshot, FlowValues {
1265
- }
1266
1284
  /**
1267
1285
  * One flow taken out of an engine (`engine.captureFlow`) to be put back later (`engine.restoreFlow`), under the
1268
1286
  * same id or another: its snapshot as a save writes it, and its own property values without its id. Plain data
@@ -1270,19 +1288,8 @@ interface FlowSnapshotV2 extends FlowSnapshot, FlowValues {
1270
1288
  * visit counts, shared selectors) is never in it, so restoring one rewinds nothing else.
1271
1289
  */
1272
1290
  interface FlowState extends FlowSnapshot, FlowValues {
1273
- version: typeof SAVE_VERSION;
1274
- }
1275
- /** A version 2 save, from before the registry held the properties. Every runtime still reads it: its
1276
- * property sections move into the registry (under the keys in the header above) as it loads. */
1277
- interface SaveGameV2 {
1278
- version: 2;
1279
- /** Shared `@patter` globals (owned scope "patter"). */
1280
- shared: EngineSave;
1281
- sharedVisits: Record<string, number>;
1282
- sharedSelectors: Record<string, SelectorSnapshot>;
1283
- /** Shared, scene-namespaced `@scene` bags (scene id -> name -> value). */
1284
- stageBags: Record<string, Record<string, ScalarValue>>;
1285
- flows: Record<string, FlowSnapshotV2>;
1291
+ /** `SAVE_VERSION` when written; a version 3 state still restores. */
1292
+ version: typeof SAVE_VERSION | 3;
1286
1293
  }
1287
1294
  /** Where a project keeps its shards unless its `layout` says otherwise, relative to the project root. */
1288
1295
  declare const DEFAULT_LAYOUT: {
@@ -1394,4 +1401,4 @@ interface ExtensionFieldDecl {
1394
1401
  /** A node's extension values in a bundle: extension id -> field -> value, raw. */
1395
1402
  type NodeExtensions = Record<string, Record<string, unknown>>;
1396
1403
 
1397
- export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleExtension, 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, EXTENSIONS_DIR, EXTENSION_SCHEMA, EXTENSION_SHARD, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, type ExtensionFieldDecl, type ExtensionShard, 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, MANIFEST_EXTENSION, type NodeExtensions, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_EXTENSION_STEM, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectExtension, type ProjectFile, type ProjectLayout, 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, UNRESOLVED_KEY, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, formatSpeakerLabel, gameIdify, isContentlessBeat, isDialogueScene, isValidGameId, projectLayout, projectQualifiers, qualifierStringKey, resolveSceneRef, sceneAddressIndex, speakerLabelKey, textIsAction, usedQualifiers, usedSpeakerPairs, walkNodes };
1404
+ export { AUTHORING_SCHEMA, type AuthoringFile, type Beat, type Block, type Bundle, type BundleCastMember, type BundleExtension, 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, EXTENSIONS_DIR, EXTENSION_SCHEMA, EXTENSION_SHARD, type EditRecord, type Effect, type EngineSave, type EstimatingConfig, type Expression, type ExtensionFieldDecl, type ExtensionShard, FLOW_SCHEMA, type FlowCursor, type FlowFile, type FlowNode, type FlowSnapshot, 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, MANIFEST_EXTENSION, type NodeExtensions, PAD_AFTER_MAX, PAD_AFTER_MIN, PROJECT_EXTENSION_STEM, PROJECT_LOCALE_SCENE, PROJECT_SCHEMA, type ProjectDictionary, type ProjectExtension, type ProjectFile, type ProjectLayout, 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 SaveGameV3, 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, UNRESOLVED_KEY, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, formatSpeakerLabel, isContentlessBeat, isDialogueScene, projectLayout, projectQualifiers, qualifierStringKey, resolveSceneRef, sceneAddressIndex, speakerLabelKey, textIsAction, usedQualifiers, usedSpeakerPairs, walkNodes };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/index.ts
2
+ import { gameIdify, isValidGameId } from "@wildwinter/toolkit";
2
3
  import {
3
4
  propertyNameify,
4
5
  isValidPropertyName,
@@ -19,12 +20,6 @@ function isContentlessBeat(beat, hasText) {
19
20
  if (beat.tags && beat.tags.length > 0) return false;
20
21
  return beat.kind === "text" || !beat.character && !beat.direction;
21
22
  }
22
- function gameIdify(text) {
23
- return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
24
- }
25
- function isValidGameId(gameId) {
26
- return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);
27
- }
28
23
  function effectiveGameId(entity) {
29
24
  const g = entity.gameId?.trim();
30
25
  return g ? g : gameIdify(entity.name);
@@ -168,7 +163,7 @@ var HANDOFF_SCHEMA = "patter/handoff@0";
168
163
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
169
164
  var DEFAULT_CAPTION_CHARACTER = "SFX";
170
165
  var SAVE_SCHEMA = "patter/save@0";
171
- var SAVE_VERSION = 3;
166
+ var SAVE_VERSION = 4;
172
167
  var DEFAULT_LAYOUT = { flow: "scenes/", strings: "loc/", authoring: "authoring/", extensions: "extensions/" };
173
168
  function projectLayout(project) {
174
169
  return { ...DEFAULT_LAYOUT, ...project.layout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/model",
3
- "version": "0.20.0",
3
+ "version": "0.21.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,7 @@
34
34
  "build": "tsup src/index.ts --format esm,cjs --dts --clean"
35
35
  },
36
36
  "dependencies": {
37
- "@wildwinter/expr": "^0.6.0"
37
+ "@wildwinter/expr": "^0.6.0",
38
+ "@wildwinter/toolkit": "^0.4.0"
38
39
  }
39
40
  }