@patterkit/ops 0.14.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -395,7 +395,7 @@ interface WebExport {
395
395
  indexHtml: string;
396
396
  /** The look of the page, separated so it's approachable to edit. Published once. */
397
397
  styleCss: string;
398
- /** The Patterplay runtime + this page's player glue. Refreshed on every publish. */
398
+ /** The Patterplay runtime, the line-timing estimate, and this page's player glue. Refreshed on every publish. */
399
399
  patterplayJs: string;
400
400
  /** The compiled story as `window.PATTER_BUNDLE = …`. Refreshed on every publish. */
401
401
  storyJs: string;
@@ -415,17 +415,24 @@ interface PlayOptions {
415
415
  maxSteps?: number;
416
416
  }
417
417
  /** One thing that happened during a playthrough, in order. */
418
- type PlayEvent = {
418
+ type PlayEvent =
419
+ /** `padAfter` is the step's resolved pause after it, in seconds (line padding), as the runtime delivers it. */
420
+ {
419
421
  type: "line";
420
422
  id: string;
421
423
  text: string;
422
424
  character?: string;
425
+ characterName?: string;
423
426
  direction?: string;
427
+ qualifier?: string;
428
+ qualifierName?: string;
429
+ padAfter?: number;
424
430
  gameData?: GameData;
425
431
  } | {
426
432
  type: "text";
427
433
  id: string;
428
434
  text: string;
435
+ padAfter?: number;
429
436
  gameData?: GameData;
430
437
  } | {
431
438
  type: "gameEvent";
@@ -777,7 +784,8 @@ declare function renderReportText(data: ReportData): string[];
777
784
  /** Render the report as an .xlsx workbook. */
778
785
  declare function runReportXlsx(data: ReportData): Promise<Buffer>;
779
786
 
780
- /** One localisable string. `id` is the beat id (or `cast:<NAME>` for a display name). */
787
+ /** One localisable string. `id` is the beat id (or `cast:<NAME>` for a display name, `qualifier:<gameId>` for a
788
+ * speaker qualifier's name). */
781
789
  interface LocEntry {
782
790
  id: string;
783
791
  /** Owning scene id, or `@project` for project-level strings (display names). */
@@ -795,6 +803,7 @@ interface LocEntry {
795
803
  character?: string;
796
804
  kind?: string;
797
805
  gender?: GrammaticalGender;
806
+ qualifier?: string;
798
807
  };
799
808
  /** Translated, but the source changed since (source modifiedAt > localisedAt[locale]). */
800
809
  stale: boolean;
@@ -860,6 +869,8 @@ interface VoiceLine {
860
869
  id: string;
861
870
  /** The canonical speaker token. */
862
871
  character: string;
872
+ /** The speaker qualifier's shown name (`V.O.`), when the line has one. Never part of `character`. */
873
+ qualifier?: string;
863
874
  /** The cast member's voice actor, if known. */
864
875
  actor?: string;
865
876
  /** The line's source-locale text. */
@@ -888,6 +899,13 @@ declare function runVoiceScript(loaded: LoadedProject, opts?: {
888
899
  /** Render the VO script as an .xlsx workbook (a single "Voice Script" sheet). */
889
900
  declare function voiceScriptToXlsx(data: VoiceScript): Promise<Buffer>;
890
901
 
902
+ /** A speaker cue as the script shows it: the character, then the speaker qualifier's name in brackets, the
903
+ * screenplay convention (`TAM (O.S.)`). Upper case, as every cue is set. The qualifier is how the line is
904
+ * delivered, never part of who says it, so a cue's colour and any counting stay on the character alone. */
905
+ declare function cueLabel(character: string, qualifierName?: string): string;
906
+ /** A speaker qualifier's shown name by its `gameId`, from the project's list. A `gameId` the list lacks (one
907
+ * removed, which validation flags) shows as itself, so the script still says something was there. */
908
+ declare function qualifierNamer(project: Pick<ProjectFile, "qualifiers">): (gameId: string | undefined) => string | undefined;
891
909
  /** A formatting run of body text. `code` marks an inline `{@property}` interpolation (rendered as accent
892
910
  * mono); `bold`/`italic` carry Patter's closed `<b>/<i>/<bi>` markup. Renderers turn these into Word runs
893
911
  * or PDF font/colour switches. */
@@ -914,13 +932,17 @@ type ScriptElement = {
914
932
  id: string;
915
933
  text: string;
916
934
  }
917
- /** A spoken line: SPEAKER cue (coloured, uppercase), an optional (direction), and the body runs. */
935
+ /** A spoken line: SPEAKER cue (coloured, uppercase), an optional (direction), and the body runs. A speaker
936
+ * qualifier rides as its `gameId` (`qualifier`) and its shown name (`qualifierName`), for the cue to print
937
+ * after the character (`cueLabel`). */
918
938
  | {
919
939
  kind: "line";
920
940
  id: string;
921
941
  indent: number;
922
942
  snippet?: number;
923
943
  character: string;
944
+ qualifier?: string;
945
+ qualifierName?: string;
924
946
  direction?: string;
925
947
  runs: TextRun[];
926
948
  }
@@ -1588,7 +1610,7 @@ interface ImportOptions {
1588
1610
  /** Something about the returned file the person importing should know. */
1589
1611
  interface ImportProblem {
1590
1612
  severity: "warning" | "info";
1591
- kind: "damaged-marker" | "repaired-marker" | "duplicate-marker" | "missing-box" | "moved" | "placeholders" | "line-gone" | "unknown-speaker" | "added-text" | "context-edited" | "untracked-edits" | "joined-paragraphs" | "dropped-formatting" | "stale";
1613
+ kind: "damaged-marker" | "repaired-marker" | "duplicate-marker" | "missing-box" | "moved" | "placeholders" | "line-gone" | "unknown-speaker" | "unknown-qualifier" | "added-text" | "context-edited" | "untracked-edits" | "joined-paragraphs" | "dropped-formatting" | "stale";
1592
1614
  message: string;
1593
1615
  /** The project line or node it concerns, for "Go to". */
1594
1616
  anchor?: string;
@@ -1623,4 +1645,4 @@ declare const REFUSE_MISSING_SHARE = 0.25;
1623
1645
  /** Plan an import. Reads the handoff record named in the document from the project. */
1624
1646
  declare function planEditableImport(loaded: LoadedProject, returned: ReturnedDoc, opts: ImportOptions): ImportPlan;
1625
1647
 
1626
- export { AUDIO_MANIFEST_FILE, AUDIO_MANIFEST_SCHEMA, AUTHORING_HANDLED, type ApplyStats, type AudioEntry, type AudioRung, type AudioSnapshot, type BuildPlan, type BundlePosture, CONFLICT_SIDECAR, type CharacterReport, type Conflict, type ConflictKind, type ContentError, type CoverageAsyncHooks, type CoverageBeat, type CoverageHooks, type CoverageOptions, type CoverageOrder, type CoverageReport, type DecisionPlan, type DecisionResult, type DocumentManifest, type EditableScript, type EditableScriptOptions, type ExportOptions, type FormatResult, GAME_SCOPES_OWNER, type GameScopes, type GameScopesIssue, HANDOFF_DIR, type HandoffIssue, type HygieneIssue, type ImportOptions, type ImportPlan, type ImportProblem, type ImportReport, type InitOptions, type InitResult, type InitVcs, type LoadedProject, type LocCatalog, type LocEntry, type LocaleReport, type MergeFileType, type MergeResult, type MergedShard, NotAPackError, type OpenSuggestion, PATTER_SCOPES_FILE, PATTER_SCOPES_OWNER, type PinPlan, type PinnedName, type PlannedWrite, type PlayEvent, type PlayOptions, type PlayOutcome, type PlayResult, type ProvenanceCheck, RARE_REACH_PCT, REFUSE_MISSING_SHARE, type Random, type ReadBox, type ReadComment, type ReadItem, type ReadMarker, type ReadParagraph, type ReadText, type ReplaceHit, type ReplaceOptions, type ReplacePlan, type ReportData, type ReportTotals, type ResolveEntry, type ReturnedDoc, SHARD_EXTENSIONS, type SceneReport, type ScriptDoc, type ScriptElement, type SearchFocus, type SuggestionDecision, type TrackedChange, type UnpackMergeResult, type UnpackResult, UnsafeEntryError, UnsupportedMergeError, type ValidateResult, type VoiceLine, type VoiceScript, type VoicedCounts, type WebExport, type WrittenCounts, applyLoc, applySceneOrder, applySuggestionDecisions, applyWrites, audioFolderRungs, audioFoldersActive, audioManifest, audioManifestWrite, besideBundlePath, bundleOutputPath, catalogToJson, catalogToPo, catalogToXlsx, classesForChannel, compileLoaded, currentBundlePosture, defaultBundlePath, defaultGameScopesDir, detectMergeType, discoverGameScopes, drivableScopes, exportBlockers, exportEditableScript, extractLoc, findProjectFile, formatMarker, gameScopeTokens, gameScopesCatalogue, gameScopesSnapshot, handoffPath, handoffWrite, hostScopeTokens, issueMarkerCodes, jsonToCatalog, leastReachedFirst, listOpenSuggestions, listProjectTags, loadProject, loadProjectLanding, markerLength, newHandoffId, parseHandoff, patterScopesFile, patterScopesStale, patterScopesWrite, pickAudio, planBuild, planEditableImport, planPins, planReturnedWorld, planShareScopes, planWorldSave, poToCatalog, previewRegistry, proposeCoverageDrivers, reachabilityIssues, readEditableDocx, readGameScopes, readHandoff, readHandoffs, readMarker, renderCoverageText, renderPlay, renderReportText, resolveDocumentation, resolveStart, runCoverage, runCoverageAsync, runExport, runExportFull, runExportHtml, runExportWeb, runFormat, runInit, runMerge, runPack, runPlay, runPropertyUsage, runReplace, runReport, runReportXlsx, runResolve, runScriptDoc, runSearch, runStatusBrowse, runTagBrowse, runUnpack, runUnpackMerge, runValidate, runVoiceScript, scanAudio, scanAudioStatus, sceneIdForShard, scriptToDocx, scriptToPdf, serialiseHandoff, setCut, sidecarIssues, staleParts, vcsConfigWrites, voiceScriptToXlsx, worldSettingsScopes, xlsxToCatalog };
1648
+ export { AUDIO_MANIFEST_FILE, AUDIO_MANIFEST_SCHEMA, AUTHORING_HANDLED, type ApplyStats, type AudioEntry, type AudioRung, type AudioSnapshot, type BuildPlan, type BundlePosture, CONFLICT_SIDECAR, type CharacterReport, type Conflict, type ConflictKind, type ContentError, type CoverageAsyncHooks, type CoverageBeat, type CoverageHooks, type CoverageOptions, type CoverageOrder, type CoverageReport, type DecisionPlan, type DecisionResult, type DocumentManifest, type EditableScript, type EditableScriptOptions, type ExportOptions, type FormatResult, GAME_SCOPES_OWNER, type GameScopes, type GameScopesIssue, HANDOFF_DIR, type HandoffIssue, type HygieneIssue, type ImportOptions, type ImportPlan, type ImportProblem, type ImportReport, type InitOptions, type InitResult, type InitVcs, type LoadedProject, type LocCatalog, type LocEntry, type LocaleReport, type MergeFileType, type MergeResult, type MergedShard, NotAPackError, type OpenSuggestion, PATTER_SCOPES_FILE, PATTER_SCOPES_OWNER, type PinPlan, type PinnedName, type PlannedWrite, type PlayEvent, type PlayOptions, type PlayOutcome, type PlayResult, type ProvenanceCheck, RARE_REACH_PCT, REFUSE_MISSING_SHARE, type Random, type ReadBox, type ReadComment, type ReadItem, type ReadMarker, type ReadParagraph, type ReadText, type ReplaceHit, type ReplaceOptions, type ReplacePlan, type ReportData, type ReportTotals, type ResolveEntry, type ReturnedDoc, SHARD_EXTENSIONS, type SceneReport, type ScriptDoc, type ScriptElement, type SearchFocus, type SuggestionDecision, type TrackedChange, type UnpackMergeResult, type UnpackResult, UnsafeEntryError, UnsupportedMergeError, type ValidateResult, type VoiceLine, type VoiceScript, type VoicedCounts, type WebExport, type WrittenCounts, applyLoc, applySceneOrder, applySuggestionDecisions, applyWrites, audioFolderRungs, audioFoldersActive, audioManifest, audioManifestWrite, besideBundlePath, bundleOutputPath, catalogToJson, catalogToPo, catalogToXlsx, classesForChannel, compileLoaded, cueLabel, currentBundlePosture, defaultBundlePath, defaultGameScopesDir, detectMergeType, discoverGameScopes, drivableScopes, exportBlockers, exportEditableScript, extractLoc, findProjectFile, formatMarker, gameScopeTokens, gameScopesCatalogue, gameScopesSnapshot, handoffPath, handoffWrite, hostScopeTokens, issueMarkerCodes, jsonToCatalog, leastReachedFirst, listOpenSuggestions, listProjectTags, loadProject, loadProjectLanding, markerLength, newHandoffId, parseHandoff, patterScopesFile, patterScopesStale, patterScopesWrite, pickAudio, planBuild, planEditableImport, planPins, planReturnedWorld, planShareScopes, planWorldSave, poToCatalog, previewRegistry, proposeCoverageDrivers, qualifierNamer, reachabilityIssues, readEditableDocx, readGameScopes, readHandoff, readHandoffs, readMarker, renderCoverageText, renderPlay, renderReportText, resolveDocumentation, resolveStart, runCoverage, runCoverageAsync, runExport, runExportFull, runExportHtml, runExportWeb, runFormat, runInit, runMerge, runPack, runPlay, runPropertyUsage, runReplace, runReport, runReportXlsx, runResolve, runScriptDoc, runSearch, runStatusBrowse, runTagBrowse, runUnpack, runUnpackMerge, runValidate, runVoiceScript, scanAudio, scanAudioStatus, sceneIdForShard, scriptToDocx, scriptToPdf, serialiseHandoff, setCut, sidecarIssues, staleParts, vcsConfigWrites, voiceScriptToXlsx, worldSettingsScopes, xlsxToCatalog };
package/dist/index.js CHANGED
@@ -498,6 +498,7 @@ import { basename as basename4, isAbsolute as isAbsolute2, join as join5, relati
498
498
 
499
499
  // src/merge.ts
500
500
  import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
501
+ import { DEFAULT_QUALIFIERS } from "@patterkit/model";
501
502
  var UnsupportedMergeError = class extends Error {
502
503
  };
503
504
  var eq = (a, b) => canonicalStringify2(a) === canonicalStringify2(b);
@@ -715,6 +716,9 @@ function mergeProject(base, ours, theirs) {
715
716
  } else if (k === "gameDataFields") {
716
717
  const v = mergeGameDataFields(asMap(base[k]), asMap(ours[k]), asMap(theirs[k]), conflicts);
717
718
  if (Object.keys(v).length > 0) merged[k] = v;
719
+ } else if (k === "qualifiers") {
720
+ const v = mergeQualifiers(base[k], ours[k], theirs[k], conflicts);
721
+ if (v !== void 0) merged[k] = v;
718
722
  } else if (k === "locales") {
719
723
  merged.locales = mergeLocales(asMap(base.locales), asMap(ours.locales), asMap(theirs.locales), conflicts);
720
724
  } else {
@@ -732,6 +736,23 @@ function mergeGameDataFields(b, o, t, conflicts) {
732
736
  }
733
737
  return out;
734
738
  }
739
+ function mergeQualifiers(base, ours, theirs, conflicts) {
740
+ if (base === void 0 && ours === void 0 && theirs === void 0) return void 0;
741
+ const keyed = (arr) => new Set(arr.map((q) => isObj(q) && typeof q.gameId === "string" ? q.gameId : null)).size === arr.length && arr.every((q) => isObj(q) && typeof q.gameId === "string");
742
+ const list = (v) => v === void 0 ? DEFAULT_QUALIFIERS : Array.isArray(v) && keyed(v) ? v : null;
743
+ const b = list(base), o = list(ours), t = list(theirs);
744
+ if (!b || !o || !t) return merge3(base, ours, theirs, "", "qualifiers", conflicts);
745
+ const byId = (arr) => {
746
+ const m = {};
747
+ for (const q of arr) if (isObj(q) && typeof q.gameId === "string") m[q.gameId] = q;
748
+ return m;
749
+ };
750
+ const merged = mergeMap(byId(b), byId(o), byId(t), "qualifiers", conflicts);
751
+ const order = [];
752
+ for (const q of [...o, ...t]) if (isObj(q) && typeof q.gameId === "string" && q.gameId in merged && !order.includes(q.gameId)) order.push(q.gameId);
753
+ const out = order.map((id) => merged[id]);
754
+ return eq(out, DEFAULT_QUALIFIERS) ? void 0 : out;
755
+ }
735
756
  function mergeLocales(b, o, t, conflicts) {
736
757
  const out = {};
737
758
  const def = merge3(b.default, o.default, t.default, "", "locales.default", conflicts);
@@ -1592,9 +1613,58 @@ function planBuild(loaded, opts = {}) {
1592
1613
 
1593
1614
  // src/export-html.ts
1594
1615
  import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
1616
+ import { DEFAULT_PAD_AFTER } from "@patterkit/model";
1595
1617
 
1596
1618
  // src/playable-runtime.ts
1597
- var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var W=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var $e=Object.getOwnPropertyNames;var He=Object.prototype.hasOwnProperty;var qe=(n,e)=>{for(var t in e)W(n,t,{get:e[t],enumerable:!0})},_e=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of $e(e))!He.call(n,s)&&s!==t&&W(n,s,{get:()=>e[s],enumerable:!(r=Me(e,s))||r.enumerable});return n};var Ue=n=>_e(W({},"__esModule",{value:!0}),n);var pt={};qe(pt,{Checkpoint:()=>T,Engine:()=>_,Flow:()=>D,buildTagIndex:()=>q,describeBundle:()=>Ae,effectiveGameData:()=>Ge,gameDataFields:()=>Te,gameDataValue:()=>se});function R(n,e){return n!==void 0&&Object.prototype.hasOwnProperty.call(n,e)?n[e]:void 0}function k(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:k(n[2])};case"bin":return{kind:"binary",op:n[1],left:k(n[2]),right:k(n[3])};case"call":{let e=n.slice(2).map(k);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var d=class extends Error{constructor(e){super(e),this.name="EvalError"}};function V(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=R(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):R(o,i.name);if(a===void 0){if(r.get(i.scope)==="throw")throw new d(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!R(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new d(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=J(a,e);if(c===void 0)throw new d("advance() needs a quality reference (@scope.name of a quality property)");let l=w(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=R(t.functions,i.name);if(!o)throw new d(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new d("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new d(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new d(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let f=s(i.left);if(typeof f!="boolean")throw new d(`\'and\' requires boolean operands, left is ${typeof f}`);if(!f)return!1;let u=s(i.right);if(typeof u!="boolean")throw new d(`\'and\' requires boolean operands, right is ${typeof u}`);return u}if(i.op==="or"){let f=s(i.left);if(typeof f!="boolean")throw new d(`\'or\' requires boolean operands, left is ${typeof f}`);if(f)return!0;let u=s(i.right);if(typeof u!="boolean")throw new d(`\'or\' requires boolean operands, right is ${typeof u}`);return u}let o=s(i.left),a=s(i.right),c=J(i.left,e),l=J(i.right,e),p=c??l;if(p!==void 0){if(c&&l&&!We(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new d(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return w(o,p,">")>w(a,p,">");case">=":return w(o,p,">=")>=w(a,p,">=");case"<":return w(o,p,"<")<w(a,p,"<");case"<=":return w(o,p,"<=")<=w(a,p,"<=");case"+":case"-":case"*":case"/":throw new d(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return pe(o,a);case"!=":return!pe(o,a);case">":return E(o,a,">"),o>a;case">=":return E(o,a,">="),o>=a;case"<":return E(o,a,"<"),o<a;case"<=":return E(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new d(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return E(o,a,"-"),o-a;case"*":return E(o,a,"*"),o*a;case"/":if(E(o,a,"/"),a===0)throw new d("division by zero");return o/a}}}};return s(n)}function pe(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function E(n,e,t){if(typeof n!="number"||typeof e!="number")throw new d(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function J(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function w(n,e,t){if(typeof n!="string")throw new d(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new d(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var We=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function O(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function K(n){let e=O(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var Je={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ke=[Je];function de(n,e,t){let r=t?.countingCalls??Ke;return L(n,t?.want??!0,e,r)}function L(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=L(n.left,e,t,r),i=L(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return L(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var b=class n{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??P(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}writable(e){return this.decls.get(this.norm(e))?.writable!==!1}set(e,t,r){let s=this.norm(e);if(!r?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of[...this.auditors])o(i);if(!i.silent)for(let o of[...this.subscribers])o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>ue(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function ue(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??P(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var ze=n=>n.toLowerCase(),z=1,M=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new b(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(t.load(s),this.parked.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&this.parked.set(e,r.bag.save()),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??ze,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){if(!s?.host&&!i.bag.writable(t))throw new Error(`\'@${e}.${t}\' is read-only`);i.bag.set(t,r,s?.host?{host:!0}:void 0);return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...ue(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get(Ye(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of ge(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=ge(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||this.parked.clear();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:z,scopes:this.save()}}loadFragment(e){if(e.version!==z)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${z})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function ge(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function Ye(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function P(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var he=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function B(n){return n.ctx.host??{}}var Y={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new d("random(a, b) requires exactly 2 arguments");let t=B(e).nextRandom;if(!t)throw new d("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new d("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new d("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:me("check_flags"),eval(n,e){let t=fe(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new d("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:me("set_flags"),eval(n,e){let t=[...fe(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new d("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:H("visits"),eval:(n,e)=>B(e).visits?.($(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:H("seen"),eval:(n,e)=>(B(e).visits?.($(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:H("patter_visits"),eval:(n,e)=>B(e).patterVisits?.($(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:H("patter_seen"),eval:(n,e)=>(B(e).patterVisits?.($(n,e,"patter_seen"))??0)>0}}},Rt=he.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function Se(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function $(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new d(`${t}(id) requires a string node id`);return r}function H(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function fe(n,e,t){if(!n)throw new d(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new d(`${t}() first argument must be a flags property`)}function me(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var Qe=/^@[A-Za-z0-9_.]+$/;function*Xe(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:Qe.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function Ze(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function et(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function tt(n){let e="",t=!1;for(let r of n){if(et(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ye(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?tt(r):n}function ve(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of Xe(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":Ze(s)}return t}function I(n,e){for(let t of n){e(t);let r=t.children;r&&I(r,e)}}function rt(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function S(n){let e=n.gameId?.trim();return e||rt(n.name)}function Q(n){return`cast:${n}`}var X={open:"[",close:"]"},we="SFX";var Z=3;function N(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function q(n){let e=new Map,t=(r,s)=>{let i=N([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group"&&r.prompt&&e.set(r.prompt.id,N([...i,...r.prompt.tags??[]])),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,N([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=N(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=N([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var be=new WeakMap,xe=new WeakMap;function nt(n){let e=xe.get(n);return e||(e=q(n),xe.set(n,e)),e}function C(n,e){return n.tagIndex.get(e)?.slice()}var A="Patter",F=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),ke=Symbol("ownsRegistry"),y={stage:n=>`patter/scene/${F(n)}`,flow:n=>`patter/flow/${F(n)}/`,flowGlobals:n=>`patter/flow/${F(n)}/patter`,flowScene:(n,e)=>`patter/flow/${F(n)}/scene/${F(e)}`},T=class{constructor(e){this.journal=e}journal},_=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineSeq=0;engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let g of e.cast??[])g.displayName&&l.set(g.name,g.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let p=new Map,f=new Map,u=new Map;for(let[g,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(S(h),g);let v=new Map;for(let m of h.blocks)f.set(m.id,{sceneId:g}),u.set(m.id,m),v.set(S(m),m.id),I(m.children,j=>p.set(j.id,j));this.blockGameIdToId.set(g,v)}let ie=e.properties??[],U=ie.filter(g=>g.shared??!0).map(De),je=ie.filter(g=>!(g.shared??!0)).map(De),Ve=new Set(U.map(g=>g.name.toLowerCase())),x=t.registry??new M,oe=!t.registry||t[ke]===!0,ae=new b(U,{pathPrefix:"@patter."}),ce=[],G=[];try{x.mountOwned("patter",ae,{owner:A}),G.push("patter");let g=new Set;for(let[h,v]of Object.entries(Re(t))){let m=e.scopeRegistry?.scopes.find(Le=>Le.token===h),j=(m?.declarations??[]).map(Ne);x.defineForeign(h,v,j,{writable:m?.writable??!0,owner:A}),G.push(h),g.add(h)}for(let h of oe?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let v=(h.declarations??[]).map(m=>it(m,h.writable));x.defineOwned(h.token,v,{owner:A}),G.push(h.token),ce.push(h.token)}}catch(g){for(let h of G)x.remove(h,{keep:!0});throw g}let le=new Map;for(let[g,h]of Object.entries(e.scenes)){let v=new Set((h.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));le.set(g,v)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(g,h,v)=>this.emitEngine(g,h,v),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:p,blockIndex:f,blockById:u,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:nt(e),registry:x,ownsRegistry:oe,patterBag:ae,hostScopes:ce,patterSharedDecls:U,patterLocalDecls:je,patterSharedNames:Ve,sceneSharedNames:le,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(g=>console.warn(`Patterplay: ${g.kind} on ${g.node} in flow \'${g.flow}\' failed, played through: ${g.message}`)),replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??X).open,captionClose:(e.closedCaptions??X).close,captionCharacter:e.closedCaptions?.character||we,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}get buildId(){return this.host.bundle.content?.hash??""}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){this.refuseInCheckpoint("hotSwap");let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[ke]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:r,blockId:s}=this.resolveOpenAddress(t.scene,t.block),i=this.flowsById.get(e),o=this.host.journal;if(o&&i&&!i.isClosed)throw new Error(`openFlow would replace the open flow \'${e}\', which can\'t be undone while a checkpoint is open`);i?.close();let a=new D(e,this.host,t.seed??this.defaultSeed);return o&&(o.flows.add(a),o.opened.add(a),o.undo.push(()=>{a.close(),i?this.flowsById.set(e,i):this.flowsById.delete(e)})),this.flowsById.set(e,a),a.begin(r,s),a}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveOpenAddress(e,t){if(e!==void 0){let r=te(this.host,e);if(r===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:r};let s=re(this.host,r,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:r,blockId:s}}if(t!==void 0){if(!this.host.blockIndex.has(t))throw new Error(`unknown block: ${t}`);return{blockId:t}}if(Object.keys(this.host.bundle.scenes).length===0)throw new Error("no scenes in bundle");return{}}resolveSceneRef(e){if(e!=null)return te(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:re(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?S(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?S(t):void 0}tagsForBeat(e){return C(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?C(this.host,t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?C(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let r=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(r!=null?this.host.blockById.get(r):void 0)?.gameData}}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Ee(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Ee(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...S(e)?{gameId:S(e)}:{},name:e.name,...Ce(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...S(t)?{gameId:S(t)}:{},name:t.name,...Ce(t.gameData),...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)I(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=C(this.host,e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[Q(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}e.direction!==void 0&&(r.direction=e.direction)}if(e.kind==="line"||e.kind==="text"){let s=this.host.defaultStrings[e.id];s!==void 0&&(r.text=s)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=C(this.host,e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}checkpoint(){if(this.host.journal)throw new Error("a checkpoint is already open: roll it back or commit it first");let e={undo:[],flows:new Set,opened:new Set,selectors:new Set,flowSelectors:new Map};return this.host.journal=e,new T(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let r=t.length-1;r>=0;r--)t[r]();n.forget(e.journal)}commit(e){this.endCheckpoint(e),n.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(y.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(y.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});for(let r of Object.keys(Re(this.creationOptions)))t.has(r)&&t.remove(r)}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e),i=this.host.journal?this.host.registry.get(r,s):void 0;this.host.registry.set(r,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(r,s,i,{host:!0}))}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),this.host.tracing=!0,()=>{this.engineTraceHandlers.delete(e),this.host.tracing=this.host.logEnabled||this.engineTraceHandlers.size>0}}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...r?{scene:r}:{}})}getVisitCounts(){return Object.fromEntries(this.host.sharedVisits)}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:P(e),writable:e.writable??!0}))}splitShared(e){let t=Fe(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:Z,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:Pe(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==Z)throw new Error(`unsupported save version: ${String(t)}`);let r=e.flows;if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("malformed save: no flows");this.assertExternalScopes();let s=this.host.registry;for(let[o,a]of this.flowsById)a.releaseBags(o in e.flows),a.close();this.flowsById.clear();for(let o of this.host.stageBags.keys())s.remove(y.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.version===2?ot(e):e.registry;i&&(this.host.ownsRegistry?s.load(i):s.load(i,{keepParked:!0})),this.host.sharedVisits.clear();for(let[o,a]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(o,a);this.host.sharedSelectors.clear();for(let[o,a]of Be(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new D(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPrompt=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=O(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase();if(e==="patter")return ee(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(r);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:ee(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(r)}return this.registryQualities?.(e,t)??ee(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(r)}begin(e,t){if(this.host.journal&&!this.host.journal.opened.has(this))throw new Error("a flow can\'t be started or reset while a checkpoint is open");if(this.releaseBags(!1),this.host.registry.discardParked(y.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.clearPending(),this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.begin(e,t)}start(e,t){this.reset(e,t)}goto(e,t){if(this.closed)return!1;if(this.touch(),e==="END")return this.started=!0,this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=te(this.host,e);if(r===void 0)return!1;let s;return t!==void 0&&(s=re(this.host,r,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.begin(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.clearPending()}clearPending(){this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null}touch(){let e=this.host.journal;if(!e||e.flows.has(this))return;e.flows.add(this);let{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p}=this,f=this.stack.map(u=>({...u}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p,stack:f})})}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.touch(),this.pendingPrompt){let e=this.pendingPrompt;return this.pendingPrompt=null,this.pendingPromptOwnerId=null,e.shown?this.promptResult(e.beat,e.shown):this.beatResult(e.beat)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit,this.activeSnippet.id);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getVisitCounts(){return Object.fromEntries(this.visitCounts)}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(o=>o.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);this.touch();let s=t.byId.get(e);this.host.tracing&&this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null;let i=this.host.replayPromptOnChoose?this.authoredPromptOf(s):void 0;this.pendingPrompt=i&&r.prompt?{beat:i,shown:{...r.prompt}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e),o=this.host.journal;if(s==="patter"){if(o){let a=this.host.patterSharedNames.has(i),c=a?this.host.patterBag.get(i):this.local.get(i),l=this.local;c!==void 0&&o.undo.push(a?()=>this.host.registry.set("patter",i,c):()=>l.set(i,c))}this.patterResolver.set(i,t)}else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);if(o){let a=this.sceneBagFor(this.currentSceneId,i),c=a?.get(i);a&&c!==void 0&&o.undo.push(()=>a.set(i,c))}this.sceneResolver.set(i,t)}else{let a=o?this.host.registry.get(s,i):void 0;this.host.registry.set(s,i,t,r?{host:!0}:void 0),o&&a!==void 0&&o.undo.push(()=>this.host.registry.set(s,i,a,{host:!0}))}}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:st(this.pendingPrompt.shown)}:{},selectors:Pe(this.selectors)}}}restore(e){if(this.host.journal)throw new Error("a flow can\'t be restored while a checkpoint is open");this.restoreCursor(e),this.releaseBags(!0),this.mountLocal();for(let t of new Set([this.currentSceneId,...this.stack.map(r=>r.sceneId)]))t!==null&&this.host.bundle.scenes[t]&&this.ensureSceneBags(t)}restoreCursor(e){this.rngState=O(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;if(this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=Be(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);o&&(r.set(i.id,o),s.push({...i}))}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPrompt=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId),s=r?this.authoredPromptOf(r):void 0;s?this.pendingPrompt={beat:s,...t.pendingPrompt?{shown:{...t.pendingPrompt}}:{}}:this.pendingPromptOwnerId=null}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry,t.id)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.some(o=>o.eligible)){this.host.tracing&&this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.host.tracing&&this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.host.tracing&&this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter((c,l)=>t[l].eligible),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.host.tracing&&this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(u=>u.id),a=new Set(e.map(u=>u.id));if(r.bag===void 0&&(r.bag=o()),!r.bag.some(u=>a.has(u))){if(t==="once")return null;if(i){let u=e[s-1];return r.last=u.id,u}if(r.bag=o(),r.bag.length===0)return null}let c=r.bag.filter(u=>a.has(u)),l=r.last!==void 0&&c.length>1?c.indexOf(r.last):-1,p=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&p>=l&&p++;let f=c[p];return r.bag.splice(r.bag.indexOf(f),1),r.last=f,e.find(u=>u.id===f)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(p=>p.id));let l=new Set(r.bag);if(s=e.filter(p=>l.has(p.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(p=>p.id===r.last)??null:null}let i=-1,a=s.map(l=>{let p=this.specScore(l);return p>i&&(i=p),{c:l,s:p}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(f=>f.id===r.last):-1,p=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&p>=l&&p++,c=a[p]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,r){return de(e,i=>{try{return Oe(V(i,this.context(),Y))}catch(o){return this.reportError("best-match",r.id,r.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=this.host.journal,s;if(r&&(s=e.shared?r.selectors:r.flowSelectors.get(this),s||(s=new Set,r.flowSelectors.set(this,s))),r&&s&&!s.has(e.id)){s.add(e.id);let o=t.get(e.id),a=o?{...o,...o.bag?{bag:[...o.bag]}:{}}:void 0;r.undo.push(()=>{a?t.set(e.id,a):t.delete(e.id)})}let i=t.get(e.id);return i||(i={},t.set(e.id,i)),i}runEffects(e,t){for(let r of e??[]){let s;try{s=this.evalExpr(r.value);let i=this.host.tracing?this.getProperty(r.target):void 0;this.writeProperty(r.target,s,!1),this.host.tracing&&this.emit({type:"write",target:r.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,r.value,i)}}}eligible(e){if(!e.condition)return!0;try{return Oe(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,r,s){let i=s instanceof Error?s.message:String(s),o=r?.src;this.host.onError({flow:this.id,kind:e,node:t,...o?{source:o}:{},message:i}),this.host.tracing&&this.emit({type:"diagnostic",kind:e,node:t,...o?{source:o}:{},message:i})}evalExpr(e){return V(this.conditionAst(e),this.context(),Y)}conditionAst(e){let t=be.get(e);return t||(t=k(e.ast),be.set(e,t)),t}enter(e){let t=this.visitCounts,r=t.get(e);t.set(e,(r??0)+1);let s=this.host.sharedVisits,i=s.get(e);s.set(e,(i??0)+1),this.host.journal&&this.host.journal.undo.push(()=>{r===void 0?t.delete(e):t.set(e,r),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=K(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=C(this.host,e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,gameData:e.gameData,...r}}}}interpolate(e){return ve(e,t=>this.getProperty(t))}stripCaptions(e){return ye(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id));return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction}:{kind:"text",text:r}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let r=C(this.host,e.id),s=r&&r.length?{tags:r}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,gameData:e.gameData,...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,gameData:e.gameData,...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return I(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=Q(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){return Fe(this.host,e)}newLocal(){return new b(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=y.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:A}),this.registered.add(e)}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal;if(!this.sceneBags.has(e)){let i=new b(r.filter(c=>!t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),o=y.flowScene(this.id,e),a=Ie(this.host.registry,o,i,s!==null);this.registered.add(o),this.sceneBags.set(e,i),s&&s.undo.push(()=>{this.sceneBags.get(e)===i&&(this.host.registry.remove(o,a?{keep:!0}:void 0),this.registered.delete(o),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let i=new b(r.filter(a=>t.has(a.name.toLowerCase())),{pathPrefix:"@scene."}),o=Ie(this.host.registry,y.stage(e),i,s!==null);this.host.stageBags.set(e,i),s&&s.undo.push(()=>{this.host.stageBags.get(e)===i&&(this.host.registry.remove(y.stage(e),o?{keep:!0}:void 0),this.host.stageBags.delete(e))})}}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);if(!i)continue;let o=this.host.journal?i.get(s):void 0;o!==void 0&&this.host.journal.undo.push(()=>i.set(s,o)),i.set(s,P(r))}}};function Ee(n,e){I(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function Ce(n){return n&&Object.keys(n).length?{gameData:n}:{}}function st(n){let e={kind:n.kind,text:n.text};return n.character!==void 0&&(e.character=n.character),n.characterName!==void 0&&(e.characterName=n.characterName),n.direction!==void 0&&(e.direction=n.direction),e}function Pe(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function Be(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function Ie(n,e,t,r){let s=r?JSON.stringify(t.save()):"";return n.mountOwned(e,t,{owner:A}),r&&JSON.stringify(t.save())!==s}function De(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Re(n){return{...n.world?{world:n.world}:{},...n.hostScopes}}function ee(n,e,t){let r=n.qualityLadders.get(e);if(!r){r=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!r.has(i)&&r.set(i,s.stages)}n.qualityLadders.set(e,r)}return r}function Ne(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function it(n,e){let t=Ne(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function te(n,e){return n.sceneGameIdToId.get(e)??(n.bundle.scenes[e]?e:void 0)}function re(n,e,t){return n.blockGameIdToId.get(e)?.get(t)??(n.blockIndex.get(t)?.sceneId===e?t:void 0)}function Fe(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=Se(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function ot(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[y.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{})){r.scopes?.patter&&(e[y.flowGlobals(t)]=r.scopes.patter);for(let[s,i]of Object.entries(r.sceneBags??{}))e[y.flowScene(t,s)]=i}return e}function Oe(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var at=(n,e)=>n.shared??e;function ne(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:at(n,e)}}function ct(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function lt(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function Ae(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=S(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:S(c),name:c.name}))});for(let c of o.blocks)lt(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>ne(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>ne(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(ct)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>ne(o,!0)),scene:r},gameData:i,counts:e}}function Te(n,e){return n.gameDataFields?.[e]??[]}function se(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function Ge(n,e){let t={};for(let r of n){let s=se(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return Ue(pt);})();\n';
1619
+ var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var K=Object.defineProperty;var Qe=Object.getOwnPropertyDescriptor;var Ke=Object.getOwnPropertyNames;var ze=Object.prototype.hasOwnProperty;var Ye=(n,e)=>{for(var t in e)K(n,t,{get:e[t],enumerable:!0})},Xe=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Ke(e))!ze.call(n,s)&&s!==t&&K(n,s,{get:()=>e[s],enumerable:!(r=Qe(e,s))||r.enumerable});return n};var Ze=n=>Xe(K({},"__esModule",{value:!0}),n);var bt={};Ye(bt,{Checkpoint:()=>j,DEFAULT_PAD_AFTER:()=>x,Engine:()=>J,Flow:()=>D,buildTagIndex:()=>W,describeBundle:()=>$e,effectiveGameData:()=>_e,gameDataFields:()=>He,gameDataValue:()=>ae});function P(n,e){return n!==void 0&&Object.prototype.hasOwnProperty.call(n,e)?n[e]:void 0}function E(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:E(n[2])};case"bin":return{kind:"binary",op:n[1],left:E(n[2]),right:E(n[3])};case"call":{let e=n.slice(2).map(E);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var u=class extends Error{constructor(e){super(e),this.name="EvalError"}};function q(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=P(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):P(o,i.name);if(a===void 0){if(r.get(i.scope)==="throw")throw new u(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!P(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new u(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=z(a,e);if(c===void 0)throw new u("advance() needs a quality reference (@scope.name of a quality property)");let l=v(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=P(t.functions,i.name);if(!o)throw new u(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new u("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new u(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new u(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let f=s(i.left);if(typeof f!="boolean")throw new u(`\'and\' requires boolean operands, left is ${typeof f}`);if(!f)return!1;let g=s(i.right);if(typeof g!="boolean")throw new u(`\'and\' requires boolean operands, right is ${typeof g}`);return g}if(i.op==="or"){let f=s(i.left);if(typeof f!="boolean")throw new u(`\'or\' requires boolean operands, left is ${typeof f}`);if(f)return!0;let g=s(i.right);if(typeof g!="boolean")throw new u(`\'or\' requires boolean operands, right is ${typeof g}`);return g}let o=s(i.left),a=s(i.right),c=z(i.left,e),l=z(i.right,e),p=c??l;if(p!==void 0){if(c&&l&&!et(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new u(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return v(o,p,">")>v(a,p,">");case">=":return v(o,p,">=")>=v(a,p,">=");case"<":return v(o,p,"<")<v(a,p,"<");case"<=":return v(o,p,"<=")<=v(a,p,"<=");case"+":case"-":case"*":case"/":throw new u(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return fe(o,a);case"!=":return!fe(o,a);case">":return C(o,a,">"),o>a;case">=":return C(o,a,">="),o>=a;case"<":return C(o,a,"<"),o<a;case"<=":return C(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new u(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return C(o,a,"-"),o-a;case"*":return C(o,a,"*"),o*a;case"/":if(C(o,a,"/"),a===0)throw new u("division by zero");return o/a}}}};return s(n)}function fe(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function C(n,e,t){if(typeof n!="number"||typeof e!="number")throw new u(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function z(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function v(n,e,t){if(typeof n!="string")throw new u(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new u(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var et=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function O(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function Y(n){let e=O(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var tt={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},rt=[tt];function he(n,e,t){let r=t?.countingCalls??rt;return $(n,t?.want??!0,e,r)}function $(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=$(n.left,e,t,r),i=$(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return $(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var b=class n{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??B(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}writable(e){return this.decls.get(this.norm(e))?.writable!==!1}set(e,t,r){let s=this.norm(e);if(!r?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of[...this.auditors])o(i);if(!i.silent)for(let o of[...this.subscribers])o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>Se(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function Se(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??B(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var nt=n=>n.toLowerCase(),X=1,H=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new b(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(t.load(s),this.parked.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&this.parked.set(e,r.bag.save()),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??nt,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){if(!s?.host&&!i.bag.writable(t))throw new Error(`\'@${e}.${t}\' is read-only`);i.bag.set(t,r,s?.host?{host:!0}:void 0);return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...Se(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get(st(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of me(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=me(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||this.parked.clear();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:X,scopes:this.save()}}loadFragment(e){if(e.version!==X)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${X})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function me(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function st(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function B(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var ye=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function A(n){return n.ctx.host??{}}var Z={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new u("random(a, b) requires exactly 2 arguments");let t=A(e).nextRandom;if(!t)throw new u("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new u("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new u("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:ve("check_flags"),eval(n,e){let t=we(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new u("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:ve("set_flags"),eval(n,e){let t=[...we(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new u("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:U("visits"),eval:(n,e)=>A(e).visits?.(_(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:U("seen"),eval:(n,e)=>(A(e).visits?.(_(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:U("patter_visits"),eval:(n,e)=>A(e).patterVisits?.(_(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:U("patter_seen"),eval:(n,e)=>(A(e).patterVisits?.(_(n,e,"patter_seen"))??0)>0}}},Mt=ye.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function be(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function _(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new u(`${t}(id) requires a string node id`);return r}function U(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function we(n,e,t){if(!n)throw new u(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new u(`${t}() first argument must be a flags property`)}function ve(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var it=/^@[A-Za-z0-9_.]+$/;function*ot(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:it.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function at(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function ct(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function lt(n){let e="",t=!1;for(let r of n){if(ct(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function xe(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?lt(r):n}function ke(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of ot(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":at(s)}return t}function R(n,e){for(let t of n){e(t);let r=t.children;r&&R(r,e)}}function pt(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function y(n){let e=n.gameId?.trim();return e||pt(n.name)}var x=.6;function ee(n){return`cast:${n}`}function te(n){return`qualifier:${n}`}var re={open:"[",close:"]"},Ee="SFX";var F=3;function N(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function W(n){let e=new Map,t=(r,s)=>{let i=N([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group"&&r.prompt&&e.set(r.prompt.id,N([...i,...r.prompt.tags??[]])),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,N([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=N(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=N([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var Ce=n=>n.kind==="line"||n.kind==="text",T=n=>typeof n=="number"&&Number.isFinite(n)?n:void 0;function Ie(n){let e=new Map,t=(i,o,a)=>{let c=i.kind==="gameEvent"?void 0:T(i.padAfter),l=c??o;a&&l<0&&(l=0),e.set(i.id,c!==void 0?{own:c,resolved:l}:{resolved:l})},r=(i,o)=>{let a=T(i.padAfterDefault)??o;if(i.type==="group"){i.prompt&&t(i.prompt,a,!1);for(let p of i.children)r(p,a);return}let c=i.beats??[],l=-1;c.forEach((p,f)=>{Ce(p)&&(l=f)}),c.forEach((p,f)=>{Ce(p)&&t(p,a,f===l)})},s=T(n.padAfterDefault)??x;for(let i of Object.values(n.scenes)){let o=T(i.padAfterDefault)??s;for(let a of i.blocks){let c=T(a.padAfterDefault)??o;for(let l of a.children)r(l,c)}}return e}var De=new WeakMap,Re=new WeakMap;function dt(n){let e=Re.get(n);return e||(e=W(n),Re.set(n,e)),e}var Pe=new WeakMap;function ut(n){let e=Pe.get(n);return e||(e=Ie(n),Pe.set(n,e)),e}function I(n,e){return n.tagIndex.get(e)?.slice()}var V="Patter",G=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),gt=n=>n.replace(/%2F/g,"/").replace(/%25/g,"%"),Oe=Symbol("ownsRegistry"),S={stage:n=>`patter/scene/${G(n)}`,flow:n=>`patter/flow/${G(n)}/`,flowGlobals:n=>`patter/flow/${G(n)}/patter`,flowScene:(n,e)=>`patter/flow/${G(n)}/scene/${G(e)}`},j=class{constructor(e){this.journal=e}journal},J=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineSeq=0;engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let d of e.cast??[])d.displayName&&l.set(d.name,d.displayName);let p=new Map;for(let d of e.qualifiers??[])d?.gameId&&p.set(d.gameId,d.name);this.defaultSeed=(t.seed??2654435769)>>>0;let f=new Map,g=new Map,ce=new Map;for(let[d,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(y(h),d);let w=new Map;for(let m of h.blocks)g.set(m.id,{sceneId:d}),ce.set(m.id,m),w.set(y(m),m.id),R(m.children,L=>f.set(L.id,L));this.blockGameIdToId.set(d,w)}let le=e.properties??[],Q=le.filter(d=>d.shared??!0).map(Ne),Ue=le.filter(d=>!(d.shared??!0)).map(Ne),We=new Set(Q.map(d=>d.name.toLowerCase())),k=t.registry??new H,pe=!t.registry||t[Oe]===!0,de=new b(Q,{pathPrefix:"@patter."}),ue=[],M=[];try{k.mountOwned("patter",de,{owner:V}),M.push("patter");let d=new Set;for(let[h,w]of Object.entries(Te(t))){let m=e.scopeRegistry?.scopes.find(Je=>Je.token===h),L=(m?.declarations??[]).map(Me);k.defineForeign(h,w,L,{writable:m?.writable??!0,owner:V}),M.push(h),d.add(h)}for(let h of pe?e.scopeRegistry?.scopes??[]:[]){if(d.has(h.token)||k.has(h.token))continue;let w=(h.declarations??[]).map(m=>ht(m,h.writable));k.defineOwned(h.token,w,{owner:V}),M.push(h.token),ue.push(h.token)}}catch(d){for(let h of M)k.remove(h,{keep:!0});throw d}let ge=new Map;for(let[d,h]of Object.entries(e.scenes)){let w=new Set((h.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));ge.set(d,w)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(d,h,w)=>this.emitEngine(d,h,w),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,qualifierDisplay:p,nodeIndex:f,blockIndex:g,blockById:ce,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:dt(e),padIndex:ut(e),registry:k,ownsRegistry:pe,patterBag:de,hostScopes:ue,patterSharedDecls:Q,patterLocalDecls:Ue,patterSharedNames:We,sceneSharedNames:ge,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(d=>console.warn(`Patterplay: ${d.kind} on ${d.node} in flow \'${d.flow}\' failed, played through: ${d.message}`)),replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??re).open,captionClose:(e.closedCaptions??re).close,captionCharacter:e.closedCaptions?.character||Ee,refSplitCache:new Map,refSplitRevision:k.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}get buildId(){return this.host.bundle.content?.hash??""}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){this.refuseInCheckpoint("hotSwap");let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[Oe]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:r,blockId:s}=this.resolveOpenAddress(t.scene,t.block),i=this.flowsById.get(e),o=this.host.journal;if(o&&i&&!i.isClosed)throw new Error(`openFlow would replace the open flow \'${e}\', which can\'t be undone while a checkpoint is open`);i?.close();let a=new D(e,this.host,t.seed??this.defaultSeed);return o&&(o.flows.add(a),o.opened.add(a),o.undo.push(()=>{a.close(),i?this.flowsById.set(e,i):this.flowsById.delete(e)})),this.flowsById.set(e,a),a.begin(r,s),a}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveOpenAddress(e,t){if(e!==void 0){let r=se(this.host,e);if(r===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:r};let s=ie(this.host,r,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:r,blockId:s}}if(t!==void 0){if(!this.host.blockIndex.has(t))throw new Error(`unknown block: ${t}`);return{blockId:t}}if(Object.keys(this.host.bundle.scenes).length===0)throw new Error("no scenes in bundle");return{}}resolveSceneRef(e){if(e!=null)return se(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:ie(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?y(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?y(t):void 0}tagsForBeat(e){return I(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?I(this.host,t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?I(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let r=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(r!=null?this.host.blockById.get(r):void 0)?.gameData}}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Be(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Be(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...y(e)?{gameId:y(e)}:{},name:e.name,...Ae(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...y(t)?{gameId:y(t)}:{},name:t.name,...Ae(t.gameData),...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)R(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=I(this.host,e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[ee(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}if(e.direction!==void 0&&(r.direction=e.direction),e.qualifier!==void 0){r.qualifier=e.qualifier;let s=this.host.defaultStrings[te(e.qualifier)]??this.host.qualifierDisplay.get(e.qualifier);s!==void 0&&(r.qualifierName=s)}}if(e.kind==="line"||e.kind==="text"){let s=this.host.padIndex.get(e.id);r.padAfter=s?.resolved??x,s?.own!==void 0&&(r.ownPadAfter=s.own);let i=this.host.defaultStrings[e.id];i!==void 0&&(r.text=i)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=I(this.host,e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}checkpoint(){if(this.host.journal)throw new Error("a checkpoint is already open: roll it back or commit it first");let e={undo:[],flows:new Set,opened:new Set,selectors:new Set,flowSelectors:new Map};return this.host.journal=e,new j(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let r=t.length-1;r>=0;r--)t[r]();n.forget(e.journal)}commit(e){this.endCheckpoint(e),n.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}captureFlow(e){let t=this.flowsById.get(e);if(!t)throw new Error(`no open flow \'${e}\' to capture`);return{version:F,...t.snapshot(),...St(e,this.host.registry.save())}}restoreFlow(e,t){this.refuseInCheckpoint("restoreFlow");let r=t.version;if(r!==2&&r!==F)throw new Error(`unsupported flow state version: ${String(r)}`);let s=t.cursor;if(typeof s!="object"||s===null)throw new Error("malformed flow state: no cursor");if(this.flowsById.has(e))throw new Error(`a flow \'${e}\' is already open; close it before restoring another under that id`);this.assertExternalScopes();let i=S.flow(e);this.host.registry.discardParked(i),this.host.registry.load(qe(e,t),{keepParked:!0});let o=new D(e,this.host,this.defaultSeed);try{o.restore(t)}catch(a){throw o.close(),this.host.registry.discardParked(i),a}return this.flowsById.set(e,o),o}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(S.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(S.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});for(let r of Object.keys(Te(this.creationOptions)))t.has(r)&&t.remove(r)}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e),i=this.host.journal?this.host.registry.get(r,s):void 0;this.host.registry.set(r,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(r,s,i,{host:!0}))}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),this.host.tracing=!0,()=>{this.engineTraceHandlers.delete(e),this.host.tracing=this.host.logEnabled||this.engineTraceHandlers.size>0}}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...r?{scene:r}:{}})}getVisitCounts(){return Object.fromEntries(this.host.sharedVisits)}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:B(e),writable:e.writable??!0}))}splitShared(e){let t=Le(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:F,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:Ve(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==F)throw new Error(`unsupported save version: ${String(t)}`);let r=e.flows;if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("malformed save: no flows");this.assertExternalScopes();let s=this.host.registry;for(let[o,a]of this.flowsById)a.releaseBags(o in e.flows),a.close();this.flowsById.clear();for(let o of this.host.stageBags.keys())s.remove(S.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.version===2?mt(e):e.registry;i&&(this.host.ownsRegistry?s.load(i):s.load(i,{keepParked:!0})),this.host.sharedVisits.clear();for(let[o,a]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(o,a);this.host.sharedSelectors.clear();for(let[o,a]of je(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new D(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPrompt=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=O(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase();if(e==="patter")return ne(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(r);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:ne(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(r)}return this.registryQualities?.(e,t)??ne(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(r)}begin(e,t){if(this.host.journal&&!this.host.journal.opened.has(this))throw new Error("a flow can\'t be started or reset while a checkpoint is open");if(this.releaseBags(!1),this.host.registry.discardParked(S.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.clearPending(),this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.begin(e,t)}start(e,t){this.reset(e,t)}goto(e,t){if(this.closed)return!1;if(this.touch(),e==="END")return this.started=!0,this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=se(this.host,e);if(r===void 0)return!1;let s;return t!==void 0&&(s=ie(this.host,r,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.begin(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.clearPending()}clearPending(){this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null}touch(){let e=this.host.journal;if(!e||e.flows.has(this))return;e.flows.add(this);let{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p}=this,f=this.stack.map(g=>({...g}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p,stack:f})})}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.touch(),this.pendingPrompt){let e=this.pendingPrompt;return this.pendingPrompt=null,this.pendingPromptOwnerId=null,e.shown?this.promptResult(e.beat,e.shown):this.beatResult(e.beat)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit,this.activeSnippet.id);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getVisitCounts(){return Object.fromEntries(this.visitCounts)}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(o=>o.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);this.touch();let s=t.byId.get(e);this.host.tracing&&this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null;let i=this.host.replayPromptOnChoose?this.authoredPromptOf(s):void 0;this.pendingPrompt=i&&r.prompt?{beat:i,shown:{...r.prompt}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e),o=this.host.journal;if(s==="patter"){if(o){let a=this.host.patterSharedNames.has(i),c=a?this.host.patterBag.get(i):this.local.get(i),l=this.local;c!==void 0&&o.undo.push(a?()=>this.host.registry.set("patter",i,c):()=>l.set(i,c))}this.patterResolver.set(i,t)}else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);if(o){let a=this.sceneBagFor(this.currentSceneId,i),c=a?.get(i);a&&c!==void 0&&o.undo.push(()=>a.set(i,c))}this.sceneResolver.set(i,t)}else{let a=o?this.host.registry.get(s,i):void 0;this.host.registry.set(s,i,t,r?{host:!0}:void 0),o&&a!==void 0&&o.undo.push(()=>this.host.registry.set(s,i,a,{host:!0}))}}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:ft(this.pendingPrompt.shown)}:{},selectors:Ve(this.selectors)}}}restore(e){if(this.host.journal)throw new Error("a flow can\'t be restored while a checkpoint is open");this.restoreCursor(e),this.releaseBags(!0),this.mountLocal();for(let t of new Set([this.currentSceneId,...this.stack.map(r=>r.sceneId)]))t!==null&&this.host.bundle.scenes[t]&&this.ensureSceneBags(t)}restoreCursor(e){this.rngState=O(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;if(this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=je(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);if(!o)continue;r.set(i.id,o);let{prompt:a,...c}=i;s.push({...c,...a?{prompt:{...a,padAfter:a.padAfter??this.promptPadOf(o)}}:{}})}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPrompt=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId),s=r?this.authoredPromptOf(r):void 0;s?this.pendingPrompt={beat:s,...t.pendingPrompt?{shown:{...t.pendingPrompt,padAfter:t.pendingPrompt.padAfter??this.padOf(s.id)}}:{}}:this.pendingPromptOwnerId=null}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry,t.id)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.some(o=>o.eligible)){this.host.tracing&&this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.host.tracing&&this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.host.tracing&&this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter((c,l)=>t[l].eligible),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.host.tracing&&this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(g=>g.id),a=new Set(e.map(g=>g.id));if(r.bag===void 0&&(r.bag=o()),!r.bag.some(g=>a.has(g))){if(t==="once")return null;if(i){let g=e[s-1];return r.last=g.id,g}if(r.bag=o(),r.bag.length===0)return null}let c=r.bag.filter(g=>a.has(g)),l=r.last!==void 0&&c.length>1?c.indexOf(r.last):-1,p=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&p>=l&&p++;let f=c[p];return r.bag.splice(r.bag.indexOf(f),1),r.last=f,e.find(g=>g.id===f)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(p=>p.id));let l=new Set(r.bag);if(s=e.filter(p=>l.has(p.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(p=>p.id===r.last)??null:null}let i=-1,a=s.map(l=>{let p=this.specScore(l);return p>i&&(i=p),{c:l,s:p}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(f=>f.id===r.last):-1,p=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&p>=l&&p++,c=a[p]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,r){return he(e,i=>{try{return Ge(q(i,this.context(),Z))}catch(o){return this.reportError("best-match",r.id,r.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=this.host.journal,s;if(r&&(s=e.shared?r.selectors:r.flowSelectors.get(this),s||(s=new Set,r.flowSelectors.set(this,s))),r&&s&&!s.has(e.id)){s.add(e.id);let o=t.get(e.id),a=o?{...o,...o.bag?{bag:[...o.bag]}:{}}:void 0;r.undo.push(()=>{a?t.set(e.id,a):t.delete(e.id)})}let i=t.get(e.id);return i||(i={},t.set(e.id,i)),i}runEffects(e,t){for(let r of e??[]){let s;try{s=this.evalExpr(r.value);let i=this.host.tracing?this.getProperty(r.target):void 0;this.writeProperty(r.target,s,!1),this.host.tracing&&this.emit({type:"write",target:r.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,r.value,i)}}}eligible(e){if(!e.condition)return!0;try{return Ge(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,r,s){let i=s instanceof Error?s.message:String(s),o=r?.src;this.host.onError({flow:this.id,kind:e,node:t,...o?{source:o}:{},message:i}),this.host.tracing&&this.emit({type:"diagnostic",kind:e,node:t,...o?{source:o}:{},message:i})}evalExpr(e){return q(this.conditionAst(e),this.context(),Z)}conditionAst(e){let t=De.get(e);return t||(t=E(e.ast),De.set(e,t)),t}enter(e){let t=this.visitCounts,r=t.get(e);t.set(e,(r??0)+1);let s=this.host.sharedVisits,i=s.get(e);s.set(e,(i??0)+1),this.host.journal&&this.host.journal.undo.push(()=>{r===void 0?t.delete(e):t.set(e,r),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=Y(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=I(this.host,e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),padAfter:this.padOf(e.id),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,qualifier:c?void 0:e.qualifier,qualifierName:c?void 0:this.resolveQualifierName(e.qualifier),padAfter:this.padOf(e.id),gameData:e.gameData,...r}}}}interpolate(e){return ke(e,t=>this.getProperty(t))}stripCaptions(e){return xe(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id)),s=this.padOf(t.id);return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction,qualifier:t.qualifier,qualifierName:this.resolveQualifierName(t.qualifier),padAfter:s}:{kind:"text",text:r,padAfter:s}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let r=I(this.host,e.id),s=r&&r.length?{tags:r}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,padAfter:this.padOf(e.id),gameData:e.gameData,...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,qualifier:t.qualifier,qualifierName:t.qualifierName,padAfter:this.padOf(e.id),gameData:e.gameData,...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return R(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=ee(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}promptPadOf(e){let t=this.promptBeatOf(e);return t?this.padOf(t.id):x}padOf(e){return this.host.padIndex.get(e)?.resolved??x}resolveQualifierName(e){if(e===void 0||this.host.emitIds)return;let t=te(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.qualifierDisplay.get(e)}splitRef(e){return Le(this.host,e)}newLocal(){return new b(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=S.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:V}),this.registered.add(e)}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal;if(!this.sceneBags.has(e)){let i=new b(r.filter(c=>!t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),o=S.flowScene(this.id,e),a=Fe(this.host.registry,o,i,s!==null);this.registered.add(o),this.sceneBags.set(e,i),s&&s.undo.push(()=>{this.sceneBags.get(e)===i&&(this.host.registry.remove(o,a?{keep:!0}:void 0),this.registered.delete(o),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let i=new b(r.filter(a=>t.has(a.name.toLowerCase())),{pathPrefix:"@scene."}),o=Fe(this.host.registry,S.stage(e),i,s!==null);this.host.stageBags.set(e,i),s&&s.undo.push(()=>{this.host.stageBags.get(e)===i&&(this.host.registry.remove(S.stage(e),o?{keep:!0}:void 0),this.host.stageBags.delete(e))})}}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);if(!i)continue;let o=this.host.journal?i.get(s):void 0;o!==void 0&&this.host.journal.undo.push(()=>i.set(s,o)),i.set(s,B(r))}}};function Be(n,e){R(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function Ae(n){return n&&Object.keys(n).length?{gameData:n}:{}}function ft(n){let e={kind:n.kind,text:n.text};return n.character!==void 0&&(e.character=n.character),n.characterName!==void 0&&(e.characterName=n.characterName),n.direction!==void 0&&(e.direction=n.direction),n.qualifier!==void 0&&(e.qualifier=n.qualifier),n.qualifierName!==void 0&&(e.qualifierName=n.qualifierName),typeof n.padAfter=="number"&&(e.padAfter=n.padAfter),e}function Ve(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function je(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function Fe(n,e,t,r){let s=r?JSON.stringify(t.save()):"";return n.mountOwned(e,t,{owner:V}),r&&JSON.stringify(t.save())!==s}function Ne(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Te(n){return{...n.world?{world:n.world}:{},...n.hostScopes}}function ne(n,e,t){let r=n.qualityLadders.get(e);if(!r){r=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!r.has(i)&&r.set(i,s.stages)}n.qualityLadders.set(e,r)}return r}function Me(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function ht(n,e){let t=Me(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function se(n,e){return n.sceneGameIdToId.get(e)??(n.bundle.scenes[e]?e:void 0)}function ie(n,e,t){return n.blockGameIdToId.get(e)?.get(t)??(n.blockIndex.get(t)?.sceneId===e?t:void 0)}function Le(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=be(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function mt(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[S.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{}))Object.assign(e,qe(t,r));return e}function qe(n,e){let t={};e.scopes?.patter&&(t[S.flowGlobals(n)]=e.scopes.patter);for(let[r,s]of Object.entries(e.sceneBags??{}))t[S.flowScene(n,r)]=s;return t}function St(n,e){let t={scopes:{},sceneBags:{}},r=S.flow(n);for(let[s,i]of Object.entries(e)){if(!s.startsWith(r))continue;let o=s.slice(r.length);o==="patter"?t.scopes.patter=i:o.startsWith("scene/")&&(t.sceneBags[gt(o.slice(6))]=i)}return t}function Ge(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var yt=(n,e)=>n.shared??e;function oe(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:yt(n,e)}}function wt(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function vt(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function $e(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=y(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:y(c),name:c.name}))});for(let c of o.blocks)vt(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>oe(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>oe(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(wt)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>oe(o,!0)),scene:r},gameData:i,counts:e}}function He(n,e){return n.gameDataFields?.[e]??[]}function ae(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function _e(n,e){let t={};for(let r of n){let s=ae(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return Ze(bt);})();\n';
1620
+ var PLAYABLE_TIMING_JS = 'var GameSubtitles=(()=>{var l=Object.defineProperty;var a=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var u=(t,e)=>{for(var s in e)l(t,s,{get:e[s],enumerable:!0})},f=(t,e,s,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of c(e))!_.call(t,i)&&i!==s&&l(t,i,{get:()=>e[i],enumerable:!(n=a(e,i))||n.enumerable});return t};var p=t=>f(l({},"__esModule",{value:!0}),t);var d={};u(d,{estimateDuration:()=>h});function h(t,{charsPerSecond:e=14,minSeconds:s=1.5,maxSeconds:n=18}={}){let i=0;for(let o of t??"")o!=="\\xAD"&&i++;let r=e>0?e:14;return Math.min(Math.max(i/r,s),n)}return p(d);})();\n';
1621
+
1622
+ // src/play-timeline.ts
1623
+ function createTimeline(defaultPad) {
1624
+ let anchor = 0;
1625
+ let pad = null;
1626
+ let floor = 0;
1627
+ let last = 0;
1628
+ const startOf = () => pad === null ? anchor : Math.max(anchor + pad, floor);
1629
+ return {
1630
+ startOf,
1631
+ place(beat, duration) {
1632
+ const start = startOf();
1633
+ const end = start + Math.max(0, duration);
1634
+ anchor = end;
1635
+ pad = typeof beat.padAfter === "number" ? beat.padAfter : defaultPad;
1636
+ floor = start;
1637
+ last = Math.max(last, end);
1638
+ return { start, end };
1639
+ },
1640
+ choicesAt: () => floor,
1641
+ endAt: () => Math.max(last, anchor)
1642
+ };
1643
+ }
1644
+ function createClock(now, rate) {
1645
+ let wall = now;
1646
+ let script = 0;
1647
+ let r = rate;
1648
+ const position = (at) => r === Infinity ? script : script + (at - wall) * r / 1e3;
1649
+ return {
1650
+ at(seconds) {
1651
+ if (r === Infinity) {
1652
+ script = Math.max(script, seconds);
1653
+ return wall;
1654
+ }
1655
+ return wall + (seconds - script) * 1e3 / r;
1656
+ },
1657
+ position,
1658
+ setRate(at, rate2) {
1659
+ script = position(at);
1660
+ wall = at;
1661
+ r = rate2;
1662
+ },
1663
+ delay(ms) {
1664
+ if (ms > 0) wall += ms;
1665
+ }
1666
+ };
1667
+ }
1598
1668
 
1599
1669
  // src/export-html.ts
1600
1670
  var esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
@@ -1614,48 +1684,94 @@ var PLAYER_JS = String.raw`
1614
1684
  function add(cls, html) { var d = document.createElement("div"); d.className = cls; d.innerHTML = html; stage.appendChild(d); return d; }
1615
1685
  function esc(s) { return String(s == null ? "" : s).replace(/[&<>]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]; }); }
1616
1686
 
1617
- // Pacing: a source-only reader carries no audio, so each line is held on screen for a reading-length delay
1618
- // (proportional to its word count) before the next appears, and every line fades in (see CSS). The Speed
1619
- // control sets the reading pace (words/min); "Instant" drops the delay entirely. The choice is persisted.
1620
- // A run token cancels an in-flight paced read the moment the player restarts, loads, or picks a choice.
1621
- var SPEED_WPM = { slow: 300, normal: 500, fast: 800, instant: 0 }, runToken = 0;
1687
+ // Pacing (line padding): a source-only reader carries no audio, so each line is timed by the estimate
1688
+ // Patterstage uses for a line with no recording (game-subtitles' estimateDuration), and the step's padAfter
1689
+ // says when the next one starts: after a pause, at once, or cutting in before this one ends. The player
1690
+ // rules are play-timeline.ts's, inlined below by their source: the first line starts at once, as does the
1691
+ // reply to a choice; a cut-in never starts before the line it cuts into; a game event is never held here,
1692
+ // so it's done at once; the choices appear as the last line before them starts (while it plays); and the
1693
+ // end comes when every line still playing has finished. Every line fades in (see CSS).
1694
+ // The Speed control scales the whole timing (half, normal, or double speed) or drops it (instant), from
1695
+ // the moment it's changed: the clock keeps the time already played and re-times only what's to come. It
1696
+ // is stored under the key Patterpad's Play window uses, so the two keep one choice; the old reading paces
1697
+ // carry over (slow is half speed, fast is double).
1698
+ // A run token cancels an in-flight wait the moment the player restarts, loads, or picks a choice.
1699
+ var createTimeline = (${createTimeline.toString()});
1700
+ var createClock = (${createClock.toString()});
1701
+ var DEFAULT_PAD = ${DEFAULT_PAD_AFTER}; // the built-in pause, for a bundle from before padding
1702
+ var SPEED_RATE = { half: 0.5, normal: 1, double: 2, instant: Infinity }, OLD_SPEED = { slow: "half", fast: "double" }, runToken = 0;
1703
+ function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }
1622
1704
  var speedKey = (function () { try { return localStorage.getItem("patter.playSpeed") || "normal"; } catch (e) { return "normal"; } })();
1623
- if (SPEED_WPM[speedKey] === undefined) speedKey = "normal";
1624
- function fakeDuration(text) {
1625
- var wpm = SPEED_WPM[speedKey];
1626
- if (!wpm) return 0; // "Instant" - reveal the next line immediately
1627
- var words = String(text == null ? "" : text).trim().split(/\s+/).filter(Boolean).length;
1628
- return Math.max(450, Math.round((words / wpm) * 60000)); // a beat needs to land
1705
+ if (own(OLD_SPEED, speedKey)) speedKey = OLD_SPEED[speedKey];
1706
+ if (!own(SPEED_RATE, speedKey)) speedKey = "normal";
1707
+ var estimate = window.GameSubtitles.estimateDuration;
1708
+ var clock = null, wake = null; // the run's clock, and a hook that re-times the wait in flight
1709
+ // Call then() when script time "at" comes on the run's clock. A change of speed wakes the wait, which
1710
+ // works out the time again; a superseded run's wait just stops.
1711
+ function waitUntil(at, token, then) {
1712
+ if (token !== runToken) return;
1713
+ var ms = clock.at(at) - Date.now();
1714
+ if (ms <= 0) { wake = null; then(); return; }
1715
+ var t = setTimeout(function () { waitUntil(at, token, then); }, ms);
1716
+ wake = function () { clearTimeout(t); waitUntil(at, token, then); };
1629
1717
  }
1630
1718
  function scrollDown() { window.scrollTo(0, document.body.scrollHeight); }
1631
1719
  var speedSel = document.getElementById("speed");
1632
1720
  if (speedSel) {
1633
- speedSel.value = speedKey; // a fresh fakeDuration() reads speedKey each line, so a change applies next line
1634
- speedSel.onchange = function () { speedKey = speedSel.value; try { localStorage.setItem("patter.playSpeed", speedKey); } catch (e) {} };
1721
+ speedSel.value = speedKey;
1722
+ speedSel.onchange = function () {
1723
+ speedKey = speedSel.value;
1724
+ try { localStorage.setItem("patter.playSpeed", speedKey); } catch (e) {}
1725
+ if (clock) clock.setRate(Date.now(), SPEED_RATE[speedKey]);
1726
+ if (wake) wake();
1727
+ };
1635
1728
  }
1636
1729
 
1637
1730
  function build() { engine = new Engine(BUNDLE); flow = engine.openFlow("main", { scene: startScene }); }
1638
1731
 
1639
1732
  function newGame() { stage.innerHTML = ""; controls.innerHTML = ""; build(); run(); }
1640
1733
 
1734
+ // The player reads one step ahead of what it shows, as a game's would, to know whether a choice, the end,
1735
+ // or another line follows the line on screen. While a step is read but not yet shown, the engine is past
1736
+ // what the reader sees, so Save keeps the game as it was before the read (resumeSave).
1737
+ var resumeSave = null;
1738
+ function readNext() {
1739
+ try { resumeSave = JSON.stringify(engine.saveGame()); } catch (e) { resumeSave = null; }
1740
+ var s = flow.advance();
1741
+ while (s && s.type === "gameEvent") s = flow.advance(); // a host cue with nothing to show, done at once
1742
+ return s;
1743
+ }
1744
+
1641
1745
  function run() {
1642
1746
  controls.innerHTML = "";
1643
1747
  var token = ++runToken;
1644
- (function step() {
1645
- if (token !== runToken) return; // superseded by a restart / load / choice pick
1646
- var s = flow.advance();
1647
- if (!s) { add("end", "The End"); return; } // defensive: a step should always be returned; never hard-crash
1748
+ var timeline = createTimeline(DEFAULT_PAD);
1749
+ clock = createClock(Date.now(), SPEED_RATE[speedKey]);
1750
+ // A step read ahead appears when its time comes: a line or text when its pause is over, the choices
1751
+ // as the last line starts, the end as the last line still playing ends.
1752
+ function next(s) {
1753
+ var at = !s || s.type === "end" ? timeline.endAt() : s.type === "choice" ? timeline.choicesAt() : timeline.startOf();
1754
+ waitUntil(at, token, function () { show(s); });
1755
+ }
1756
+ function show(s) {
1757
+ resumeSave = null; // what's shown is where the engine is
1758
+ if (!s || s.type === "end") { add("end", "The End"); return; } // no step is defensive: never hard-crash
1759
+ if (s.type === "choice") { renderChoice(s); return; }
1760
+ timeline.place(s, estimate(s.text));
1648
1761
  if (s.type === "line") {
1649
1762
  var who = s.characterName || s.character || "";
1763
+ // A speaker qualifier follows the name, as the script's cue has it: TAM (O.S.).
1764
+ var qual = s.qualifierName || s.qualifier;
1765
+ if (qual && who) who += " (" + qual + ")";
1650
1766
  var dir = s.direction ? '<em class="dir">(' + esc(s.direction) + ')</em> ' : "";
1651
1767
  add("line", '<span class="who" style="color:hsl(' + hueOf(s.character || who) + ',55%,38%)">' + esc(who) + '</span>' + dir + esc(s.text));
1652
- scrollDown(); setTimeout(step, fakeDuration(s.text));
1653
- } else if (s.type === "text") {
1654
- add("text", esc(s.text)); scrollDown(); setTimeout(step, fakeDuration(s.text));
1655
- } else if (s.type === "choice") { renderChoice(s); }
1656
- else if (s.type === "end") { add("end", "The End"); }
1657
- else { step(); } // game-event beat: a host cue with no player-facing text - skip it, advance at once
1658
- })();
1768
+ } else {
1769
+ add("text", esc(s.text));
1770
+ }
1771
+ scrollDown();
1772
+ next(readNext());
1773
+ }
1774
+ next(readNext()); // the first line (or the reply to a choice) starts at once
1659
1775
  }
1660
1776
 
1661
1777
  function renderChoice(step) {
@@ -1673,7 +1789,7 @@ var PLAYER_JS = String.raw`
1673
1789
  }
1674
1790
 
1675
1791
  document.getElementById("restart").onclick = newGame;
1676
- document.getElementById("save").onclick = function () { try { localStorage.setItem(SAVE_KEY, JSON.stringify(engine.saveGame())); flash("Saved"); } catch (e) {} };
1792
+ document.getElementById("save").onclick = function () { try { localStorage.setItem(SAVE_KEY, resumeSave || JSON.stringify(engine.saveGame())); flash("Saved"); } catch (e) {} };
1677
1793
  document.getElementById("load").onclick = function () {
1678
1794
  var blob; try { blob = localStorage.getItem(SAVE_KEY); } catch (e) {}
1679
1795
  if (!blob) { flash("No save yet"); return; }
@@ -1745,10 +1861,10 @@ ${head}
1745
1861
  <button id="save">Save</button>
1746
1862
  <button id="load">Load</button>
1747
1863
  <label class="speedlabel">Speed
1748
- <select id="speed" title="Reading speed: how long each line is held before the next appears">
1749
- <option value="slow">Slow</option>
1864
+ <select id="speed" title="How fast the scene plays: its timing at half speed, as written, at double speed, or with no waits">
1865
+ <option value="half">Half</option>
1750
1866
  <option value="normal">Normal</option>
1751
- <option value="fast">Fast</option>
1867
+ <option value="double">Double</option>
1752
1868
  <option value="instant">Instant</option>
1753
1869
  </select>
1754
1870
  </label>
@@ -1766,6 +1882,7 @@ function runExportHtml(loaded) {
1766
1882
  lang,
1767
1883
  `<style>${STYLE}</style>`,
1768
1884
  `<script>${PLAYABLE_RUNTIME_JS}</script>
1885
+ <script>${PLAYABLE_TIMING_JS}</script>
1769
1886
  <script>window.PATTER_BUNDLE=${bundleJson};</script>
1770
1887
  <script>${PLAYER_JS}</script>`
1771
1888
  );
@@ -1784,6 +1901,7 @@ function runExportWeb(loaded) {
1784
1901
  styleCss: `${STYLE.trim()}
1785
1902
  `,
1786
1903
  patterplayJs: `${PLAYABLE_RUNTIME_JS}
1904
+ ${PLAYABLE_TIMING_JS}
1787
1905
  ${PLAYER_JS}`,
1788
1906
  storyJs: `window.PATTER_BUNDLE=${bundleJson};
1789
1907
  `
@@ -1902,9 +2020,11 @@ function renderPlay(result) {
1902
2020
  const out = [];
1903
2021
  for (const e of result.events) {
1904
2022
  switch (e.type) {
1905
- case "line":
1906
- out.push(`${e.character ?? "?"}: ${e.text}`);
2023
+ case "line": {
2024
+ const q = e.qualifierName ?? e.qualifier;
2025
+ out.push(`${e.character ?? "?"}${q ? ` (${q})` : ""}: ${e.text}`);
1907
2026
  break;
2027
+ }
1908
2028
  case "text":
1909
2029
  out.push(` ${e.text}`);
1910
2030
  break;
@@ -2861,7 +2981,7 @@ async function runReportXlsx(data) {
2861
2981
  // src/localisation.ts
2862
2982
  import { basename as basename5, join as join7, sep as sep3 } from "path";
2863
2983
  import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
2864
- import { walkNodes as walkNodes2, castStringKey, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2, projectLayout as projectLayout3, STRINGS_SCHEMA, AUTHORING_SCHEMA } from "@patterkit/model";
2984
+ import { walkNodes as walkNodes2, castStringKey, qualifierStringKey, usedQualifiers, projectQualifiers, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2, projectLayout as projectLayout3, STRINGS_SCHEMA, AUTHORING_SCHEMA } from "@patterkit/model";
2865
2985
 
2866
2986
  // src/documentation.ts
2867
2987
  import { DEFAULT_DOCUMENTATION_CLASSES } from "@patterkit/model";
@@ -2926,6 +3046,11 @@ function extractLoc(loaded, opts = {}) {
2926
3046
  const g = context?.character ? genderOf.get(context.character) : void 0;
2927
3047
  return g ? { ...context, gender: g } : context;
2928
3048
  };
3049
+ const qualifierName = new Map(projectQualifiers(loaded.project).map((q) => [q.gameId, q.name]));
3050
+ const lineContext = (b) => {
3051
+ const q = b.kind === "line" && b.qualifier ? qualifierName.get(b.qualifier) ?? b.qualifier : void 0;
3052
+ return { character: b.kind === "line" ? b.character : void 0, kind: b.kind, ...q ? { qualifier: q } : {} };
3053
+ };
2929
3054
  const entries = [];
2930
3055
  const push = (id, scene, context) => {
2931
3056
  const src = source[id];
@@ -2938,11 +3063,11 @@ function extractLoc(loaded, opts = {}) {
2938
3063
  walkNodes2(block.children, (node) => {
2939
3064
  if (node.type === "group") {
2940
3065
  const p = node.prompt;
2941
- if (p) push(p.id, scene.id, { character: p.kind === "line" ? p.character : void 0, kind: p.kind });
3066
+ if (p) push(p.id, scene.id, lineContext(p));
2942
3067
  return;
2943
3068
  }
2944
3069
  for (const beat of node.beats ?? []) {
2945
- if (beat.kind !== "gameEvent") push(beat.id, scene.id, { character: beat.kind === "line" ? beat.character : void 0, kind: beat.kind });
3070
+ if (beat.kind !== "gameEvent") push(beat.id, scene.id, lineContext(beat));
2946
3071
  }
2947
3072
  });
2948
3073
  }
@@ -2954,6 +3079,12 @@ function extractLoc(loaded, opts = {}) {
2954
3079
  const translation = isTemplate ? "" : target[id] ?? "";
2955
3080
  entries.push({ id, scene: PROJECT_LOCALE_SCENE2, source: src, translation, comments: commentsOf(id), context: withGender({ character: c.name }), stale: staleFor(id, translation) });
2956
3081
  }
3082
+ for (const q of usedQualifiers(loaded.project, loaded.scenes)) {
3083
+ const id = qualifierStringKey(q.gameId);
3084
+ const src = source[id] ?? q.name;
3085
+ const translation = isTemplate ? "" : target[id] ?? "";
3086
+ entries.push({ id, scene: PROJECT_LOCALE_SCENE2, source: src, translation, comments: commentsOf(id), context: { kind: "qualifier" }, stale: staleFor(id, translation) });
3087
+ }
2957
3088
  return { project: loaded.project.project.id, defaultLocale, locale: targetLocale, entries };
2958
3089
  }
2959
3090
  var layoutOf = (loaded) => projectLayout3(loaded.project);
@@ -3089,7 +3220,8 @@ function catalogToPo(catalog) {
3089
3220
  for (const e of catalog.entries) {
3090
3221
  for (const c of e.comments) out.push(`#. ${c}`);
3091
3222
  if (e.context?.character || e.context?.kind) {
3092
- out.push(`#. [${[e.context.kind, e.context.character].filter(Boolean).join(" ")}]`);
3223
+ const speaker = e.context.character && e.context.qualifier ? `${e.context.character} (${e.context.qualifier})` : e.context.character;
3224
+ out.push(`#. [${[e.context.kind, speaker].filter(Boolean).join(" ")}]`);
3093
3225
  }
3094
3226
  if (e.context?.gender) out.push(`#. Gender: ${e.context.gender}`);
3095
3227
  out.push(`#: ${e.scene}`);
@@ -3185,7 +3317,7 @@ function poToCatalog(text) {
3185
3317
  }
3186
3318
 
3187
3319
  // src/loc-xlsx.ts
3188
- var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender", "Scene"];
3320
+ var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender", "Scene", "Qualifier"];
3189
3321
  var SCENE_COLUMN = HEADERS.indexOf("Scene") + 1;
3190
3322
  var SHEET_NAME_MAX = 31;
3191
3323
  function uniqueSheetName(scene, used) {
@@ -3228,7 +3360,9 @@ async function catalogToXlsx(catalog) {
3228
3360
  { header: "Status", key: "status", width: 10 },
3229
3361
  { header: "Gender", key: "gender", width: 12 },
3230
3362
  // Hidden: the translator has no use for it, and it is the import's only faithful record of the scene.
3231
- { header: "Scene", key: "scene", width: 22, hidden: true }
3363
+ { header: "Scene", key: "scene", width: 22, hidden: true },
3364
+ // After the hidden Scene column, so the columns an older reader finds by position stay where they were.
3365
+ { header: "Qualifier", key: "qualifier", width: 11 }
3232
3366
  ];
3233
3367
  ws.getRow(1).font = { bold: true };
3234
3368
  ws.views = [{ state: "frozen", ySplit: 1 }];
@@ -3240,7 +3374,8 @@ async function catalogToXlsx(catalog) {
3240
3374
  comments: e.comments.join("\n"),
3241
3375
  status: e.stale ? "stale" : e.translation ? "translated" : "",
3242
3376
  gender: e.context?.gender ?? "",
3243
- scene
3377
+ scene,
3378
+ qualifier: e.context?.qualifier ?? ""
3244
3379
  });
3245
3380
  }
3246
3381
  }
@@ -3281,7 +3416,7 @@ async function xlsxToCatalog(buffer) {
3281
3416
  }
3282
3417
 
3283
3418
  // src/voice-script.ts
3284
- import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES2 } from "@patterkit/model";
3419
+ import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES2, projectQualifiers as projectQualifiers2 } from "@patterkit/model";
3285
3420
  function plainVoice(text) {
3286
3421
  return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
3287
3422
  }
@@ -3305,6 +3440,7 @@ function runVoiceScript(loaded, opts = {}) {
3305
3440
  const source = sourceStrings(loaded);
3306
3441
  const actorOf = /* @__PURE__ */ new Map();
3307
3442
  for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
3443
+ const qualifierName = new Map(projectQualifiers2(project).map((q) => [q.gameId, q.name]));
3308
3444
  const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, "vo");
3309
3445
  const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
3310
3446
  const lines = [];
@@ -3317,6 +3453,7 @@ function runVoiceScript(loaded, opts = {}) {
3317
3453
  scope: scope.join(" \u203A "),
3318
3454
  id: beat.id,
3319
3455
  character: beat.character ?? "",
3456
+ ...beat.qualifier ? { qualifier: qualifierName.get(beat.qualifier) ?? beat.qualifier } : {},
3320
3457
  actor: beat.character ? actorOf.get(beat.character) : void 0,
3321
3458
  text: plainVoice(source[beat.id] ?? ""),
3322
3459
  comments: leadingVo ? [...leadingVo, ...own] : own,
@@ -3364,6 +3501,7 @@ async function voiceScriptToXlsx(data) {
3364
3501
  { header: "Scope", key: "scope", width: 28 },
3365
3502
  { header: "Line ID", key: "id", width: 18 },
3366
3503
  { header: "Character", key: "character", width: 16 },
3504
+ { header: "Qualifier", key: "qualifier", width: 11 },
3367
3505
  { header: "Actor", key: "actor", width: 16 },
3368
3506
  { header: "Text", key: "text", width: 60 },
3369
3507
  { header: "Comments", key: "comments", width: 36 },
@@ -3376,6 +3514,7 @@ async function voiceScriptToXlsx(data) {
3376
3514
  scope: l.scope,
3377
3515
  id: l.id,
3378
3516
  character: l.character,
3517
+ qualifier: l.qualifier ?? "",
3379
3518
  actor: l.actor ?? "",
3380
3519
  text: l.text,
3381
3520
  comments: l.comments.join("\n"),
@@ -3388,7 +3527,7 @@ async function voiceScriptToXlsx(data) {
3388
3527
  }
3389
3528
 
3390
3529
  // src/script-doc.ts
3391
- import { DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3 } from "@patterkit/model";
3530
+ import { DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3, projectQualifiers as projectQualifiers3 } from "@patterkit/model";
3392
3531
  import { humanizeNodeRefs } from "@patterkit/core";
3393
3532
  var TOKENS = {
3394
3533
  ink: "26221c",
@@ -3434,6 +3573,13 @@ function colourIndex(name) {
3434
3573
  function characterColour(name) {
3435
3574
  return CHAR_PALETTE[colourIndex(name)];
3436
3575
  }
3576
+ function cueLabel(character, qualifierName) {
3577
+ return qualifierName ? `${character.toUpperCase()} (${qualifierName.toUpperCase()})` : character.toUpperCase();
3578
+ }
3579
+ function qualifierNamer(project) {
3580
+ const names = new Map(projectQualifiers3(project).map((q) => [q.gameId, q.name]));
3581
+ return (gameId) => gameId ? names.get(gameId) ?? gameId : void 0;
3582
+ }
3437
3583
  var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
3438
3584
  var INTERP_RE = /\{@[^}]*\}/g;
3439
3585
  function textRuns(text) {
@@ -3512,6 +3658,12 @@ function runScriptDoc(loaded, opts = {}) {
3512
3658
  }
3513
3659
  const humanize = (cond) => humanizeNodeRefs(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
3514
3660
  const els = [];
3661
+ const nameOf = qualifierNamer(project);
3662
+ const speaker = (beat) => ({
3663
+ character: beat.character ?? "",
3664
+ ...beat.qualifier ? { qualifier: beat.qualifier, qualifierName: nameOf(beat.qualifier) } : {},
3665
+ direction: beat.direction
3666
+ });
3515
3667
  const textOf = (id) => source[id] ?? "";
3516
3668
  const notes = (id, indent, sid) => {
3517
3669
  for (const l of ownNotes(id)) els.push({ kind: "note", id, indent, snippet: sid, text: l.text, class: l.type });
@@ -3525,7 +3677,7 @@ function runScriptDoc(loaded, opts = {}) {
3525
3677
  const emitBeats = (node, indent, sid) => {
3526
3678
  for (const beat of node.beats ?? []) {
3527
3679
  if (cut.has(beat.id)) continue;
3528
- if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3680
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent, snippet: sid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
3529
3681
  else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
3530
3682
  else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent, snippet: sid, text: gameEventLabel(beat) });
3531
3683
  else continue;
@@ -3570,7 +3722,7 @@ function runScriptDoc(loaded, opts = {}) {
3570
3722
  continue;
3571
3723
  }
3572
3724
  if (cut.has(beat.id)) continue;
3573
- if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3725
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent: indent + 1, snippet: cid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
3574
3726
  else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
3575
3727
  else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
3576
3728
  else continue;
@@ -3657,7 +3809,7 @@ function paragraph(el, prev) {
3657
3809
  });
3658
3810
  case "line": {
3659
3811
  const kids2 = [];
3660
- if (el.character) kids2.push(new TextRun({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
3812
+ if (el.character) kids2.push(new TextRun({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
3661
3813
  if (el.direction) kids2.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
3662
3814
  kids2.push(...bodyRuns(el.runs, TOKENS.inkRead));
3663
3815
  return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids2 });
@@ -3814,13 +3966,14 @@ async function scriptToPdf(doc) {
3814
3966
  case "line": {
3815
3967
  const cueX = cx + DIALOGUE_INDENT2;
3816
3968
  let bodyX = cueX;
3817
- if (el.character) {
3969
+ const cue = el.character ? cueLabel(el.character, el.qualifierName) : "";
3970
+ if (cue) {
3818
3971
  pdf.font("Sans-Bold").fontSize(CUE);
3819
- bodyX = cueX + pdf.widthOfString(el.character.toUpperCase()) + CUE_GAP;
3972
+ bodyX = cueX + pdf.widthOfString(cue) + CUE_GAP;
3820
3973
  }
3821
3974
  const yTop = pdf.y;
3822
- if (el.character) {
3823
- pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character))).text(el.character.toUpperCase(), cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
3975
+ if (cue) {
3976
+ pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character))).text(cue, cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
3824
3977
  pdf.y = yTop;
3825
3978
  }
3826
3979
  pdf.fontSize(BODY);
@@ -4742,8 +4895,9 @@ function staleParts(s, liveText, line) {
4742
4895
  const stale = [];
4743
4896
  if (liveText !== s.baseline) stale.push("the text");
4744
4897
  if (s.proposedCharacter !== void 0 && (line?.character ?? "") !== (s.baselineCharacter ?? "")) stale.push("the speaker");
4898
+ if (s.proposedQualifier !== void 0 && (line?.qualifier ?? "") !== (s.baselineQualifier ?? "")) stale.push("the speaker qualifier");
4745
4899
  if (s.proposedDirection !== void 0 && (line?.direction ?? "") !== (s.baselineDirection ?? "")) stale.push("the direction");
4746
- if ((s.proposedCharacter !== void 0 || s.proposedDirection !== void 0) && !line) stale.push("the line (no longer a spoken line)");
4900
+ if ((s.proposedCharacter !== void 0 || s.proposedQualifier !== void 0 || s.proposedDirection !== void 0) && !line) stale.push("the line (no longer a spoken line)");
4747
4901
  return stale;
4748
4902
  }
4749
4903
  function listOpenSuggestions(loaded, filter = {}) {
@@ -4898,6 +5052,12 @@ function applySuggestionDecisions(loaded, decisions, opts = {}) {
4898
5052
  work.touch(flow.path);
4899
5053
  edited = true;
4900
5054
  }
5055
+ if (line && flow && s.proposedQualifier !== void 0 && s.proposedQualifier !== (line.qualifier ?? "")) {
5056
+ if (s.proposedQualifier === "") delete line.qualifier;
5057
+ else line.qualifier = s.proposedQualifier;
5058
+ work.touch(flow.path);
5059
+ edited = true;
5060
+ }
4901
5061
  if (line && flow && s.proposedDirection !== void 0 && s.proposedDirection !== (line.direction ?? "")) {
4902
5062
  if (s.proposedDirection === "") delete line.direction;
4903
5063
  else line.direction = s.proposedDirection;
@@ -4990,7 +5150,7 @@ async function exportEditableScript(loaded, opts) {
4990
5150
  if (isEditable(el)) {
4991
5151
  const code = markerOf.get(el);
4992
5152
  const baseline = source[el.id] ?? "";
4993
- lines[code] = el.kind === "line" ? { id: el.id, kind: "line", character: el.character, ...el.direction ? { direction: el.direction } : {}, baseline } : { id: el.id, kind: el.kind === "option" ? "option" : "narration", baseline };
5153
+ lines[code] = el.kind === "line" ? { id: el.id, kind: "line", character: el.character, ...el.qualifier ? { qualifier: el.qualifier, ...el.qualifierName ? { qualifierName: el.qualifierName } : {} } : {}, ...el.direction ? { direction: el.direction } : {}, baseline } : { id: el.id, kind: el.kind === "option" ? "option" : "narration", baseline };
4994
5154
  skeleton.push({ kind: "box", marker: code });
4995
5155
  } else {
4996
5156
  skeleton.push({ kind: el.kind, node: el.id, text: contextText(el) });
@@ -5057,7 +5217,7 @@ function box(el, code, status) {
5057
5217
  const textW = Math.max(MIN_TEXT, FULL - ind - LEAD2 - MARGIN2);
5058
5218
  const cell = (children, width, extra = {}) => new TableCell({ width: { size: width, type: WidthType.DXA }, borders: { top: NONE, bottom: NONE, left: NONE, right: NONE }, margins: { top: 60, bottom: 60, left: 100, right: 100 }, children, ...extra });
5059
5219
  const lead = [];
5060
- if (el.kind === "line" && el.character) lead.push(new TextRun2({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character) }));
5220
+ if (el.kind === "line" && el.character) lead.push(new TextRun2({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character) }));
5061
5221
  if (el.kind === "line" && el.direction) lead.push(new TextRun2({ text: `(${el.direction})`, font: SERIF, italics: true, size: S.cue, color: TOKENS.muted, break: el.character ? 1 : 0 }));
5062
5222
  if (el.kind === "option") lead.push(new TextRun2({ text: "\u25C7", color: TOKENS.accent, size: S.body }));
5063
5223
  const ink = el.kind === "narration" ? TOKENS.inkSoft : el.kind === "option" ? TOKENS.ink : TOKENS.inkRead;
@@ -5430,7 +5590,7 @@ async function readEditableDocx(bytes) {
5430
5590
 
5431
5591
  // src/editable-import.ts
5432
5592
  import { canonicalStringify as canonicalStringify12 } from "@patterkit/core";
5433
- import { AUTHORING_SCHEMA as AUTHORING_SCHEMA4 } from "@patterkit/model";
5593
+ import { AUTHORING_SCHEMA as AUTHORING_SCHEMA4, projectQualifiers as projectQualifiers4 } from "@patterkit/model";
5434
5594
  var REFUSE_MISSING_SHARE = 0.25;
5435
5595
  var tidy = (s) => s.replace(/[\s   ]+/g, " ").trim();
5436
5596
  function normalise(s, strictQuotes) {
@@ -5523,6 +5683,7 @@ function planEditableImport(loaded, returned, opts) {
5523
5683
  const places = indexPlaces(loaded);
5524
5684
  const live = sourceStrings(loaded);
5525
5685
  const cast = loaded.project.cast ?? [];
5686
+ const qualifiers = projectQualifiers4(loaded.project);
5526
5687
  const suggestions = [];
5527
5688
  const comments = [];
5528
5689
  let unchanged = 0, stale = 0;
@@ -5606,22 +5767,25 @@ function planEditableImport(loaded, returned, opts) {
5606
5767
  problems.push({ severity: "info", kind: "untracked-edits", marker: code, anchor: sent.id, message: "This line has tracked changes and some untracked ones too, so the credit may be incomplete." });
5607
5768
  }
5608
5769
  const cue = sent.kind === "line" ? readLead(b.lead.proposed) : void 0;
5609
- let proposedCharacter, proposedDirection;
5770
+ let proposedCharacter, proposedQualifier, proposedDirection;
5610
5771
  if (sent.kind === "line" && cue) {
5611
- const sentCue = (sent.character ?? "").toUpperCase();
5612
- if (cue.character && cue.character.toUpperCase() !== sentCue) {
5613
- const match = cast.find((c) => c.name.toUpperCase() === cue.character.toUpperCase());
5614
- if (match) proposedCharacter = match.name;
5615
- else {
5616
- problems.push({ severity: "warning", kind: "unknown-speaker", marker: code, anchor: sent.id, message: `The speaker was changed to ${cue.character}, who isn't in the cast; it's in a comment.` });
5617
- comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sent.character ?? "(none)"} to ${cue.character}, who isn't in the cast.`, ts);
5618
- }
5772
+ const read = readCue(cue.character, sent, cast, qualifiers);
5773
+ if (read.unknownSpeaker !== void 0) {
5774
+ problems.push({ severity: "warning", kind: "unknown-speaker", marker: code, anchor: sent.id, message: `The speaker was changed to ${read.unknownSpeaker}, who isn't in the cast; it's in a comment.` });
5775
+ comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sentCue(sent, qualifiers) || "(none)"} to ${read.unknownSpeaker}, who isn't in the cast.`, ts);
5619
5776
  }
5777
+ if (read.unknownQualifier !== void 0) {
5778
+ problems.push({ severity: "warning", kind: "unknown-qualifier", marker: code, anchor: sent.id, message: `The speaker qualifier was changed to (${read.unknownQualifier}), which isn't one of the project's qualifiers; it's in a comment.` });
5779
+ comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sentCue(sent, qualifiers) || "(none)"} to ${tidy(cue.character)}, but (${read.unknownQualifier}) isn't one of the project's qualifiers.`, ts);
5780
+ }
5781
+ if (read.character !== void 0 && read.character !== (sent.character ?? "")) proposedCharacter = read.character;
5782
+ if (read.qualifier !== void 0 && read.qualifier !== (sent.qualifier ?? "")) proposedQualifier = read.qualifier;
5620
5783
  if (tidy(cue.direction) !== tidy(sent.direction ?? "")) proposedDirection = tidy(cue.direction);
5621
5784
  }
5785
+ const cueChanged = !!proposedCharacter || proposedQualifier !== void 0 || proposedDirection !== void 0;
5622
5786
  for (const note of notes) comment(here ?? nearestLine(boxes.indexOf(b)), author, note, ts);
5623
5787
  if (!here) {
5624
- if (textChanged || proposedCharacter || proposedDirection !== void 0) {
5788
+ if (textChanged || cueChanged) {
5625
5789
  problems.push({ severity: "warning", kind: "line-gone", marker: code, message: `The line ${quote(sent.baseline)} is no longer in the project; the editor's version is in a comment nearby.` });
5626
5790
  comment(nearestLine(boxes.indexOf(b)), author, `For a line no longer in the project (${quote(sent.baseline)}), the editor wrote: ${quote(proposed)}`, ts);
5627
5791
  } else unchanged++;
@@ -5634,16 +5798,17 @@ function planEditableImport(loaded, returned, opts) {
5634
5798
  if (textChanged && !sameList(placeholders(proposed), placeholders(sent.baseline))) {
5635
5799
  problems.push({ severity: "warning", kind: "placeholders", marker: code, anchor: sent.id, message: `The {@\u2026} placeholders changed, so this line wasn't suggested; the editor's version is in a comment.` });
5636
5800
  comment(here, author, `The editor's version changes the {@\u2026} placeholders (${placeholders(sent.baseline).join(", ") || "none"} became ${placeholders(proposed).join(", ") || "none"}), so it wasn't made a suggestion: ${quote(proposed)}`, ts);
5637
- if (!proposedCharacter && proposedDirection === void 0) continue;
5801
+ if (!cueChanged) continue;
5638
5802
  }
5639
5803
  const keepText = textChanged && sameList(placeholders(proposed), placeholders(sent.baseline));
5640
- if (!keepText && !proposedCharacter && proposedDirection === void 0) {
5804
+ if (!keepText && !cueChanged) {
5641
5805
  unchanged++;
5642
5806
  continue;
5643
5807
  }
5644
5808
  suggestions.push(suggestion2({
5645
5809
  proposed: keepText ? proposed : sent.baseline,
5646
5810
  ...proposedCharacter ? { proposedCharacter, baselineCharacter: sent.character ?? "" } : {},
5811
+ ...proposedQualifier !== void 0 ? { proposedQualifier, baselineQualifier: sent.qualifier ?? "" } : {},
5647
5812
  ...proposedDirection !== void 0 ? { proposedDirection, baselineDirection: sent.direction ?? "" } : {}
5648
5813
  }));
5649
5814
  }
@@ -5724,6 +5889,32 @@ function readLead(text) {
5724
5889
  const character = lines.find((l) => l !== direction) ?? "";
5725
5890
  return { character, direction: direction ? direction.slice(1, -1) : "" };
5726
5891
  }
5892
+ function sentQualifierName(sent, qualifiers) {
5893
+ if (!sent.qualifier) return "";
5894
+ return sent.qualifierName ?? qualifiers.find((q) => q.gameId === sent.qualifier)?.name ?? sent.qualifier;
5895
+ }
5896
+ function sentCue(sent, qualifiers) {
5897
+ if (!sent.character) return "";
5898
+ const name = sentQualifierName(sent, qualifiers);
5899
+ return name ? `${sent.character} (${name})` : sent.character;
5900
+ }
5901
+ function readCue(text, sent, cast, qualifiers) {
5902
+ const cue = tidy(text);
5903
+ if (!cue) return {};
5904
+ const upper = (s) => s.toUpperCase();
5905
+ if (upper(cue) === upper(sentCue(sent, qualifiers))) return {};
5906
+ const sentCharacter = sent.character ?? "";
5907
+ const speaker = (name) => cast.find((c) => upper(c.name) === upper(name))?.name ?? (sentCharacter && upper(name) === upper(sentCharacter) ? sentCharacter : void 0);
5908
+ const whole = speaker(cue);
5909
+ if (whole !== void 0) return { character: whole, qualifier: "" };
5910
+ const m = /^(.*?)\s*\(([^()]*)\)$/s.exec(cue);
5911
+ const rest = m ? speaker(m[1].trim()) : void 0;
5912
+ if (!m || rest === void 0) return { unknownSpeaker: cue };
5913
+ const bracket = m[2].trim();
5914
+ if (sent.qualifier && upper(bracket) === upper(sentQualifierName(sent, qualifiers))) return { character: rest, qualifier: sent.qualifier };
5915
+ const q = qualifiers.find((x) => upper(x.name) === upper(bracket));
5916
+ return q ? { character: rest, qualifier: q.gameId } : { character: rest, unknownQualifier: bracket };
5917
+ }
5727
5918
  function rankAuthors(changes) {
5728
5919
  const counts = /* @__PURE__ */ new Map();
5729
5920
  for (const c of changes) if (c.author) counts.set(c.author, (counts.get(c.author) ?? 0) + 1);
@@ -5854,6 +6045,7 @@ export {
5854
6045
  catalogToXlsx,
5855
6046
  classesForChannel,
5856
6047
  compileLoaded,
6048
+ cueLabel,
5857
6049
  currentBundlePosture,
5858
6050
  defaultBundlePath,
5859
6051
  defaultGameScopesDir,
@@ -5895,6 +6087,7 @@ export {
5895
6087
  poToCatalog,
5896
6088
  previewRegistry,
5897
6089
  proposeCoverageDrivers,
6090
+ qualifierNamer,
5898
6091
  reachabilityIssues,
5899
6092
  readEditableDocx,
5900
6093
  readGameScopes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/ops",
3
- "version": "0.14.2",
3
+ "version": "0.16.1",
4
4
  "description": "The shared operations layer: every project operation (load, validate, export, format, play, ...) as a pure function, consumed by the patter CLI and Patterpad alike.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,10 +27,10 @@
27
27
  "build": "tsup src/index.ts --format esm --dts --clean"
28
28
  },
29
29
  "dependencies": {
30
- "@patterkit/compiler": "0.4.7",
31
- "@patterkit/core": "0.4.1",
32
- "@patterkit/model": "0.10.0",
33
- "@patterkit/runtime": "0.21.0",
30
+ "@patterkit/compiler": "0.6.1",
31
+ "@patterkit/core": "0.6.1",
32
+ "@patterkit/model": "0.13.0",
33
+ "@patterkit/runtime": "0.24.0",
34
34
  "@wildwinter/toolkit": "^0.1.0",
35
35
  "@xmldom/xmldom": "^0.8.15",
36
36
  "docx": "^9.7.1",
@@ -43,6 +43,7 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/pdfkit": "^0.17.6",
46
+ "@wildwinter/game-subtitles": "^0.3.2",
46
47
  "@wildwinter/scoperegistry": ">=0.8.0 <1.0.0",
47
48
  "esbuild": "^0.28.2"
48
49
  }