@patterkit/ops 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +207 -29
  2. package/dist/index.js +948 -476
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "fs";
3
3
  import { join as join2, dirname as dirname2, resolve as resolve2, basename } from "path";
4
4
  import { parseSource } from "@patterkit/core";
5
- import { isCaseOnlyPropertyName } from "@patterkit/model";
5
+ import { isCaseOnlyPropertyName, projectLayout } from "@patterkit/model";
6
6
 
7
7
  // src/game-scopes.ts
8
8
  import { existsSync, readdirSync, readFileSync } from "fs";
@@ -68,7 +68,13 @@ function gameScopesSnapshot(root, project) {
68
68
  names = readdirSync(found.dir).filter((n) => n.endsWith(SCOPES_FILE_SUFFIX)).sort();
69
69
  } catch {
70
70
  }
71
- return names.map((fileName) => ({ fileName, text: readFileSync(join(found.dir, fileName), "utf8") }));
71
+ return names.flatMap((fileName) => {
72
+ try {
73
+ return [{ fileName, text: readFileSync(join(found.dir, fileName), "utf8") }];
74
+ } catch {
75
+ return [];
76
+ }
77
+ });
72
78
  }
73
79
  function patterScopesFile(project) {
74
80
  const declarations = (project.properties ?? []).filter((p) => p.shared ?? true).map((p) => ({
@@ -266,6 +272,22 @@ function walkFiles(dir, ext) {
266
272
  }
267
273
  return out.sort();
268
274
  }
275
+ function walkFilesByExt(dir, exts) {
276
+ const out = new Map(exts.map((ext) => [ext, []]));
277
+ const walk = (d) => {
278
+ for (const e of readDirSafe(d)) {
279
+ if (e.name.startsWith(".")) continue;
280
+ const p = join2(d, e.name);
281
+ if (e.isDirectory()) walk(p);
282
+ else if (e.isFile()) {
283
+ for (const ext of exts) if (e.name.endsWith(ext)) out.get(ext).push(p);
284
+ }
285
+ }
286
+ };
287
+ walk(dir);
288
+ for (const files of out.values()) files.sort();
289
+ return out;
290
+ }
269
291
  function findProjectFile(startPath) {
270
292
  let dir = resolve2(startPath);
271
293
  try {
@@ -350,7 +372,7 @@ function sceneIdForShard(shardPath) {
350
372
  if (p.endsWith(".patterx")) {
351
373
  const projectFile = findProjectFile(p);
352
374
  const project = parseFile(projectFile, "patter/project", "project");
353
- const flowDir = join2(dirname2(projectFile), project.layout?.flow ?? "scenes/");
375
+ const flowDir = join2(dirname2(projectFile), projectLayout(project).flow);
354
376
  const flowPath = join2(flowDir, basename(p).replace(/\.patterx$/, ".patterflow"));
355
377
  return parseSource(readFileSync2(flowPath, "utf8")).scene?.id;
356
378
  }
@@ -362,7 +384,7 @@ function loadProjectLanding(startPath, opts) {
362
384
  const projectFile = findProjectFile(startPath);
363
385
  const root = dirname2(projectFile);
364
386
  const project = parseFile(projectFile, "patter/project", "project");
365
- const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
387
+ const layout = projectLayout(project);
366
388
  const flowFiles = walkFiles(join2(root, layout.flow), ".patterflow");
367
389
  const launchPath = opts?.launchPath ? resolve2(opts.launchPath) : void 0;
368
390
  let landingFile;
@@ -407,10 +429,10 @@ function loadProjectLanding(startPath, opts) {
407
429
  if (!found) {
408
430
  for (const f of walkFiles(strings, ".patterloc")) {
409
431
  const loc = parseFile(f, "patter/strings", "strings");
410
- if (loc.scene !== landingScene.id) continue;
432
+ if (loc.scene !== landingScene.id || loc.locale !== defaultLocale) continue;
411
433
  locales.push(loc);
412
434
  localeFiles.push(f);
413
- if (loc.locale === defaultLocale || loc.default) break;
435
+ break;
414
436
  }
415
437
  }
416
438
  }
@@ -432,7 +454,7 @@ function loadProject(startPath) {
432
454
  const projectFile = findProjectFile(startPath);
433
455
  const root = dirname2(projectFile);
434
456
  const project = parseFile(projectFile, "patter/project", "project");
435
- const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
457
+ const layout = projectLayout(project);
436
458
  const scenes = [];
437
459
  const sceneFiles = {};
438
460
  for (const f of walkFiles(join2(root, layout.flow), ".patterflow")) {
@@ -471,8 +493,8 @@ function applyWrites(writes) {
471
493
  }
472
494
 
473
495
  // src/validate.ts
474
- import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
475
- import { basename as basename2, isAbsolute, join as join3, relative as relative2 } from "path";
496
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
497
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
476
498
 
477
499
  // src/merge.ts
478
500
  import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
@@ -487,12 +509,12 @@ function sidecarIssues(sidecarPaths) {
487
509
  message: "unresolved merge conflict - resolve it and delete the .patterconflict sidecar before committing"
488
510
  }));
489
511
  }
512
+ var MERGE_TYPES = { flow: "flow", strings: "loc", authoring: "authoring", project: "project" };
490
513
  function detectMergeType(file) {
491
514
  const s = typeof file.schema === "string" ? file.schema : "";
492
- if (s.startsWith("patter/flow")) return "flow";
493
- if (s.startsWith("patter/strings")) return "loc";
494
- if (s.startsWith("patter/authoring")) return "authoring";
495
- if (s.startsWith("patter/project")) return "project";
515
+ const name = /^patter\/([a-z]+)(?:@|$)/.exec(s)?.[1];
516
+ const type = name ? MERGE_TYPES[name] : void 0;
517
+ if (type) return type;
496
518
  throw new UnsupportedMergeError(`cannot detect a Patter merge type from schema '${s}'`);
497
519
  }
498
520
  function runMerge(base, ours, theirs, opts) {
@@ -519,7 +541,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
519
541
  conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
520
542
  return ours;
521
543
  }
522
- function mergeMap(base, ours, theirs, prefix, conflicts, resolve6) {
544
+ function mergeMap(base, ours, theirs, prefix, conflicts, resolve7) {
523
545
  const out = {};
524
546
  for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
525
547
  const b = base[k], o = ours[k], t = theirs[k];
@@ -528,7 +550,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve6) {
528
550
  else if (eq(b, o)) v = t;
529
551
  else if (eq(b, t)) v = o;
530
552
  else {
531
- const r = resolve6?.(k, b, o, t);
553
+ const r = resolve7?.(k, b, o, t);
532
554
  if (r && r.resolved) v = r.value;
533
555
  else {
534
556
  conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
@@ -587,11 +609,50 @@ function mergeById(base, ours, theirs, path, conflicts) {
587
609
  return Object.values(merged).sort((a, b) => String(asMap(a).ts ?? "").localeCompare(String(asMap(b).ts ?? "")));
588
610
  }
589
611
  function mergeComments(base, ours, theirs) {
590
- const byId = /* @__PURE__ */ new Map();
591
- for (const c of [...base, ...ours, ...theirs]) {
592
- if (isObj(c) && typeof c.id === "string" && !byId.has(c.id)) byId.set(c.id, c);
612
+ const index = (arr) => {
613
+ const m = /* @__PURE__ */ new Map();
614
+ for (const c of arr) if (isObj(c) && typeof c.id === "string") m.set(c.id, c);
615
+ return m;
616
+ };
617
+ const b = index(base), o = index(ours), t = index(theirs);
618
+ const msgKey = (m) => `${String(m.author ?? "")}|${String(m.ts ?? "")}`;
619
+ const out = [];
620
+ for (const id of /* @__PURE__ */ new Set([...b.keys(), ...o.keys(), ...t.keys()])) {
621
+ const bc = b.get(id), oc = o.get(id), tc = t.get(id);
622
+ const sides = [oc, tc].filter((c) => c !== void 0);
623
+ if (sides.length === 0) continue;
624
+ if (![bc, oc, tc].some((c) => Array.isArray(c?.messages))) {
625
+ out.push(oc ?? tc);
626
+ continue;
627
+ }
628
+ const deletedBy = /* @__PURE__ */ new Set();
629
+ for (const c of [oc, tc]) {
630
+ if (c === void 0 && bc) {
631
+ for (const m of asArr(bc.messages)) if (isObj(m)) deletedBy.add(msgKey(m));
632
+ }
633
+ }
634
+ const messages = /* @__PURE__ */ new Map();
635
+ for (const c of [bc, ...sides]) {
636
+ for (const m of asArr(c?.messages)) {
637
+ if (!isObj(m)) continue;
638
+ const key = msgKey(m);
639
+ const seen = messages.get(key);
640
+ if (!seen || m.deleted === true && seen.deleted !== true) messages.set(key, m);
641
+ }
642
+ }
643
+ for (const key of deletedBy) {
644
+ const m = messages.get(key);
645
+ if (m && m.deleted !== true) messages.set(key, { ...m, body: "", deleted: true });
646
+ }
647
+ const merged = [...messages.values()].sort((x, y) => String(x.ts ?? "").localeCompare(String(y.ts ?? "")));
648
+ if (merged.length === 0 || merged.every((m) => m.deleted === true)) continue;
649
+ const was = bc?.resolved === true;
650
+ const oursSays = oc ? oc.resolved === true : was, theirsSays = tc ? tc.resolved === true : was;
651
+ const resolved = oursSays !== was ? oursSays : theirsSays;
652
+ const { resolved: _r, messages: _m, ...rest } = oc ?? tc;
653
+ out.push({ ...rest, ...resolved ? { resolved: true } : {}, messages: merged });
593
654
  }
594
- return [...byId.values()].sort((a, b) => String(a.ts ?? "").localeCompare(String(b.ts ?? "")));
655
+ return out.sort((x, y) => String(asArr(x.messages).map(asMap)[0]?.ts ?? x.ts ?? "").localeCompare(String(asArr(y.messages).map(asMap)[0]?.ts ?? y.ts ?? "")));
595
656
  }
596
657
  function lastWriterWins(oursEdits, theirsEdits) {
597
658
  return (id, _b, o, t) => {
@@ -817,11 +878,52 @@ var idMap = (nodes) => new Map(nodes.filter((n) => n.id).map((n) => [n.id, n]));
817
878
  var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
818
879
 
819
880
  // src/validate.ts
820
- import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
821
- import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
881
+ import { validateProject } from "@patterkit/core";
882
+ import { validateConditions, validateInterpolation, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
822
883
 
823
- // src/reachability.ts
884
+ // src/compile.ts
885
+ import { basename as basename2, extname, isAbsolute, resolve as resolve3 } from "path";
824
886
  import { exportBundle } from "@patterkit/compiler";
887
+ import { isContentlessBeat } from "@patterkit/model";
888
+ function compileScenes(loaded) {
889
+ const def = loaded.project.locales.default;
890
+ const written = /* @__PURE__ */ new Set();
891
+ for (const l of loaded.locales) if (l.locale === def) {
892
+ for (const [k, v] of Object.entries(l.strings)) if (v) written.add(k);
893
+ }
894
+ const clean = (n) => {
895
+ if (n.type === "group") return { ...n, children: (n.children ?? []).map(clean) };
896
+ if (!n.beats) return n;
897
+ const kept = n.beats.filter((b) => !isContentlessBeat(b, written.has(b.id)));
898
+ if (kept.length === n.beats.length) return n;
899
+ const { beats, ...rest } = n;
900
+ return kept.length ? { ...rest, beats: kept } : rest;
901
+ };
902
+ return loaded.scenes.map((s) => ({
903
+ ...s,
904
+ blocks: s.blocks.map((b) => ({ ...b, children: b.children.map(clean) }))
905
+ }));
906
+ }
907
+ function compileLoaded(loaded) {
908
+ return exportBundle({ project: loaded.project, scenes: compileScenes(loaded), locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
909
+ }
910
+ function defaultBundlePath(loaded) {
911
+ return `../patter-dist/${basename2(loaded.projectFile).replace(/\.patterproj$/, "")}.patterc`;
912
+ }
913
+ function bundleOutputPath(loaded) {
914
+ const rel = loaded.project.export?.bundle ?? defaultBundlePath(loaded);
915
+ return isAbsolute(rel) ? rel : resolve3(loaded.root, rel);
916
+ }
917
+ function besideBundlePath(loaded, ext) {
918
+ const bundle = bundleOutputPath(loaded);
919
+ const own = extname(bundle);
920
+ const path = (own ? bundle.slice(0, -own.length) : bundle) + ext;
921
+ if (path === bundle) throw new Error(`the ${ext} file would replace the bundle at ${bundle}; pass -o`);
922
+ return path;
923
+ }
924
+
925
+ // src/validate.ts
926
+ import { PROJECT_LOCALE_SCENE, projectLayout as projectLayout2 } from "@patterkit/model";
825
927
 
826
928
  // ../../../expr/packages/expr/src/ast.ts
827
929
  function deserialiseAst(node) {
@@ -928,8 +1030,7 @@ var termsOfAst = (ast, sceneId) => terms(ast, sceneId, keyOf);
928
1030
  var requirementsOf = (expr, sceneId) => expr === void 0 ? [] : termsOfAst(expr.ast, sceneId);
929
1031
  var asFlags = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
930
1032
  function reachabilityIssues(loaded, compiled) {
931
- const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
932
- const hostTokens = hostScopeTokens(loaded, bundle);
1033
+ const bundle = compiled ?? compileLoaded(loaded);
933
1034
  const preset = /* @__PURE__ */ new Set();
934
1035
  const notALatch = (decl, key) => {
935
1036
  if (decl.temporary) {
@@ -993,8 +1094,14 @@ function reachabilityIssues(loaded, compiled) {
993
1094
  const at = key.indexOf(":");
994
1095
  if (at > 0 && broken.has(key.slice(0, at))) broken.add(key);
995
1096
  }
996
- const hostDriven = (key) => hostTokens.has(key.slice(1, key.indexOf(".")));
997
- const monotonic = (key) => latched.has(key) && !broken.has(key) && !preset.has(key) && !hostDriven(key);
1097
+ const sharedWithGame = new Set(loaded.gameScopes ? (loaded.project.properties ?? []).filter((p) => p.shared ?? true).map((p) => `@patter.${p.name}`) : []);
1098
+ const projectOwned = (key) => {
1099
+ const bare = key.includes(SEP) ? key.slice(key.indexOf(SEP) + 1) : key;
1100
+ const prop = bare.includes(":") ? bare.slice(0, bare.indexOf(":")) : bare;
1101
+ if (bare.startsWith("@patter.")) return !sharedWithGame.has(prop);
1102
+ return bare.startsWith("@scene.");
1103
+ };
1104
+ const monotonic = (key) => latched.has(key) && !broken.has(key) && !preset.has(key) && projectOwned(key);
998
1105
  const cache = /* @__PURE__ */ new Map();
999
1106
  const inFlight = /* @__PURE__ */ new Set();
1000
1107
  const mustHold = (key) => {
@@ -1068,6 +1175,134 @@ function reachabilityIssues(loaded, compiled) {
1068
1175
  return issues;
1069
1176
  }
1070
1177
 
1178
+ // src/pack.ts
1179
+ import JSZip from "jszip";
1180
+ import { readFileSync as readFileSync4 } from "fs";
1181
+ import { dirname as dirname4, join as join4, relative as relative2, sep } from "path";
1182
+ import { parseSource as parseSource2 } from "@patterkit/core";
1183
+ import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
1184
+
1185
+ // src/handoff.ts
1186
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1187
+ import { basename as basename3, join as join3 } from "path";
1188
+ import { HANDOFF_SCHEMA } from "@patterkit/model";
1189
+ var HANDOFF_DIR = "handoffs";
1190
+ var HANDOFF_EXT = ".json";
1191
+ function handoffPath(root, id) {
1192
+ return join3(root, HANDOFF_DIR, `${id}${HANDOFF_EXT}`);
1193
+ }
1194
+ function serialiseHandoff(handoff) {
1195
+ return JSON.stringify(handoff, null, 2) + "\n";
1196
+ }
1197
+ function handoffWrite(root, handoff) {
1198
+ return { path: handoffPath(root, handoff.id), content: serialiseHandoff(handoff) };
1199
+ }
1200
+ function parseHandoff(text) {
1201
+ const h = JSON.parse(text);
1202
+ if (h.schema !== HANDOFF_SCHEMA) throw new Error(`not a handoff record (schema ${JSON.stringify(h.schema)})`);
1203
+ if (typeof h.id !== "string" || !h.lines || !Array.isArray(h.skeleton)) throw new Error("handoff record is missing its id, lines, or skeleton");
1204
+ return h;
1205
+ }
1206
+ function readHandoffs(root) {
1207
+ const handoffs = [];
1208
+ const issues = [];
1209
+ for (const file of walkFiles(join3(root, HANDOFF_DIR), HANDOFF_EXT)) {
1210
+ try {
1211
+ handoffs.push(parseHandoff(readFileSync3(file, "utf8")));
1212
+ } catch (e) {
1213
+ issues.push({ file, message: `${basename3(file)}: ${e instanceof Error ? e.message : String(e)}` });
1214
+ }
1215
+ }
1216
+ handoffs.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
1217
+ return { handoffs, issues };
1218
+ }
1219
+ function readHandoff(root, id) {
1220
+ const path = handoffPath(root, id);
1221
+ if (!existsSync3(path)) return void 0;
1222
+ return parseHandoff(readFileSync3(path, "utf8"));
1223
+ }
1224
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1225
+ function checkChar(body) {
1226
+ let sum = 0;
1227
+ for (let i = 0; i < body.length; i++) sum += (i + 1) * ALPHABET.indexOf(body[i]);
1228
+ return ALPHABET[sum % 31];
1229
+ }
1230
+ function randomBody(length, random) {
1231
+ let s = "";
1232
+ for (let i = 0; i < length; i++) s += ALPHABET[Math.floor(random() * ALPHABET.length)];
1233
+ return s;
1234
+ }
1235
+ function markerLength(count) {
1236
+ let length = 3;
1237
+ while (ALPHABET.length ** length < Math.max(1, count) * 1e3) length++;
1238
+ return length;
1239
+ }
1240
+ function issueMarkerCodes(count, random = Math.random) {
1241
+ const length = markerLength(count);
1242
+ const codes = /* @__PURE__ */ new Set();
1243
+ while (codes.size < count) {
1244
+ const body = randomBody(length, random);
1245
+ codes.add(body + checkChar(body));
1246
+ }
1247
+ return [...codes];
1248
+ }
1249
+ function formatMarker(code) {
1250
+ return `[#${code}]`;
1251
+ }
1252
+ function newHandoffId(taken = [], random = Math.random) {
1253
+ const used = new Set(taken);
1254
+ for (; ; ) {
1255
+ const id = `H-${randomBody(4, random)}`;
1256
+ if (!used.has(id)) return id;
1257
+ }
1258
+ }
1259
+ function readMarker(text) {
1260
+ const m = /\[\s*#?\s*([0-9A-Za-z]{2,9})\s*\]/.exec(text);
1261
+ if (!m) return void 0;
1262
+ const code = m[1].toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1");
1263
+ if ([...code].some((c) => !ALPHABET.includes(c))) return { code, valid: false };
1264
+ const body = code.slice(0, -1);
1265
+ return { code, valid: body.length >= 2 && checkChar(body) === code.slice(-1) };
1266
+ }
1267
+
1268
+ // src/pack.ts
1269
+ var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
1270
+ var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
1271
+ async function runPack(startPath) {
1272
+ const projectFile = findProjectFile(startPath);
1273
+ const root = dirname4(projectFile);
1274
+ const project = parseSource2(readFileSync4(projectFile, "utf8"));
1275
+ const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
1276
+ if (unresolved.length) {
1277
+ throw new Error(
1278
+ `${unresolved.length} unresolved merge conflict(s) - resolve them before packing, or the recipient gets your side of a disagreement with no sign of it:
1279
+ ` + unresolved.map((i) => ` ${i.file}`).join("\n")
1280
+ );
1281
+ }
1282
+ const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative2(root, abs).split(sep).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
1283
+ const scopes = gameScopesSnapshot(root, project);
1284
+ const handoffs = walkFiles(join4(root, HANDOFF_DIR), ".json").map((abs) => ({ rel: relative2(root, abs).split(sep).join("/"), text: readFileSync4(abs, "utf8") })).filter((f) => {
1285
+ try {
1286
+ return !parseHandoff(f.text).closed;
1287
+ } catch {
1288
+ return false;
1289
+ }
1290
+ });
1291
+ const manifest = {
1292
+ schema: "patter/document@0",
1293
+ project: { id: project.project.id, name: project.project.name },
1294
+ files: files.map((f) => f.rel),
1295
+ ...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {},
1296
+ ...handoffs.length ? { handoffs: handoffs.map((f) => f.rel) } : {}
1297
+ };
1298
+ const zip = new JSZip();
1299
+ zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
1300
+ for (const f of files) zip.file(f.rel, readFileSync4(f.abs, "utf8"), ENTRY_OPTS);
1301
+ for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
1302
+ for (const f of handoffs) zip.file(f.rel, f.text, ENTRY_OPTS);
1303
+ return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
1304
+ }
1305
+
1071
1306
  // src/validate.ts
1072
1307
  function runValidate(loaded) {
1073
1308
  const { project, scenes, locales } = loaded;
@@ -1079,10 +1314,20 @@ function runValidate(loaded) {
1079
1314
  const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
1080
1315
  const gameScopes = gameScopesIssues(loaded, scopes.notes);
1081
1316
  const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
1082
- const staleBundles = checkBundles(loaded);
1083
- const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
1084
- const orphans = orphanShards(loaded);
1085
- const reachability = structural.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded) : [];
1317
+ const tree = walkFilesByExt(loaded.root, [...SHARD_EXTENSIONS, CONFLICT_SIDECAR, ".patterc"]);
1318
+ const localisation = locIssues(loaded);
1319
+ let compiled;
1320
+ try {
1321
+ compiled = compileLoaded(loaded);
1322
+ } catch {
1323
+ compiled = void 0;
1324
+ }
1325
+ const staleBundles = checkBundles(loaded, compiled, tree.get(".patterc"));
1326
+ if (compiled) gameScopes.push(...unplayableScopes(loaded, compiled));
1327
+ const unresolvedMerges = sidecarIssues(tree.get(CONFLICT_SIDECAR));
1328
+ const orphans = orphanShards(loaded, tree);
1329
+ const structuralErrors = structural.filter((i) => i.severity !== "warning");
1330
+ const reachability = compiled && structuralErrors.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded, compiled) : [];
1086
1331
  return {
1087
1332
  structural,
1088
1333
  conditions,
@@ -1093,23 +1338,45 @@ function runValidate(loaded) {
1093
1338
  unresolvedMerges,
1094
1339
  orphans,
1095
1340
  gameScopes,
1096
- ok: structural.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
1341
+ localisation,
1342
+ ok: !localisation.some(isError) && structuralErrors.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
1097
1343
  };
1098
1344
  }
1099
1345
  var isError = (i) => i.severity === "error";
1346
+ function exportBlockers(loaded) {
1347
+ const { project, scenes, locales } = loaded;
1348
+ const merged = loaded.gameScopes?.merged;
1349
+ const foreignScopes = hostScopesToSpec(projectScopes(project, merged).host);
1350
+ const structural = validateProject({ project, scenes, authoring: loaded.authoring }).filter((i) => i.severity !== "warning");
1351
+ const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged }).filter(isError);
1352
+ const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged }).filter(isError);
1353
+ return [
1354
+ ...structural.map((i) => `[${i.code}] ${i.message}`),
1355
+ ...[...conditions, ...interpolation].map((i) => `[${i.field}] ${i.nodeId}: ${i.message} (${i.src})`),
1356
+ ...locIssues(loaded).filter(isError).map((i) => `[localisation] ${i.file}: ${i.message}`)
1357
+ ];
1358
+ }
1100
1359
  function gameScopesIssues(loaded, notes) {
1101
1360
  const out = [];
1102
1361
  if (loaded.gameScopesMissing) out.push({ file: loaded.projectFile, severity: "error", message: loaded.gameScopesMissing });
1103
1362
  const gs = loaded.gameScopes;
1104
1363
  if (!gs) return out;
1105
1364
  for (const i of gs.issues) out.push({ file: i.file, severity: i.severity, message: i.message });
1106
- for (const n of notes) out.push({ file: join3(gs.dir, n.file), severity: "warning", message: n.message });
1365
+ for (const n of notes) out.push({ file: join5(gs.dir, n.file), severity: "warning", message: n.message });
1107
1366
  const stale = patterScopesStale(loaded.project, gs);
1108
1367
  if (stale) out.push({ ...stale, severity: "warning" });
1109
1368
  return out;
1110
1369
  }
1111
- function orphanShards(loaded) {
1112
- const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...loaded.project.layout };
1370
+ function unplayableScopes(loaded, compiled) {
1371
+ const registry = previewRegistry(loaded.gameScopes, compiled);
1372
+ return (compiled.externalScopes ?? []).filter((t) => !registry?.has(t)).map((t) => ({
1373
+ file: loaded.projectFile,
1374
+ severity: "warning",
1375
+ message: `the content names @${t}, another engine's scope, and no game scopes folder declares it: the game can play it, but play, coverage, and the Play window cannot until the game shares its scopes (patter share-scopes)`
1376
+ }));
1377
+ }
1378
+ function orphanShards(loaded, tree = walkFilesByExt(loaded.root, SHARD_EXTENSIONS)) {
1379
+ const layout = projectLayout2(loaded.project);
1113
1380
  const kind = {
1114
1381
  ".patterflow": { what: "scene", home: layout.flow },
1115
1382
  ".patterloc": { what: "strings", home: layout.strings },
@@ -1117,37 +1384,33 @@ function orphanShards(loaded) {
1117
1384
  ".patterproj": { what: "project", home: null }
1118
1385
  };
1119
1386
  const inside = (file, dir) => {
1120
- const r = relative2(dir, file);
1121
- return r !== "" && !r.startsWith("..") && !isAbsolute(r);
1387
+ const r = relative3(dir, file);
1388
+ return r !== "" && !r.startsWith("..") && !isAbsolute2(r);
1122
1389
  };
1123
1390
  const out = [];
1124
1391
  for (const [ext, { what, home }] of Object.entries(kind)) {
1125
- for (const file of walkFiles(loaded.root, ext)) {
1126
- if (home ? inside(file, join3(loaded.root, home)) : file === loaded.projectFile) continue;
1127
- const rel = relative2(loaded.root, file);
1128
- const message = home ? `${rel} is a ${what} file outside the project's folders: nothing loads it, so none of it is in the project. Move it under ${home}, or delete it.` : `${rel} is a second project file: only ${basename2(loaded.projectFile)} is read. Delete it, or move it out of the project.`;
1392
+ for (const file of tree.get(ext) ?? []) {
1393
+ if (home ? inside(file, join5(loaded.root, home)) : file === loaded.projectFile) continue;
1394
+ const rel = relative3(loaded.root, file);
1395
+ const message = home ? `${rel} is a ${what} file outside the project's folders: nothing loads it, so none of it is in the project. Move it under ${home}, or delete it.` : `${rel} is a second project file: only ${basename4(loaded.projectFile)} is read. Delete it, or move it out of the project.`;
1129
1396
  out.push({ file, message });
1130
1397
  }
1131
1398
  }
1132
1399
  return out;
1133
1400
  }
1134
- function checkBundles(loaded) {
1401
+ function checkBundles(loaded, compiled, inTree) {
1135
1402
  const issues = [];
1136
- const bundles = walkFiles(loaded.root, ".patterc");
1137
- if (bundles.length === 0) return issues;
1138
- let fresh;
1139
- try {
1140
- fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged }).content.hash;
1141
- } catch {
1142
- return issues;
1143
- }
1403
+ const own = bundleOutputPath(loaded);
1404
+ const bundles = [.../* @__PURE__ */ new Set([...inTree, ...existsSync4(own) ? [own] : []])];
1405
+ if (bundles.length === 0 || !compiled) return issues;
1406
+ const fresh = compiled.content.hash;
1144
1407
  for (const file of bundles) {
1145
1408
  let hash;
1146
1409
  try {
1147
- const parsed = parseSource2(readFileSync3(file, "utf8"));
1410
+ const parsed = JSON.parse(readFileSync5(file, "utf8"));
1148
1411
  hash = parsed?.content?.hash;
1149
1412
  } catch {
1150
- issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
1413
+ issues.push({ file, message: "compiled bundle is not strict JSON, so no game can read it - run `patter export`" });
1151
1414
  continue;
1152
1415
  }
1153
1416
  if (hash !== fresh) {
@@ -1156,8 +1419,35 @@ function checkBundles(loaded) {
1156
1419
  }
1157
1420
  return issues;
1158
1421
  }
1422
+ function locIssues(loaded) {
1423
+ const out = [];
1424
+ const scenes = /* @__PURE__ */ new Set([...loaded.scenes.map((sc) => sc.id), PROJECT_LOCALE_SCENE]);
1425
+ const declared = new Set(loaded.project.locales.all);
1426
+ const bySlot = /* @__PURE__ */ new Map();
1427
+ const byKey = /* @__PURE__ */ new Map();
1428
+ loaded.locales.forEach((l, i) => {
1429
+ const file = loaded.localeFiles[i] ?? "";
1430
+ if (!scenes.has(l.scene)) out.push({ file, severity: "warning", message: `strings for scene '${l.scene}', which the project does not have: nothing reads them` });
1431
+ if (!declared.has(l.locale)) out.push({ file, severity: "warning", message: `strings in '${l.locale}', which is not one of the project's languages (${[...declared].join(", ")})` });
1432
+ const slot = `${l.scene}|${l.locale}`;
1433
+ const first = bySlot.get(slot);
1434
+ if (first) out.push({ file, severity: "warning", message: `a second file of '${l.locale}' strings for scene '${l.scene}' (the first is ${first})` });
1435
+ else bySlot.set(slot, file);
1436
+ for (const [key, text] of Object.entries(l.strings)) {
1437
+ const seen = byKey.get(`${l.locale}|${key}`);
1438
+ if (!seen) {
1439
+ byKey.set(`${l.locale}|${key}`, { file, text });
1440
+ continue;
1441
+ }
1442
+ if (seen.text !== text) out.push({ file, severity: "error", message: `'${key}' has different '${l.locale}' text here and in ${seen.file}; the project will not build until one is removed` });
1443
+ }
1444
+ });
1445
+ return out;
1446
+ }
1159
1447
  var hygieneCache = /* @__PURE__ */ new Map();
1160
1448
  function checkHygiene(files) {
1449
+ const wanted = new Set(files);
1450
+ for (const file of hygieneCache.keys()) if (!wanted.has(file)) hygieneCache.delete(file);
1161
1451
  const issues = [];
1162
1452
  for (const file of files) {
1163
1453
  let mtimeMs;
@@ -1173,7 +1463,7 @@ function checkHygiene(files) {
1173
1463
  }
1174
1464
  const fileIssues = [];
1175
1465
  try {
1176
- const text = readFileSync3(file, "utf8");
1466
+ const text = readFileSync5(file, "utf8");
1177
1467
  if (text.charCodeAt(0) === 65279) {
1178
1468
  fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
1179
1469
  }
@@ -1190,28 +1480,72 @@ function checkHygiene(files) {
1190
1480
  }
1191
1481
 
1192
1482
  // src/export.ts
1193
- import { basename as basename3, isAbsolute as isAbsolute2, join as join4 } from "path";
1194
- import { exportBundle as exportBundle3 } from "@patterkit/compiler";
1195
- import { isContentlessBeat } from "@patterkit/model";
1196
- function compileScenes(loaded) {
1197
- const def = loaded.project.locales.default;
1198
- const written = /* @__PURE__ */ new Set();
1199
- for (const l of loaded.locales) if (l.locale === def) {
1200
- for (const [k, v] of Object.entries(l.strings)) if (v) written.add(k);
1201
- }
1202
- const clean = (n) => {
1203
- if (n.type === "group") return { ...n, children: (n.children ?? []).map(clean) };
1204
- if (!n.beats) return n;
1205
- const kept = n.beats.filter((b) => !isContentlessBeat(b, written.has(b.id)));
1206
- if (kept.length === n.beats.length) return n;
1207
- const { beats, ...rest } = n;
1208
- return kept.length ? { ...rest, beats: kept } : rest;
1209
- };
1210
- return loaded.scenes.map((s) => ({
1211
- ...s,
1212
- blocks: s.blocks.map((b) => ({ ...b, children: b.children.map(clean) }))
1483
+ import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
1484
+
1485
+ // src/audio-index.ts
1486
+ import { readdirSync as readdirSync3 } from "fs";
1487
+ import { join as join6, relative as relative4, resolve as resolve4, sep as sep2 } from "path";
1488
+ import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
1489
+ var AUDIO_MANIFEST_SCHEMA = "patter/audio@0";
1490
+ var AUDIO_MANIFEST_FILE = "patteraudio.json";
1491
+ var AUDIO_EXT = [".wav", ".mp3"];
1492
+ function audioFoldersActive(p) {
1493
+ return !!p.voiced && (p.trackAudioStatus ?? false) && !!p.audioFolders && !!p.audioRoot?.trim();
1494
+ }
1495
+ function audioFolderRungs(root, rungs) {
1496
+ return rungs.filter((r) => r.folder?.trim()).reverse().map((r) => ({ name: r.name, dir: resolve4(root, r.folder.trim()) }));
1497
+ }
1498
+ function pickAudio(listings) {
1499
+ const snap = {};
1500
+ for (const { name, dir, files } of listings) {
1501
+ const byId = /* @__PURE__ */ new Map();
1502
+ for (const f of files ?? []) {
1503
+ const dot = f.lastIndexOf(".");
1504
+ if (dot <= 0) continue;
1505
+ const ext = f.slice(dot).toLowerCase();
1506
+ if (!AUDIO_EXT.includes(ext)) continue;
1507
+ const id = f.slice(0, dot);
1508
+ const existing = byId.get(id);
1509
+ if (!existing || ext === ".wav" && !existing.toLowerCase().endsWith(".wav")) byId.set(id, f);
1510
+ }
1511
+ for (const [id, file] of byId) if (!(id in snap)) snap[id] = { status: name, path: resolve4(dir, file) };
1512
+ }
1513
+ return snap;
1514
+ }
1515
+ function scanAudio(loaded) {
1516
+ const p = loaded.project;
1517
+ if (!audioFoldersActive(p)) return void 0;
1518
+ const rungs = audioFolderRungs(loaded.root, deriveRecordingFolders(p.audioRoot, p.recordingStatuses ?? DEFAULT_RECORDING_STATUSES));
1519
+ return pickAudio(rungs.map((r) => {
1520
+ let files;
1521
+ try {
1522
+ files = readdirSync3(r.dir);
1523
+ } catch {
1524
+ files = void 0;
1525
+ }
1526
+ return { ...r, files };
1213
1527
  }));
1214
1528
  }
1529
+ function scanAudioStatus(loaded) {
1530
+ const snap = scanAudio(loaded);
1531
+ return snap && new Map(Object.entries(snap).map(([id, e]) => [id, e.status]));
1532
+ }
1533
+ function audioManifest(snapshot, projectRoot, audioRoot) {
1534
+ const base = resolve4(projectRoot, audioRoot);
1535
+ const clips = {};
1536
+ for (const beatId of Object.keys(snapshot).sort()) {
1537
+ const entry = snapshot[beatId];
1538
+ clips[beatId] = { file: relative4(base, entry.path).split(sep2).join("/"), status: entry.status };
1539
+ }
1540
+ return JSON.stringify({ schema: AUDIO_MANIFEST_SCHEMA, clips }, null, 2) + "\n";
1541
+ }
1542
+ function audioManifestWrite(loaded, snapshot) {
1543
+ const p = loaded.project;
1544
+ if (!snapshot || !audioFoldersActive(p) || !Object.keys(snapshot).length) return void 0;
1545
+ return { path: join6(resolve4(loaded.root, p.audioRoot), AUDIO_MANIFEST_FILE), content: audioManifest(snapshot, loaded.root, p.audioRoot) };
1546
+ }
1547
+
1548
+ // src/export.ts
1215
1549
  function refuseUnresolvedMerge(loaded) {
1216
1550
  const issues = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
1217
1551
  if (!issues.length) return;
@@ -1220,32 +1554,47 @@ function refuseUnresolvedMerge(loaded) {
1220
1554
  ` + issues.map((i) => ` ${i.file}`).join("\n")
1221
1555
  );
1222
1556
  }
1223
- function runExport(loaded) {
1557
+ function refuseInvalid(loaded, opts) {
1558
+ if (opts.allowInvalid) return;
1559
+ const blockers = exportBlockers(loaded);
1560
+ if (!blockers.length) return;
1561
+ const shown2 = blockers.slice(0, 10).map((b) => ` ${b}`);
1562
+ if (blockers.length > shown2.length) shown2.push(` ... and ${blockers.length - shown2.length} more (patter validate lists them)`);
1563
+ throw new Error(`${blockers.length} problem(s) to fix before this project builds:
1564
+ ${shown2.join("\n")}`);
1565
+ }
1566
+ function runExport(loaded, opts = {}) {
1224
1567
  refuseUnresolvedMerge(loaded);
1225
- const { project, locales } = loaded;
1226
- const full = exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
1568
+ refuseInvalid(loaded, opts);
1569
+ const { project } = loaded;
1570
+ const full = compileLoaded(loaded);
1227
1571
  const loc = project.export?.localisation;
1228
1572
  if (!loc || loc.mode === "embedded") return full;
1229
1573
  const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
1230
1574
  return { ...full, strings, localisation: { mode: "ids", ...loc.sourceDebug ? { sourceDebug: true } : {} } };
1231
1575
  }
1232
- function runExportFull(loaded) {
1576
+ function runExportFull(loaded, opts = {}) {
1233
1577
  refuseUnresolvedMerge(loaded);
1234
- const { project, locales } = loaded;
1235
- return exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
1236
- }
1237
- function bundleOutputPath(loaded) {
1238
- const override = loaded.project.export?.bundle;
1239
- if (override) return isAbsolute2(override) ? override : join4(loaded.root, override);
1240
- const stem = basename3(loaded.projectFile).replace(/\.patterproj$/, "");
1241
- return join4(loaded.root, "dist", `${stem}.patterc`);
1578
+ if (opts.refuseInvalid) refuseInvalid(loaded, opts);
1579
+ return compileLoaded(loaded);
1580
+ }
1581
+ function planBuild(loaded, opts = {}) {
1582
+ const bundle = runExport(loaded, opts);
1583
+ const text = canonicalStringify3(bundle, { trailingComma: false });
1584
+ const scopes = patterScopesWrite(loaded.project, loaded.gameScopes);
1585
+ const audio = audioManifestWrite(loaded, opts.audio ?? scanAudio(loaded));
1586
+ return {
1587
+ bundle,
1588
+ text,
1589
+ writes: [{ path: opts.bundlePath ?? bundleOutputPath(loaded), content: text }, ...scopes ? [scopes] : [], ...audio ? [audio] : []]
1590
+ };
1242
1591
  }
1243
1592
 
1244
1593
  // src/export-html.ts
1245
- import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
1594
+ import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
1246
1595
 
1247
1596
  // src/playable-runtime.ts
1248
- var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var _=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var Ge=Object.getOwnPropertyNames;var je=Object.prototype.hasOwnProperty;var Ve=(n,e)=>{for(var t in e)_(n,t,{get:e[t],enumerable:!0})},Le=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Ge(e))!je.call(n,s)&&s!==t&&_(n,s,{get:()=>e[s],enumerable:!(r=Te(e,s))||r.enumerable});return n};var Me=n=>Le(_({},"__esModule",{value:!0}),n);var st={};Ve(st,{Checkpoint:()=>A,Engine:()=>H,Flow:()=>I,buildTagIndex:()=>$,describeBundle:()=>Oe,effectiveGameData:()=>Ne,gameDataFields:()=>Be,gameDataValue:()=>te});function D(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 G(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=D(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):D(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"&&!D(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=U(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=D(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=U(i.left,e),l=U(i.right,e),p=c??l;if(p!==void 0){if(c&&l&&!$e(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 ce(o,a);case"!=":return!ce(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 ce(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 U(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 $e=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function R(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function W(n){let e=R(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 He={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},qe=[He];function le(n,e,t){let r=t?.countingCalls??qe;return j(n,t?.want??!0,e,r)}function j(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=j(n.left,e,t,r),i=j(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 j(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])=>de(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 de(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 _e=n=>n.toLowerCase(),J=1,V=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??_e,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,...de(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(Ue(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 pe(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=pe(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:J,scopes:this.save()}}loadFragment(e){if(e.version!==J)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${J})`);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 pe(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function Ue(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 ge=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function O(n){return n.ctx.host??{}}var K={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=O(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:he("check_flags"),eval(n,e){let t=ue(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:he("set_flags"),eval(n,e){let t=[...ue(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:M("visits"),eval:(n,e)=>O(e).visits?.(L(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:M("seen"),eval:(n,e)=>(O(e).visits?.(L(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:M("patter_visits"),eval:(n,e)=>O(e).patterVisits?.(L(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:M("patter_seen"),eval:(n,e)=>(O(e).patterVisits?.(L(n,e,"patter_seen"))??0)>0}}},xt=ge.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function fe(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 L(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 M(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 ue(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 he(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 We=/^@[A-Za-z0-9_.]+$/;function*Je(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:We.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 Ke(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function ze(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function Ye(n){let e="",t=!1;for(let r of n){if(ze(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function me(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?Ye(r):n}function Se(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of Je(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?"":Ke(s)}return t}function C(n,e){for(let t of n){e(t);let r=t.children;r&&C(r,e)}}function Qe(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||Qe(n.name)}function z(n){return`cast:${n}`}var Y={open:"[",close:"]"},ye="SFX";var Q=3;function B(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function $(n){let e=new Map,t=(r,s)=>{let i=B([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group"&&r.prompt&&e.set(r.prompt.id,B([...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,B([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=B(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=B([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var ve=new WeakMap,F="Patter",N=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),we=Symbol("ownsRegistry"),y={stage:n=>`patter/scene/${N(n)}`,flow:n=>`patter/flow/${N(n)}/`,flowGlobals:n=>`patter/flow/${N(n)}/patter`,flowScene:(n,e)=>`patter/flow/${N(n)}/scene/${N(e)}`},A=class{constructor(e){this.journal=e}journal},H=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),C(m.children,ae=>p.set(ae.id,ae));this.blockGameIdToId.set(g,v)}let re=e.properties??[],q=re.filter(g=>g.shared??!0).map(Ee),Fe=re.filter(g=>!(g.shared??!0)).map(Ee),Ae=new Set(q.map(g=>g.name.toLowerCase())),x=t.registry??new V,ne=!t.registry||t[we]===!0,se=new b(q,{pathPrefix:"@patter."}),ie=[],T=[];try{x.mountOwned("patter",se,{owner:F}),T.push("patter");let g=new Set;if(t.world){let h=e.scopeRegistry?.scopes.find(m=>m.token==="world"),v=(h?.declarations??[]).map(Re);x.defineForeign("world",t.world,v,{writable:h?.writable??!0,owner:F}),T.push("world"),g.add("world")}for(let h of ne?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let v=(h.declarations??[]).map(m=>Ze(m,h.writable));x.defineOwned(h.token,v,{owner:F}),T.push(h.token),ie.push(h.token)}}catch(g){for(let h of T)x.remove(h,{keep:!0});throw g}let oe=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()));oe.set(g,v)}this.host={logEnabled: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:$(e),registry:x,ownsRegistry:ne,patterBag:se,hostScopes:ie,patterSharedDecls:q,patterLocalDecls:Fe,patterSharedNames:Ae,sceneSharedNames:oe,sharedVisits: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??Y).open,captionClose:(e.closedCaptions??Y).close,captionCharacter:e.closedCaptions?.character||ye,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}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,[we]: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 I(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.start(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=X(this.host,e);if(r===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:r};let s=Z(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 X(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:Z(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 this.host.tagIndex.get(e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?this.host.tagIndex.get(t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?this.host.tagIndex.get(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,...xe(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...S(t)?{gameId:S(t)}:{},name:t.name,...xe(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)C(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=this.host.tagIndex.get(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)}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=this.host.tagIndex.get(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 A(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});this.creationOptions.world&&t.has("world")&&t.remove("world")}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.engineTraceHandlers.delete(e)}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}:{}})}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=Pe(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:Q,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:Ie(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==Q)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?et(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 De(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new I(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}}},I=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=R(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(),s=i=>i?.find(o=>o.name.toLowerCase()===r&&o.type==="quality")?.stages;if(e==="patter")return s(this.host.patterSharedDecls)??s(this.host.patterLocalDecls);if(e==="scene"){let i=this.currentSceneId!=null?this.host.bundle.scenes[this.currentSceneId]:void 0;return s(i?.sceneProps)}return this.registryQualities?.(e,t)??s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(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.pendingChoice=null,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.start(e,t)}goto(e,t){if(this.closed)return!1;if(this.touch(),e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=X(this.host,e);if(r===void 0)return!1;let s;return t!==void 0&&(s=Z(this.host,r,t),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.start(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,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.logEnabled&&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}:{}})}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.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:Xe(this.pendingPrompt.shown)}:{},selectors:Ie(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=R(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=De(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.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.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.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.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 le(e,i=>{try{return Ce(G(i,this.context(),K))}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.logEnabled?this.getProperty(r.target):void 0;this.writeProperty(r.target,s,!1),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 Ce(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.emit({type:"diagnostic",kind:e,node:t,...o?{source:o}:{},message:i})}evalExpr(e){return G(this.conditionAst(e),this.context(),K)}conditionAst(e){let t=ve.get(e);return t||(t=k(e.ast),ve.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=W(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=this.host.tagIndex.get(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 Se(e,t=>this.getProperty(t))}stripCaptions(e){return me(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=this.host.tagIndex.get(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 C(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)}splitRef(e){return Pe(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:F}),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=ke(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=ke(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 be(n,e){C(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 xe(n){return n&&Object.keys(n).length?{gameData:n}:{}}function Xe(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 Ie(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 De(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 ke(n,e,t,r){let s=r?JSON.stringify(t.save()):"";return n.mountOwned(e,t,{owner:F}),r&&JSON.stringify(t.save())!==s}function Ee(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Re(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function Ze(n,e){let t=Re(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function X(n,e){return n.sceneGameIdToId.get(e)??(n.bundle.scenes[e]?e:void 0)}function Z(n,e,t){return n.blockGameIdToId.get(e)?.get(t)??(n.blockIndex.get(t)?.sceneId===e?t:void 0)}function Pe(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=fe(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function et(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 Ce(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var tt=(n,e)=>n.shared??e;function ee(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:tt(n,e)}}function rt(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function nt(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 Oe(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)nt(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>ee(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=>ee(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(rt)}));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=>ee(o,!0)),scene:r},gameData:i,counts:e}}function Be(n,e){return n.gameDataFields?.[e]??[]}function te(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function Ne(n,e){let t={};for(let r of n){let s=te(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 Me(st);})();\n';
1597
+ var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var W=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var $e=Object.getOwnPropertyNames;var He=Object.prototype.hasOwnProperty;var qe=(n,e)=>{for(var t in e)W(n,t,{get:e[t],enumerable:!0})},_e=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of $e(e))!He.call(n,s)&&s!==t&&W(n,s,{get:()=>e[s],enumerable:!(r=Me(e,s))||r.enumerable});return n};var Ue=n=>_e(W({},"__esModule",{value:!0}),n);var pt={};qe(pt,{Checkpoint:()=>T,Engine:()=>_,Flow:()=>D,buildTagIndex:()=>q,describeBundle:()=>Ae,effectiveGameData:()=>Ge,gameDataFields:()=>Te,gameDataValue:()=>se});function R(n,e){return n!==void 0&&Object.prototype.hasOwnProperty.call(n,e)?n[e]:void 0}function k(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:k(n[2])};case"bin":return{kind:"binary",op:n[1],left:k(n[2]),right:k(n[3])};case"call":{let e=n.slice(2).map(k);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var d=class extends Error{constructor(e){super(e),this.name="EvalError"}};function V(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=R(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):R(o,i.name);if(a===void 0){if(r.get(i.scope)==="throw")throw new d(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!R(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new d(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=J(a,e);if(c===void 0)throw new d("advance() needs a quality reference (@scope.name of a quality property)");let l=w(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=R(t.functions,i.name);if(!o)throw new d(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new d("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new d(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new d(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let f=s(i.left);if(typeof f!="boolean")throw new d(`\'and\' requires boolean operands, left is ${typeof f}`);if(!f)return!1;let u=s(i.right);if(typeof u!="boolean")throw new d(`\'and\' requires boolean operands, right is ${typeof u}`);return u}if(i.op==="or"){let f=s(i.left);if(typeof f!="boolean")throw new d(`\'or\' requires boolean operands, left is ${typeof f}`);if(f)return!0;let u=s(i.right);if(typeof u!="boolean")throw new d(`\'or\' requires boolean operands, right is ${typeof u}`);return u}let o=s(i.left),a=s(i.right),c=J(i.left,e),l=J(i.right,e),p=c??l;if(p!==void 0){if(c&&l&&!We(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new d(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return w(o,p,">")>w(a,p,">");case">=":return w(o,p,">=")>=w(a,p,">=");case"<":return w(o,p,"<")<w(a,p,"<");case"<=":return w(o,p,"<=")<=w(a,p,"<=");case"+":case"-":case"*":case"/":throw new d(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return pe(o,a);case"!=":return!pe(o,a);case">":return E(o,a,">"),o>a;case">=":return E(o,a,">="),o>=a;case"<":return E(o,a,"<"),o<a;case"<=":return E(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new d(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return E(o,a,"-"),o-a;case"*":return E(o,a,"*"),o*a;case"/":if(E(o,a,"/"),a===0)throw new d("division by zero");return o/a}}}};return s(n)}function pe(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function E(n,e,t){if(typeof n!="number"||typeof e!="number")throw new d(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function J(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function w(n,e,t){if(typeof n!="string")throw new d(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new d(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var We=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function O(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function K(n){let e=O(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var Je={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ke=[Je];function de(n,e,t){let r=t?.countingCalls??Ke;return L(n,t?.want??!0,e,r)}function L(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=L(n.left,e,t,r),i=L(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return L(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var b=class n{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??P(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}writable(e){return this.decls.get(this.norm(e))?.writable!==!1}set(e,t,r){let s=this.norm(e);if(!r?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of[...this.auditors])o(i);if(!i.silent)for(let o of[...this.subscribers])o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>ue(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function ue(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??P(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var ze=n=>n.toLowerCase(),z=1,M=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new b(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(t.load(s),this.parked.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&this.parked.set(e,r.bag.save()),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??ze,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){if(!s?.host&&!i.bag.writable(t))throw new Error(`\'@${e}.${t}\' is read-only`);i.bag.set(t,r,s?.host?{host:!0}:void 0);return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...ue(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get(Ye(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of ge(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=ge(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||this.parked.clear();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:z,scopes:this.save()}}loadFragment(e){if(e.version!==z)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${z})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function ge(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function Ye(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function P(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var he=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function B(n){return n.ctx.host??{}}var Y={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new d("random(a, b) requires exactly 2 arguments");let t=B(e).nextRandom;if(!t)throw new d("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new d("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new d("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:me("check_flags"),eval(n,e){let t=fe(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new d("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:me("set_flags"),eval(n,e){let t=[...fe(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new d("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:H("visits"),eval:(n,e)=>B(e).visits?.($(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:H("seen"),eval:(n,e)=>(B(e).visits?.($(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:H("patter_visits"),eval:(n,e)=>B(e).patterVisits?.($(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:H("patter_seen"),eval:(n,e)=>(B(e).patterVisits?.($(n,e,"patter_seen"))??0)>0}}},Rt=he.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function Se(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function $(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new d(`${t}(id) requires a string node id`);return r}function H(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function fe(n,e,t){if(!n)throw new d(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new d(`${t}() first argument must be a flags property`)}function me(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var Qe=/^@[A-Za-z0-9_.]+$/;function*Xe(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:Qe.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function Ze(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function et(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function tt(n){let e="",t=!1;for(let r of n){if(et(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ye(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?tt(r):n}function ve(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of Xe(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":Ze(s)}return t}function I(n,e){for(let t of n){e(t);let r=t.children;r&&I(r,e)}}function rt(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function S(n){let e=n.gameId?.trim();return e||rt(n.name)}function Q(n){return`cast:${n}`}var X={open:"[",close:"]"},we="SFX";var Z=3;function N(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function q(n){let e=new Map,t=(r,s)=>{let i=N([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group"&&r.prompt&&e.set(r.prompt.id,N([...i,...r.prompt.tags??[]])),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,N([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=N(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=N([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var be=new WeakMap,xe=new WeakMap;function nt(n){let e=xe.get(n);return e||(e=q(n),xe.set(n,e)),e}function C(n,e){return n.tagIndex.get(e)?.slice()}var A="Patter",F=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),ke=Symbol("ownsRegistry"),y={stage:n=>`patter/scene/${F(n)}`,flow:n=>`patter/flow/${F(n)}/`,flowGlobals:n=>`patter/flow/${F(n)}/patter`,flowScene:(n,e)=>`patter/flow/${F(n)}/scene/${F(e)}`},T=class{constructor(e){this.journal=e}journal},_=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineSeq=0;engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let g of e.cast??[])g.displayName&&l.set(g.name,g.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let p=new Map,f=new Map,u=new Map;for(let[g,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(S(h),g);let v=new Map;for(let m of h.blocks)f.set(m.id,{sceneId:g}),u.set(m.id,m),v.set(S(m),m.id),I(m.children,j=>p.set(j.id,j));this.blockGameIdToId.set(g,v)}let ie=e.properties??[],U=ie.filter(g=>g.shared??!0).map(De),je=ie.filter(g=>!(g.shared??!0)).map(De),Ve=new Set(U.map(g=>g.name.toLowerCase())),x=t.registry??new M,oe=!t.registry||t[ke]===!0,ae=new b(U,{pathPrefix:"@patter."}),ce=[],G=[];try{x.mountOwned("patter",ae,{owner:A}),G.push("patter");let g=new Set;for(let[h,v]of Object.entries(Re(t))){let m=e.scopeRegistry?.scopes.find(Le=>Le.token===h),j=(m?.declarations??[]).map(Ne);x.defineForeign(h,v,j,{writable:m?.writable??!0,owner:A}),G.push(h),g.add(h)}for(let h of oe?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let v=(h.declarations??[]).map(m=>it(m,h.writable));x.defineOwned(h.token,v,{owner:A}),G.push(h.token),ce.push(h.token)}}catch(g){for(let h of G)x.remove(h,{keep:!0});throw g}let le=new Map;for(let[g,h]of Object.entries(e.scenes)){let v=new Set((h.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));le.set(g,v)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(g,h,v)=>this.emitEngine(g,h,v),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:p,blockIndex:f,blockById:u,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:nt(e),registry:x,ownsRegistry:oe,patterBag:ae,hostScopes:ce,patterSharedDecls:U,patterLocalDecls:je,patterSharedNames:Ve,sceneSharedNames:le,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(g=>console.warn(`Patterplay: ${g.kind} on ${g.node} in flow \'${g.flow}\' failed, played through: ${g.message}`)),replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??X).open,captionClose:(e.closedCaptions??X).close,captionCharacter:e.closedCaptions?.character||we,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}get buildId(){return this.host.bundle.content?.hash??""}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){this.refuseInCheckpoint("hotSwap");let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[ke]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:r,blockId:s}=this.resolveOpenAddress(t.scene,t.block),i=this.flowsById.get(e),o=this.host.journal;if(o&&i&&!i.isClosed)throw new Error(`openFlow would replace the open flow \'${e}\', which can\'t be undone while a checkpoint is open`);i?.close();let a=new D(e,this.host,t.seed??this.defaultSeed);return o&&(o.flows.add(a),o.opened.add(a),o.undo.push(()=>{a.close(),i?this.flowsById.set(e,i):this.flowsById.delete(e)})),this.flowsById.set(e,a),a.begin(r,s),a}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveOpenAddress(e,t){if(e!==void 0){let r=te(this.host,e);if(r===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:r};let s=re(this.host,r,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:r,blockId:s}}if(t!==void 0){if(!this.host.blockIndex.has(t))throw new Error(`unknown block: ${t}`);return{blockId:t}}if(Object.keys(this.host.bundle.scenes).length===0)throw new Error("no scenes in bundle");return{}}resolveSceneRef(e){if(e!=null)return te(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:re(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?S(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?S(t):void 0}tagsForBeat(e){return C(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?C(this.host,t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?C(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let r=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(r!=null?this.host.blockById.get(r):void 0)?.gameData}}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Ee(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Ee(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...S(e)?{gameId:S(e)}:{},name:e.name,...Ce(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...S(t)?{gameId:S(t)}:{},name:t.name,...Ce(t.gameData),...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)I(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=C(this.host,e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[Q(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}e.direction!==void 0&&(r.direction=e.direction)}if(e.kind==="line"||e.kind==="text"){let s=this.host.defaultStrings[e.id];s!==void 0&&(r.text=s)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=C(this.host,e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}checkpoint(){if(this.host.journal)throw new Error("a checkpoint is already open: roll it back or commit it first");let e={undo:[],flows:new Set,opened:new Set,selectors:new Set,flowSelectors:new Map};return this.host.journal=e,new T(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let r=t.length-1;r>=0;r--)t[r]();n.forget(e.journal)}commit(e){this.endCheckpoint(e),n.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(y.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(y.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});for(let r of Object.keys(Re(this.creationOptions)))t.has(r)&&t.remove(r)}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e),i=this.host.journal?this.host.registry.get(r,s):void 0;this.host.registry.set(r,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(r,s,i,{host:!0}))}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),this.host.tracing=!0,()=>{this.engineTraceHandlers.delete(e),this.host.tracing=this.host.logEnabled||this.engineTraceHandlers.size>0}}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...r?{scene:r}:{}})}getVisitCounts(){return Object.fromEntries(this.host.sharedVisits)}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:P(e),writable:e.writable??!0}))}splitShared(e){let t=Fe(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:Z,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:Pe(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==Z)throw new Error(`unsupported save version: ${String(t)}`);let r=e.flows;if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("malformed save: no flows");this.assertExternalScopes();let s=this.host.registry;for(let[o,a]of this.flowsById)a.releaseBags(o in e.flows),a.close();this.flowsById.clear();for(let o of this.host.stageBags.keys())s.remove(y.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.version===2?ot(e):e.registry;i&&(this.host.ownsRegistry?s.load(i):s.load(i,{keepParked:!0})),this.host.sharedVisits.clear();for(let[o,a]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(o,a);this.host.sharedSelectors.clear();for(let[o,a]of Be(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new D(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPrompt=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=O(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase();if(e==="patter")return ee(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(r);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:ee(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(r)}return this.registryQualities?.(e,t)??ee(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(r)}begin(e,t){if(this.host.journal&&!this.host.journal.opened.has(this))throw new Error("a flow can\'t be started or reset while a checkpoint is open");if(this.releaseBags(!1),this.host.registry.discardParked(y.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.clearPending(),this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.begin(e,t)}start(e,t){this.reset(e,t)}goto(e,t){if(this.closed)return!1;if(this.touch(),e==="END")return this.started=!0,this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=te(this.host,e);if(r===void 0)return!1;let s;return t!==void 0&&(s=re(this.host,r,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.begin(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.clearPending()}clearPending(){this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null}touch(){let e=this.host.journal;if(!e||e.flows.has(this))return;e.flows.add(this);let{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p}=this,f=this.stack.map(u=>({...u}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p,stack:f})})}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.touch(),this.pendingPrompt){let e=this.pendingPrompt;return this.pendingPrompt=null,this.pendingPromptOwnerId=null,e.shown?this.promptResult(e.beat,e.shown):this.beatResult(e.beat)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit,this.activeSnippet.id);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getVisitCounts(){return Object.fromEntries(this.visitCounts)}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(o=>o.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);this.touch();let s=t.byId.get(e);this.host.tracing&&this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null;let i=this.host.replayPromptOnChoose?this.authoredPromptOf(s):void 0;this.pendingPrompt=i&&r.prompt?{beat:i,shown:{...r.prompt}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e),o=this.host.journal;if(s==="patter"){if(o){let a=this.host.patterSharedNames.has(i),c=a?this.host.patterBag.get(i):this.local.get(i),l=this.local;c!==void 0&&o.undo.push(a?()=>this.host.registry.set("patter",i,c):()=>l.set(i,c))}this.patterResolver.set(i,t)}else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);if(o){let a=this.sceneBagFor(this.currentSceneId,i),c=a?.get(i);a&&c!==void 0&&o.undo.push(()=>a.set(i,c))}this.sceneResolver.set(i,t)}else{let a=o?this.host.registry.get(s,i):void 0;this.host.registry.set(s,i,t,r?{host:!0}:void 0),o&&a!==void 0&&o.undo.push(()=>this.host.registry.set(s,i,a,{host:!0}))}}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:st(this.pendingPrompt.shown)}:{},selectors:Pe(this.selectors)}}}restore(e){if(this.host.journal)throw new Error("a flow can\'t be restored while a checkpoint is open");this.restoreCursor(e),this.releaseBags(!0),this.mountLocal();for(let t of new Set([this.currentSceneId,...this.stack.map(r=>r.sceneId)]))t!==null&&this.host.bundle.scenes[t]&&this.ensureSceneBags(t)}restoreCursor(e){this.rngState=O(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;if(this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=Be(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);o&&(r.set(i.id,o),s.push({...i}))}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPrompt=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId),s=r?this.authoredPromptOf(r):void 0;s?this.pendingPrompt={beat:s,...t.pendingPrompt?{shown:{...t.pendingPrompt}}:{}}:this.pendingPromptOwnerId=null}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry,t.id)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.some(o=>o.eligible)){this.host.tracing&&this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.host.tracing&&this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.host.tracing&&this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter((c,l)=>t[l].eligible),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.host.tracing&&this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(u=>u.id),a=new Set(e.map(u=>u.id));if(r.bag===void 0&&(r.bag=o()),!r.bag.some(u=>a.has(u))){if(t==="once")return null;if(i){let u=e[s-1];return r.last=u.id,u}if(r.bag=o(),r.bag.length===0)return null}let c=r.bag.filter(u=>a.has(u)),l=r.last!==void 0&&c.length>1?c.indexOf(r.last):-1,p=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&p>=l&&p++;let f=c[p];return r.bag.splice(r.bag.indexOf(f),1),r.last=f,e.find(u=>u.id===f)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(p=>p.id));let l=new Set(r.bag);if(s=e.filter(p=>l.has(p.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(p=>p.id===r.last)??null:null}let i=-1,a=s.map(l=>{let p=this.specScore(l);return p>i&&(i=p),{c:l,s:p}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(f=>f.id===r.last):-1,p=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&p>=l&&p++,c=a[p]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,r){return de(e,i=>{try{return Oe(V(i,this.context(),Y))}catch(o){return this.reportError("best-match",r.id,r.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=this.host.journal,s;if(r&&(s=e.shared?r.selectors:r.flowSelectors.get(this),s||(s=new Set,r.flowSelectors.set(this,s))),r&&s&&!s.has(e.id)){s.add(e.id);let o=t.get(e.id),a=o?{...o,...o.bag?{bag:[...o.bag]}:{}}:void 0;r.undo.push(()=>{a?t.set(e.id,a):t.delete(e.id)})}let i=t.get(e.id);return i||(i={},t.set(e.id,i)),i}runEffects(e,t){for(let r of e??[]){let s;try{s=this.evalExpr(r.value);let i=this.host.tracing?this.getProperty(r.target):void 0;this.writeProperty(r.target,s,!1),this.host.tracing&&this.emit({type:"write",target:r.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,r.value,i)}}}eligible(e){if(!e.condition)return!0;try{return Oe(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,r,s){let i=s instanceof Error?s.message:String(s),o=r?.src;this.host.onError({flow:this.id,kind:e,node:t,...o?{source:o}:{},message:i}),this.host.tracing&&this.emit({type:"diagnostic",kind:e,node:t,...o?{source:o}:{},message:i})}evalExpr(e){return V(this.conditionAst(e),this.context(),Y)}conditionAst(e){let t=be.get(e);return t||(t=k(e.ast),be.set(e,t)),t}enter(e){let t=this.visitCounts,r=t.get(e);t.set(e,(r??0)+1);let s=this.host.sharedVisits,i=s.get(e);s.set(e,(i??0)+1),this.host.journal&&this.host.journal.undo.push(()=>{r===void 0?t.delete(e):t.set(e,r),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=K(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=C(this.host,e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,gameData:e.gameData,...r}}}}interpolate(e){return ve(e,t=>this.getProperty(t))}stripCaptions(e){return ye(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id));return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction}:{kind:"text",text:r}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let r=C(this.host,e.id),s=r&&r.length?{tags:r}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,gameData:e.gameData,...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,gameData:e.gameData,...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return I(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=Q(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){return Fe(this.host,e)}newLocal(){return new b(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=y.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:A}),this.registered.add(e)}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal;if(!this.sceneBags.has(e)){let i=new b(r.filter(c=>!t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),o=y.flowScene(this.id,e),a=Ie(this.host.registry,o,i,s!==null);this.registered.add(o),this.sceneBags.set(e,i),s&&s.undo.push(()=>{this.sceneBags.get(e)===i&&(this.host.registry.remove(o,a?{keep:!0}:void 0),this.registered.delete(o),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let i=new b(r.filter(a=>t.has(a.name.toLowerCase())),{pathPrefix:"@scene."}),o=Ie(this.host.registry,y.stage(e),i,s!==null);this.host.stageBags.set(e,i),s&&s.undo.push(()=>{this.host.stageBags.get(e)===i&&(this.host.registry.remove(y.stage(e),o?{keep:!0}:void 0),this.host.stageBags.delete(e))})}}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);if(!i)continue;let o=this.host.journal?i.get(s):void 0;o!==void 0&&this.host.journal.undo.push(()=>i.set(s,o)),i.set(s,P(r))}}};function Ee(n,e){I(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function Ce(n){return n&&Object.keys(n).length?{gameData:n}:{}}function st(n){let e={kind:n.kind,text:n.text};return n.character!==void 0&&(e.character=n.character),n.characterName!==void 0&&(e.characterName=n.characterName),n.direction!==void 0&&(e.direction=n.direction),e}function Pe(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function Be(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function Ie(n,e,t,r){let s=r?JSON.stringify(t.save()):"";return n.mountOwned(e,t,{owner:A}),r&&JSON.stringify(t.save())!==s}function De(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Re(n){return{...n.world?{world:n.world}:{},...n.hostScopes}}function ee(n,e,t){let r=n.qualityLadders.get(e);if(!r){r=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!r.has(i)&&r.set(i,s.stages)}n.qualityLadders.set(e,r)}return r}function Ne(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function it(n,e){let t=Ne(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function te(n,e){return n.sceneGameIdToId.get(e)??(n.bundle.scenes[e]?e:void 0)}function re(n,e,t){return n.blockGameIdToId.get(e)?.get(t)??(n.blockIndex.get(t)?.sceneId===e?t:void 0)}function Fe(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=Se(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function ot(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[y.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{})){r.scopes?.patter&&(e[y.flowGlobals(t)]=r.scopes.patter);for(let[s,i]of Object.entries(r.sceneBags??{}))e[y.flowScene(t,s)]=i}return e}function Oe(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var at=(n,e)=>n.shared??e;function ne(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:at(n,e)}}function ct(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function lt(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function Ae(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=S(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:S(c),name:c.name}))});for(let c of o.blocks)lt(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>ne(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>ne(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(ct)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>ne(o,!0)),scene:r},gameData:i,counts:e}}function Te(n,e){return n.gameDataFields?.[e]??[]}function se(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function Ge(n,e){let t={};for(let r of n){let s=se(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return Ue(pt);})();\n';
1249
1598
 
1250
1599
  // src/export-html.ts
1251
1600
  var esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
@@ -1366,11 +1715,11 @@ var STYLE = String.raw`
1366
1715
  #flash { margin-left: auto; color: #9a8f78; font: 0.8rem ui-sans-serif, system-ui, sans-serif; opacity: 0; transition: opacity .2s; }
1367
1716
  `;
1368
1717
  function sourceStory(loaded) {
1369
- const full = runExportFull(loaded);
1718
+ const full = runExportFull(loaded, { refuseInvalid: true });
1370
1719
  const def = full.locales?.default ?? Object.keys(full.strings)[0] ?? "en";
1371
1720
  const bundle = { ...full, locales: { default: def, included: [def] }, strings: { [def]: full.strings[def] ?? {} } };
1372
1721
  const title = loaded.project.project.name?.trim() || "A Patter story";
1373
- const bundleJson = canonicalStringify3(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
1722
+ const bundleJson = canonicalStringify4(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
1374
1723
  return { title, lang: def, bundleJson };
1375
1724
  }
1376
1725
  function page(title, lang, head, tail) {
@@ -1441,39 +1790,11 @@ ${PLAYER_JS}`,
1441
1790
  };
1442
1791
  }
1443
1792
 
1444
- // src/audio-scan.ts
1445
- import { readdirSync as readdirSync3 } from "fs";
1446
- import { resolve as resolve3, extname, basename as basename4 } from "path";
1447
- import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
1448
- var AUDIO_EXT = /* @__PURE__ */ new Set([".wav", ".mp3"]);
1449
- function scanAudioStatus(loaded) {
1450
- const p = loaded.project;
1451
- const tracked = !!p.voiced && (p.trackAudioStatus ?? false);
1452
- if (!tracked || !p.audioFolders || !p.audioRoot?.trim()) return void 0;
1453
- const rungs = deriveRecordingFolders(p.audioRoot, p.recordingStatuses ?? DEFAULT_RECORDING_STATUSES);
1454
- const out = /* @__PURE__ */ new Map();
1455
- for (const rung of rungs) {
1456
- if (!rung.folder) continue;
1457
- let files;
1458
- try {
1459
- files = readdirSync3(resolve3(loaded.root, rung.folder));
1460
- } catch {
1461
- continue;
1462
- }
1463
- for (const f of files) {
1464
- const ext = extname(f).toLowerCase();
1465
- if (AUDIO_EXT.has(ext)) out.set(basename4(f, extname(f)), rung.name);
1466
- }
1467
- }
1468
- return out;
1469
- }
1470
-
1471
1793
  // src/play.ts
1472
- import { exportBundle as exportBundle4 } from "@patterkit/compiler";
1473
1794
  import { Engine } from "@patterkit/runtime";
1474
1795
 
1475
1796
  // src/loaded-helpers.ts
1476
- import { RERECORD_STATUS } from "@patterkit/model";
1797
+ import { RERECORD_STATUS, effectiveGameId } from "@patterkit/model";
1477
1798
  function effectiveRecording(id, base, rerecord, lowest) {
1478
1799
  return rerecord.has(id) ? RERECORD_STATUS : base.get(id) ?? lowest;
1479
1800
  }
@@ -1487,9 +1808,25 @@ function sourceStrings(loaded) {
1487
1808
  }
1488
1809
  function resolveStart(loaded, override) {
1489
1810
  if (override?.scene) return { scene: override.scene, block: override.block };
1811
+ if (override?.block) return findBlockAnywhere(loaded, override.block);
1490
1812
  if (loaded.project.start) return { scene: loaded.project.start.scene, block: loaded.project.start.block };
1491
1813
  return {};
1492
1814
  }
1815
+ function findBlockAnywhere(loaded, ref) {
1816
+ const matches = [];
1817
+ for (const scene of loaded.scenes) {
1818
+ for (const block of scene.blocks) {
1819
+ if (block.id === ref) return { scene: scene.id, block: block.id };
1820
+ if (block.name === ref || effectiveGameId(block) === ref) matches.push({ scene, block });
1821
+ }
1822
+ }
1823
+ if (matches.length === 0) throw new Error(`no scene has a block "${ref}"`);
1824
+ if (matches.length > 1) {
1825
+ const scenes = [...new Set(matches.map((m) => `"${m.scene.name}" (${m.scene.id})`))].join(", ");
1826
+ throw new Error(`more than one block is called "${ref}" (in ${scenes}): name the scene as well`);
1827
+ }
1828
+ return { scene: matches[0].scene.id, block: matches[0].block.id };
1829
+ }
1493
1830
  function stringsByLocale(loaded) {
1494
1831
  const byLocale = /* @__PURE__ */ new Map();
1495
1832
  for (const l of loaded.locales) {
@@ -1523,7 +1860,7 @@ function mergeAuthoring2(loaded) {
1523
1860
 
1524
1861
  // src/play.ts
1525
1862
  function runPlay(loaded, opts = {}) {
1526
- const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1863
+ const bundle = compileLoaded(loaded);
1527
1864
  const events = [];
1528
1865
  const registry = previewRegistry(loaded.gameScopes, bundle);
1529
1866
  const engine = new Engine(bundle, { seed: opts.seed, onError: (error) => events.push({ type: "error", error }), ...registry ? { registry } : {} });
@@ -1531,17 +1868,33 @@ function runPlay(loaded, opts = {}) {
1531
1868
  const flow = engine.openFlow("main", { scene: start.scene, block: start.block });
1532
1869
  const scripted = [...opts.choices ?? []];
1533
1870
  const maxSteps = opts.maxSteps ?? 1e3;
1534
- for (let i = 0; i < maxSteps; i++) {
1535
- const r = flow.advance();
1536
- if (r.type === "end") return { events, outcome: "end" };
1537
- if (r.type === "choice") {
1538
- const picked = scripted.shift() ?? r.options.find((o) => o.eligible)?.id;
1539
- events.push({ type: "choice", options: r.options, picked });
1540
- if (picked === void 0) return { events, outcome: "stalled" };
1541
- flow.choose(picked);
1542
- } else {
1543
- events.push(r);
1871
+ const failed = (message) => {
1872
+ events.push({ type: "error", fatal: true, message });
1873
+ return { events, outcome: "error" };
1874
+ };
1875
+ try {
1876
+ for (let i = 0; i < maxSteps; i++) {
1877
+ const r = flow.advance();
1878
+ if (r.type === "end") return { events, outcome: "end" };
1879
+ if (r.type === "choice") {
1880
+ const wanted = scripted.shift();
1881
+ if (wanted !== void 0) {
1882
+ const offered = r.options.find((o) => o.id === wanted);
1883
+ if (!offered || !offered.eligible) {
1884
+ events.push({ type: "choice", options: r.options });
1885
+ const onOffer = r.options.filter((o) => o.eligible).map((o) => o.id).join(", ");
1886
+ return failed(`scripted choice '${wanted}' is ${offered ? "offered but its condition does not hold" : "not offered"} here; on offer: ${onOffer}`);
1887
+ }
1888
+ }
1889
+ const picked = wanted ?? r.options.find((o) => o.eligible).id;
1890
+ events.push({ type: "choice", options: r.options, picked });
1891
+ flow.choose(picked);
1892
+ } else {
1893
+ events.push(r);
1894
+ }
1544
1895
  }
1896
+ } catch (err) {
1897
+ return failed(err instanceof Error ? err.message : String(err));
1545
1898
  }
1546
1899
  return { events, outcome: "max-steps" };
1547
1900
  }
@@ -1559,6 +1912,10 @@ function renderPlay(result) {
1559
1912
  out.push(` (game event ${JSON.stringify(e.gameData ?? {})})`);
1560
1913
  break;
1561
1914
  case "error": {
1915
+ if ("fatal" in e) {
1916
+ out.push(` ! stopped: ${e.message}`);
1917
+ break;
1918
+ }
1562
1919
  const { kind, node, source, message } = e.error;
1563
1920
  out.push(` ! ${kind} on '${node}'${source ? ` (${source})` : ""} failed, played through: ${message}`);
1564
1921
  break;
@@ -1573,20 +1930,19 @@ function renderPlay(result) {
1573
1930
  case "end":
1574
1931
  out.push("--- END ---");
1575
1932
  break;
1576
- case "stalled":
1577
- out.push(" (no eligible choice - stopping)");
1578
- break;
1579
1933
  case "max-steps":
1580
1934
  out.push("--- stopped: max steps reached ---");
1581
1935
  break;
1936
+ case "error":
1937
+ out.push("--- stopped: the run could not go on ---");
1938
+ break;
1582
1939
  }
1583
1940
  return out;
1584
1941
  }
1585
1942
 
1586
1943
  // src/coverage.ts
1587
- import { exportBundle as exportBundle5 } from "@patterkit/compiler";
1588
1944
  import { Engine as Engine2 } from "@patterkit/runtime";
1589
- import { walkNodes } from "@patterkit/model";
1945
+ import { isContentlessBeat as isContentlessBeat2, walkNodes } from "@patterkit/model";
1590
1946
  var CADENCE_PROB = {
1591
1947
  rarely: 0.15,
1592
1948
  sometimes: 0.4,
@@ -1667,8 +2023,23 @@ function analyzeHostScopes(bundle, hostTokens) {
1667
2023
  const fineGatesByBeat = /* @__PURE__ */ new Map();
1668
2024
  const proposals = /* @__PURE__ */ new Map();
1669
2025
  const writerSites = /* @__PURE__ */ new Map();
2026
+ const siteBeats = /* @__PURE__ */ new Map();
1670
2027
  const opaqueWrites = /* @__PURE__ */ new Set();
1671
- const empty = { written, gatesByBeat, proposals, fineGatesByBeat, writerSites, opaqueWrites };
2028
+ const shadowed = /* @__PURE__ */ new Set();
2029
+ const blockOfBeat = /* @__PURE__ */ new Map();
2030
+ const routes = [];
2031
+ const empty = {
2032
+ written,
2033
+ gatesByBeat,
2034
+ proposals,
2035
+ fineGatesByBeat,
2036
+ writerSites,
2037
+ siteBeats,
2038
+ opaqueWrites,
2039
+ shadowed,
2040
+ blockOfBeat,
2041
+ routes
2042
+ };
1672
2043
  if (hostTokens.size === 0) return empty;
1673
2044
  const refsIn = (expr) => {
1674
2045
  const refs = /* @__PURE__ */ new Set();
@@ -1680,56 +2051,83 @@ function analyzeHostScopes(bundle, hostTokens) {
1680
2051
  if (expr) fineRefsIn(deserialiseAst(expr.ast), hostTokens, refs);
1681
2052
  return refs;
1682
2053
  };
1683
- const addSite = (key, witnesses) => {
1684
- (writerSites.get(key) ?? writerSites.set(key, []).get(key)).push(witnesses);
2054
+ const addSite = (key, site) => {
2055
+ const sites = writerSites.get(key) ?? writerSites.set(key, []).get(key);
2056
+ if (!sites.includes(site)) sites.push(site);
1685
2057
  };
1686
- const scanEffects = (effects, witnesses) => {
2058
+ const scanEffects = (effects, site) => {
1687
2059
  for (const e of effects ?? []) {
1688
2060
  const target = targetHostRef(e.target, hostTokens);
1689
2061
  if (target) {
1690
2062
  written.add(target);
1691
- addSite(target, witnesses);
2063
+ addSite(target, site);
1692
2064
  const flags = e.value ? flagKeys(deserialiseAst(e.value.ast), hostTokens, "set_flags") : [];
1693
- if (flags.length) for (const k of flags) addSite(k, witnesses);
2065
+ if (flags.length) for (const k of flags) addSite(k, site);
1694
2066
  else opaqueWrites.add(target);
1695
2067
  }
1696
2068
  refsIn(e.value);
1697
2069
  }
1698
2070
  };
1699
- const walk = (nodes, gate, fine) => {
2071
+ const walk = (nodes, blockId, gate, fine, branch, dead) => {
2072
+ let won = false;
1700
2073
  for (const node of nodes) {
2074
+ const shadow = dead || won;
2075
+ if (branch && !node.condition) won = true;
1701
2076
  const here = /* @__PURE__ */ new Set([...gate, ...refsIn(node.condition)]);
1702
2077
  const hereFine = /* @__PURE__ */ new Set([...fine, ...fineIn(node.condition)]);
1703
2078
  if (node.type === "group") {
1704
- walk(node.children, here, hereFine);
2079
+ walk(node.children, blockId, here, hereFine, node.selector === "branch", shadow);
1705
2080
  } else {
1706
- const witnesses = (node.beats ?? []).map((b) => b.id);
1707
- scanEffects(node.onEnter, witnesses);
1708
- scanEffects(node.onExit, witnesses);
1709
- for (const beat of node.beats ?? []) {
1710
- gatesByBeat.set(beat.id, here);
1711
- fineGatesByBeat.set(beat.id, hereFine);
2081
+ const beats = (node.beats ?? []).map((b) => b.id);
2082
+ if (beats.length) siteBeats.set(node.id, beats);
2083
+ scanEffects(node.onEnter, node.id);
2084
+ scanEffects(node.onExit, node.id);
2085
+ for (const id of beats) {
2086
+ gatesByBeat.set(id, here);
2087
+ fineGatesByBeat.set(id, hereFine);
2088
+ blockOfBeat.set(id, blockId);
2089
+ if (shadow) shadowed.add(id);
1712
2090
  }
2091
+ if (node.jump && node.jump.to !== "END" && !shadow) routes.push({ from: blockId, to: node.jump.to, gate: here, fine: hereFine });
1713
2092
  }
1714
2093
  }
1715
2094
  };
1716
2095
  for (const scene of Object.values(bundle.scenes)) {
1717
- const sceneBeats = [];
1718
- const collect = (nodes) => {
1719
- for (const n of nodes) {
1720
- if (n.type === "group") collect(n.children);
1721
- else for (const b of n.beats ?? []) sceneBeats.push(b.id);
1722
- }
1723
- };
1724
- for (const block of scene.blocks) collect(block.children);
1725
- scanEffects(scene.onEntry, sceneBeats);
1726
- for (const block of scene.blocks) walk(block.children, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2096
+ scanEffects(scene.onEntry, scene.id);
2097
+ for (const block of scene.blocks) walk(block.children, block.id, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false, false);
1727
2098
  }
1728
2099
  return empty;
1729
2100
  }
1730
- function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
2101
+ function entryGates(bundle, routes, start) {
2102
+ const firstBlock = (sceneId) => sceneId === void 0 ? void 0 : bundle.scenes[sceneId]?.blocks[0]?.id;
2103
+ const blockOf = (to) => bundle.scenes[to] ? firstBlock(to) : to;
2104
+ const startBlock = start.block ?? firstBlock(start.scene ?? Object.keys(bundle.scenes)[0]);
2105
+ if (startBlock === void 0) return /* @__PURE__ */ new Map();
2106
+ const none = () => ({ gate: /* @__PURE__ */ new Set(), fine: /* @__PURE__ */ new Set() });
2107
+ const meet = (a, b) => new Set([...a].filter((g) => b.has(g)));
2108
+ let entry = /* @__PURE__ */ new Map([[startBlock, none()]]);
2109
+ for (; ; ) {
2110
+ const next = /* @__PURE__ */ new Map([[startBlock, none()]]);
2111
+ for (const r of routes) {
2112
+ const from = entry.get(r.from);
2113
+ const to = blockOf(r.to);
2114
+ if (from === void 0 || to === void 0) continue;
2115
+ const via = { gate: /* @__PURE__ */ new Set([...from.gate, ...r.gate]), fine: /* @__PURE__ */ new Set([...from.fine, ...r.fine]) };
2116
+ const had = next.get(to);
2117
+ next.set(to, had === void 0 ? via : { gate: meet(had.gate, via.gate), fine: meet(had.fine, via.fine) });
2118
+ }
2119
+ const same = next.size === entry.size && [...next].every(([k, v]) => {
2120
+ const was = entry.get(k);
2121
+ return was !== void 0 && was.gate.size === v.gate.size && was.fine.size === v.fine.size;
2122
+ });
2123
+ entry = next;
2124
+ if (same) return entry;
2125
+ }
2126
+ }
2127
+ function blockedGates(beatId, analysis, drivenRefs, siteRuns, entry) {
1731
2128
  const out = [];
1732
- const gates = /* @__PURE__ */ new Set([...analysis.gatesByBeat.get(beatId) ?? [], ...analysis.fineGatesByBeat.get(beatId) ?? []]);
2129
+ const own = /* @__PURE__ */ new Set([...analysis.gatesByBeat.get(beatId) ?? [], ...analysis.fineGatesByBeat.get(beatId) ?? []]);
2130
+ const gates = /* @__PURE__ */ new Set([...own, ...entry?.gate ?? [], ...entry?.fine ?? []]);
1733
2131
  for (const ref of [...gates].sort()) {
1734
2132
  const coarse = ref.includes(":") ? ref.slice(0, ref.indexOf(":")) : ref;
1735
2133
  if (drivenRefs.has(coarse)) continue;
@@ -1738,15 +2136,14 @@ function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
1738
2136
  if (ref === coarse && [...gates].some((g) => g !== ref && g.startsWith(`${ref}:`))) continue;
1739
2137
  const sites = analysis.writerSites.get(ref) ?? [];
1740
2138
  if (!sites.length) continue;
1741
- if (sites.some((s) => !s.length || s.some((w) => !reachedRuns.has(w)))) continue;
1742
- if (!sites.every((s) => s.every((w) => reachedRuns.get(w) === 0))) continue;
1743
- const writers = [...new Set(sites.flat())].sort();
1744
- out.push({ ref, writers });
2139
+ if (sites.some((s) => (siteRuns.get(s) ?? 0) > 0)) continue;
2140
+ const writers = [...new Set(sites.flatMap((s) => analysis.siteBeats.get(s) ?? [s]))].sort();
2141
+ out.push({ ref, writers, ...own.has(ref) ? {} : { onTheWayIn: true } });
1745
2142
  }
1746
2143
  return out;
1747
2144
  }
1748
2145
  function proposeCoverageDrivers(loaded) {
1749
- const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
2146
+ const bundle = compileLoaded(loaded);
1750
2147
  const hostScopes = bundle.scopeRegistry?.scopes ?? [];
1751
2148
  const hostTokens = new Set(hostScopes.map((s) => s.token));
1752
2149
  if (hostTokens.size === 0) return [];
@@ -1798,6 +2195,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
1798
2195
  }
1799
2196
  for (const beat of node.beats ?? []) {
1800
2197
  if (meta.has(beat.id)) continue;
2198
+ if (isContentlessBeat2(beat, !!src[beat.id])) continue;
1801
2199
  order.push(beat.id);
1802
2200
  meta.set(beat.id, {
1803
2201
  scene: scene.id,
@@ -1814,7 +2212,8 @@ function* sweep(loaded, options = {}, hooks = {}) {
1814
2212
  const dryRuns = /* @__PURE__ */ new Map();
1815
2213
  const errorRuns = /* @__PURE__ */ new Map();
1816
2214
  const termination = { ended: 0, capped: 0, stalled: 0, evalError: 0 };
1817
- const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
2215
+ const siteRuns = /* @__PURE__ */ new Map();
2216
+ const bundle = compileLoaded(loaded);
1818
2217
  const rng = mulberry32(seed);
1819
2218
  const hostTokens = hostScopeTokens(loaded, bundle);
1820
2219
  const analysis = analyzeHostScopes(bundle, hostTokens);
@@ -1825,6 +2224,8 @@ function* sweep(loaded, options = {}, hooks = {}) {
1825
2224
  const recurringDrivers = drivers.filter((d) => d.kind === "recurring");
1826
2225
  const drivenRefs = new Set(drivers.map((d) => d.ref));
1827
2226
  const pick = (vals) => vals[Math.floor(rng() * vals.length)];
2227
+ const sites = [...new Set([...analysis.writerSites.values()].flat())];
2228
+ const entered = entryGates(bundle, analysis.routes, start);
1828
2229
  let executed = 0;
1829
2230
  let cancelled = false;
1830
2231
  for (let run = 0; run < runs; run++) {
@@ -1872,8 +2273,15 @@ function* sweep(loaded, options = {}, hooks = {}) {
1872
2273
  seenThisRun.add(r.id);
1873
2274
  }
1874
2275
  }
1875
- } catch {
2276
+ } catch (err) {
1876
2277
  term = "evalError";
2278
+ const node = flow.currentScene ?? start.scene ?? "";
2279
+ const message = err instanceof Error ? err.message : String(err);
2280
+ errorsThisRun.set(`stopped|${node}|${message}`, { kind: "stopped", node, message });
2281
+ }
2282
+ if (sites.length) {
2283
+ const visits = engine.getVisitCounts();
2284
+ for (const id of sites) if ((visits[id] ?? 0) > 0) siteRuns.set(id, (siteRuns.get(id) ?? 0) + 1);
1877
2285
  }
1878
2286
  for (const id of seenThisRun) reachedRuns.set(id, reachedRuns.get(id) + 1);
1879
2287
  for (const id of dryThisRun) dryRuns.set(id, (dryRuns.get(id) ?? 0) + 1);
@@ -1894,13 +2302,16 @@ function* sweep(loaded, options = {}, hooks = {}) {
1894
2302
  const reached = reachedRuns.get(id);
1895
2303
  let needsInput;
1896
2304
  let blockedBy;
1897
- if (reached === 0) {
1898
- const gates = [...analysis.gatesByBeat.get(id) ?? []].filter((r) => !analysis.written.has(r) && !drivenRefs.has(r));
2305
+ if (reached === 0 && !analysis.shadowed.has(id)) {
2306
+ const block = analysis.blockOfBeat.get(id);
2307
+ const way = block !== void 0 ? entered.get(block) : void 0;
2308
+ const all = /* @__PURE__ */ new Set([...analysis.gatesByBeat.get(id) ?? [], ...way?.gate ?? []]);
2309
+ const gates = [...all].filter((r) => !analysis.written.has(r) && !drivenRefs.has(r));
1899
2310
  if (gates.length) {
1900
2311
  needsInput = gates;
1901
2312
  for (const g of gates) unwrittenInputs.add(g);
1902
2313
  }
1903
- const blocked = blockedGates(id, analysis, drivenRefs, reachedRuns);
2314
+ const blocked = blockedGates(id, analysis, drivenRefs, siteRuns, way);
1904
2315
  if (blocked.length) blockedBy = blocked;
1905
2316
  }
1906
2317
  const reachPct = executed ? reached / executed * 100 : 0;
@@ -1966,7 +2377,7 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1966
2377
  );
1967
2378
  out.push(`${report.runs} run(s) - ${report.maxSteps} max steps - seed ${report.seed}${report.cancelled ? " - CANCELLED" : ""}`);
1968
2379
  const term = report.termination;
1969
- out.push(`runs ended: ${term.ended} reached the end, ${term.stalled} stalled at a choice with nothing to pick, ${term.capped} hit the step limit, ${term.evalError} errored`);
2380
+ out.push(`runs ended: ${term.ended} reached the end, ${term.stalled ? `${term.stalled} stalled at a choice with nothing to pick, ` : ""}${term.capped} hit the step limit, ${term.evalError} errored`);
1970
2381
  if (report.drivers.length) out.push(`input drivers: ${report.drivers.map((d) => d.ref).join(", ")}`);
1971
2382
  if (report.unwrittenInputs.length) {
1972
2383
  out.push(`? = gated on an input nothing writes/drives: ${report.unwrittenInputs.join(", ")} (add a coverage driver?)`);
@@ -1980,9 +2391,10 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1980
2391
  }
1981
2392
  if (report.contentErrors.length) {
1982
2393
  out.push("");
1983
- out.push(`content errors (a condition or effect failed and play went on without it - fix the expression): ${report.contentErrors.length}`);
2394
+ out.push(`content errors (a condition or effect failed and play went on without it, or a run stopped - fix the content): ${report.contentErrors.length}`);
1984
2395
  for (const e of report.contentErrors) {
1985
- out.push(` \u203C ${String(e.runs).padStart(6)} run(s) ${sceneName(e.scene)} ${e.kind} on '${e.node}'${e.source ? ` (${e.source})` : ""}: ${e.message}`);
2396
+ const what = e.kind === "stopped" ? "run stopped" : `${e.kind} on '${e.node}'${e.source ? ` (${e.source})` : ""}`;
2397
+ out.push(` \u203C ${String(e.runs).padStart(6)} run(s) ${sceneName(e.scene)} ${what}: ${e.message}`);
1986
2398
  }
1987
2399
  }
1988
2400
  if (!report.beats.length) return out;
@@ -1997,9 +2409,9 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1997
2409
  for (const bg of b.blockedBy ?? []) {
1998
2410
  const names = bg.writers.map((w) => {
1999
2411
  const target = report.beats.find((x) => x.id === w);
2000
- return target ? clip(target.preview || target.id, 28) : w;
2412
+ return target ? clip(target.preview || target.id, 28) : sceneName(w);
2001
2413
  });
2002
- out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
2414
+ out.push(` gated on ${bg.ref}${bg.onTheWayIn ? " on the way in" : ""}, written only by: ${names.join(", ")} (never played either)`);
2003
2415
  }
2004
2416
  };
2005
2417
  out.push("");
@@ -2027,14 +2439,27 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
2027
2439
  }
2028
2440
 
2029
2441
  // src/format.ts
2030
- import { readFileSync as readFileSync4 } from "fs";
2031
- import { canonicalStringify as canonicalStringify4, parseSource as parseSource3 } from "@patterkit/core";
2032
- function runFormat(files) {
2033
- return files.map((file) => {
2034
- const original = readFileSync4(file, "utf8");
2442
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
2443
+ import { dirname as dirname5 } from "path";
2444
+ import { canonicalStringify as canonicalStringify5, parseSource as parseSource4 } from "@patterkit/core";
2445
+ var isShard = (file) => SHARD_EXTENSIONS.some((ext) => file.endsWith(ext));
2446
+ function expand(path) {
2447
+ let dir = false;
2448
+ try {
2449
+ dir = statSync3(path).isDirectory();
2450
+ } catch {
2451
+ }
2452
+ if (!dir) return [path];
2453
+ const root = dirname5(findProjectFile(path));
2454
+ return SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).sort();
2455
+ }
2456
+ function runFormat(paths) {
2457
+ return paths.flatMap(expand).map((file) => {
2458
+ if (!isShard(file)) return { file, changed: false, skipped: true };
2459
+ const original = readFileSync6(file, "utf8");
2035
2460
  let canonical;
2036
2461
  try {
2037
- canonical = canonicalStringify4(parseSource3(original));
2462
+ canonical = canonicalStringify5(parseSource4(original));
2038
2463
  } catch (e) {
2039
2464
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
2040
2465
  }
@@ -2044,7 +2469,7 @@ function runFormat(files) {
2044
2469
  }
2045
2470
 
2046
2471
  // src/report.ts
2047
- import { walkNodes as walkNodes2, DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES2, RERECORD_STATUS as RERECORD_STATUS2 } from "@patterkit/model";
2472
+ import { DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES2, RERECORD_STATUS as RERECORD_STATUS2 } from "@patterkit/model";
2048
2473
  var DEFAULT_WORDS_PER_LINE = 6;
2049
2474
  var UNATTRIBUTED = "(unattributed)";
2050
2475
  function allocateLargestRemainder(total, weights) {
@@ -2097,35 +2522,51 @@ function runReport(loaded, recordingOverride) {
2097
2522
  const localeIds = [];
2098
2523
  const estimatedScenes = [];
2099
2524
  let actualWrittenLines = 0, actualWords = 0;
2100
- const rungIdxOf = (id) => writingIndex.get(writingOf.get(id) ?? stub) ?? 0;
2525
+ const wsOf = (id) => {
2526
+ const w = writingOf.get(id);
2527
+ return w !== void 0 && writingIndex.has(w) ? w : stub;
2528
+ };
2529
+ const rungIdxOf = (id) => writingIndex.get(wsOf(id)) ?? 0;
2101
2530
  for (const scene of loaded.scenes) {
2102
2531
  const sceneCut = cutSet.has(scene.id);
2103
2532
  const units = [];
2533
+ const underCut = /* @__PURE__ */ new Set();
2104
2534
  let choices = 0;
2105
2535
  for (const block of scene.blocks) {
2106
- walkNodes2(block.children, (node) => {
2107
- if (node.type === "group") {
2108
- if (node.selector === "choice") choices++;
2109
- if (node.prompt) units.push({
2110
- id: node.prompt.id,
2111
- voiced: node.prompt.kind === "line",
2112
- words: wordsOf(node.prompt.id),
2113
- character: node.prompt.kind === "line" ? node.prompt.character : void 0
2114
- });
2115
- return;
2116
- }
2117
- for (const beat of node.beats ?? []) {
2118
- if (beat.kind === "gameEvent") continue;
2119
- units.push({
2120
- id: beat.id,
2121
- voiced: beat.kind === "line",
2122
- words: wordsOf(beat.id),
2123
- character: beat.kind === "line" ? beat.character : void 0
2124
- });
2536
+ const blockCut = cutSet.has(block.id);
2537
+ const visit = (nodes, inCut) => {
2538
+ for (const node of nodes) {
2539
+ const nodeCut = inCut || cutSet.has(node.id);
2540
+ const add = (u) => {
2541
+ units.push(u);
2542
+ if (nodeCut) underCut.add(u.id);
2543
+ };
2544
+ if (node.type === "group") {
2545
+ if (node.selector === "choice") choices++;
2546
+ if (node.prompt) add({
2547
+ id: node.prompt.id,
2548
+ voiced: node.prompt.kind === "line",
2549
+ words: wordsOf(node.prompt.id),
2550
+ character: node.prompt.kind === "line" ? node.prompt.character : void 0
2551
+ });
2552
+ visit(node.children ?? [], nodeCut);
2553
+ continue;
2554
+ }
2555
+ for (const beat of node.beats ?? []) {
2556
+ if (beat.kind === "gameEvent") continue;
2557
+ add({
2558
+ id: beat.id,
2559
+ voiced: beat.kind === "line",
2560
+ words: wordsOf(beat.id),
2561
+ character: beat.kind === "line" ? beat.character : void 0
2562
+ });
2563
+ }
2125
2564
  }
2126
- });
2565
+ };
2566
+ visit(block.children, blockCut);
2127
2567
  }
2128
- for (const u of units) if (sceneCut || cutSet.has(u.id)) {
2568
+ const isCut = (id) => cutSet.has(id) || underCut.has(id);
2569
+ for (const u of units) if (sceneCut || isCut(u.id)) {
2129
2570
  cut.writtenLines++;
2130
2571
  if (u.voiced) cut.voicedLines++;
2131
2572
  }
@@ -2133,7 +2574,7 @@ function runReport(loaded, recordingOverride) {
2133
2574
  cut.scenes++;
2134
2575
  continue;
2135
2576
  }
2136
- const live = units.filter((u) => !cutSet.has(u.id));
2577
+ const live = units.filter((u) => !isCut(u.id));
2137
2578
  for (const u of live) if (sourceStrings2[u.id]) localeIds.push(u.id);
2138
2579
  const statusIdx = live.length ? Math.min(...live.map((u) => rungIdxOf(u.id))) : void 0;
2139
2580
  const status = statusIdx === void 0 ? void 0 : writingLadder[statusIdx];
@@ -2176,7 +2617,7 @@ function runReport(loaded, recordingOverride) {
2176
2617
  const voiced = emptyVoiced(writingLadder, recordingLadder);
2177
2618
  const written = emptyWritten(writingLadder);
2178
2619
  for (const u of live) {
2179
- const ws = writingOf.get(u.id) ?? stub;
2620
+ const ws = wsOf(u.id);
2180
2621
  written.count++;
2181
2622
  written.words += u.words;
2182
2623
  written.byWriting[ws] = (written.byWriting[ws] ?? 0) + 1;
@@ -2418,9 +2859,9 @@ async function runReportXlsx(data) {
2418
2859
  }
2419
2860
 
2420
2861
  // src/localisation.ts
2421
- import { basename as basename5, join as join5, sep } from "path";
2422
- import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
2423
- import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
2862
+ import { basename as basename5, join as join7, sep as sep3 } from "path";
2863
+ import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
2864
+ import { walkNodes as walkNodes2, castStringKey, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2, projectLayout as projectLayout3, STRINGS_SCHEMA, AUTHORING_SCHEMA } from "@patterkit/model";
2424
2865
 
2425
2866
  // src/documentation.ts
2426
2867
  import { DEFAULT_DOCUMENTATION_CLASSES } from "@patterkit/model";
@@ -2494,7 +2935,7 @@ function extractLoc(loaded, opts = {}) {
2494
2935
  };
2495
2936
  for (const scene of loaded.scenes) {
2496
2937
  for (const block of scene.blocks) {
2497
- walkNodes3(block.children, (node) => {
2938
+ walkNodes2(block.children, (node) => {
2498
2939
  if (node.type === "group") {
2499
2940
  const p = node.prompt;
2500
2941
  if (p) push(p.id, scene.id, { character: p.kind === "line" ? p.character : void 0, kind: p.kind });
@@ -2511,14 +2952,11 @@ function extractLoc(loaded, opts = {}) {
2511
2952
  const id = castStringKey(c.name);
2512
2953
  const src = source[id] ?? c.displayName;
2513
2954
  const translation = isTemplate ? "" : target[id] ?? "";
2514
- entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: withGender({ character: c.name }), stale: staleFor(id, translation) });
2955
+ entries.push({ id, scene: PROJECT_LOCALE_SCENE2, source: src, translation, comments: commentsOf(id), context: withGender({ character: c.name }), stale: staleFor(id, translation) });
2515
2956
  }
2516
2957
  return { project: loaded.project.project.id, defaultLocale, locale: targetLocale, entries };
2517
2958
  }
2518
- var layoutOf = (loaded) => ({
2519
- strings: loaded.project.layout?.strings ?? "loc/",
2520
- authoring: loaded.project.layout?.authoring ?? "authoring/"
2521
- });
2959
+ var layoutOf = (loaded) => projectLayout3(loaded.project);
2522
2960
  function existingLocPath(loaded, scene, locale) {
2523
2961
  for (let i = 0; i < loaded.locales.length; i++) {
2524
2962
  const l = loaded.locales[i];
@@ -2531,23 +2969,23 @@ function targetLocPath(loaded, scene, locale, defaultLocale) {
2531
2969
  if (existing) return existing;
2532
2970
  const fromDefault = existingLocPath(loaded, scene, defaultLocale);
2533
2971
  if (fromDefault) {
2534
- const parts = fromDefault.split(sep);
2972
+ const parts = fromDefault.split(sep3);
2535
2973
  for (let i = parts.length - 2; i >= 0; i--) {
2536
2974
  if (parts[i] === defaultLocale) {
2537
2975
  parts[i] = locale;
2538
- return parts.join(sep);
2976
+ return parts.join(sep3);
2539
2977
  }
2540
2978
  }
2541
2979
  }
2542
- const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
2543
- return join5(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
2980
+ const stem = scene === PROJECT_LOCALE_SCENE2 ? "_project" : scene;
2981
+ return join7(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
2544
2982
  }
2545
2983
  function authoringPath(loaded, scene) {
2546
- const dir = join5(loaded.root, layoutOf(loaded).authoring);
2547
- if (scene === PROJECT_LOCALE_SCENE) return join5(dir, "_project.patterx");
2984
+ const dir = join7(loaded.root, layoutOf(loaded).authoring);
2985
+ if (scene === PROJECT_LOCALE_SCENE2) return join7(dir, "_project.patterx");
2548
2986
  const flow = loaded.sceneFiles[scene];
2549
2987
  const stem = flow ? basename5(flow).replace(/\.patterflow$/, "") : scene;
2550
- return join5(dir, `${stem}.patterx`);
2988
+ return join7(dir, `${stem}.patterx`);
2551
2989
  }
2552
2990
  var findFile = (files, items, path) => {
2553
2991
  const i = files.indexOf(path);
@@ -2558,6 +2996,14 @@ function applyLoc(loaded, catalog, opts = {}) {
2558
2996
  if (!locale || locale === loaded.project.locales.default) {
2559
2997
  return { writes: [], stats: { updated: 0, files: 0 } };
2560
2998
  }
2999
+ if (!loaded.project.locales.all.includes(locale)) {
3000
+ throw new Error(`'${locale}' is not one of the project's languages (${loaded.project.locales.all.join(", ")}); add it to the project first`);
3001
+ }
3002
+ const known = /* @__PURE__ */ new Set([...loaded.scenes.map((sc) => sc.id), PROJECT_LOCALE_SCENE2]);
3003
+ const unknown = [...new Set(catalog.entries.filter((e) => e.translation.trim() && !known.has(e.scene)).map((e) => e.scene))];
3004
+ if (unknown.length) {
3005
+ throw new Error(`the file names scene(s) this project does not have: ${unknown.map((u) => `'${u}'`).join(", ")}; nothing was imported`);
3006
+ }
2561
3007
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
2562
3008
  const bySceneText = /* @__PURE__ */ new Map();
2563
3009
  const bySceneFresh = /* @__PURE__ */ new Map();
@@ -2576,22 +3022,31 @@ function applyLoc(loaded, catalog, opts = {}) {
2576
3022
  if (merged[id] !== text) updated++;
2577
3023
  merged[id] = text;
2578
3024
  }
2579
- const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
2580
- writes.push({ path, content: canonicalStringify5(file) });
3025
+ const file = { schema: existing?.schema ?? STRINGS_SCHEMA, scene, locale, strings: merged };
3026
+ writes.push({ path, content: canonicalStringify6(file) });
2581
3027
  const fresh = bySceneFresh.get(scene);
2582
3028
  if (!fresh?.size) continue;
2583
3029
  const aPath = authoringPath(loaded, scene);
2584
3030
  const aExisting = findFile(loaded.authoringFiles, loaded.authoring, aPath);
2585
- const authoring = aExisting ? { ...aExisting, edits: { ...aExisting.edits } } : { schema: "patter/authoring@0" };
3031
+ const authoring = aExisting ? { ...aExisting, edits: { ...aExisting.edits } } : { schema: AUTHORING_SCHEMA };
2586
3032
  const editsMap = { ...authoring.edits ?? {} };
2587
3033
  for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
2588
3034
  authoring.edits = editsMap;
2589
- writes.push({ path: aPath, content: canonicalStringify5(authoring) });
3035
+ writes.push({ path: aPath, content: canonicalStringify6(authoring) });
2590
3036
  }
2591
3037
  return { writes, stats: { updated, files: bySceneText.size } };
2592
3038
  }
2593
3039
 
2594
3040
  // src/loc-format.ts
3041
+ function checkCatalogField(format, field, value) {
3042
+ if (typeof value !== "string" || value.trim() === "") {
3043
+ throw new Error(`${format}: '${field}' must be a non-empty string (got ${JSON.stringify(value) ?? String(value)})`);
3044
+ }
3045
+ if (/[\\/]/.test(value) || value.includes("..")) {
3046
+ throw new Error(`${format}: '${field}' ${JSON.stringify(value)} is not a plain name (it holds a path separator or '..')`);
3047
+ }
3048
+ return value;
3049
+ }
2595
3050
  function catalogToJson(catalog) {
2596
3051
  return JSON.stringify(catalog, null, 2) + "\n";
2597
3052
  }
@@ -2602,10 +3057,10 @@ function jsonToCatalog(text) {
2602
3057
  return {
2603
3058
  project: typeof o.project === "string" ? o.project : "",
2604
3059
  defaultLocale: o.defaultLocale,
2605
- locale: typeof o.locale === "string" ? o.locale : void 0,
3060
+ locale: o.locale === void 0 || o.locale === null ? void 0 : checkCatalogField("localisation JSON", "locale", o.locale),
2606
3061
  entries: o.entries.map((e) => ({
2607
3062
  id: String(e.id),
2608
- scene: String(e.scene),
3063
+ scene: checkCatalogField("localisation JSON", "scene", e.scene),
2609
3064
  source: String(e.source ?? ""),
2610
3065
  translation: String(e.translation ?? ""),
2611
3066
  comments: Array.isArray(e.comments) ? e.comments.map(String) : [],
@@ -2614,8 +3069,10 @@ function jsonToCatalog(text) {
2614
3069
  }))
2615
3070
  };
2616
3071
  }
2617
- var poEscape = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
2618
- var poUnescape = (s) => s.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
3072
+ var PO_ESCAPES = { "\\": "\\\\", '"': '\\"', "\n": "\\n", " ": "\\t", "\r": "\\r" };
3073
+ var PO_UNESCAPES = { "\\": "\\", '"': '"', n: "\n", t: " ", r: "\r" };
3074
+ var poEscape = (s) => s.replace(/[\\"\n\t\r]/g, (c) => PO_ESCAPES[c]);
3075
+ var poUnescape = (s) => s.replace(/\\(.)/gs, (all, c) => PO_UNESCAPES[c] ?? all);
2619
3076
  var poField = (key, value) => `${key} "${poEscape(value)}"`;
2620
3077
  function catalogToPo(catalog) {
2621
3078
  const out = [];
@@ -2665,10 +3122,10 @@ function poToCatalog(text) {
2665
3122
  let i = 0;
2666
3123
  const flush = (msgstr) => {
2667
3124
  if (ctxt !== void 0) {
2668
- entries.push({ id: ctxt, scene, source: msgid, translation: msgstr, comments, stale });
3125
+ entries.push({ id: ctxt, scene: checkCatalogField("PO", "scene", scene), source: msgid, translation: msgstr, comments, stale });
2669
3126
  } else if (msgid === "") {
2670
3127
  const lang = /Language:\s*([^\\\n]*)/.exec(msgstr);
2671
- if (lang?.[1]?.trim()) locale = lang[1].trim();
3128
+ if (lang?.[1]?.trim()) locale = checkCatalogField("PO", "locale", lang[1].trim());
2672
3129
  const pid = /Project-Id-Version:\s*([^\\\n]*)/.exec(msgstr);
2673
3130
  if (pid?.[1]?.trim()) project = pid[1].trim();
2674
3131
  }
@@ -2728,23 +3185,50 @@ function poToCatalog(text) {
2728
3185
  }
2729
3186
 
2730
3187
  // src/loc-xlsx.ts
2731
- var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender"];
2732
- var sheetName = (scene) => scene.replace(/[:\\/?*[\]]/g, "-").slice(0, 31);
3188
+ var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender", "Scene"];
3189
+ var SCENE_COLUMN = HEADERS.indexOf("Scene") + 1;
3190
+ var SHEET_NAME_MAX = 31;
3191
+ function uniqueSheetName(scene, used) {
3192
+ const tidy2 = (s) => s.replace(/^'+|'+$/g, "");
3193
+ const base = tidy2(scene.replace(/[:\\/?*[\]]/g, "-").slice(0, SHEET_NAME_MAX)) || "Scene";
3194
+ let name = base;
3195
+ for (let n = 2; used.has(name.toLowerCase()) || name.toLowerCase() === "history"; n++) {
3196
+ const suffix = ` (${n})`;
3197
+ name = tidy2(base.slice(0, SHEET_NAME_MAX - suffix.length)) + suffix;
3198
+ }
3199
+ used.add(name.toLowerCase());
3200
+ return name;
3201
+ }
3202
+ function cellText(v) {
3203
+ if (v == null) return "";
3204
+ if (typeof v === "string") return v;
3205
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
3206
+ if (v instanceof Date) return v.toISOString();
3207
+ if (typeof v !== "object") return "";
3208
+ const o = v;
3209
+ if (Array.isArray(o.richText)) return o.richText.map((run) => cellText(run?.text)).join("");
3210
+ if ("text" in o) return cellText(o.text);
3211
+ if ("result" in o) return cellText(o.result);
3212
+ return "";
3213
+ }
2733
3214
  async function catalogToXlsx(catalog) {
2734
3215
  const { default: ExcelJS } = await import("exceljs");
2735
3216
  const wb = new ExcelJS.Workbook();
2736
3217
  wb.creator = "patter";
2737
3218
  const byScene = /* @__PURE__ */ new Map();
2738
3219
  for (const e of catalog.entries) (byScene.get(e.scene) ?? byScene.set(e.scene, []).get(e.scene)).push(e);
3220
+ const used = /* @__PURE__ */ new Set();
2739
3221
  for (const [scene, entries] of byScene) {
2740
- const ws = wb.addWorksheet(sheetName(scene));
3222
+ const ws = wb.addWorksheet(uniqueSheetName(scene, used));
2741
3223
  ws.columns = [
2742
3224
  { header: "ID", key: "id", width: 22 },
2743
3225
  { header: "Source", key: "source", width: 40 },
2744
3226
  { header: "Translation", key: "translation", width: 40 },
2745
3227
  { header: "Comments", key: "comments", width: 30 },
2746
3228
  { header: "Status", key: "status", width: 10 },
2747
- { header: "Gender", key: "gender", width: 12 }
3229
+ { header: "Gender", key: "gender", width: 12 },
3230
+ // Hidden: the translator has no use for it, and it is the import's only faithful record of the scene.
3231
+ { header: "Scene", key: "scene", width: 22, hidden: true }
2748
3232
  ];
2749
3233
  ws.getRow(1).font = { bold: true };
2750
3234
  ws.views = [{ state: "frozen", ySplit: 1 }];
@@ -2755,7 +3239,8 @@ async function catalogToXlsx(catalog) {
2755
3239
  translation: e.translation,
2756
3240
  comments: e.comments.join("\n"),
2757
3241
  status: e.stale ? "stale" : e.translation ? "translated" : "",
2758
- gender: e.context?.gender ?? ""
3242
+ gender: e.context?.gender ?? "",
3243
+ scene
2759
3244
  });
2760
3245
  }
2761
3246
  }
@@ -2767,18 +3252,21 @@ async function xlsxToCatalog(buffer) {
2767
3252
  await wb.xlsx.load(buffer);
2768
3253
  const entries = [];
2769
3254
  for (const ws of wb.worksheets) {
2770
- const scene = ws.name;
2771
- const header = ws.getRow(1).values.slice(1).map((v) => String(v ?? ""));
3255
+ const header = ws.getRow(1).values.slice(1).map(cellText);
2772
3256
  if (header[0] !== HEADERS[0]) continue;
3257
+ const hasSceneColumn = header[SCENE_COLUMN - 1] === "Scene";
3258
+ const sceneOf = (row) => hasSceneColumn ? cellText(row.getCell(SCENE_COLUMN).value).trim() : "";
3259
+ let sheetScene = "";
3260
+ ws.eachRow((row, n) => {
3261
+ if (n > 1 && !sheetScene) sheetScene = sceneOf(row);
3262
+ });
2773
3263
  ws.eachRow((row, n) => {
2774
3264
  if (n === 1) return;
2775
- const cell = (c) => {
2776
- const v = row.getCell(c).value;
2777
- return v == null ? "" : String(typeof v === "object" && "text" in v ? v.text : v);
2778
- };
3265
+ const cell = (c) => cellText(row.getCell(c).value);
2779
3266
  const id = cell(1).trim();
2780
3267
  if (!id) return;
2781
3268
  const status = cell(5).trim().toLowerCase();
3269
+ const scene = checkCatalogField("spreadsheet", "scene", sceneOf(row) || sheetScene || ws.name);
2782
3270
  entries.push({
2783
3271
  id,
2784
3272
  scene,
@@ -2820,33 +3308,37 @@ function runVoiceScript(loaded, opts = {}) {
2820
3308
  const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, "vo");
2821
3309
  const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
2822
3310
  const lines = [];
3311
+ const emitLine = (beat, scope, leadingVo) => {
3312
+ if (beat.kind !== "line" || cutSet.has(beat.id)) return false;
3313
+ const ws = writingOf.get(beat.id) ?? stub;
3314
+ if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) return false;
3315
+ const own = ownVo(beat.id);
3316
+ lines.push({
3317
+ scope: scope.join(" \u203A "),
3318
+ id: beat.id,
3319
+ character: beat.character ?? "",
3320
+ actor: beat.character ? actorOf.get(beat.character) : void 0,
3321
+ text: plainVoice(source[beat.id] ?? ""),
3322
+ comments: leadingVo ? [...leadingVo, ...own] : own,
3323
+ // first line of the run gets the enclosing context
3324
+ recordingStatus: recordingOf(beat.id)
3325
+ });
3326
+ return true;
3327
+ };
2823
3328
  const walkNode = (node, scope, ancestorVo) => {
2824
3329
  if (node.type === "group") {
2825
3330
  if (cutSet.has(node.id)) return;
2826
3331
  const label = groupLabel(node, source);
2827
3332
  const childScope = label ? [...scope, label] : scope;
2828
3333
  const childVo = [...ancestorVo, ...ownVo(node.id)];
3334
+ if (node.prompt) emitLine(node.prompt, childScope, childVo);
2829
3335
  for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
2830
3336
  return;
2831
3337
  }
2832
3338
  if (cutSet.has(node.id)) return;
2833
3339
  let leading = true;
2834
3340
  for (const beat of node.beats ?? []) {
2835
- if (beat.kind !== "line" || cutSet.has(beat.id)) continue;
2836
- const ws = writingOf.get(beat.id) ?? stub;
2837
- if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) continue;
2838
- const own = ownVo(beat.id);
2839
- lines.push({
2840
- scope: scope.join(" \u203A "),
2841
- id: beat.id,
2842
- character: beat.character ?? "",
2843
- actor: beat.character ? actorOf.get(beat.character) : void 0,
2844
- text: plainVoice(source[beat.id] ?? ""),
2845
- comments: leading ? [...ancestorVo, ...own] : own,
2846
- // first line of the run gets the enclosing context
2847
- recordingStatus: recordingOf(beat.id)
2848
- });
2849
- leading = false;
3341
+ if (emitLine(beat, scope, leading ? ancestorVo : void 0)) leading = false;
2850
3342
  }
2851
3343
  };
2852
3344
  for (const scene of loaded.scenes) {
@@ -3229,7 +3721,7 @@ var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Ser
3229
3721
  var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
3230
3722
  async function scriptToPdf(doc) {
3231
3723
  const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
3232
- return new Promise((resolve6, reject) => {
3724
+ return new Promise((resolve7, reject) => {
3233
3725
  const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
3234
3726
  pdf.registerFont("Serif", scriptFont("serif"));
3235
3727
  pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
@@ -3270,7 +3762,7 @@ async function scriptToPdf(doc) {
3270
3762
  const bodyPieces = (runs, color, base = {}) => runs.flatMap((r) => r.code ? split(r.text, "Mono", ACCENT) : split(r.text, SERIF2(base.bold || r.bold, base.italic || r.italic), color));
3271
3763
  const chunks = [];
3272
3764
  pdf.on("data", (c) => chunks.push(c));
3273
- pdf.on("end", () => resolve6(Buffer.concat(chunks)));
3765
+ pdf.on("end", () => resolve7(Buffer.concat(chunks)));
3274
3766
  pdf.on("error", reject);
3275
3767
  const pageRight = pdf.page.width - MARGIN;
3276
3768
  const pageBottom = pdf.page.height - MARGIN;
@@ -3421,11 +3913,12 @@ async function scriptToPdf(doc) {
3421
3913
  }
3422
3914
 
3423
3915
  // src/init.ts
3424
- import { join as join6, basename as basename6, resolve as resolve4 } from "path";
3425
- import { readdirSync as readdirSync4, existsSync as existsSync3 } from "fs";
3426
- import { newId, slug, canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3916
+ import { join as join8, basename as basename6, resolve as resolve5 } from "path";
3917
+ import { readdirSync as readdirSync4, existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
3918
+ import { newId, slug, canonicalStringify as canonicalStringify7 } from "@patterkit/core";
3919
+ import { FLOW_SCHEMA, PROJECT_SCHEMA, STRINGS_SCHEMA as STRINGS_SCHEMA2 } from "@patterkit/model";
3427
3920
  function runInit(opts) {
3428
- const dir = resolve4(opts.dir);
3921
+ const dir = resolve5(opts.dir);
3429
3922
  const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
3430
3923
  const fileSlug = slug(name);
3431
3924
  let existing = [];
@@ -3437,7 +3930,7 @@ function runInit(opts) {
3437
3930
  const sceneId = newId("scn");
3438
3931
  const beatId = newId("T");
3439
3932
  const project = {
3440
- schema: "patter/project@0",
3933
+ schema: PROJECT_SCHEMA,
3441
3934
  project: { id: newId("proj"), name },
3442
3935
  locales: { default: "en", all: ["en"] },
3443
3936
  ...opts.vcs ? { vcs: opts.vcs } : {},
@@ -3445,7 +3938,7 @@ function runInit(opts) {
3445
3938
  voiced: true
3446
3939
  };
3447
3940
  const flow = {
3448
- schema: "patter/flow@0",
3941
+ schema: FLOW_SCHEMA,
3449
3942
  scene: {
3450
3943
  id: sceneId,
3451
3944
  type: "scene",
@@ -3464,7 +3957,7 @@ function runInit(opts) {
3464
3957
  }
3465
3958
  };
3466
3959
  const locale = {
3467
- schema: "patter/strings@0",
3960
+ schema: STRINGS_SCHEMA2,
3468
3961
  scene: sceneId,
3469
3962
  locale: "en",
3470
3963
  default: true,
@@ -3474,27 +3967,39 @@ function runInit(opts) {
3474
3967
  strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
3475
3968
  };
3476
3969
  const bundle = opts.bundle ?? "commit";
3477
- const projectFile = join6(dir, `${fileSlug}.patterproj`);
3970
+ const projectFile = join8(dir, `${fileSlug}.patterproj`);
3478
3971
  const writes = [
3479
- { path: projectFile, content: canonicalStringify6(project) },
3480
- { path: join6(dir, "scenes", "start.patterflow"), content: canonicalStringify6(flow) },
3481
- { path: join6(dir, "loc", "en", "start.patterloc"), content: canonicalStringify6(locale) },
3482
- { path: join6(dir, ".editorconfig"), content: EDITORCONFIG },
3483
- ...vcsConfigWrites(dir, opts.vcs, bundle)
3972
+ { path: join8(dir, "scenes", "start.patterflow"), content: canonicalStringify7(flow) },
3973
+ { path: join8(dir, "loc", "en", "start.patterloc"), content: canonicalStringify7(locale) },
3974
+ { path: join8(dir, ".editorconfig"), content: EDITORCONFIG },
3975
+ ...vcsConfigWrites(dir, opts.vcs, bundle),
3976
+ { path: projectFile, content: canonicalStringify7(project) }
3484
3977
  ];
3485
- const collisions = writes.map((w) => w.path).filter((p) => existsSync3(p));
3978
+ const collisions = writes.map((w) => w.path).filter((p) => existsSync5(p));
3486
3979
  if (collisions.length > 0) {
3487
3980
  throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
3488
3981
  }
3489
3982
  return { writes, projectFile, name };
3490
3983
  }
3491
3984
  function vcsConfigWrites(dir, vcs, bundle = "commit") {
3492
- const writes = [{ path: join6(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
3493
- if (vcs === "git") writes.push({ path: join6(dir, ".gitattributes"), content: GITATTRIBUTES });
3985
+ const writes = [{ path: join8(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
3986
+ if (vcs === "git") writes.push({ path: join8(dir, ".gitattributes"), content: GITATTRIBUTES });
3494
3987
  const ignore = ignoreFileFor(vcs);
3495
- if (ignore) writes.push({ path: join6(dir, ignore), content: ignoreContent(bundle) });
3988
+ if (ignore) writes.push({ path: join8(dir, ignore), content: ignoreContent(bundle) });
3496
3989
  return writes;
3497
3990
  }
3991
+ function currentBundlePosture(dir, vcs) {
3992
+ const read = (name) => {
3993
+ try {
3994
+ return readFileSync7(join8(dir, name), "utf8");
3995
+ } catch {
3996
+ return "";
3997
+ }
3998
+ };
3999
+ const ignore = ignoreFileFor(vcs);
4000
+ if (ignore && read(ignore).split(/\r?\n/).some((l) => l.trim() === "*.patterc")) return "ignore";
4001
+ return read("vcs-setup.md").includes("This project IGNORES the compiled") ? "ignore" : "commit";
4002
+ }
3498
4003
  var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
3499
4004
  root = true
3500
4005
 
@@ -3573,13 +4078,13 @@ drivers once per clone (git config is not repo-tracked) - until then git falls
3573
4078
  back to a normal text merge for those files:
3574
4079
 
3575
4080
  git config merge.patter.name "Patter structured merge"
3576
- git config merge.patter.driver "patter merge %O %A %B -o %A"
4081
+ git config merge.patter.driver "patter merge %O %A %B -o %A --path %P"
3577
4082
  git config merge.ours.driver true
3578
4083
 
3579
4084
  git invokes the per-path driver directly (no \`mergetool\` wrapper needed). \`%O %A
3580
- %B\` are base / ours / theirs; the merged result is written back to \`%A\`. On a
3581
- conflict \`patter merge\` exits non-zero and writes a \`.patterconflict\` sidecar
3582
- beside the file, so the merge stays unresolved.
4085
+ %B\` are base / ours / theirs; the merged result is written back to \`%A\`, and
4086
+ \`%P\` is the file's real path. On a conflict \`patter merge\` exits non-zero and
4087
+ writes a \`.patterconflict\` sidecar beside that file, so the merge stays unresolved.
3583
4088
  `,
3584
4089
  perforce: `## Perforce
3585
4090
 
@@ -3675,7 +4180,7 @@ return via \`patter unpack\`.
3675
4180
  }
3676
4181
 
3677
4182
  // src/resolve.ts
3678
- import { walkNodes as walkNodes4, effectiveGameId, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
4183
+ import { walkNodes as walkNodes3, effectiveGameId as effectiveGameId2, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
3679
4184
  function runResolve(loaded, query) {
3680
4185
  const entries = indexProject(loaded);
3681
4186
  const q = query.trim();
@@ -3729,7 +4234,7 @@ function runStatusBrowse(loaded, status, dimension = "writing", focus, recording
3729
4234
  if (statusAt(beat.id) !== status) return;
3730
4235
  out.push({ id: beat.id, kind: "beat", text: strings[beat.id], location: segments, sceneId: scene.id, file });
3731
4236
  };
3732
- walkNodes4(block.children, (node) => {
4237
+ walkNodes3(block.children, (node) => {
3733
4238
  if (node.type === "group") {
3734
4239
  if (node.prompt) consider(node.prompt);
3735
4240
  return;
@@ -3750,7 +4255,7 @@ function listProjectTags(loaded) {
3750
4255
  add(scene.tags);
3751
4256
  for (const block of scene.blocks) {
3752
4257
  add(block.tags);
3753
- walkNodes4(block.children, (node) => {
4258
+ walkNodes3(block.children, (node) => {
3754
4259
  add(node.tags);
3755
4260
  if (node.type === "group") {
3756
4261
  add(node.prompt?.tags);
@@ -3769,11 +4274,11 @@ function runTagBrowse(loaded, tag, focus) {
3769
4274
  const out = [];
3770
4275
  for (const scene of loaded.scenes) {
3771
4276
  const file = loaded.sceneFiles[scene.id];
3772
- if (has(scene.tags)) out.push({ id: scene.id, kind: "scene", name: scene.name, gameId: effectiveGameId(scene), location: [scene.name], sceneId: scene.id, file });
4277
+ if (has(scene.tags)) out.push({ id: scene.id, kind: "scene", name: scene.name, gameId: effectiveGameId2(scene), location: [scene.name], sceneId: scene.id, file });
3773
4278
  for (const block of scene.blocks) {
3774
4279
  const segments = [scene.name, block.name];
3775
- if (has(block.tags)) out.push({ id: block.id, kind: "block", name: block.name, gameId: effectiveGameId(block), location: segments, sceneId: scene.id, file });
3776
- walkNodes4(block.children, (node) => {
4280
+ if (has(block.tags)) out.push({ id: block.id, kind: "block", name: block.name, gameId: effectiveGameId2(block), location: segments, sceneId: scene.id, file });
4281
+ walkNodes3(block.children, (node) => {
3777
4282
  if (has(node.tags)) {
3778
4283
  const preview = node.type === "group" ? node.prompt ? strings[node.prompt.id] : void 0 : strings[node.beats?.[0]?.id ?? ""];
3779
4284
  out.push({ id: node.id, kind: node.type, text: preview, location: segments, sceneId: scene.id, file });
@@ -3808,7 +4313,7 @@ function runPropertyUsage(loaded, query, focus) {
3808
4313
  }
3809
4314
  for (const block of scene.blocks) {
3810
4315
  const segments = [scene.name, block.name];
3811
- walkNodes4(block.children, (node) => {
4316
+ walkNodes3(block.children, (node) => {
3812
4317
  if (refs(node.condition) && valueOk(node.condition)) push(node.id, node.type, segments, `if ${node.condition}`);
3813
4318
  if (node.type === "group") {
3814
4319
  if (node.prompt && refs(strings[node.prompt.id]) && valueOk(strings[node.prompt.id])) push(node.prompt.id, "beat", segments, strings[node.prompt.id]);
@@ -3838,18 +4343,19 @@ function parsePropertyQuery(query) {
3838
4343
  const regexes = prop.includes(".") ? [new RegExp(`@${esc2(prop)}\\b`, "i")] : [new RegExp(`@${esc2(prop)}\\b`, "i"), new RegExp(`@patter\\.${esc2(prop)}\\b`, "i")];
3839
4344
  return { regexes, value };
3840
4345
  }
3841
- var indexCache = null;
4346
+ var indexCache = /* @__PURE__ */ new WeakMap();
3842
4347
  function sameRefs(a, b) {
3843
4348
  if (a.length !== b.length) return false;
3844
4349
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
3845
4350
  return true;
3846
4351
  }
3847
4352
  function indexProject(loaded) {
3848
- if (indexCache && indexCache.loaded === loaded && sameRefs(indexCache.scenes, loaded.scenes) && sameRefs(indexCache.locales, loaded.locales)) {
3849
- return indexCache.entries;
4353
+ const hit = indexCache.get(loaded);
4354
+ if (hit && sameRefs(hit.scenes, loaded.scenes) && sameRefs(hit.locales, loaded.locales)) {
4355
+ return hit.entries;
3850
4356
  }
3851
4357
  const entries = buildIndex(loaded);
3852
- indexCache = { loaded, scenes: [...loaded.scenes], locales: [...loaded.locales], entries };
4358
+ indexCache.set(loaded, { scenes: [...loaded.scenes], locales: [...loaded.locales], entries });
3853
4359
  return entries;
3854
4360
  }
3855
4361
  function buildIndex(loaded) {
@@ -3861,7 +4367,7 @@ function buildIndex(loaded) {
3861
4367
  id: scene.id,
3862
4368
  kind: "scene",
3863
4369
  name: scene.name,
3864
- gameId: effectiveGameId(scene),
4370
+ gameId: effectiveGameId2(scene),
3865
4371
  location: [scene.name],
3866
4372
  sceneId: scene.id,
3867
4373
  file
@@ -3872,12 +4378,12 @@ function buildIndex(loaded) {
3872
4378
  id: block.id,
3873
4379
  kind: "block",
3874
4380
  name: block.name,
3875
- gameId: effectiveGameId(block),
4381
+ gameId: effectiveGameId2(block),
3876
4382
  location: segments,
3877
4383
  sceneId: scene.id,
3878
4384
  file
3879
4385
  });
3880
- walkNodes4(block.children, (node) => {
4386
+ walkNodes3(block.children, (node) => {
3881
4387
  out.push({
3882
4388
  id: node.id,
3883
4389
  kind: node.type,
@@ -3914,21 +4420,23 @@ function buildIndex(loaded) {
3914
4420
  }
3915
4421
 
3916
4422
  // src/replace.ts
3917
- import { canonicalStringify as canonicalStringify7 } from "@patterkit/core";
3918
- import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
4423
+ import { canonicalStringify as canonicalStringify8 } from "@patterkit/core";
4424
+ import { walkNodes as walkNodes4, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE3, AUTHORING_SCHEMA as AUTHORING_SCHEMA2 } from "@patterkit/model";
3919
4425
  import { findMatcher } from "@wildwinter/toolkit";
3920
4426
  function runReplace(loaded, opts) {
3921
- const plan = { hits: [], writes: [], shards: [], scenes: 0 };
4427
+ const plan = { hits: [], writes: [], shards: [], authoring: [], scenes: 0 };
3922
4428
  const re = findMatcher(opts);
3923
4429
  if (!re) return plan;
3924
4430
  const def = loaded.project.locales.default;
4431
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4432
+ const stamped = /* @__PURE__ */ new Map();
3925
4433
  const locOf = /* @__PURE__ */ new Map();
3926
4434
  const sceneName = /* @__PURE__ */ new Map();
3927
4435
  for (const scene of loaded.scenes) {
3928
4436
  sceneName.set(scene.id, scene.name);
3929
4437
  for (const block of scene.blocks) {
3930
4438
  const segs = [scene.name, block.name];
3931
- walkNodes5(block.children, (node) => {
4439
+ walkNodes4(block.children, (node) => {
3932
4440
  if (node.type === "group") {
3933
4441
  if (node.prompt) locOf.set(node.prompt.id, segs);
3934
4442
  return;
@@ -3939,10 +4447,10 @@ function runReplace(loaded, opts) {
3939
4447
  }
3940
4448
  for (let i = 0; i < loaded.locales.length; i++) {
3941
4449
  const shard = loaded.locales[i];
3942
- if (shard.locale !== def || shard.scene === PROJECT_LOCALE_SCENE2) continue;
4450
+ if (shard.locale !== def || shard.scene === PROJECT_LOCALE_SCENE3) continue;
3943
4451
  const path = loaded.localeFiles[i];
3944
4452
  if (!path) continue;
3945
- let changed = false;
4453
+ const changedIds = [];
3946
4454
  const merged = { ...shard.strings };
3947
4455
  for (const [id, before] of Object.entries(shard.strings)) {
3948
4456
  if (opts.onlyId && id !== opts.onlyId) continue;
@@ -3952,26 +4460,40 @@ function runReplace(loaded, opts) {
3952
4460
  if (after === before) continue;
3953
4461
  plan.hits.push({ id, sceneId: shard.scene, location: locOf.get(id) ?? [sceneName.get(shard.scene) ?? shard.scene], before, after });
3954
4462
  merged[id] = after;
3955
- changed = true;
4463
+ changedIds.push(id);
3956
4464
  }
3957
- if (changed) {
4465
+ if (changedIds.length) {
3958
4466
  const next = { ...shard, strings: merged };
3959
- plan.writes.push({ path, content: canonicalStringify7(next) });
4467
+ plan.writes.push({ path, content: canonicalStringify8(next) });
3960
4468
  plan.shards.push(next);
3961
4469
  plan.scenes++;
4470
+ const aPath = authoringPath(loaded, shard.scene);
4471
+ let af = stamped.get(aPath);
4472
+ if (!af) {
4473
+ const ai = loaded.authoringFiles.indexOf(aPath);
4474
+ af = ai >= 0 ? structuredClone(loaded.authoring[ai]) : { schema: AUTHORING_SCHEMA2 };
4475
+ stamped.set(aPath, af);
4476
+ }
4477
+ const edits = { ...af.edits };
4478
+ for (const id of changedIds) edits[id] = { ...edits[id], modifiedAt: now };
4479
+ af.edits = edits;
3962
4480
  }
3963
4481
  }
4482
+ for (const [path, file] of stamped) {
4483
+ plan.authoring.push({ path, file });
4484
+ plan.writes.push({ path, content: canonicalStringify8(file) });
4485
+ }
3964
4486
  return plan;
3965
4487
  }
3966
4488
 
3967
4489
  // src/pin.ts
3968
- import { readFileSync as readFileSync5 } from "fs";
3969
- import { canonicalStringify as canonicalStringify8, parseSource as parseSource4 } from "@patterkit/core";
3970
- import { effectiveGameId as effectiveGameId2, gameIdify } from "@patterkit/model";
4490
+ import { readFileSync as readFileSync8 } from "fs";
4491
+ import { canonicalStringify as canonicalStringify9, parseSource as parseSource5 } from "@patterkit/core";
4492
+ import { effectiveGameId as effectiveGameId3, gameIdify } from "@patterkit/model";
3971
4493
  function toPin(node) {
3972
4494
  if (node.gameId?.trim()) return void 0;
3973
4495
  if (!gameIdify(node.name)) return void 0;
3974
- return effectiveGameId2(node);
4496
+ return effectiveGameId3(node);
3975
4497
  }
3976
4498
  function planPins(loaded) {
3977
4499
  const plan = { pinned: [], writes: [], scenes: [] };
@@ -3993,146 +4515,18 @@ function planPins(loaded) {
3993
4515
  });
3994
4516
  if (!changed) continue;
3995
4517
  const next = { ...scene, ...sceneGameId !== void 0 ? { gameId: sceneGameId } : {}, blocks };
3996
- const file = parseSource4(readFileSync5(path, "utf8"));
3997
- plan.writes.push({ path, content: canonicalStringify8({ ...file, scene: next }) });
4518
+ const file = parseSource5(readFileSync8(path, "utf8"));
4519
+ plan.writes.push({ path, content: canonicalStringify9({ ...file, scene: next }) });
3998
4520
  plan.scenes.push(next);
3999
4521
  }
4000
4522
  return plan;
4001
4523
  }
4002
4524
 
4003
- // src/pack.ts
4004
- import JSZip from "jszip";
4005
- import { readFileSync as readFileSync7 } from "fs";
4006
- import { dirname as dirname5, join as join8, relative as relative3, sep as sep2 } from "path";
4007
- import { parseSource as parseSource5 } from "@patterkit/core";
4008
- import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
4009
-
4010
- // src/handoff.ts
4011
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
4012
- import { basename as basename7, join as join7 } from "path";
4013
- import { HANDOFF_SCHEMA } from "@patterkit/model";
4014
- var HANDOFF_DIR = "handoffs";
4015
- var HANDOFF_EXT = ".json";
4016
- function handoffPath(root, id) {
4017
- return join7(root, HANDOFF_DIR, `${id}${HANDOFF_EXT}`);
4018
- }
4019
- function serialiseHandoff(handoff) {
4020
- return JSON.stringify(handoff, null, 2) + "\n";
4021
- }
4022
- function handoffWrite(root, handoff) {
4023
- return { path: handoffPath(root, handoff.id), content: serialiseHandoff(handoff) };
4024
- }
4025
- function parseHandoff(text) {
4026
- const h = JSON.parse(text);
4027
- if (h.schema !== HANDOFF_SCHEMA) throw new Error(`not a handoff record (schema ${JSON.stringify(h.schema)})`);
4028
- if (typeof h.id !== "string" || !h.lines || !Array.isArray(h.skeleton)) throw new Error("handoff record is missing its id, lines, or skeleton");
4029
- return h;
4030
- }
4031
- function readHandoffs(root) {
4032
- const handoffs = [];
4033
- const issues = [];
4034
- for (const file of walkFiles(join7(root, HANDOFF_DIR), HANDOFF_EXT)) {
4035
- try {
4036
- handoffs.push(parseHandoff(readFileSync6(file, "utf8")));
4037
- } catch (e) {
4038
- issues.push({ file, message: `${basename7(file)}: ${e instanceof Error ? e.message : String(e)}` });
4039
- }
4040
- }
4041
- handoffs.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
4042
- return { handoffs, issues };
4043
- }
4044
- function readHandoff(root, id) {
4045
- const path = handoffPath(root, id);
4046
- if (!existsSync4(path)) return void 0;
4047
- return parseHandoff(readFileSync6(path, "utf8"));
4048
- }
4049
- var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4050
- function checkChar(body) {
4051
- let sum = 0;
4052
- for (let i = 0; i < body.length; i++) sum += (i + 1) * ALPHABET.indexOf(body[i]);
4053
- return ALPHABET[sum % 31];
4054
- }
4055
- function randomBody(length, random) {
4056
- let s = "";
4057
- for (let i = 0; i < length; i++) s += ALPHABET[Math.floor(random() * ALPHABET.length)];
4058
- return s;
4059
- }
4060
- function markerLength(count) {
4061
- let length = 3;
4062
- while (ALPHABET.length ** length < Math.max(1, count) * 1e3) length++;
4063
- return length;
4064
- }
4065
- function issueMarkerCodes(count, random = Math.random) {
4066
- const length = markerLength(count);
4067
- const codes = /* @__PURE__ */ new Set();
4068
- while (codes.size < count) {
4069
- const body = randomBody(length, random);
4070
- codes.add(body + checkChar(body));
4071
- }
4072
- return [...codes];
4073
- }
4074
- function formatMarker(code) {
4075
- return `[#${code}]`;
4076
- }
4077
- function newHandoffId(taken = [], random = Math.random) {
4078
- const used = new Set(taken);
4079
- for (; ; ) {
4080
- const id = `H-${randomBody(4, random)}`;
4081
- if (!used.has(id)) return id;
4082
- }
4083
- }
4084
- function readMarker(text) {
4085
- const m = /\[\s*#?\s*([0-9A-Za-z]{2,9})\s*\]/.exec(text);
4086
- if (!m) return void 0;
4087
- const code = m[1].toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1");
4088
- if ([...code].some((c) => !ALPHABET.includes(c))) return { code, valid: false };
4089
- const body = code.slice(0, -1);
4090
- return { code, valid: body.length >= 2 && checkChar(body) === code.slice(-1) };
4091
- }
4092
-
4093
- // src/pack.ts
4094
- var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
4095
- var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
4096
- async function runPack(startPath) {
4097
- const projectFile = findProjectFile(startPath);
4098
- const root = dirname5(projectFile);
4099
- const project = parseSource5(readFileSync7(projectFile, "utf8"));
4100
- const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
4101
- if (unresolved.length) {
4102
- throw new Error(
4103
- `${unresolved.length} unresolved merge conflict(s) - resolve them before packing, or the recipient gets your side of a disagreement with no sign of it:
4104
- ` + unresolved.map((i) => ` ${i.file}`).join("\n")
4105
- );
4106
- }
4107
- const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative3(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
4108
- const scopes = gameScopesSnapshot(root, project);
4109
- const handoffs = walkFiles(join8(root, HANDOFF_DIR), ".json").map((abs) => ({ rel: relative3(root, abs).split(sep2).join("/"), text: readFileSync7(abs, "utf8") })).filter((f) => {
4110
- try {
4111
- return !parseHandoff(f.text).closed;
4112
- } catch {
4113
- return false;
4114
- }
4115
- });
4116
- const manifest = {
4117
- schema: "patter/document@0",
4118
- project: { id: project.project.id, name: project.project.name },
4119
- files: files.map((f) => f.rel),
4120
- ...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {},
4121
- ...handoffs.length ? { handoffs: handoffs.map((f) => f.rel) } : {}
4122
- };
4123
- const zip = new JSZip();
4124
- zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
4125
- for (const f of files) zip.file(f.rel, readFileSync7(f.abs, "utf8"), ENTRY_OPTS);
4126
- for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
4127
- for (const f of handoffs) zip.file(f.rel, f.text, ENTRY_OPTS);
4128
- return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
4129
- }
4130
-
4131
4525
  // src/unpack.ts
4132
4526
  import JSZip2 from "jszip";
4133
- import { join as join9, resolve as resolve5, sep as sep3 } from "path";
4134
- import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
4135
- import { parseSource as parseSource6, canonicalStringify as canonicalStringify9 } from "@patterkit/core";
4527
+ import { join as join9, resolve as resolve6, sep as sep4 } from "path";
4528
+ import { existsSync as existsSync6, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "fs";
4529
+ import { parseSource as parseSource6, canonicalStringify as canonicalStringify10 } from "@patterkit/core";
4136
4530
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
4137
4531
  import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
4138
4532
  import { isUnsafeEntry as isUnsafeEntry2 } from "@wildwinter/toolkit/archive";
@@ -4147,11 +4541,26 @@ var isHandoffEntry = (name) => {
4147
4541
  };
4148
4542
  var UnsafeEntryError = class extends Error {
4149
4543
  };
4150
- async function readDoc(bytes) {
4151
- const zip = await JSZip2.loadAsync(bytes);
4544
+ var NotAPackError = class extends Error {
4545
+ };
4546
+ var isShardEntry = (name) => SHARD_EXTENSIONS.some((ext) => name.endsWith(ext));
4547
+ function refuseUnsafe(name) {
4548
+ if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4549
+ if (name.split("/").some((segment) => segment.startsWith("."))) {
4550
+ throw new UnsafeEntryError(`document entry names a dot-file or dot-folder: ${name}`);
4551
+ }
4552
+ }
4553
+ async function readDoc(bytes, which = "pack") {
4554
+ let zip;
4555
+ try {
4556
+ zip = await JSZip2.loadAsync(bytes);
4557
+ } catch {
4558
+ throw new NotAPackError(`the ${which === "base" ? "base file" : "file"} is not a .patterpack (it is not a zip archive)`);
4559
+ }
4152
4560
  const shards = /* @__PURE__ */ new Map();
4153
4561
  const scopes = /* @__PURE__ */ new Map();
4154
4562
  const handoffs = /* @__PURE__ */ new Map();
4563
+ const other = [];
4155
4564
  let manifest;
4156
4565
  for (const [name, entry] of Object.entries(zip.files)) {
4157
4566
  if (entry.dir) continue;
@@ -4162,15 +4571,49 @@ async function readDoc(bytes) {
4162
4571
  }
4163
4572
  continue;
4164
4573
  }
4165
- if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4166
- (isScopesEntry(name) ? scopes : isHandoffEntry(name) ? handoffs : shards).set(name, await entry.async("string"));
4574
+ refuseUnsafe(name);
4575
+ if (isScopesEntry(name)) scopes.set(name, await entry.async("string"));
4576
+ else if (isHandoffEntry(name)) handoffs.set(name, await entry.async("string"));
4577
+ else if (isShardEntry(name)) shards.set(name, await entry.async("string"));
4578
+ else other.push(name);
4167
4579
  }
4168
- return { shards, scopes, handoffs, ...manifest ? { manifest } : {} };
4580
+ return { shards, scopes, handoffs, other: other.sort(), ...manifest ? { manifest } : {} };
4169
4581
  }
4170
4582
  async function runUnpack(bytes, targetDir) {
4171
4583
  const doc = await readDoc(bytes);
4584
+ if (projectEntry(doc.shards) === void 0) throw new NotAPackError("the file is not a .patterpack (it holds no project file)");
4585
+ const here = projectIdIn(targetDir);
4586
+ const packed = projectIdOf(doc.shards);
4587
+ if (here !== void 0 && packed !== void 0 && here !== packed) {
4588
+ throw new Error(`${targetDir} already holds a different project (${here}); unpack into an empty folder, or use --merge to fold a returned pack into this one`);
4589
+ }
4172
4590
  const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
4173
- return { shards: plan(new Map([...doc.shards, ...doc.handoffs])), scopes: plan(doc.scopes) };
4591
+ return { shards: plan(new Map([...doc.shards, ...doc.handoffs])), scopes: plan(doc.scopes), other: doc.other };
4592
+ }
4593
+ function projectIdIn(dir) {
4594
+ let names;
4595
+ try {
4596
+ names = readdirSync5(dir).filter((n) => n.endsWith(".patterproj"));
4597
+ } catch {
4598
+ return void 0;
4599
+ }
4600
+ for (const n of names) {
4601
+ try {
4602
+ const id = parseSource6(readFileSync9(join9(dir, n), "utf8")).project?.id;
4603
+ if (id) return id;
4604
+ } catch {
4605
+ }
4606
+ }
4607
+ return void 0;
4608
+ }
4609
+ function projectIdOf(shards) {
4610
+ const rel = projectEntry(shards);
4611
+ if (rel === void 0) return void 0;
4612
+ try {
4613
+ return parseSource6(shards.get(rel)).project?.id;
4614
+ } catch {
4615
+ return void 0;
4616
+ }
4174
4617
  }
4175
4618
  function checkProvenance(returned, base, target) {
4176
4619
  const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
@@ -4183,7 +4626,7 @@ function checkProvenance(returned, base, target) {
4183
4626
  }
4184
4627
  function targetProjectId(projectDir) {
4185
4628
  try {
4186
- const pf = parseSource6(readFileSync8(findProjectFile(projectDir), "utf8"));
4629
+ const pf = parseSource6(readFileSync9(findProjectFile(projectDir), "utf8"));
4187
4630
  return pf.project?.id;
4188
4631
  } catch {
4189
4632
  return void 0;
@@ -4191,7 +4634,7 @@ function targetProjectId(projectDir) {
4191
4634
  }
4192
4635
  async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4193
4636
  const returnedDoc = await readDoc(returnedBytes);
4194
- const baseDoc = await readDoc(baseBytes);
4637
+ const baseDoc = await readDoc(baseBytes, "base");
4195
4638
  const theirs = returnedDoc.shards;
4196
4639
  const base = baseDoc.shards;
4197
4640
  const provenance = checkProvenance(returnedDoc.manifest?.project?.id, baseDoc.manifest?.project?.id, targetProjectId(projectDir));
@@ -4203,9 +4646,21 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4203
4646
  let projectSides;
4204
4647
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
4205
4648
  const outPath = containedWrite(projectDir, rel);
4206
- if (!existsSync5(outPath)) {
4649
+ const baseText = base.get(rel);
4650
+ if (!existsSync6(outPath)) {
4651
+ if (baseText === theirText) continue;
4207
4652
  writes.push({ path: outPath, content: theirText });
4208
- shards.push({ path: rel, added: true });
4653
+ shards.push({ path: rel, added: true, changed: true });
4654
+ continue;
4655
+ }
4656
+ const oursText = readFileSync9(outPath, "utf8");
4657
+ if (theirText === baseText || theirText === oursText) {
4658
+ if (rel === projectRel && baseText !== void 0) {
4659
+ const read = (t) => parseSource6(t);
4660
+ const ours = read(oursText);
4661
+ projectSides = { ours, base: read(baseText), theirs: read(theirText), merged: ours };
4662
+ }
4663
+ shards.push({ path: rel, added: false, changed: false });
4209
4664
  continue;
4210
4665
  }
4211
4666
  const readSide = (side, text) => {
@@ -4215,12 +4670,13 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4215
4670
  throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
4216
4671
  }
4217
4672
  };
4218
- const oursObj = readSide("ours", readFileSync8(outPath, "utf8"));
4673
+ const oursObj = readSide("ours", oursText);
4219
4674
  const theirsObj = readSide("theirs", theirText);
4220
- const baseText = base.get(rel);
4221
4675
  const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
4222
4676
  const result = runMerge(baseObj, oursObj, theirsObj);
4223
- writes.push({ path: outPath, content: canonicalStringify9(result.merged) });
4677
+ const mergedText = canonicalStringify10(result.merged);
4678
+ const changed = mergedText !== oursText;
4679
+ if (changed) writes.push({ path: outPath, content: mergedText });
4224
4680
  if (rel === projectRel && baseText !== void 0) {
4225
4681
  projectSides = { ours: oursObj, base: baseObj, theirs: theirsObj, merged: result.merged };
4226
4682
  }
@@ -4229,10 +4685,10 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4229
4685
  conflicts += result.conflicts.length;
4230
4686
  }
4231
4687
  warnings += result.warnings.length;
4232
- shards.push({ path: rel, result, added: false });
4688
+ shards.push({ path: rel, result, added: false, changed });
4233
4689
  }
4234
4690
  let gameScopes;
4235
- const local2 = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
4691
+ const local2 = projectSides && discoverGameScopes(resolve6(projectDir), projectSides.ours).gameScopes;
4236
4692
  if (projectSides && local2) {
4237
4693
  const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local2);
4238
4694
  if (world.write) {
@@ -4243,18 +4699,18 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4243
4699
  const handoffs = [];
4244
4700
  for (const [rel, theirText] of [...returnedDoc.handoffs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
4245
4701
  const outPath = containedWrite(projectDir, rel);
4246
- if (!existsSync5(outPath)) {
4702
+ if (!existsSync6(outPath)) {
4247
4703
  writes.push({ path: outPath, content: theirText });
4248
4704
  handoffs.push(rel);
4249
4705
  continue;
4250
4706
  }
4251
- const merged = mergeHandoff(parseHandoff(readFileSync8(outPath, "utf8")), parseHandoff(theirText));
4707
+ const merged = mergeHandoff(parseHandoff(readFileSync9(outPath, "utf8")), parseHandoff(theirText));
4252
4708
  if (merged) {
4253
4709
  writes.push({ path: outPath, content: serialiseHandoff(merged) });
4254
4710
  handoffs.push(rel);
4255
4711
  }
4256
4712
  }
4257
- return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {}, ...handoffs.length ? { handoffs } : {} };
4713
+ return { shards, writes, sidecars, other: returnedDoc.other, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {}, ...handoffs.length ? { handoffs } : {} };
4258
4714
  }
4259
4715
  function mergeHandoff(ours, theirs) {
4260
4716
  const key = (i) => `${i.at}|${i.fileHash}`;
@@ -4270,18 +4726,18 @@ function projectEntry(shards) {
4270
4726
  return at.length === 1 ? at[0] : void 0;
4271
4727
  }
4272
4728
  function containedWrite(dir, name) {
4273
- const root = resolve5(dir);
4274
- const full = resolve5(join9(dir, name));
4275
- if (full !== root && !full.startsWith(root + sep3)) {
4729
+ const root = resolve6(dir);
4730
+ const full = resolve6(join9(dir, name));
4731
+ if (full !== root && !full.startsWith(root + sep4)) {
4276
4732
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4277
4733
  }
4278
4734
  return full;
4279
4735
  }
4280
4736
 
4281
4737
  // src/suggestions.ts
4282
- import { readFileSync as readFileSync9 } from "fs";
4283
- import { canonicalStringify as canonicalStringify10, parseSource as parseSource7 } from "@patterkit/core";
4284
- import { walkNodes as walkNodes6 } from "@patterkit/model";
4738
+ import { readFileSync as readFileSync10 } from "fs";
4739
+ import { canonicalStringify as canonicalStringify11, parseSource as parseSource7 } from "@patterkit/core";
4740
+ import { walkNodes as walkNodes5, AUTHORING_SCHEMA as AUTHORING_SCHEMA3 } from "@patterkit/model";
4285
4741
  function staleParts(s, liveText, line) {
4286
4742
  const stale = [];
4287
4743
  if (liveText !== s.baseline) stale.push("the text");
@@ -4311,7 +4767,7 @@ function indexPlaces(loaded) {
4311
4767
  places.set(scene.id, { sceneId: scene.id });
4312
4768
  for (const block of scene.blocks) {
4313
4769
  places.set(block.id, { sceneId: scene.id });
4314
- walkNodes6(block.children, (node) => {
4770
+ walkNodes5(block.children, (node) => {
4315
4771
  places.set(node.id, { sceneId: scene.id });
4316
4772
  if (node.type === "group") {
4317
4773
  if (node.prompt) places.set(node.prompt.id, { sceneId: scene.id });
@@ -4342,7 +4798,7 @@ var Working = class {
4342
4798
  if (!af) {
4343
4799
  const i = this.loaded.authoringFiles.indexOf(path);
4344
4800
  const existing = i >= 0 ? this.loaded.authoring[i] : void 0;
4345
- af = existing ? structuredClone(existing) : { schema: "patter/authoring@0" };
4801
+ af = existing ? structuredClone(existing) : { schema: AUTHORING_SCHEMA3 };
4346
4802
  this.authoring.set(path, af);
4347
4803
  }
4348
4804
  return af;
@@ -4365,7 +4821,7 @@ var Working = class {
4365
4821
  if (!path) return void 0;
4366
4822
  let file = this.flows.get(path);
4367
4823
  if (!file) {
4368
- const onDisk = parseSource7(readFileSync9(path, "utf8"));
4824
+ const onDisk = parseSource7(readFileSync10(path, "utf8"));
4369
4825
  const scene = this.loaded.scenes.find((s) => s.id === sceneId);
4370
4826
  file = { ...onDisk, scene: structuredClone(scene ?? onDisk.scene) };
4371
4827
  this.flows.set(path, file);
@@ -4373,13 +4829,13 @@ var Working = class {
4373
4829
  return { path, file };
4374
4830
  }
4375
4831
  writes() {
4376
- return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify10(file) })).sort((a, b) => a.path.localeCompare(b.path));
4832
+ return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify11(file) })).sort((a, b) => a.path.localeCompare(b.path));
4377
4833
  }
4378
4834
  };
4379
4835
  function beatIn(scene, id) {
4380
4836
  let found;
4381
4837
  for (const block of scene.blocks) {
4382
- walkNodes6(block.children, (node) => {
4838
+ walkNodes5(block.children, (node) => {
4383
4839
  if (found || node.type !== "snippet") return;
4384
4840
  found = (node.beats ?? []).find((b) => b.id === id);
4385
4841
  });
@@ -4865,22 +5321,22 @@ function plain(t) {
4865
5321
  }
4866
5322
  function readRow(tr, indent) {
4867
5323
  const cells = kids(tr).filter((c) => local(c) === "tc");
4868
- const cellText = (tc, joiner = " ") => readParagraphs(tc ? paragraphsIn(tc) : [], joiner);
4869
- const last = cellText(cells[cells.length - 1]);
5324
+ const cellText2 = (tc, joiner = " ") => readParagraphs(tc ? paragraphsIn(tc) : [], joiner);
5325
+ const last = cellText2(cells[cells.length - 1]);
4870
5326
  const found = readMarker(last.text.proposed);
4871
5327
  const recovered = found ? void 0 : readMarker(last.text.original);
4872
5328
  const marker = found ?? (recovered ? { ...recovered, recovered: true } : void 0);
4873
5329
  if (cells.length < 2 || !marker) {
4874
5330
  return { kind: "table", text: readParagraphs(cells.flatMap(paragraphsIn), " ").text };
4875
5331
  }
4876
- const words = cellText(cells.length >= 3 ? cells[1] : cells[0]);
5332
+ const words = cellText2(cells.length >= 3 ? cells[1] : cells[0]);
4877
5333
  const trPr = child(tr, "trPr");
4878
5334
  return {
4879
5335
  kind: "box",
4880
5336
  marker,
4881
5337
  text: words.text,
4882
5338
  paragraphs: words.paragraphs,
4883
- lead: plain(cells.length >= 3 ? cellText(cells[0], "\n").text : readParagraphs([], " ").text),
5339
+ lead: plain(cells.length >= 3 ? cellText2(cells[0], "\n").text : readParagraphs([], " ").text),
4884
5340
  margin: plain(last.text),
4885
5341
  rowDeleted: !!(trPr && child(trPr, "del")),
4886
5342
  ...indent !== void 0 ? { indent } : {}
@@ -4973,7 +5429,8 @@ async function readEditableDocx(bytes) {
4973
5429
  }
4974
5430
 
4975
5431
  // src/editable-import.ts
4976
- import { canonicalStringify as canonicalStringify11 } from "@patterkit/core";
5432
+ import { canonicalStringify as canonicalStringify12 } from "@patterkit/core";
5433
+ import { AUTHORING_SCHEMA as AUTHORING_SCHEMA4 } from "@patterkit/model";
4977
5434
  var REFUSE_MISSING_SHARE = 0.25;
4978
5435
  var tidy = (s) => s.replace(/[\s   ]+/g, " ").trim();
4979
5436
  function normalise(s, strictQuotes) {
@@ -5214,7 +5671,7 @@ function planEditableImport(loaded, returned, opts) {
5214
5671
  let af = shards.get(path);
5215
5672
  if (!af) {
5216
5673
  const i = loaded.authoringFiles.indexOf(path);
5217
- af = i >= 0 ? structuredClone(loaded.authoring[i]) : { schema: "patter/authoring@0" };
5674
+ af = i >= 0 ? structuredClone(loaded.authoring[i]) : { schema: AUTHORING_SCHEMA4 };
5218
5675
  shards.set(path, af);
5219
5676
  }
5220
5677
  return af;
@@ -5235,7 +5692,7 @@ function planEditableImport(loaded, returned, opts) {
5235
5692
  const af = shardFor(c.anchor);
5236
5693
  af.comments = [...af.comments ?? [], c];
5237
5694
  }
5238
- let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify11(af) }));
5695
+ let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify12(af) }));
5239
5696
  if (opts.direct && suggestions.length) {
5240
5697
  const warned = new Set(problems.filter((p) => p.severity === "warning" && p.anchor).map((p) => p.anchor));
5241
5698
  const clean = suggestions.filter((s) => (live[s.anchor] ?? "") === s.baseline && !warned.has(s.anchor));
@@ -5366,12 +5823,15 @@ function alignParagraphs(items, skeleton, codeOfBox, onEdited, onAdded, lineIdOf
5366
5823
  return anchors;
5367
5824
  }
5368
5825
  export {
5826
+ AUDIO_MANIFEST_FILE,
5827
+ AUDIO_MANIFEST_SCHEMA,
5369
5828
  AUTHORING_HANDLED,
5370
5829
  CONFLICT_SIDECAR,
5371
5830
  GAME_SCOPES_DIR,
5372
5831
  GAME_SCOPES_FILE,
5373
5832
  GAME_SCOPES_OWNER,
5374
5833
  HANDOFF_DIR,
5834
+ NotAPackError,
5375
5835
  PATTER_SCOPES_FILE,
5376
5836
  PATTER_SCOPES_OWNER,
5377
5837
  RARE_REACH_PCT,
@@ -5383,15 +5843,24 @@ export {
5383
5843
  applySceneOrder,
5384
5844
  applySuggestionDecisions,
5385
5845
  applyWrites,
5846
+ audioFolderRungs,
5847
+ audioFoldersActive,
5848
+ audioManifest,
5849
+ audioManifestWrite,
5850
+ besideBundlePath,
5386
5851
  bundleOutputPath,
5387
5852
  catalogToJson,
5388
5853
  catalogToPo,
5389
5854
  catalogToXlsx,
5390
5855
  classesForChannel,
5856
+ compileLoaded,
5857
+ currentBundlePosture,
5858
+ defaultBundlePath,
5391
5859
  defaultGameScopesDir,
5392
5860
  detectMergeType,
5393
5861
  discoverGameScopes,
5394
5862
  drivableScopes,
5863
+ exportBlockers,
5395
5864
  exportEditableScript,
5396
5865
  extractLoc,
5397
5866
  findProjectFile,
@@ -5416,6 +5885,8 @@ export {
5416
5885
  patterScopesFile,
5417
5886
  patterScopesStale,
5418
5887
  patterScopesWrite,
5888
+ pickAudio,
5889
+ planBuild,
5419
5890
  planEditableImport,
5420
5891
  planPins,
5421
5892
  planReturnedWorld,
@@ -5459,6 +5930,7 @@ export {
5459
5930
  runUnpackMerge,
5460
5931
  runValidate,
5461
5932
  runVoiceScript,
5933
+ scanAudio,
5462
5934
  scanAudioStatus,
5463
5935
  sceneIdForShard,
5464
5936
  scriptToDocx,