@patterkit/cli 0.9.3 → 0.10.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.
Files changed (2) hide show
  1. package/dist/cli.js +564 -134
  2. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -18844,17 +18844,17 @@ var require_saxes = __commonJS({
18844
18844
  this.fail(this.isName(entity) ? "undefined entity." : "disallowed character in entity name.");
18845
18845
  return `&${entity};`;
18846
18846
  }
18847
- let num = NaN;
18847
+ let num2 = NaN;
18848
18848
  if (entity[1] === "x" && /^#x[0-9a-f]+$/i.test(entity)) {
18849
- num = parseInt(entity.slice(2), 16);
18849
+ num2 = parseInt(entity.slice(2), 16);
18850
18850
  } else if (/^#[0-9]+$/.test(entity)) {
18851
- num = parseInt(entity.slice(1), 10);
18851
+ num2 = parseInt(entity.slice(1), 10);
18852
18852
  }
18853
- if (!this.isChar(num)) {
18853
+ if (!this.isChar(num2)) {
18854
18854
  this.fail("malformed character entity.");
18855
18855
  return `&${entity};`;
18856
18856
  }
18857
- return String.fromCodePoint(num);
18857
+ return String.fromCodePoint(num2);
18858
18858
  }
18859
18859
  };
18860
18860
  exports2.SaxesParser = SaxesParser;
@@ -45761,9 +45761,9 @@ var require_buffer_crc32 = __commonJS({
45761
45761
  throw new Error("input must be buffer, number, or string, received " + typeof input);
45762
45762
  }
45763
45763
  }
45764
- function bufferizeInt(num) {
45764
+ function bufferizeInt(num2) {
45765
45765
  var tmp = ensureBuffer(4);
45766
- tmp.writeInt32BE(num, 0);
45766
+ tmp.writeInt32BE(num2, 0);
45767
45767
  return tmp;
45768
45768
  }
45769
45769
  function _crc32(buf, previous) {
@@ -47150,9 +47150,9 @@ var require_headers = __commonJS({
47150
47150
  }
47151
47151
  return 0;
47152
47152
  };
47153
- var indexOf = function(block, num, offset, end) {
47153
+ var indexOf = function(block, num2, offset, end) {
47154
47154
  for (; offset < end; offset++) {
47155
- if (block[offset] === num) return offset;
47155
+ if (block[offset] === num2) return offset;
47156
47156
  }
47157
47157
  return end;
47158
47158
  };
@@ -64132,8 +64132,8 @@ var require_base64_js2 = __commonJS({
64132
64132
  }
64133
64133
  return arr;
64134
64134
  }
64135
- function tripletToBase64(num) {
64136
- return lookup[num >> 18 & 63] + lookup[num >> 12 & 63] + lookup[num >> 6 & 63] + lookup[num & 63];
64135
+ function tripletToBase64(num2) {
64136
+ return lookup[num2 >> 18 & 63] + lookup[num2 >> 12 & 63] + lookup[num2 >> 6 & 63] + lookup[num2 & 63];
64137
64137
  }
64138
64138
  function encodeChunk(uint82, start, end) {
64139
64139
  var tmp;
@@ -64242,16 +64242,16 @@ var require_tiny_inflate = __commonJS({
64242
64242
  for (i = 0; i < 32; ++i) dt.trans[i] = i;
64243
64243
  }
64244
64244
  var offs = new Uint16Array(16);
64245
- function tinf_build_tree(t, lengths2, off, num) {
64245
+ function tinf_build_tree(t, lengths2, off, num2) {
64246
64246
  var i, sum;
64247
64247
  for (i = 0; i < 16; ++i) t.table[i] = 0;
64248
- for (i = 0; i < num; ++i) t.table[lengths2[off + i]]++;
64248
+ for (i = 0; i < num2; ++i) t.table[lengths2[off + i]]++;
64249
64249
  t.table[0] = 0;
64250
64250
  for (sum = 0, i = 0; i < 16; ++i) {
64251
64251
  offs[i] = sum;
64252
64252
  sum += t.table[i];
64253
64253
  }
64254
- for (i = 0; i < num; ++i) {
64254
+ for (i = 0; i < num2; ++i) {
64255
64255
  if (lengths2[off + i]) t.trans[offs[lengths2[off + i]]++] = i;
64256
64256
  }
64257
64257
  }
@@ -64264,16 +64264,16 @@ var require_tiny_inflate = __commonJS({
64264
64264
  d.tag >>>= 1;
64265
64265
  return bit;
64266
64266
  }
64267
- function tinf_read_bits(d, num, base) {
64268
- if (!num)
64267
+ function tinf_read_bits(d, num2, base) {
64268
+ if (!num2)
64269
64269
  return base;
64270
64270
  while (d.bitcount < 24) {
64271
64271
  d.tag |= d.source[d.sourceIndex++] << d.bitcount;
64272
64272
  d.bitcount += 8;
64273
64273
  }
64274
- var val = d.tag & 65535 >>> 16 - num;
64275
- d.tag >>>= num;
64276
- d.bitcount -= num;
64274
+ var val = d.tag & 65535 >>> 16 - num2;
64275
+ d.tag >>>= num2;
64276
+ d.bitcount -= num2;
64277
64277
  return val + base;
64278
64278
  }
64279
64279
  function tinf_decode_symbol(d, t) {
@@ -64296,7 +64296,7 @@ var require_tiny_inflate = __commonJS({
64296
64296
  }
64297
64297
  function tinf_decode_trees(d, lt, dt) {
64298
64298
  var hlit, hdist, hclen;
64299
- var i, num, length;
64299
+ var i, num2, length;
64300
64300
  hlit = tinf_read_bits(d, 5, 257);
64301
64301
  hdist = tinf_read_bits(d, 5, 1);
64302
64302
  hclen = tinf_read_bits(d, 4, 4);
@@ -64306,27 +64306,27 @@ var require_tiny_inflate = __commonJS({
64306
64306
  lengths[clcidx[i]] = clen;
64307
64307
  }
64308
64308
  tinf_build_tree(code_tree, lengths, 0, 19);
64309
- for (num = 0; num < hlit + hdist; ) {
64309
+ for (num2 = 0; num2 < hlit + hdist; ) {
64310
64310
  var sym = tinf_decode_symbol(d, code_tree);
64311
64311
  switch (sym) {
64312
64312
  case 16:
64313
- var prev = lengths[num - 1];
64313
+ var prev = lengths[num2 - 1];
64314
64314
  for (length = tinf_read_bits(d, 2, 3); length; --length) {
64315
- lengths[num++] = prev;
64315
+ lengths[num2++] = prev;
64316
64316
  }
64317
64317
  break;
64318
64318
  case 17:
64319
64319
  for (length = tinf_read_bits(d, 3, 3); length; --length) {
64320
- lengths[num++] = 0;
64320
+ lengths[num2++] = 0;
64321
64321
  }
64322
64322
  break;
64323
64323
  case 18:
64324
64324
  for (length = tinf_read_bits(d, 7, 11); length; --length) {
64325
- lengths[num++] = 0;
64325
+ lengths[num2++] = 0;
64326
64326
  }
64327
64327
  break;
64328
64328
  default:
64329
- lengths[num++] = sym;
64329
+ lengths[num2++] = sym;
64330
64330
  break;
64331
64331
  }
64332
64332
  }
@@ -190959,11 +190959,11 @@ var require_b64 = __commonJS({
190959
190959
  }
190960
190960
  function uint8ToBase64(uint82) {
190961
190961
  var i, extraBytes = uint82.length % 3, output = "", temp, length;
190962
- function encode(num) {
190963
- return lookup.charAt(num);
190962
+ function encode(num2) {
190963
+ return lookup.charAt(num2);
190964
190964
  }
190965
- function tripletToBase64(num) {
190966
- return encode(num >> 18 & 63) + encode(num >> 12 & 63) + encode(num >> 6 & 63) + encode(num & 63);
190965
+ function tripletToBase64(num2) {
190966
+ return encode(num2 >> 18 & 63) + encode(num2 >> 12 & 63) + encode(num2 >> 6 & 63) + encode(num2 & 63);
190967
190967
  }
190968
190968
  for (i = 0, length = uint82.length - extraBytes; i < length; i += 3) {
190969
190969
  temp = (uint82[i] << 16) + (uint82[i + 1] << 8) + uint82[i + 2];
@@ -197883,6 +197883,32 @@ function effectiveGameId(entity) {
197883
197883
  const g = entity.gameId?.trim();
197884
197884
  return g ? g : gameIdify(entity.name);
197885
197885
  }
197886
+ var DEFAULT_QUALIFIERS = [
197887
+ { gameId: "vo", name: "V.O.", description: "Voice-over: the character isn't in the scene (a thought, narration, a voice in the ear)." },
197888
+ { gameId: "os", name: "O.S.", description: "Off-screen: the character is there, but not seen (the next room, behind a door)." },
197889
+ { gameId: "radio", name: "RADIO", description: "Heard through a radio, a phone, or a loudspeaker." }
197890
+ ];
197891
+ function projectQualifiers(project) {
197892
+ return project.qualifiers ?? DEFAULT_QUALIFIERS;
197893
+ }
197894
+ function usedQualifiers(project, scenes) {
197895
+ const used = /* @__PURE__ */ new Set();
197896
+ for (const scene of scenes) {
197897
+ for (const block of scene.blocks) {
197898
+ walkNodes(block.children, (node) => {
197899
+ if (node.type === "group") {
197900
+ if (node.prompt?.kind === "line" && node.prompt.qualifier) used.add(node.prompt.qualifier);
197901
+ return;
197902
+ }
197903
+ for (const beat of node.beats ?? []) if (beat.kind === "line" && beat.qualifier) used.add(beat.qualifier);
197904
+ });
197905
+ }
197906
+ }
197907
+ return projectQualifiers(project).filter((q) => used.has(q.gameId));
197908
+ }
197909
+ var DEFAULT_PAD_AFTER = 0.6;
197910
+ var PAD_AFTER_MIN = -10;
197911
+ var PAD_AFTER_MAX = 60;
197886
197912
  var DEFAULT_WRITING_STATUSES = [
197887
197913
  { name: "stub", colour: 0 },
197888
197914
  // red
@@ -197927,6 +197953,9 @@ var PROJECT_LOCALE_SCENE = "@project";
197927
197953
  function castStringKey(name) {
197928
197954
  return `cast:${name}`;
197929
197955
  }
197956
+ function qualifierStringKey(gameId) {
197957
+ return `qualifier:${gameId}`;
197958
+ }
197930
197959
  var HANDOFF_SCHEMA = "patter/handoff@0";
197931
197960
  var DEFAULT_CAPTION_DELIMITERS = { open: "[", close: "]" };
197932
197961
  var DEFAULT_CAPTION_CHARACTER = "SFX";
@@ -198047,6 +198076,45 @@ function validateProject(input) {
198047
198076
  const sceneGameIds = /* @__PURE__ */ new Map();
198048
198077
  const jumps = [];
198049
198078
  const castNames = new Set((project.cast ?? []).map((c2) => c2.name));
198079
+ const qualifierIds = project.qualifiers === void 0 || Array.isArray(project.qualifiers) ? new Set(projectQualifiers(project).flatMap((q) => isQualifierEntry(q) && typeof q.gameId === "string" ? [q.gameId] : [])) : null;
198080
+ const checkPad = (value, what, id) => {
198081
+ if (value === void 0) return;
198082
+ if (typeof value !== "number" || !Number.isFinite(value) || value < PAD_AFTER_MIN || value > PAD_AFTER_MAX) {
198083
+ issues.push({ code: "invalid-pad", message: `${what} pause '${String(value)}' must be a number of seconds from ${PAD_AFTER_MIN} to ${PAD_AFTER_MAX}`, id });
198084
+ }
198085
+ };
198086
+ const checkSeamPad = (beats) => {
198087
+ const last2 = (beats ?? []).at(-1);
198088
+ if (!last2 || last2.kind !== "line" && last2.kind !== "text" || typeof last2.padAfter !== "number" || last2.padAfter >= 0) return;
198089
+ issues.push({
198090
+ code: "pad-overlaps-seam",
198091
+ severity: "warning",
198092
+ id: last2.id,
198093
+ message: `beat '${last2.id}' is its snippet's last line, so its negative pause can't cut in on what follows; it plays as no pause`
198094
+ });
198095
+ };
198096
+ const checkPromptPad = (option) => {
198097
+ const p = option.prompt;
198098
+ if (!p || p.kind !== "line" && p.kind !== "text" || typeof p.padAfter !== "number" || p.padAfter >= 0) return;
198099
+ if ((option.selector ?? "run") !== "run" || !Array.isArray(option.children)) return;
198100
+ const first2 = option.children[0];
198101
+ if (first2 && (first2.type !== "snippet" || first2.condition || (first2.beats?.length ?? 0) > 0)) return;
198102
+ issues.push({
198103
+ code: "prompt-pad-without-beat",
198104
+ severity: "warning",
198105
+ id: p.id,
198106
+ message: `prompt '${p.id}' has a negative pause, but its option plays nothing before it moves on, so nothing cuts in on it`
198107
+ });
198108
+ };
198109
+ const checkQualifier = (beat, what) => {
198110
+ if (beat.kind !== "line" || beat.qualifier === void 0) return;
198111
+ if (qualifierIds && !qualifierIds.has(beat.qualifier)) {
198112
+ issues.push({ code: "unknown-qualifier", message: `${what} '${beat.id}' qualifier '${beat.qualifier}' is not in the project's qualifiers`, id: beat.id });
198113
+ }
198114
+ if (!beat.character) {
198115
+ issues.push({ code: "qualifier-without-speaker", severity: "warning", message: `${what} '${beat.id}' has the qualifier '${beat.qualifier}' but no speaker`, id: beat.id });
198116
+ }
198117
+ };
198050
198118
  const checkTags = (tags, where, id) => {
198051
198119
  for (const t of tags ?? []) {
198052
198120
  if (typeof t !== "string" || t.length === 0 || /[\s,]/.test(t)) {
@@ -198071,6 +198139,7 @@ function validateProject(input) {
198071
198139
  validateProjectFile(project, issues);
198072
198140
  for (const scene of scenes) {
198073
198141
  seeId(scene.id, "scene", "project");
198142
+ checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
198074
198143
  const where = `scene '${scene.id}'`;
198075
198144
  if (typeof scene.id === "string") addressable.add(scene.id);
198076
198145
  if (!scene.name?.trim()) {
@@ -198106,6 +198175,7 @@ function validateProject(input) {
198106
198175
  }
198107
198176
  for (const block of scene.blocks) {
198108
198177
  seeId(block.id, "block", where);
198178
+ checkPad(block.padAfterDefault, `block '${block.id}' default`, block.id);
198109
198179
  checkTags(block.tags, `block '${block.id}'`, block.id);
198110
198180
  if (typeof block.id === "string") addressable.add(block.id);
198111
198181
  if (!block.name?.trim()) {
@@ -198146,6 +198216,12 @@ function validateProject(input) {
198146
198216
  walkNodes(block.children, (node) => {
198147
198217
  const inBlock = `block '${block.id}'`;
198148
198218
  checkTags(node.tags, `${node.type} '${node.id}'`, node.id);
198219
+ checkPad(node.padAfterDefault, `${node.type} '${node.id}' default`, node.id);
198220
+ if (node.type === "group" && node.prompt) checkPad(node.prompt.padAfter, `prompt '${node.prompt.id}'`, node.prompt.id);
198221
+ if (node.type === "snippet") {
198222
+ for (const beat of node.beats ?? []) if (beat.kind !== "gameEvent") checkPad(beat.padAfter, `beat '${beat.id}'`, beat.id);
198223
+ checkSeamPad(node.beats);
198224
+ }
198149
198225
  if (node.type === "group") {
198150
198226
  seeId(node.id, "group", inBlock);
198151
198227
  if (!Array.isArray(node.children)) {
@@ -198170,6 +198246,8 @@ function validateProject(input) {
198170
198246
  if (p.kind === "line" && p.character && !castNames.has(p.character)) {
198171
198247
  issues.push({ code: "unknown-character", message: `prompt '${p.id}' speaker '${p.character}' is not in the project cast`, id: p.id });
198172
198248
  }
198249
+ checkQualifier(p, "prompt");
198250
+ checkPromptPad(opt);
198173
198251
  }
198174
198252
  }
198175
198253
  if (fallbacks > 1) {
@@ -198199,6 +198277,7 @@ function validateProject(input) {
198199
198277
  id: beat.id
198200
198278
  });
198201
198279
  }
198280
+ checkQualifier(beat, "beat");
198202
198281
  }
198203
198282
  if (node.jump) jumps.push({ to: node.jump.to, from: node.id });
198204
198283
  });
@@ -198220,6 +198299,9 @@ function validateProject(input) {
198220
198299
  }
198221
198300
  return issues;
198222
198301
  }
198302
+ function isQualifierEntry(q) {
198303
+ return typeof q === "object" && q !== null && !Array.isArray(q);
198304
+ }
198223
198305
  function validateProjectFile(project, issues) {
198224
198306
  checkDecls(project.properties, "project properties", issues);
198225
198307
  for (const scope of project.scopeRegistry?.scopes ?? []) {
@@ -198271,6 +198353,30 @@ function validateProjectFile(project, issues) {
198271
198353
  if (project.recordingStatuses) {
198272
198354
  checkLadderNames(project.recordingStatuses.map((s) => s.name), "recordingStatuses", issues);
198273
198355
  }
198356
+ const pad2 = project.padAfterDefault;
198357
+ if (pad2 !== void 0 && (typeof pad2 !== "number" || !Number.isFinite(pad2) || pad2 < PAD_AFTER_MIN || pad2 > PAD_AFTER_MAX)) {
198358
+ issues.push({ code: "invalid-pad", message: `the project default pause '${String(pad2)}' must be a number of seconds from ${PAD_AFTER_MIN} to ${PAD_AFTER_MAX}` });
198359
+ }
198360
+ const seenQualifiers = /* @__PURE__ */ new Set();
198361
+ const qualifiers = project.qualifiers;
198362
+ if (qualifiers !== void 0 && !Array.isArray(qualifiers)) {
198363
+ issues.push({ code: "invalid-qualifier", message: "qualifiers must be a list" });
198364
+ }
198365
+ for (const q of Array.isArray(qualifiers) ? qualifiers : []) {
198366
+ if (!isQualifierEntry(q)) {
198367
+ issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q)}' is not a qualifier (a name and an address)` });
198368
+ continue;
198369
+ }
198370
+ if (typeof q.name !== "string" || !q.name.trim()) {
198371
+ issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q.gameId)}' has no name` });
198372
+ }
198373
+ if (typeof q.gameId !== "string" || !isValidGameId(q.gameId)) {
198374
+ issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q.name)}' address '${String(q.gameId)}' is invalid (lowercase letters, digits, hyphens; no leading/trailing hyphen)` });
198375
+ } else if (seenQualifiers.has(q.gameId)) {
198376
+ issues.push({ code: "invalid-qualifier", message: `qualifier address '${q.gameId}' is used more than once` });
198377
+ }
198378
+ if (typeof q.gameId === "string") seenQualifiers.add(q.gameId);
198379
+ }
198274
198380
  for (const [kind, fields] of Object.entries(project.gameDataFields ?? {})) {
198275
198381
  const seen = /* @__PURE__ */ new Set();
198276
198382
  for (const f of fields ?? []) {
@@ -199701,6 +199807,8 @@ function compileNode(node, foreign) {
199701
199807
  shared: node.shared,
199702
199808
  options: node.options,
199703
199809
  children: node.children.map((c2) => compileNode(c2, foreign)),
199810
+ padAfterDefault: node.padAfterDefault,
199811
+ // line padding: the default for the beats inside
199704
199812
  gameData: node.gameData,
199705
199813
  tags: node.tags,
199706
199814
  // author tags (#215), accumulated at runtime
@@ -199718,6 +199826,8 @@ function compileNode(node, foreign) {
199718
199826
  beats: node.beats,
199719
199827
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
199720
199828
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
199829
+ padAfterDefault: node.padAfterDefault,
199830
+ // line padding: the default for the beats inside
199721
199831
  gameData: node.gameData,
199722
199832
  tags: node.tags,
199723
199833
  // author tags (#215)
@@ -199735,6 +199845,8 @@ function compileBlock(block, foreign) {
199735
199845
  ...block.gameId ? { gameId: block.gameId } : {},
199736
199846
  // host-facing address; runtime falls back to the name slug
199737
199847
  children: block.children.map((c2) => compileNode(c2, foreign)),
199848
+ padAfterDefault: block.padAfterDefault,
199849
+ // line padding: the default for the beats inside
199738
199850
  gameData: block.gameData,
199739
199851
  tags: block.tags
199740
199852
  // author tags (#215)
@@ -199747,6 +199859,8 @@ function compileScene(scene, foreign) {
199747
199859
  name: scene.name,
199748
199860
  ...scene.gameId ? { gameId: scene.gameId } : {},
199749
199861
  // host-facing address; runtime falls back to the name slug
199862
+ padAfterDefault: scene.padAfterDefault,
199863
+ // line padding: the default for the beats inside
199750
199864
  gameData: scene.gameData,
199751
199865
  tags: scene.tags,
199752
199866
  // author tags (#215)
@@ -199786,6 +199900,7 @@ function exportBundle(input) {
199786
199900
  const scenesOut = {};
199787
199901
  for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
199788
199902
  const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
199903
+ const qualifiers = usedQualifiers(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
199789
199904
  const strings = {};
199790
199905
  for (const loc of locales) {
199791
199906
  if (includeLocales && !includeLocales.includes(loc.locale)) continue;
@@ -199804,21 +199919,26 @@ function exportBundle(input) {
199804
199919
  // Binds saves to the content they were taken against, and gates bundle
199805
199920
  // staleness (schema §10.5). A stable strict-JSON fingerprint - independent
199806
199921
  // of the source-form trailing-comma policy.
199807
- // `scopeRegistry` is folded in only when present, so projects without host scopes keep their exact
199808
- // prior hash (no ripple to existing bundles / the conformance corpus).
199922
+ // `scopeRegistry` and `qualifiers` are folded in only when present, so projects without them keep
199923
+ // their exact prior hash (no ripple to existing bundles / the conformance corpus).
199809
199924
  hash: hash32(canonicalStringify({
199810
199925
  scenes: scenesOut,
199811
199926
  strings,
199812
199927
  properties: project.properties ?? [],
199813
- ...foreignScopes ? { scopeRegistry: foreignScopes } : {}
199928
+ ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
199929
+ ...qualifiers.length > 0 ? { qualifiers } : {},
199930
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
199814
199931
  }, { trailingComma: false })),
199815
199932
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
199816
199933
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
199817
199934
  // `engine.replaceStrings()`; a changed structureHash needs the full save/load hot swap.
199935
+ // A qualifier's name rides in `qualifiers`, not the string tables, so renaming one is a structure change.
199818
199936
  structureHash: hash32(canonicalStringify({
199819
199937
  scenes: scenesOut,
199820
199938
  properties: project.properties ?? [],
199821
- ...foreignScopes ? { scopeRegistry: foreignScopes } : {}
199939
+ ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
199940
+ ...qualifiers.length > 0 ? { qualifiers } : {},
199941
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
199822
199942
  }, { trailingComma: false }))
199823
199943
  },
199824
199944
  voiced: project.voiced ?? false,
@@ -199834,6 +199954,9 @@ function exportBundle(input) {
199834
199954
  if (c2.gameData !== void 0) m.gameData = c2.gameData;
199835
199955
  return m;
199836
199956
  }),
199957
+ ...qualifiers.length > 0 ? { qualifiers } : {},
199958
+ // The project's own line-padding default, only when it sets one: absent, every runtime uses the built-in one.
199959
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
199837
199960
  properties: project.properties,
199838
199961
  scopeRegistry: hostScopes,
199839
199962
  gameDataFields: project.gameDataFields,
@@ -200447,6 +200570,9 @@ function mergeProject(base, ours, theirs) {
200447
200570
  } else if (k === "gameDataFields") {
200448
200571
  const v = mergeGameDataFields(asMap(base[k]), asMap(ours[k]), asMap(theirs[k]), conflicts);
200449
200572
  if (Object.keys(v).length > 0) merged[k] = v;
200573
+ } else if (k === "qualifiers") {
200574
+ const v = mergeQualifiers(base[k], ours[k], theirs[k], conflicts);
200575
+ if (v !== void 0) merged[k] = v;
200450
200576
  } else if (k === "locales") {
200451
200577
  merged.locales = mergeLocales(asMap(base.locales), asMap(ours.locales), asMap(theirs.locales), conflicts);
200452
200578
  } else {
@@ -200464,6 +200590,23 @@ function mergeGameDataFields(b, o, t, conflicts) {
200464
200590
  }
200465
200591
  return out;
200466
200592
  }
200593
+ function mergeQualifiers(base, ours, theirs, conflicts) {
200594
+ if (base === void 0 && ours === void 0 && theirs === void 0) return void 0;
200595
+ 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");
200596
+ const list = (v) => v === void 0 ? DEFAULT_QUALIFIERS : Array.isArray(v) && keyed(v) ? v : null;
200597
+ const b = list(base), o = list(ours), t = list(theirs);
200598
+ if (!b || !o || !t) return merge3(base, ours, theirs, "", "qualifiers", conflicts);
200599
+ const byId = (arr) => {
200600
+ const m = {};
200601
+ for (const q of arr) if (isObj(q) && typeof q.gameId === "string") m[q.gameId] = q;
200602
+ return m;
200603
+ };
200604
+ const merged = mergeMap(byId(b), byId(o), byId(t), "qualifiers", conflicts);
200605
+ const order = [];
200606
+ 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);
200607
+ const out = order.map((id) => merged[id]);
200608
+ return eq(out, DEFAULT_QUALIFIERS) ? void 0 : out;
200609
+ }
200467
200610
  function mergeLocales(b, o, t, conflicts) {
200468
200611
  const out = {};
200469
200612
  const def = merge3(b.default, o.default, t.default, "", "locales.default", conflicts);
@@ -201225,7 +201368,55 @@ function planBuild(loaded, opts = {}) {
201225
201368
  }
201226
201369
 
201227
201370
  // ../ops/src/playable-runtime.ts
201228
- 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';
201371
+ var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var Q=Object.defineProperty;var Je=Object.getOwnPropertyDescriptor;var Qe=Object.getOwnPropertyNames;var Ke=Object.prototype.hasOwnProperty;var ze=(n,e)=>{for(var t in e)Q(n,t,{get:e[t],enumerable:!0})},Ye=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Qe(e))!Ke.call(n,s)&&s!==t&&Q(n,s,{get:()=>e[s],enumerable:!(r=Je(e,s))||r.enumerable});return n};var Xe=n=>Ye(Q({},"__esModule",{value:!0}),n);var yt={};ze(yt,{Checkpoint:()=>j,DEFAULT_PAD_AFTER:()=>x,Engine:()=>W,Flow:()=>R,buildTagIndex:()=>U,describeBundle:()=>qe,effectiveGameData:()=>He,gameDataFields:()=>$e,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 L(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=K(a,e);if(c===void 0)throw new u("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=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=K(i.left,e),l=K(i.right,e),p=c??l;if(p!==void 0){if(c&&l&&!Ze(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 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 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 K(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 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 Ze=(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 z(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 et={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},tt=[et];function he(n,e,t){let r=t?.countingCalls??tt;return q(n,t?.want??!0,e,r)}function q(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=q(n.left,e,t,r),i=q(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 q(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 rt=n=>n.toLowerCase(),Y=1,$=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??rt,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(nt(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:Y,scopes:this.save()}}loadFragment(e){if(e.version!==Y)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${Y})`);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 nt(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 X={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:we("check_flags"),eval(n,e){let t=ve(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:we("set_flags"),eval(n,e){let t=[...ve(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:_("visits"),eval:(n,e)=>A(e).visits?.(H(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:_("seen"),eval:(n,e)=>(A(e).visits?.(H(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:_("patter_visits"),eval:(n,e)=>A(e).patterVisits?.(H(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:_("patter_seen"),eval:(n,e)=>(A(e).patterVisits?.(H(n,e,"patter_seen"))??0)>0}}},Gt=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 H(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 _(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 ve(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 we(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 st=/^@[A-Za-z0-9_.]+$/;function*it(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:st.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 ot(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function at(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function ct(n){let e="",t=!1;for(let r of n){if(at(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?ct(r):n}function ke(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of it(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?"":ot(s)}return t}function I(n,e){for(let t of n){e(t);let r=t.children;r&&I(r,e)}}function lt(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||lt(n.name)}var x=.6;function Z(n){return`cast:${n}`}function ee(n){return`qualifier:${n}`}var te={open:"[",close:"]"},Ee="SFX";var re=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 U(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",F=n=>typeof n=="number"&&Number.isFinite(n)?n:void 0;function De(n){let e=new Map,t=(i,o,a)=>{let c=i.kind==="gameEvent"?void 0:F(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=F(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=F(n.padAfterDefault)??x;for(let i of Object.values(n.scenes)){let o=F(i.padAfterDefault)??s;for(let a of i.blocks){let c=F(a.padAfterDefault)??o;for(let l of a.children)r(l,c)}}return e}var Ie=new WeakMap,Re=new WeakMap;function pt(n){let e=Re.get(n);return e||(e=U(n),Re.set(n,e)),e}var Pe=new WeakMap;function dt(n){let e=Pe.get(n);return e||(e=De(n),Pe.set(n,e)),e}function D(n,e){return n.tagIndex.get(e)?.slice()}var G="Patter",T=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),Oe=Symbol("ownsRegistry"),y={stage:n=>`patter/scene/${T(n)}`,flow:n=>`patter/flow/${T(n)}/`,flowGlobals:n=>`patter/flow/${T(n)}/patter`,flowScene:(n,e)=>`patter/flow/${T(n)}/scene/${T(e)}`},j=class{constructor(e){this.journal=e}journal},W=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(S(h),d);let v=new Map;for(let m of h.blocks)g.set(m.id,{sceneId:d}),ce.set(m.id,m),v.set(S(m),m.id),I(m.children,V=>f.set(V.id,V));this.blockGameIdToId.set(d,v)}let le=e.properties??[],J=le.filter(d=>d.shared??!0).map(Fe),_e=le.filter(d=>!(d.shared??!0)).map(Fe),Ue=new Set(J.map(d=>d.name.toLowerCase())),k=t.registry??new $,pe=!t.registry||t[Oe]===!0,de=new b(J,{pathPrefix:"@patter."}),ue=[],M=[];try{k.mountOwned("patter",de,{owner:G}),M.push("patter");let d=new Set;for(let[h,v]of Object.entries(Te(t))){let m=e.scopeRegistry?.scopes.find(We=>We.token===h),V=(m?.declarations??[]).map(Ve);k.defineForeign(h,v,V,{writable:m?.writable??!0,owner:G}),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 v=(h.declarations??[]).map(m=>gt(m,h.writable));k.defineOwned(h.token,v,{owner:G}),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 v=new Set((h.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));ge.set(d,v)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(d,h,v)=>this.emitEngine(d,h,v),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:pt(e),padIndex:dt(e),registry:k,ownsRegistry:pe,patterBag:de,hostScopes:ue,patterSharedDecls:J,patterLocalDecls:_e,patterSharedNames:Ue,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??te).open,captionClose:(e.closedCaptions??te).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 R(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?S(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?S(t):void 0}tagsForBeat(e){return D(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?D(this.host,t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?D(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,...S(e)?{gameId:S(e)}:{},name:e.name,...Ae(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...S(t)?{gameId:S(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)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=D(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[Z(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[ee(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=D(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`)}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(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:re,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:je(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==re)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?ft(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 Me(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new R(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}}},R=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(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=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:ut(this.pendingPrompt.shown)}:{},selectors:je(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=Me(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(L(i,this.context(),X))}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 L(this.conditionAst(e),this.context(),X)}conditionAst(e){let t=Ie.get(e);return t||(t=E(e.ast),Ie.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=z(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=D(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=D(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 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=Z(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=ee(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=y.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:G}),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=Ne(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=Ne(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,B(r))}}};function Be(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 Ae(n){return n&&Object.keys(n).length?{gameData:n}:{}}function ut(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 je(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 Me(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 Ne(n,e,t,r){let s=r?JSON.stringify(t.save()):"";return n.mountOwned(e,t,{owner:G}),r&&JSON.stringify(t.save())!==s}function Fe(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 Ve(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function gt(n,e){let t=Ve(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 ft(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 Ge(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var ht=(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:ht(n,e)}}function mt(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function St(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 qe(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)St(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(mt)}));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 $e(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 He(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 Xe(yt);})();\n';
201372
+ 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';
201373
+
201374
+ // ../ops/src/play-timeline.ts
201375
+ function createTimeline(defaultPad) {
201376
+ let anchor = 0;
201377
+ let pad2 = null;
201378
+ let floor = 0;
201379
+ let last2 = 0;
201380
+ const startOf = () => pad2 === null ? anchor : Math.max(anchor + pad2, floor);
201381
+ return {
201382
+ startOf,
201383
+ place(beat, duration) {
201384
+ const start = startOf();
201385
+ const end = start + Math.max(0, duration);
201386
+ anchor = end;
201387
+ pad2 = typeof beat.padAfter === "number" ? beat.padAfter : defaultPad;
201388
+ floor = start;
201389
+ last2 = Math.max(last2, end);
201390
+ return { start, end };
201391
+ },
201392
+ choicesAt: () => floor,
201393
+ endAt: () => Math.max(last2, anchor)
201394
+ };
201395
+ }
201396
+ function createClock(now, rate) {
201397
+ let wall = now;
201398
+ let script = 0;
201399
+ let r = rate;
201400
+ const position = (at) => r === Infinity ? script : script + (at - wall) * r / 1e3;
201401
+ return {
201402
+ at(seconds) {
201403
+ if (r === Infinity) {
201404
+ script = Math.max(script, seconds);
201405
+ return wall;
201406
+ }
201407
+ return wall + (seconds - script) * 1e3 / r;
201408
+ },
201409
+ position,
201410
+ setRate(at, rate2) {
201411
+ script = position(at);
201412
+ wall = at;
201413
+ r = rate2;
201414
+ },
201415
+ delay(ms) {
201416
+ if (ms > 0) wall += ms;
201417
+ }
201418
+ };
201419
+ }
201229
201420
 
201230
201421
  // ../ops/src/export-html.ts
201231
201422
  var esc = (s) => s.replace(/[&<>]/g, (c2) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c2]);
@@ -201245,48 +201436,94 @@ var PLAYER_JS = String.raw`
201245
201436
  function add(cls, html) { var d = document.createElement("div"); d.className = cls; d.innerHTML = html; stage.appendChild(d); return d; }
201246
201437
  function esc(s) { return String(s == null ? "" : s).replace(/[&<>]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]; }); }
201247
201438
 
201248
- // Pacing: a source-only reader carries no audio, so each line is held on screen for a reading-length delay
201249
- // (proportional to its word count) before the next appears, and every line fades in (see CSS). The Speed
201250
- // control sets the reading pace (words/min); "Instant" drops the delay entirely. The choice is persisted.
201251
- // A run token cancels an in-flight paced read the moment the player restarts, loads, or picks a choice.
201252
- var SPEED_WPM = { slow: 300, normal: 500, fast: 800, instant: 0 }, runToken = 0;
201439
+ // Pacing (line padding): a source-only reader carries no audio, so each line is timed by the estimate
201440
+ // Patterstage uses for a line with no recording (game-subtitles' estimateDuration), and the step's padAfter
201441
+ // says when the next one starts: after a pause, at once, or cutting in before this one ends. The player
201442
+ // rules are play-timeline.ts's, inlined below by their source: the first line starts at once, as does the
201443
+ // reply to a choice; a cut-in never starts before the line it cuts into; a game event is never held here,
201444
+ // so it's done at once; the choices appear as the last line before them starts (while it plays); and the
201445
+ // end comes when every line still playing has finished. Every line fades in (see CSS).
201446
+ // The Speed control scales the whole timing (half, normal, or double speed) or drops it (instant), from
201447
+ // the moment it's changed: the clock keeps the time already played and re-times only what's to come. It
201448
+ // is stored under the key Patterpad's Play window uses, so the two keep one choice; the old reading paces
201449
+ // carry over (slow is half speed, fast is double).
201450
+ // A run token cancels an in-flight wait the moment the player restarts, loads, or picks a choice.
201451
+ var createTimeline = (${createTimeline.toString()});
201452
+ var createClock = (${createClock.toString()});
201453
+ var DEFAULT_PAD = ${DEFAULT_PAD_AFTER}; // the built-in pause, for a bundle from before padding
201454
+ var SPEED_RATE = { half: 0.5, normal: 1, double: 2, instant: Infinity }, OLD_SPEED = { slow: "half", fast: "double" }, runToken = 0;
201455
+ function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }
201253
201456
  var speedKey = (function () { try { return localStorage.getItem("patter.playSpeed") || "normal"; } catch (e) { return "normal"; } })();
201254
- if (SPEED_WPM[speedKey] === undefined) speedKey = "normal";
201255
- function fakeDuration(text) {
201256
- var wpm = SPEED_WPM[speedKey];
201257
- if (!wpm) return 0; // "Instant" - reveal the next line immediately
201258
- var words = String(text == null ? "" : text).trim().split(/\s+/).filter(Boolean).length;
201259
- return Math.max(450, Math.round((words / wpm) * 60000)); // a beat needs to land
201457
+ if (own(OLD_SPEED, speedKey)) speedKey = OLD_SPEED[speedKey];
201458
+ if (!own(SPEED_RATE, speedKey)) speedKey = "normal";
201459
+ var estimate = window.GameSubtitles.estimateDuration;
201460
+ var clock = null, wake = null; // the run's clock, and a hook that re-times the wait in flight
201461
+ // Call then() when script time "at" comes on the run's clock. A change of speed wakes the wait, which
201462
+ // works out the time again; a superseded run's wait just stops.
201463
+ function waitUntil(at, token, then) {
201464
+ if (token !== runToken) return;
201465
+ var ms = clock.at(at) - Date.now();
201466
+ if (ms <= 0) { wake = null; then(); return; }
201467
+ var t = setTimeout(function () { waitUntil(at, token, then); }, ms);
201468
+ wake = function () { clearTimeout(t); waitUntil(at, token, then); };
201260
201469
  }
201261
201470
  function scrollDown() { window.scrollTo(0, document.body.scrollHeight); }
201262
201471
  var speedSel = document.getElementById("speed");
201263
201472
  if (speedSel) {
201264
- speedSel.value = speedKey; // a fresh fakeDuration() reads speedKey each line, so a change applies next line
201265
- speedSel.onchange = function () { speedKey = speedSel.value; try { localStorage.setItem("patter.playSpeed", speedKey); } catch (e) {} };
201473
+ speedSel.value = speedKey;
201474
+ speedSel.onchange = function () {
201475
+ speedKey = speedSel.value;
201476
+ try { localStorage.setItem("patter.playSpeed", speedKey); } catch (e) {}
201477
+ if (clock) clock.setRate(Date.now(), SPEED_RATE[speedKey]);
201478
+ if (wake) wake();
201479
+ };
201266
201480
  }
201267
201481
 
201268
201482
  function build() { engine = new Engine(BUNDLE); flow = engine.openFlow("main", { scene: startScene }); }
201269
201483
 
201270
201484
  function newGame() { stage.innerHTML = ""; controls.innerHTML = ""; build(); run(); }
201271
201485
 
201486
+ // The player reads one step ahead of what it shows, as a game's would, to know whether a choice, the end,
201487
+ // or another line follows the line on screen. While a step is read but not yet shown, the engine is past
201488
+ // what the reader sees, so Save keeps the game as it was before the read (resumeSave).
201489
+ var resumeSave = null;
201490
+ function readNext() {
201491
+ try { resumeSave = JSON.stringify(engine.saveGame()); } catch (e) { resumeSave = null; }
201492
+ var s = flow.advance();
201493
+ while (s && s.type === "gameEvent") s = flow.advance(); // a host cue with nothing to show, done at once
201494
+ return s;
201495
+ }
201496
+
201272
201497
  function run() {
201273
201498
  controls.innerHTML = "";
201274
201499
  var token = ++runToken;
201275
- (function step() {
201276
- if (token !== runToken) return; // superseded by a restart / load / choice pick
201277
- var s = flow.advance();
201278
- if (!s) { add("end", "The End"); return; } // defensive: a step should always be returned; never hard-crash
201500
+ var timeline = createTimeline(DEFAULT_PAD);
201501
+ clock = createClock(Date.now(), SPEED_RATE[speedKey]);
201502
+ // A step read ahead appears when its time comes: a line or text when its pause is over, the choices
201503
+ // as the last line starts, the end as the last line still playing ends.
201504
+ function next(s) {
201505
+ var at = !s || s.type === "end" ? timeline.endAt() : s.type === "choice" ? timeline.choicesAt() : timeline.startOf();
201506
+ waitUntil(at, token, function () { show(s); });
201507
+ }
201508
+ function show(s) {
201509
+ resumeSave = null; // what's shown is where the engine is
201510
+ if (!s || s.type === "end") { add("end", "The End"); return; } // no step is defensive: never hard-crash
201511
+ if (s.type === "choice") { renderChoice(s); return; }
201512
+ timeline.place(s, estimate(s.text));
201279
201513
  if (s.type === "line") {
201280
201514
  var who = s.characterName || s.character || "";
201515
+ // A speaker qualifier follows the name, as the script's cue has it: TAM (O.S.).
201516
+ var qual = s.qualifierName || s.qualifier;
201517
+ if (qual && who) who += " (" + qual + ")";
201281
201518
  var dir = s.direction ? '<em class="dir">(' + esc(s.direction) + ')</em> ' : "";
201282
201519
  add("line", '<span class="who" style="color:hsl(' + hueOf(s.character || who) + ',55%,38%)">' + esc(who) + '</span>' + dir + esc(s.text));
201283
- scrollDown(); setTimeout(step, fakeDuration(s.text));
201284
- } else if (s.type === "text") {
201285
- add("text", esc(s.text)); scrollDown(); setTimeout(step, fakeDuration(s.text));
201286
- } else if (s.type === "choice") { renderChoice(s); }
201287
- else if (s.type === "end") { add("end", "The End"); }
201288
- else { step(); } // game-event beat: a host cue with no player-facing text - skip it, advance at once
201289
- })();
201520
+ } else {
201521
+ add("text", esc(s.text));
201522
+ }
201523
+ scrollDown();
201524
+ next(readNext());
201525
+ }
201526
+ next(readNext()); // the first line (or the reply to a choice) starts at once
201290
201527
  }
201291
201528
 
201292
201529
  function renderChoice(step) {
@@ -201304,7 +201541,7 @@ var PLAYER_JS = String.raw`
201304
201541
  }
201305
201542
 
201306
201543
  document.getElementById("restart").onclick = newGame;
201307
- document.getElementById("save").onclick = function () { try { localStorage.setItem(SAVE_KEY, JSON.stringify(engine.saveGame())); flash("Saved"); } catch (e) {} };
201544
+ document.getElementById("save").onclick = function () { try { localStorage.setItem(SAVE_KEY, resumeSave || JSON.stringify(engine.saveGame())); flash("Saved"); } catch (e) {} };
201308
201545
  document.getElementById("load").onclick = function () {
201309
201546
  var blob; try { blob = localStorage.getItem(SAVE_KEY); } catch (e) {}
201310
201547
  if (!blob) { flash("No save yet"); return; }
@@ -201376,10 +201613,10 @@ ${head}
201376
201613
  <button id="save">Save</button>
201377
201614
  <button id="load">Load</button>
201378
201615
  <label class="speedlabel">Speed
201379
- <select id="speed" title="Reading speed: how long each line is held before the next appears">
201380
- <option value="slow">Slow</option>
201616
+ <select id="speed" title="How fast the scene plays: its timing at half speed, as written, at double speed, or with no waits">
201617
+ <option value="half">Half</option>
201381
201618
  <option value="normal">Normal</option>
201382
- <option value="fast">Fast</option>
201619
+ <option value="double">Double</option>
201383
201620
  <option value="instant">Instant</option>
201384
201621
  </select>
201385
201622
  </label>
@@ -201397,6 +201634,7 @@ function runExportHtml(loaded) {
201397
201634
  lang,
201398
201635
  `<style>${STYLE}</style>`,
201399
201636
  `<script>${PLAYABLE_RUNTIME_JS}</script>
201637
+ <script>${PLAYABLE_TIMING_JS}</script>
201400
201638
  <script>window.PATTER_BUNDLE=${bundleJson};</script>
201401
201639
  <script>${PLAYER_JS}</script>`
201402
201640
  );
@@ -201469,6 +201707,44 @@ function buildTagIndex(bundle) {
201469
201707
  return index2;
201470
201708
  }
201471
201709
 
201710
+ // ../runtime/src/pads.ts
201711
+ var spoken = (b) => b.kind === "line" || b.kind === "text";
201712
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
201713
+ function buildPadIndex(bundle) {
201714
+ const index2 = /* @__PURE__ */ new Map();
201715
+ const set = (beat, inherited, clamp) => {
201716
+ const own2 = beat.kind === "gameEvent" ? void 0 : num(beat.padAfter);
201717
+ let resolved = own2 ?? inherited;
201718
+ if (clamp && resolved < 0) resolved = 0;
201719
+ index2.set(beat.id, own2 !== void 0 ? { own: own2, resolved } : { resolved });
201720
+ };
201721
+ const visit = (node, inherited) => {
201722
+ const def = num(node.padAfterDefault) ?? inherited;
201723
+ if (node.type === "group") {
201724
+ if (node.prompt) set(node.prompt, def, false);
201725
+ for (const child2 of node.children) visit(child2, def);
201726
+ return;
201727
+ }
201728
+ const beats = node.beats ?? [];
201729
+ let last2 = -1;
201730
+ beats.forEach((b, i) => {
201731
+ if (spoken(b)) last2 = i;
201732
+ });
201733
+ beats.forEach((b, i) => {
201734
+ if (spoken(b)) set(b, def, i === last2);
201735
+ });
201736
+ };
201737
+ const projectDefault = num(bundle.padAfterDefault) ?? DEFAULT_PAD_AFTER;
201738
+ for (const scene of Object.values(bundle.scenes)) {
201739
+ const sceneDefault = num(scene.padAfterDefault) ?? projectDefault;
201740
+ for (const block of scene.blocks) {
201741
+ const blockDefault = num(block.padAfterDefault) ?? sceneDefault;
201742
+ for (const child2 of block.children) visit(child2, blockDefault);
201743
+ }
201744
+ }
201745
+ return index2;
201746
+ }
201747
+
201472
201748
  // ../runtime/src/engine.ts
201473
201749
  var astCache = /* @__PURE__ */ new WeakMap();
201474
201750
  var tagIndexCache = /* @__PURE__ */ new WeakMap();
@@ -201480,6 +201756,15 @@ function tagIndexFor(bundle) {
201480
201756
  }
201481
201757
  return index2;
201482
201758
  }
201759
+ var padIndexCache = /* @__PURE__ */ new WeakMap();
201760
+ function padIndexFor(bundle) {
201761
+ let index2 = padIndexCache.get(bundle);
201762
+ if (!index2) {
201763
+ index2 = buildPadIndex(bundle);
201764
+ padIndexCache.set(bundle, index2);
201765
+ }
201766
+ return index2;
201767
+ }
201483
201768
  function tagsOf(host2, id) {
201484
201769
  return host2.tagIndex.get(id)?.slice();
201485
201770
  }
@@ -201545,6 +201830,8 @@ var Engine = class _Engine {
201545
201830
  }
201546
201831
  const castDisplay = /* @__PURE__ */ new Map();
201547
201832
  for (const c2 of bundle.cast ?? []) if (c2.displayName) castDisplay.set(c2.name, c2.displayName);
201833
+ const qualifierDisplay = /* @__PURE__ */ new Map();
201834
+ for (const q of bundle.qualifiers ?? []) if (q?.gameId) qualifierDisplay.set(q.gameId, q.name);
201548
201835
  this.defaultSeed = (options.seed ?? 2654435769) >>> 0;
201549
201836
  const nodeIndex = /* @__PURE__ */ new Map();
201550
201837
  const blockIndex = /* @__PURE__ */ new Map();
@@ -201605,6 +201892,7 @@ var Engine = class _Engine {
201605
201892
  strings,
201606
201893
  defaultStrings,
201607
201894
  castDisplay,
201895
+ qualifierDisplay,
201608
201896
  nodeIndex,
201609
201897
  blockIndex,
201610
201898
  blockById,
@@ -201612,6 +201900,7 @@ var Engine = class _Engine {
201612
201900
  blockGameIdToId: this.blockGameIdToId,
201613
201901
  // same instances the engine resolves with
201614
201902
  tagIndex: tagIndexFor(bundle),
201903
+ padIndex: padIndexFor(bundle),
201615
201904
  registry,
201616
201905
  ownsRegistry,
201617
201906
  patterBag,
@@ -202009,8 +202298,16 @@ var Engine = class _Engine {
202009
202298
  if (name !== void 0) info.characterName = name;
202010
202299
  }
202011
202300
  if (beat.direction !== void 0) info.direction = beat.direction;
202301
+ if (beat.qualifier !== void 0) {
202302
+ info.qualifier = beat.qualifier;
202303
+ const name = this.host.defaultStrings[qualifierStringKey(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
202304
+ if (name !== void 0) info.qualifierName = name;
202305
+ }
202012
202306
  }
202013
202307
  if (beat.kind === "line" || beat.kind === "text") {
202308
+ const pad2 = this.host.padIndex.get(beat.id);
202309
+ info.padAfter = pad2?.resolved ?? DEFAULT_PAD_AFTER;
202310
+ if (pad2?.own !== void 0) info.ownPadAfter = pad2.own;
202014
202311
  const source2 = this.host.defaultStrings[beat.id];
202015
202312
  if (source2 !== void 0) info.text = source2;
202016
202313
  }
@@ -202804,7 +203101,8 @@ var Flow = class {
202804
203101
  const node = this.host.nodeIndex.get(o.id);
202805
203102
  if (!node) continue;
202806
203103
  byId.set(o.id, node);
202807
- options.push({ ...o });
203104
+ const { prompt: saved, ...rest } = o;
203105
+ options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node) } } : {} });
202808
203106
  }
202809
203107
  if (options.length > 0) this.pendingChoice = { groupId: c2.pendingChoice.groupId, options, byId };
202810
203108
  }
@@ -202813,7 +203111,7 @@ var Flow = class {
202813
203111
  if (this.pendingPromptOwnerId) {
202814
203112
  const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
202815
203113
  const beat = owner ? this.authoredPromptOf(owner) : void 0;
202816
- if (beat) this.pendingPrompt = { beat, ...c2.pendingPrompt ? { shown: { ...c2.pendingPrompt } } : {} };
203114
+ if (beat) this.pendingPrompt = { beat, ...c2.pendingPrompt ? { shown: { ...c2.pendingPrompt, padAfter: c2.pendingPrompt.padAfter ?? this.padOf(beat.id) } } : {} };
202817
203115
  else this.pendingPromptOwnerId = null;
202818
203116
  }
202819
203117
  }
@@ -203183,7 +203481,7 @@ var Flow = class {
203183
203481
  case "gameEvent":
203184
203482
  return { type: "gameEvent", id: beat.id, gameData: beat.gameData, ...withTags };
203185
203483
  case "text":
203186
- return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), gameData: beat.gameData, ...withTags };
203484
+ return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
203187
203485
  case "line": {
203188
203486
  const raw = this.resolveString(beat.id);
203189
203487
  const off = !this.host.captionsOn;
@@ -203197,6 +203495,10 @@ var Flow = class {
203197
203495
  character: silent ? void 0 : beat.character,
203198
203496
  characterName: silent ? void 0 : this.resolveCharacterName(beat.character),
203199
203497
  direction: silent ? void 0 : beat.direction,
203498
+ qualifier: silent ? void 0 : beat.qualifier,
203499
+ qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
203500
+ padAfter: this.padOf(beat.id),
203501
+ // a silent line still fires, so it still carries its pause
203200
203502
  gameData: beat.gameData,
203201
203503
  ...withTags
203202
203504
  };
@@ -203235,7 +203537,17 @@ var Flow = class {
203235
203537
  const beat = this.promptBeatOf(node);
203236
203538
  if (!beat) return void 0;
203237
203539
  const text = this.interpolate(this.resolveString(beat.id));
203238
- return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
203540
+ const padAfter = this.padOf(beat.id);
203541
+ return beat.kind === "line" ? {
203542
+ kind: "line",
203543
+ text: this.captionLine(text),
203544
+ character: beat.character,
203545
+ characterName: this.resolveCharacterName(beat.character),
203546
+ direction: beat.direction,
203547
+ qualifier: beat.qualifier,
203548
+ qualifierName: this.resolveQualifierName(beat.qualifier),
203549
+ padAfter
203550
+ } : { kind: "text", text, padAfter };
203239
203551
  }
203240
203552
  /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
203241
203553
  authoredPromptOf(node) {
@@ -203246,7 +203558,7 @@ var Flow = class {
203246
203558
  promptResult(beat, shown2) {
203247
203559
  const tags = tagsOf(this.host, beat.id);
203248
203560
  const withTags = tags && tags.length ? { tags } : {};
203249
- if (shown2.kind === "text") return { type: "text", id: beat.id, text: shown2.text, gameData: beat.gameData, ...withTags };
203561
+ if (shown2.kind === "text") return { type: "text", id: beat.id, text: shown2.text, padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
203250
203562
  return {
203251
203563
  type: "line",
203252
203564
  id: beat.id,
@@ -203254,6 +203566,9 @@ var Flow = class {
203254
203566
  character: shown2.character,
203255
203567
  characterName: shown2.characterName,
203256
203568
  direction: shown2.direction,
203569
+ qualifier: shown2.qualifier,
203570
+ qualifierName: shown2.qualifierName,
203571
+ padAfter: this.padOf(beat.id),
203257
203572
  gameData: beat.gameData,
203258
203573
  ...withTags
203259
203574
  };
@@ -203290,6 +203605,26 @@ var Flow = class {
203290
203605
  const key2 = castStringKey(character);
203291
203606
  return this.host.strings[key2] ?? this.host.defaultStrings[key2] ?? this.host.castDisplay.get(character);
203292
203607
  }
203608
+ /** The resolved pause of the beat an option shows as its prompt (its authored prompt, else its first content
203609
+ * line), for a saved option that predates prompts carrying one. */
203610
+ promptPadOf(node) {
203611
+ const beat = this.promptBeatOf(node);
203612
+ return beat ? this.padOf(beat.id) : DEFAULT_PAD_AFTER;
203613
+ }
203614
+ /** A line or text beat's resolved pause (line padding): its own `padAfter`, else the nearest default above
203615
+ * it, else the project's, else the built-in one; clamped to zero on a snippet's last line. */
203616
+ padOf(beatId) {
203617
+ return this.host.padIndex.get(beatId)?.resolved ?? DEFAULT_PAD_AFTER;
203618
+ }
203619
+ /** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
203620
+ * active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
203621
+ * maps the `qualifier` gameId itself). */
203622
+ resolveQualifierName(qualifier) {
203623
+ if (qualifier === void 0) return void 0;
203624
+ if (this.host.emitIds) return void 0;
203625
+ const key2 = qualifierStringKey(qualifier);
203626
+ return this.host.strings[key2] ?? this.host.defaultStrings[key2] ?? this.host.qualifierDisplay.get(qualifier);
203627
+ }
203293
203628
  /** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
203294
203629
  splitRef(ref) {
203295
203630
  return splitHostRef(this.host, ref);
@@ -203386,6 +203721,9 @@ function savedPrompt(p) {
203386
203721
  if (p.character !== void 0) out.character = p.character;
203387
203722
  if (p.characterName !== void 0) out.characterName = p.characterName;
203388
203723
  if (p.direction !== void 0) out.direction = p.direction;
203724
+ if (p.qualifier !== void 0) out.qualifier = p.qualifier;
203725
+ if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
203726
+ if (typeof p.padAfter === "number") out.padAfter = p.padAfter;
203389
203727
  return out;
203390
203728
  }
203391
203729
  function serialiseSelectors(map2) {
@@ -203584,9 +203922,11 @@ function renderPlay(result) {
203584
203922
  const out = [];
203585
203923
  for (const e of result.events) {
203586
203924
  switch (e.type) {
203587
- case "line":
203588
- out.push(`${e.character ?? "?"}: ${e.text}`);
203925
+ case "line": {
203926
+ const q = e.qualifierName ?? e.qualifier;
203927
+ out.push(`${e.character ?? "?"}${q ? ` (${q})` : ""}: ${e.text}`);
203589
203928
  break;
203929
+ }
203590
203930
  case "text":
203591
203931
  out.push(` ${e.text}`);
203592
203932
  break;
@@ -204591,6 +204931,11 @@ function extractLoc(loaded, opts = {}) {
204591
204931
  const g = context?.character ? genderOf.get(context.character) : void 0;
204592
204932
  return g ? { ...context, gender: g } : context;
204593
204933
  };
204934
+ const qualifierName = new Map(projectQualifiers(loaded.project).map((q) => [q.gameId, q.name]));
204935
+ const lineContext = (b) => {
204936
+ const q = b.kind === "line" && b.qualifier ? qualifierName.get(b.qualifier) ?? b.qualifier : void 0;
204937
+ return { character: b.kind === "line" ? b.character : void 0, kind: b.kind, ...q ? { qualifier: q } : {} };
204938
+ };
204594
204939
  const entries = [];
204595
204940
  const push2 = (id, scene, context) => {
204596
204941
  const src = source2[id];
@@ -204603,11 +204948,11 @@ function extractLoc(loaded, opts = {}) {
204603
204948
  walkNodes(block.children, (node) => {
204604
204949
  if (node.type === "group") {
204605
204950
  const p = node.prompt;
204606
- if (p) push2(p.id, scene.id, { character: p.kind === "line" ? p.character : void 0, kind: p.kind });
204951
+ if (p) push2(p.id, scene.id, lineContext(p));
204607
204952
  return;
204608
204953
  }
204609
204954
  for (const beat of node.beats ?? []) {
204610
- if (beat.kind !== "gameEvent") push2(beat.id, scene.id, { character: beat.kind === "line" ? beat.character : void 0, kind: beat.kind });
204955
+ if (beat.kind !== "gameEvent") push2(beat.id, scene.id, lineContext(beat));
204611
204956
  }
204612
204957
  });
204613
204958
  }
@@ -204619,6 +204964,12 @@ function extractLoc(loaded, opts = {}) {
204619
204964
  const translation = isTemplate ? "" : target[id] ?? "";
204620
204965
  entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: withGender({ character: c2.name }), stale: staleFor(id, translation) });
204621
204966
  }
204967
+ for (const q of usedQualifiers(loaded.project, loaded.scenes)) {
204968
+ const id = qualifierStringKey(q.gameId);
204969
+ const src = source2[id] ?? q.name;
204970
+ const translation = isTemplate ? "" : target[id] ?? "";
204971
+ entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: { kind: "qualifier" }, stale: staleFor(id, translation) });
204972
+ }
204622
204973
  return { project: loaded.project.project.id, defaultLocale, locale: targetLocale, entries };
204623
204974
  }
204624
204975
  var layoutOf = (loaded) => projectLayout(loaded.project);
@@ -204754,7 +205105,8 @@ function catalogToPo(catalog) {
204754
205105
  for (const e of catalog.entries) {
204755
205106
  for (const c2 of e.comments) out.push(`#. ${c2}`);
204756
205107
  if (e.context?.character || e.context?.kind) {
204757
- out.push(`#. [${[e.context.kind, e.context.character].filter(Boolean).join(" ")}]`);
205108
+ const speaker = e.context.character && e.context.qualifier ? `${e.context.character} (${e.context.qualifier})` : e.context.character;
205109
+ out.push(`#. [${[e.context.kind, speaker].filter(Boolean).join(" ")}]`);
204758
205110
  }
204759
205111
  if (e.context?.gender) out.push(`#. Gender: ${e.context.gender}`);
204760
205112
  out.push(`#: ${e.scene}`);
@@ -204850,7 +205202,7 @@ function poToCatalog(text) {
204850
205202
  }
204851
205203
 
204852
205204
  // ../ops/src/loc-xlsx.ts
204853
- var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender", "Scene"];
205205
+ var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender", "Scene", "Qualifier"];
204854
205206
  var SCENE_COLUMN = HEADERS.indexOf("Scene") + 1;
204855
205207
  var SHEET_NAME_MAX = 31;
204856
205208
  function uniqueSheetName(scene, used) {
@@ -204893,7 +205245,9 @@ async function catalogToXlsx(catalog) {
204893
205245
  { header: "Status", key: "status", width: 10 },
204894
205246
  { header: "Gender", key: "gender", width: 12 },
204895
205247
  // Hidden: the translator has no use for it, and it is the import's only faithful record of the scene.
204896
- { header: "Scene", key: "scene", width: 22, hidden: true }
205248
+ { header: "Scene", key: "scene", width: 22, hidden: true },
205249
+ // After the hidden Scene column, so the columns an older reader finds by position stay where they were.
205250
+ { header: "Qualifier", key: "qualifier", width: 11 }
204897
205251
  ];
204898
205252
  ws.getRow(1).font = { bold: true };
204899
205253
  ws.views = [{ state: "frozen", ySplit: 1 }];
@@ -204905,7 +205259,8 @@ async function catalogToXlsx(catalog) {
204905
205259
  comments: e.comments.join("\n"),
204906
205260
  status: e.stale ? "stale" : e.translation ? "translated" : "",
204907
205261
  gender: e.context?.gender ?? "",
204908
- scene
205262
+ scene,
205263
+ qualifier: e.context?.qualifier ?? ""
204909
205264
  });
204910
205265
  }
204911
205266
  }
@@ -204969,6 +205324,7 @@ function runVoiceScript(loaded, opts = {}) {
204969
205324
  const source2 = sourceStrings(loaded);
204970
205325
  const actorOf = /* @__PURE__ */ new Map();
204971
205326
  for (const c2 of project.cast ?? []) if (c2.actor) actorOf.set(c2.name, c2.actor);
205327
+ const qualifierName = new Map(projectQualifiers(project).map((q) => [q.gameId, q.name]));
204972
205328
  const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES, "vo");
204973
205329
  const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
204974
205330
  const lines = [];
@@ -204981,6 +205337,7 @@ function runVoiceScript(loaded, opts = {}) {
204981
205337
  scope: scope.join(" \u203A "),
204982
205338
  id: beat.id,
204983
205339
  character: beat.character ?? "",
205340
+ ...beat.qualifier ? { qualifier: qualifierName.get(beat.qualifier) ?? beat.qualifier } : {},
204984
205341
  actor: beat.character ? actorOf.get(beat.character) : void 0,
204985
205342
  text: plainVoice(source2[beat.id] ?? ""),
204986
205343
  comments: leadingVo ? [...leadingVo, ...own2] : own2,
@@ -205028,6 +205385,7 @@ async function voiceScriptToXlsx(data) {
205028
205385
  { header: "Scope", key: "scope", width: 28 },
205029
205386
  { header: "Line ID", key: "id", width: 18 },
205030
205387
  { header: "Character", key: "character", width: 16 },
205388
+ { header: "Qualifier", key: "qualifier", width: 11 },
205031
205389
  { header: "Actor", key: "actor", width: 16 },
205032
205390
  { header: "Text", key: "text", width: 60 },
205033
205391
  { header: "Comments", key: "comments", width: 36 },
@@ -205040,6 +205398,7 @@ async function voiceScriptToXlsx(data) {
205040
205398
  scope: l2.scope,
205041
205399
  id: l2.id,
205042
205400
  character: l2.character,
205401
+ qualifier: l2.qualifier ?? "",
205043
205402
  actor: l2.actor ?? "",
205044
205403
  text: l2.text,
205045
205404
  comments: l2.comments.join("\n"),
@@ -205096,6 +205455,13 @@ function colourIndex(name) {
205096
205455
  function characterColour(name) {
205097
205456
  return CHAR_PALETTE[colourIndex(name)];
205098
205457
  }
205458
+ function cueLabel(character, qualifierName) {
205459
+ return qualifierName ? `${character.toUpperCase()} (${qualifierName.toUpperCase()})` : character.toUpperCase();
205460
+ }
205461
+ function qualifierNamer(project) {
205462
+ const names = new Map(projectQualifiers(project).map((q) => [q.gameId, q.name]));
205463
+ return (gameId) => gameId ? names.get(gameId) ?? gameId : void 0;
205464
+ }
205099
205465
  var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
205100
205466
  var INTERP_RE = /\{@[^}]*\}/g;
205101
205467
  function textRuns(text) {
@@ -205174,6 +205540,12 @@ function runScriptDoc(loaded, opts = {}) {
205174
205540
  }
205175
205541
  const humanize = (cond) => humanizeNodeRefs(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
205176
205542
  const els = [];
205543
+ const nameOf = qualifierNamer(project);
205544
+ const speaker = (beat) => ({
205545
+ character: beat.character ?? "",
205546
+ ...beat.qualifier ? { qualifier: beat.qualifier, qualifierName: nameOf(beat.qualifier) } : {},
205547
+ direction: beat.direction
205548
+ });
205177
205549
  const textOf = (id) => source2[id] ?? "";
205178
205550
  const notes = (id, indent, sid) => {
205179
205551
  for (const l2 of ownNotes(id)) els.push({ kind: "note", id, indent, snippet: sid, text: l2.text, class: l2.type });
@@ -205187,7 +205559,7 @@ function runScriptDoc(loaded, opts = {}) {
205187
205559
  const emitBeats = (node, indent, sid) => {
205188
205560
  for (const beat of node.beats ?? []) {
205189
205561
  if (cut.has(beat.id)) continue;
205190
- 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)) });
205562
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent, snippet: sid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
205191
205563
  else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
205192
205564
  else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent, snippet: sid, text: gameEventLabel(beat) });
205193
205565
  else continue;
@@ -205232,7 +205604,7 @@ function runScriptDoc(loaded, opts = {}) {
205232
205604
  continue;
205233
205605
  }
205234
205606
  if (cut.has(beat.id)) continue;
205235
- 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)) });
205607
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent: indent + 1, snippet: cid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
205236
205608
  else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
205237
205609
  else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
205238
205610
  else continue;
@@ -206064,8 +206436,8 @@ var require_base64_js = /* @__PURE__ */ __commonJSMin(((exports2) => {
206064
206436
  }
206065
206437
  return arr;
206066
206438
  }
206067
- function tripletToBase64(num) {
206068
- return lookup[num >> 18 & 63] + lookup[num >> 12 & 63] + lookup[num >> 6 & 63] + lookup[num & 63];
206439
+ function tripletToBase64(num2) {
206440
+ return lookup[num2 >> 18 & 63] + lookup[num2 >> 12 & 63] + lookup[num2 >> 6 & 63] + lookup[num2 & 63];
206069
206441
  }
206070
206442
  function encodeChunk(uint82, start, end) {
206071
206443
  var tmp;
@@ -211842,26 +212214,26 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports2) => {
211842
212214
  function parseEntity(parser) {
211843
212215
  var entity = parser.entity;
211844
212216
  var entityLC = entity.toLowerCase();
211845
- var num;
212217
+ var num2;
211846
212218
  var numStr = "";
211847
212219
  if (parser.ENTITIES[entity]) return parser.ENTITIES[entity];
211848
212220
  if (parser.ENTITIES[entityLC]) return parser.ENTITIES[entityLC];
211849
212221
  entity = entityLC;
211850
212222
  if (entity.charAt(0) === "#") if (entity.charAt(1) === "x") {
211851
212223
  entity = entity.slice(2);
211852
- num = parseInt(entity, 16);
211853
- numStr = num.toString(16);
212224
+ num2 = parseInt(entity, 16);
212225
+ numStr = num2.toString(16);
211854
212226
  } else {
211855
212227
  entity = entity.slice(1);
211856
- num = parseInt(entity, 10);
211857
- numStr = num.toString(10);
212228
+ num2 = parseInt(entity, 10);
212229
+ numStr = num2.toString(10);
211858
212230
  }
211859
212231
  entity = entity.replace(/^0+/, "");
211860
- if (isNaN(num) || numStr.toLowerCase() !== entity) {
212232
+ if (isNaN(num2) || numStr.toLowerCase() !== entity) {
211861
212233
  strictFail(parser, "Invalid character entity");
211862
212234
  return "&" + parser.entity + ";";
211863
212235
  }
211864
- return String.fromCodePoint(num);
212236
+ return String.fromCodePoint(num2);
211865
212237
  }
211866
212238
  function beginWhiteSpace(parser, c2) {
211867
212239
  if (c2 === "<") {
@@ -213913,20 +214285,20 @@ var require_utils3 = /* @__PURE__ */ __commonJSMin(((exports2) => {
213913
214285
  return al + bl + cl + dl + el >>> 0;
213914
214286
  }
213915
214287
  exports2.sum64_5_lo = sum64_5_lo;
213916
- function rotr64_hi(ah, al, num) {
213917
- return (al << 32 - num | ah >>> num) >>> 0;
214288
+ function rotr64_hi(ah, al, num2) {
214289
+ return (al << 32 - num2 | ah >>> num2) >>> 0;
213918
214290
  }
213919
214291
  exports2.rotr64_hi = rotr64_hi;
213920
- function rotr64_lo(ah, al, num) {
213921
- return (ah << 32 - num | al >>> num) >>> 0;
214292
+ function rotr64_lo(ah, al, num2) {
214293
+ return (ah << 32 - num2 | al >>> num2) >>> 0;
213922
214294
  }
213923
214295
  exports2.rotr64_lo = rotr64_lo;
213924
- function shr64_hi(ah, al, num) {
213925
- return ah >>> num;
214296
+ function shr64_hi(ah, al, num2) {
214297
+ return ah >>> num2;
213926
214298
  }
213927
214299
  exports2.shr64_hi = shr64_hi;
213928
- function shr64_lo(ah, al, num) {
213929
- return (ah << 32 - num | al >>> num) >>> 0;
214300
+ function shr64_lo(ah, al, num2) {
214301
+ return (ah << 32 - num2 | al >>> num2) >>> 0;
213930
214302
  }
213931
214303
  exports2.shr64_lo = shr64_lo;
213932
214304
  }));
@@ -223440,7 +223812,7 @@ function paragraph(el, prev) {
223440
223812
  });
223441
223813
  case "line": {
223442
223814
  const kids2 = [];
223443
- 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 }));
223815
+ 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 }));
223444
223816
  if (el.direction) kids2.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
223445
223817
  kids2.push(...bodyRuns(el.runs, TOKENS.inkRead));
223446
223818
  return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids2 });
@@ -241743,8 +242115,8 @@ var StandardFont = class extends PDFFont {
241743
242115
  return name in STANDARD_FONTS;
241744
242116
  }
241745
242117
  };
241746
- var toHex = function(num) {
241747
- return `0000${num.toString(16)}`.slice(-4);
242118
+ var toHex = function(num2) {
242119
+ return `0000${num2.toString(16)}`.slice(-4);
241748
242120
  };
241749
242121
  var EmbeddedFont = class extends PDFFont {
241750
242122
  constructor(document2, font, id) {
@@ -245640,13 +246012,14 @@ async function scriptToPdf(doc) {
245640
246012
  case "line": {
245641
246013
  const cueX = cx2 + DIALOGUE_INDENT2;
245642
246014
  let bodyX = cueX;
245643
- if (el.character) {
246015
+ const cue = el.character ? cueLabel(el.character, el.qualifierName) : "";
246016
+ if (cue) {
245644
246017
  pdf.font("Sans-Bold").fontSize(CUE);
245645
- bodyX = cueX + pdf.widthOfString(el.character.toUpperCase()) + CUE_GAP;
246018
+ bodyX = cueX + pdf.widthOfString(cue) + CUE_GAP;
245646
246019
  }
245647
246020
  const yTop = pdf.y;
245648
- if (el.character) {
245649
- 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 });
246021
+ if (cue) {
246022
+ pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character))).text(cue, cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
245650
246023
  pdf.y = yTop;
245651
246024
  }
245652
246025
  pdf.fontSize(BODY);
@@ -246350,8 +246723,9 @@ function staleParts(s, liveText, line2) {
246350
246723
  const stale = [];
246351
246724
  if (liveText !== s.baseline) stale.push("the text");
246352
246725
  if (s.proposedCharacter !== void 0 && (line2?.character ?? "") !== (s.baselineCharacter ?? "")) stale.push("the speaker");
246726
+ if (s.proposedQualifier !== void 0 && (line2?.qualifier ?? "") !== (s.baselineQualifier ?? "")) stale.push("the speaker qualifier");
246353
246727
  if (s.proposedDirection !== void 0 && (line2?.direction ?? "") !== (s.baselineDirection ?? "")) stale.push("the direction");
246354
- if ((s.proposedCharacter !== void 0 || s.proposedDirection !== void 0) && !line2) stale.push("the line (no longer a spoken line)");
246728
+ if ((s.proposedCharacter !== void 0 || s.proposedQualifier !== void 0 || s.proposedDirection !== void 0) && !line2) stale.push("the line (no longer a spoken line)");
246355
246729
  return stale;
246356
246730
  }
246357
246731
  function listOpenSuggestions(loaded, filter2 = {}) {
@@ -246506,6 +246880,12 @@ function applySuggestionDecisions(loaded, decisions, opts = {}) {
246506
246880
  work.touch(flow.path);
246507
246881
  edited = true;
246508
246882
  }
246883
+ if (line2 && flow && s.proposedQualifier !== void 0 && s.proposedQualifier !== (line2.qualifier ?? "")) {
246884
+ if (s.proposedQualifier === "") delete line2.qualifier;
246885
+ else line2.qualifier = s.proposedQualifier;
246886
+ work.touch(flow.path);
246887
+ edited = true;
246888
+ }
246509
246889
  if (line2 && flow && s.proposedDirection !== void 0 && s.proposedDirection !== (line2.direction ?? "")) {
246510
246890
  if (s.proposedDirection === "") delete line2.direction;
246511
246891
  else line2.direction = s.proposedDirection;
@@ -246556,7 +246936,7 @@ async function exportEditableScript(loaded, opts) {
246556
246936
  if (isEditable(el)) {
246557
246937
  const code = markerOf.get(el);
246558
246938
  const baseline = source2[el.id] ?? "";
246559
- 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 };
246939
+ 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 };
246560
246940
  skeleton.push({ kind: "box", marker: code });
246561
246941
  } else {
246562
246942
  skeleton.push({ kind: el.kind, node: el.id, text: contextText(el) });
@@ -246623,7 +247003,7 @@ function box(el, code, status) {
246623
247003
  const textW = Math.max(MIN_TEXT, FULL - ind - LEAD2 - MARGIN2);
246624
247004
  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 });
246625
247005
  const lead = [];
246626
- if (el.kind === "line" && el.character) lead.push(new TextRun({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character) }));
247006
+ if (el.kind === "line" && el.character) lead.push(new TextRun({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character) }));
246627
247007
  if (el.kind === "line" && el.direction) lead.push(new TextRun({ text: `(${el.direction})`, font: SERIF, italics: true, size: S.cue, color: TOKENS.muted, break: el.character ? 1 : 0 }));
246628
247008
  if (el.kind === "option") lead.push(new TextRun({ text: "\u25C7", color: TOKENS.accent, size: S.body }));
246629
247009
  const ink = el.kind === "narration" ? TOKENS.inkSoft : el.kind === "option" ? TOKENS.ink : TOKENS.inkRead;
@@ -247087,6 +247467,7 @@ function planEditableImport(loaded, returned, opts) {
247087
247467
  const places = indexPlaces(loaded);
247088
247468
  const live = sourceStrings(loaded);
247089
247469
  const cast = loaded.project.cast ?? [];
247470
+ const qualifiers = projectQualifiers(loaded.project);
247090
247471
  const suggestions = [];
247091
247472
  const comments = [];
247092
247473
  let unchanged = 0, stale = 0;
@@ -247170,22 +247551,25 @@ function planEditableImport(loaded, returned, opts) {
247170
247551
  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." });
247171
247552
  }
247172
247553
  const cue = sent.kind === "line" ? readLead(b.lead.proposed) : void 0;
247173
- let proposedCharacter, proposedDirection;
247554
+ let proposedCharacter, proposedQualifier, proposedDirection;
247174
247555
  if (sent.kind === "line" && cue) {
247175
- const sentCue = (sent.character ?? "").toUpperCase();
247176
- if (cue.character && cue.character.toUpperCase() !== sentCue) {
247177
- const match = cast.find((c2) => c2.name.toUpperCase() === cue.character.toUpperCase());
247178
- if (match) proposedCharacter = match.name;
247179
- else {
247180
- 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.` });
247181
- comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sent.character ?? "(none)"} to ${cue.character}, who isn't in the cast.`, ts);
247182
- }
247556
+ const read2 = readCue(cue.character, sent, cast, qualifiers);
247557
+ if (read2.unknownSpeaker !== void 0) {
247558
+ problems.push({ severity: "warning", kind: "unknown-speaker", marker: code, anchor: sent.id, message: `The speaker was changed to ${read2.unknownSpeaker}, who isn't in the cast; it's in a comment.` });
247559
+ comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sentCue(sent, qualifiers) || "(none)"} to ${read2.unknownSpeaker}, who isn't in the cast.`, ts);
247183
247560
  }
247561
+ if (read2.unknownQualifier !== void 0) {
247562
+ problems.push({ severity: "warning", kind: "unknown-qualifier", marker: code, anchor: sent.id, message: `The speaker qualifier was changed to (${read2.unknownQualifier}), which isn't one of the project's qualifiers; it's in a comment.` });
247563
+ comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sentCue(sent, qualifiers) || "(none)"} to ${tidy(cue.character)}, but (${read2.unknownQualifier}) isn't one of the project's qualifiers.`, ts);
247564
+ }
247565
+ if (read2.character !== void 0 && read2.character !== (sent.character ?? "")) proposedCharacter = read2.character;
247566
+ if (read2.qualifier !== void 0 && read2.qualifier !== (sent.qualifier ?? "")) proposedQualifier = read2.qualifier;
247184
247567
  if (tidy(cue.direction) !== tidy(sent.direction ?? "")) proposedDirection = tidy(cue.direction);
247185
247568
  }
247569
+ const cueChanged = !!proposedCharacter || proposedQualifier !== void 0 || proposedDirection !== void 0;
247186
247570
  for (const note of notes) comment(here ?? nearestLine(boxes.indexOf(b)), author, note, ts);
247187
247571
  if (!here) {
247188
- if (textChanged || proposedCharacter || proposedDirection !== void 0) {
247572
+ if (textChanged || cueChanged) {
247189
247573
  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.` });
247190
247574
  comment(nearestLine(boxes.indexOf(b)), author, `For a line no longer in the project (${quote(sent.baseline)}), the editor wrote: ${quote(proposed)}`, ts);
247191
247575
  } else unchanged++;
@@ -247198,16 +247582,17 @@ function planEditableImport(loaded, returned, opts) {
247198
247582
  if (textChanged && !sameList(placeholders(proposed), placeholders(sent.baseline))) {
247199
247583
  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.` });
247200
247584
  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);
247201
- if (!proposedCharacter && proposedDirection === void 0) continue;
247585
+ if (!cueChanged) continue;
247202
247586
  }
247203
247587
  const keepText = textChanged && sameList(placeholders(proposed), placeholders(sent.baseline));
247204
- if (!keepText && !proposedCharacter && proposedDirection === void 0) {
247588
+ if (!keepText && !cueChanged) {
247205
247589
  unchanged++;
247206
247590
  continue;
247207
247591
  }
247208
247592
  suggestions.push(suggestion2({
247209
247593
  proposed: keepText ? proposed : sent.baseline,
247210
247594
  ...proposedCharacter ? { proposedCharacter, baselineCharacter: sent.character ?? "" } : {},
247595
+ ...proposedQualifier !== void 0 ? { proposedQualifier, baselineQualifier: sent.qualifier ?? "" } : {},
247211
247596
  ...proposedDirection !== void 0 ? { proposedDirection, baselineDirection: sent.direction ?? "" } : {}
247212
247597
  }));
247213
247598
  }
@@ -247288,6 +247673,32 @@ function readLead(text) {
247288
247673
  const character = lines.find((l2) => l2 !== direction) ?? "";
247289
247674
  return { character, direction: direction ? direction.slice(1, -1) : "" };
247290
247675
  }
247676
+ function sentQualifierName(sent, qualifiers) {
247677
+ if (!sent.qualifier) return "";
247678
+ return sent.qualifierName ?? qualifiers.find((q) => q.gameId === sent.qualifier)?.name ?? sent.qualifier;
247679
+ }
247680
+ function sentCue(sent, qualifiers) {
247681
+ if (!sent.character) return "";
247682
+ const name = sentQualifierName(sent, qualifiers);
247683
+ return name ? `${sent.character} (${name})` : sent.character;
247684
+ }
247685
+ function readCue(text, sent, cast, qualifiers) {
247686
+ const cue = tidy(text);
247687
+ if (!cue) return {};
247688
+ const upper = (s) => s.toUpperCase();
247689
+ if (upper(cue) === upper(sentCue(sent, qualifiers))) return {};
247690
+ const sentCharacter = sent.character ?? "";
247691
+ const speaker = (name) => cast.find((c2) => upper(c2.name) === upper(name))?.name ?? (sentCharacter && upper(name) === upper(sentCharacter) ? sentCharacter : void 0);
247692
+ const whole = speaker(cue);
247693
+ if (whole !== void 0) return { character: whole, qualifier: "" };
247694
+ const m = /^(.*?)\s*\(([^()]*)\)$/s.exec(cue);
247695
+ const rest = m ? speaker(m[1].trim()) : void 0;
247696
+ if (!m || rest === void 0) return { unknownSpeaker: cue };
247697
+ const bracket = m[2].trim();
247698
+ if (sent.qualifier && upper(bracket) === upper(sentQualifierName(sent, qualifiers))) return { character: rest, qualifier: sent.qualifier };
247699
+ const q = qualifiers.find((x) => upper(x.name) === upper(bracket));
247700
+ return q ? { character: rest, qualifier: q.gameId } : { character: rest, unknownQualifier: bracket };
247701
+ }
247291
247702
  function rankAuthors(changes) {
247292
247703
  const counts = /* @__PURE__ */ new Map();
247293
247704
  for (const c2 of changes) if (c2.author) counts.set(c2.author, (counts.get(c2.author) ?? 0) + 1);
@@ -248846,8 +249257,8 @@ var PlasticProvider = class {
248846
249257
  }
248847
249258
  /**
248848
249259
  * Status for a batch of files in ONE `cm status --machinereadable --all --ignored`
248849
- * spawn. The machine format lists one item per line as `<2-letter code> <path>`
248850
- * (absolute paths, quoted when they contain spaces).
249260
+ * spawn. The machine format lists one item per line, its fields split by the separator
249261
+ * we ask for: code, absolute path (unquoted), and two merge flags (see parseCmStatusLine).
248851
249262
  *
248852
249263
  * Flag choice matters: `cm status` defaults to `--controlledchanged`, which omits
248853
249264
  * a content-modified-but-not-checked-out file (CH) and local deletes/moves. `--all`
@@ -248859,8 +249270,9 @@ var PlasticProvider = class {
248859
249270
  * files (plus one `cm whoami`) to fill `outOfDate` (loaded changeset < head) and
248860
249271
  * lock holder (`openedByMe` when that's us, else `lockedBy: ["user@workspace"]`).
248861
249272
  *
248862
- * NOTE: status codes, flags, and the fileinfo format are validated against the Unity
248863
- * VCS CLI docs / UEPlasticPlugin, not a live workspace - worth one real smoke test.
249273
+ * NOTE: the status line format follows the Unreal Plastic plugin (UEPlasticPlugin), which
249274
+ * runs against live workspaces; the first version here assumed quoted paths, matched no
249275
+ * file on a real workspace, and so checked out files already checked out.
248864
249276
  *
248865
249277
  * @param {string[]} filePaths
248866
249278
  * @param {import('../vcStatus.js').VCStatusOptions} [options]
@@ -248881,7 +249293,7 @@ var PlasticProvider = class {
248881
249293
  }
248882
249294
  };
248883
249295
  function plasticStatusArgs(filePaths) {
248884
- return ["status", "--machinereadable", "--all", "--ignored", ...filePaths.map((p) => resolve8(p))];
249296
+ return ["status", "--machinereadable", `--fieldseparator=${STATUS_SEPARATOR}`, "--all", "--ignored", ...filePaths.map((p) => resolve8(p))];
248885
249297
  }
248886
249298
  function buildPlasticBase(result, filePaths) {
248887
249299
  const byPath = /* @__PURE__ */ new Map();
@@ -248977,17 +249389,33 @@ var PLASTIC_DIRTY_CODES = /* @__PURE__ */ new Set([
248977
249389
  ]);
248978
249390
  var PLASTIC_UNTRACKED_CODES = /* @__PURE__ */ new Set(["PR", "IG"]);
248979
249391
  var PLASTIC_CHECKED_OUT_CODES = /* @__PURE__ */ new Set(["CO", "AD", "CP", "RP", "MV"]);
249392
+ var STATUS_SEPARATOR = ";";
248980
249393
  function parseCmStatusLine(line2) {
248981
249394
  const trimmed = line2.trim();
248982
- const space = trimmed.indexOf(" ");
248983
- if (space === -1) return null;
248984
- const codes = trimmed.slice(0, space).split("+");
249395
+ if (!trimmed) return null;
249396
+ let fields;
249397
+ if (trimmed.includes(STATUS_SEPARATOR)) {
249398
+ fields = trimmed.split(STATUS_SEPARATOR);
249399
+ } else {
249400
+ const space = trimmed.indexOf(" ");
249401
+ if (space === -1) return null;
249402
+ fields = [trimmed.slice(0, space), trimmed.slice(space + 1).replace(/\s+(True|False)\s+\S+$/i, "")];
249403
+ }
249404
+ const codes = fields[0].trim().split("+");
248985
249405
  const dirty = codes.some((c2) => PLASTIC_DIRTY_CODES.has(c2));
248986
249406
  const untracked = codes.some((c2) => PLASTIC_UNTRACKED_CODES.has(c2));
248987
249407
  if (!dirty && !untracked) return null;
248988
- const rest = trimmed.slice(space + 1).trim();
248989
- const quoted = [...rest.matchAll(/"([^"]*)"/g)].map((m) => m[1]);
248990
- const paths = quoted.length > 0 ? quoted : [rest];
249408
+ let rest = fields.slice(1);
249409
+ if (rest.length >= 3 && /^(true|false)$/i.test(rest[rest.length - 2].trim())) rest = rest.slice(0, -2);
249410
+ let paths;
249411
+ if (codes.includes("MV")) {
249412
+ if (rest.length > 0 && /^\d+%$/.test(rest[0].trim())) rest = rest.slice(1);
249413
+ paths = rest.length >= 2 ? [rest.slice(0, -1).join(STATUS_SEPARATOR), rest[rest.length - 1]] : rest;
249414
+ } else {
249415
+ paths = [rest.join(STATUS_SEPARATOR)];
249416
+ }
249417
+ paths = paths.map((p) => p.trim().replace(/^"(.*)"$/, "$1")).filter((p) => p.length > 0);
249418
+ if (paths.length === 0) return null;
248991
249419
  const checkedOut = !untracked && codes.some((c2) => PLASTIC_CHECKED_OUT_CODES.has(c2));
248992
249420
  return { info: { tracked: !untracked, dirty, checkedOut, ignored: codes.includes("IG") }, paths };
248993
249421
  }
@@ -249561,7 +249989,7 @@ function fileStatus(filePaths, options = {}) {
249561
249989
  // package.json
249562
249990
  var package_default = {
249563
249991
  name: "@patterkit/cli",
249564
- version: "0.9.3",
249992
+ version: "0.10.1",
249565
249993
  description: "The `patter` CLI: validate, format, export, play, test coverage, localise, and merge Patter projects.",
249566
249994
  type: "module",
249567
249995
  license: "MIT",
@@ -249590,9 +250018,9 @@ var package_default = {
249590
250018
  "build:standalone:others": "node scripts/build-standalone.mjs --targets=linux-x64,linux-arm64,windows-x64"
249591
250019
  },
249592
250020
  dependencies: {
249593
- "@patterkit/core": "0.4.1",
249594
- "@patterkit/ops": "0.14.2",
249595
- "@wildwinter/simple-vc-lib": "^0.5.1"
250021
+ "@patterkit/core": "0.6.0",
250022
+ "@patterkit/ops": "0.16.0",
250023
+ "@wildwinter/simple-vc-lib": "^0.5.2"
249596
250024
  }
249597
250025
  };
249598
250026
 
@@ -250152,7 +250580,8 @@ ${count} issue(s)${advisory}`);
250152
250580
  console.log("no open suggestions");
250153
250581
  return 0;
250154
250582
  }
250155
- for (const o of open) console.log(describeSuggestion(o));
250583
+ const nameOf = qualifierNamer(loaded.project);
250584
+ for (const o of open) console.log(describeSuggestion(o, nameOf));
250156
250585
  const stale = open.filter((o) => o.stale.length).length;
250157
250586
  console.log(`${open.length} open suggestion(s)${stale ? `, ${stale} out of date` : ""}`);
250158
250587
  return 0;
@@ -250353,12 +250782,13 @@ function renderImportReport(plan) {
250353
250782
  for (const p of r.problems) out.push(` [${p.severity}] ${p.message}${p.anchor ? ` (${p.anchor})` : ""}`);
250354
250783
  return out.join("\n");
250355
250784
  }
250356
- function describeSuggestion(o) {
250785
+ function describeSuggestion(o, qualifierName) {
250357
250786
  const s = o.suggestion;
250358
250787
  const parts = [];
250359
250788
  if (s.proposedCut) parts.push("cut this line");
250360
250789
  if (s.proposed !== s.baseline) parts.push(`"${s.baseline}" -> "${s.proposed}"`);
250361
250790
  if (s.proposedCharacter !== void 0) parts.push(`speaker ${s.baselineCharacter || "(none)"} -> ${s.proposedCharacter}`);
250791
+ if (s.proposedQualifier !== void 0) parts.push(`qualifier ${qualifierName(s.baselineQualifier) ?? "(none)"} -> ${qualifierName(s.proposedQualifier) ?? "(none)"}`);
250362
250792
  if (s.proposedDirection !== void 0) parts.push(`direction "${s.baselineDirection ?? ""}" -> "${s.proposedDirection}"`);
250363
250793
  const from = s.handoff ? ` [${s.handoff.id}]` : "";
250364
250794
  const stale = o.stale.length ? ` OUT OF DATE (${o.stale.join(", ")})` : "";