@patterkit/ops 0.24.1 → 0.25.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 +186 -10
  2. package/dist/index.js +1326 -911
  3. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -972,7 +972,7 @@ var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
972
972
 
973
973
  // src/validate.ts
974
974
  import { validateProject } from "@patterkit/core";
975
- import { validateConditions, validateInterpolation, hostScopesToSpec, projectScopes, extensionPass } from "@patterkit/compiler";
975
+ import { validateConditions, validateInterpolation, hostScopesToSpec as hostScopesToSpec2, projectScopes as projectScopes2, extensionPass } from "@patterkit/compiler";
976
976
 
977
977
  // src/compile.ts
978
978
  import { basename as basename2, extname, isAbsolute, resolve as resolve3 } from "path";
@@ -1193,14 +1193,14 @@ function reachabilityIssues(loaded, compiled) {
1193
1193
  import { basename as basename3, join as join3, sep } from "path";
1194
1194
  import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
1195
1195
  import {
1196
- walkNodes,
1196
+ walkNodes as walkNodes2,
1197
1197
  textIsAction,
1198
1198
  castStringKey,
1199
1199
  qualifierStringKey,
1200
1200
  speakerLabelKey,
1201
1201
  usedQualifiers,
1202
1202
  usedSpeakerPairs,
1203
- projectQualifiers,
1203
+ projectQualifiers as projectQualifiers2,
1204
1204
  SPEAKER_PATTERN_KEY,
1205
1205
  DEFAULT_SPEAKER_PATTERN,
1206
1206
  PROJECT_LOCALE_SCENE,
@@ -1315,379 +1315,877 @@ function resolveDocumentation(loaded, channel) {
1315
1315
  return out;
1316
1316
  }
1317
1317
 
1318
- // src/localisation.ts
1319
- var SETTING_NAME = `"Localise text lines in dialogue scenes" (Project Settings, Language; \`localiseDialogueText\` in the project file)`;
1320
- function describeLocExport(loaded, opts = {}) {
1321
- const source = tableFor(loaded, loaded.project.locales.default);
1322
- let lines = 0, scenes = 0;
1323
- for (const ids of actionTextIds(loaded).values()) {
1324
- const n = [...ids].filter((id) => source[id]).length;
1325
- if (n) {
1326
- lines += n;
1327
- scenes++;
1328
- }
1329
- }
1330
- const out = [];
1331
- if (lines) {
1332
- out.push(`Left out ${lines} text line${lines === 1 ? "" : "s"} in ${scenes} dialogue scene${scenes === 1 ? "" : "s"}: ${lines === 1 ? "it's" : "they're"} action, a direction for whoever stages the scene, because ${SETTING_NAME} is off. Turn it on to translate ${lines === 1 ? "it" : "them"}.`);
1333
- }
1334
- return [...out, ...describeActionTextChanges(loaded, actionTextChanges(loaded, opts))];
1318
+ // src/script-doc.ts
1319
+ import { DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES2, projectQualifiers } from "@patterkit/model";
1320
+ import { humanizeNodeRefs } from "@patterkit/core";
1321
+ import { splitRef } from "@patterkit/dialect";
1322
+ import { parse as parse2 } from "@wildwinter/expr";
1323
+
1324
+ // src/stages.ts
1325
+ import { parse, renameStageInExpression, stageGateCount as gatesIn } from "@wildwinter/expr";
1326
+ import { dialectWithForeignScopes } from "@patterkit/dialect";
1327
+ import { hostScopesToSpec, projectScopes } from "@patterkit/compiler";
1328
+ import { walkNodes } from "@patterkit/model";
1329
+ function stageDialect(project, gameScopes) {
1330
+ return dialectWithForeignScopes(hostScopesToSpec(projectScopes(project, gameScopes?.merged).host));
1335
1331
  }
1336
- function describeActionTextChanges(loaded, changes) {
1337
- const nameOf = new Map(loaded.scenes.map((sc) => [sc.id, sc.name]));
1338
- const out = [];
1339
- const groups = /* @__PURE__ */ new Map();
1340
- for (const c of changes) {
1341
- const key = `${c.locale}|${c.change}`;
1342
- (groups.get(key) ?? groups.set(key, []).get(key)).push(c);
1343
- }
1344
- for (const [key, list] of groups) {
1345
- const [locale, change] = key.split("|");
1346
- const n = `${list.length} text line${list.length === 1 ? "" : "s"}`;
1347
- out.push(change === "dropped" ? `${n} now action, so left out of '${locale}': the next loc import drops ${list.length === 1 ? "its translation" : "their translations"} (turn on ${SETTING_NAME} to keep ${list.length === 1 ? "it" : "them"})` : `${n} no longer action, so new to '${locale}' and untranslated`);
1348
- for (const c of list) out.push(` ${c.id} in '${nameOf.get(c.scene) ?? c.scene}'`);
1332
+ var refersTo = (target) => (scope, name) => scope === target.scope && name === target.name;
1333
+ function effectTarget(ref, dialect) {
1334
+ try {
1335
+ const n = parse(ref, dialect);
1336
+ return n.kind === "scopedvar" ? { scope: n.scope, name: n.name } : void 0;
1337
+ } catch {
1338
+ return void 0;
1349
1339
  }
1350
- return out;
1351
1340
  }
1352
- function sceneTextIds(scene) {
1353
- const out = [];
1341
+ function sceneSites(scene) {
1342
+ const sites = [];
1343
+ const effects = (list) => {
1344
+ for (const e of list ?? []) {
1345
+ sites.push({ get: () => e.value, set: (src) => {
1346
+ e.value = src;
1347
+ }, assignedTo: e.target });
1348
+ }
1349
+ };
1350
+ effects(scene.onEntry);
1354
1351
  for (const block of scene.blocks) {
1355
1352
  walkNodes(block.children, (node) => {
1353
+ if (typeof node.condition === "string") {
1354
+ sites.push({ get: () => node.condition, set: (src) => {
1355
+ node.condition = src;
1356
+ } });
1357
+ }
1356
1358
  if (node.type === "snippet") {
1357
- for (const beat of node.beats ?? []) if (beat.kind === "text") out.push(beat.id);
1359
+ effects(node.onEnter);
1360
+ effects(node.onExit);
1358
1361
  }
1359
1362
  });
1360
1363
  }
1361
- return out;
1362
- }
1363
- function actionTextIds(loaded) {
1364
- const out = /* @__PURE__ */ new Map();
1365
- for (const scene of loaded.scenes) if (textIsAction(loaded.project, scene)) out.set(scene.id, new Set(sceneTextIds(scene)));
1366
- return out;
1364
+ return sites;
1367
1365
  }
1368
- function actionTextChanges(loaded, opts = {}) {
1369
- const defaultLocale = loaded.project.locales.default;
1370
- const locales = opts.locale !== void 0 ? opts.locale === defaultLocale ? [] : [opts.locale] : loaded.project.locales.all.filter((l) => l !== defaultLocale);
1371
- const source = tableFor(loaded, defaultLocale);
1372
- const out = [];
1373
- for (const locale of locales) {
1374
- for (const scene of loaded.scenes) {
1375
- const path = existingLocPath(loaded, scene.id, locale);
1376
- const shard = path ? findFile(loaded.localeFiles, loaded.locales, path) : void 0;
1377
- if (!shard) continue;
1378
- const action = textIsAction(loaded.project, scene);
1379
- for (const id of sceneTextIds(scene)) {
1380
- if (action && shard.strings[id]) out.push({ id, scene: scene.id, locale, change: "dropped" });
1381
- else if (!action && shard.textAction && source[id]) out.push({ id, scene: scene.id, locale, change: "added" });
1382
- }
1366
+ var reach = (scenes, target) => target.scope === "scene" ? scenes.filter((s) => s.id === target.scene) : scenes;
1367
+ function renameStage(scenes, target, from, to, dialect) {
1368
+ const result = { scenes: [], expressions: 0 };
1369
+ if (to.trim() === "" || from === to) return result;
1370
+ const isTarget = refersTo(target);
1371
+ for (const scene of reach(scenes, target)) {
1372
+ let changed = false;
1373
+ for (const site of sceneSites(scene)) {
1374
+ const src = site.get();
1375
+ if (src === void 0) continue;
1376
+ const t = site.assignedTo !== void 0 ? effectTarget(site.assignedTo, dialect) : void 0;
1377
+ const next = renameStageInExpression(src, dialect, isTarget, from, to, t !== void 0 && isTarget(t.scope, t.name));
1378
+ if (next === void 0) continue;
1379
+ site.set(next);
1380
+ result.expressions++;
1381
+ changed = true;
1383
1382
  }
1383
+ if (changed) result.scenes.push(scene);
1384
1384
  }
1385
- return out;
1385
+ return result;
1386
1386
  }
1387
- function projectStringSources(project, sourceTable) {
1388
- const out = {};
1389
- const put = (id, authored) => {
1390
- if (authored) out[id] = sourceTable[id] ?? authored;
1391
- };
1392
- for (const c of project.cast ?? []) {
1393
- put(castStringKey(c.name), c.displayName);
1394
- for (const [q, label] of Object.entries(c.qualifierLabels ?? {})) put(speakerLabelKey(c.name, q), label);
1387
+ function stageGateCount(scenes, target, dialect) {
1388
+ const isTarget = refersTo(target);
1389
+ let count = 0;
1390
+ for (const scene of reach(scenes, target)) {
1391
+ for (const site of sceneSites(scene)) {
1392
+ const src = site.get();
1393
+ if (src !== void 0) count += gatesIn(src, dialect, isTarget);
1394
+ }
1395
1395
  }
1396
- for (const q of projectQualifiers(project)) put(qualifierStringKey(q.gameId), q.name);
1397
- put(SPEAKER_PATTERN_KEY, project.speakerPattern ?? DEFAULT_SPEAKER_PATTERN);
1398
- return out;
1396
+ return count;
1399
1397
  }
1400
- function extractLoc(loaded, opts = {}) {
1401
- const defaultLocale = loaded.project.locales.default;
1402
- const isTemplate = opts.locale === void 0 || opts.locale === defaultLocale;
1403
- const targetLocale = isTemplate ? void 0 : opts.locale;
1404
- const source = tableFor(loaded, defaultLocale);
1405
- const target = targetLocale ? tableFor(loaded, targetLocale) : {};
1406
- const edits = mergeAuthoring2(loaded).edits;
1407
- const projectSources = projectStringSources(loaded.project, source);
1408
- const docs = resolveDocumentation(loaded, "loc");
1409
- const commentsOf = (id) => (docs.get(id) ?? []).map((d) => d.text);
1410
- const staleFor = (id, translation) => {
1411
- if (isTemplate || !translation) return false;
1412
- const e = edits.get(id);
1413
- const localised = e?.localisedAt?.[targetLocale];
1414
- return !!(e?.modifiedAt && localised && e.modifiedAt > localised);
1415
- };
1416
- const genderOf = /* @__PURE__ */ new Map();
1417
- for (const c of loaded.project.cast ?? []) if (c.gender) genderOf.set(c.name, c.gender);
1418
- const withGender = (context) => {
1419
- const g = context?.character ? genderOf.get(context.character) : void 0;
1420
- return g ? { ...context, gender: g } : context;
1421
- };
1422
- const qualifierName = new Map(projectQualifiers(loaded.project).map((q) => [q.gameId, q.name]));
1423
- const lineContext = (b) => {
1424
- const q = b.kind === "line" && b.qualifier ? qualifierName.get(b.qualifier) ?? b.qualifier : void 0;
1425
- return { character: b.kind === "line" ? b.character : void 0, kind: b.kind, ...q ? { qualifier: q } : {} };
1426
- };
1427
- const entries = [];
1428
- const push = (id, scene, context) => {
1429
- const src = source[id];
1430
- if (src === void 0) return;
1431
- const translation = isTemplate ? "" : target[id] ?? "";
1432
- entries.push({ id, scene, source: src, translation, comments: commentsOf(id), context: withGender(context), stale: staleFor(id, translation) });
1433
- };
1434
- for (const scene of loaded.scenes) {
1435
- const action = textIsAction(loaded.project, scene);
1398
+ function renameGameDataStage(scenes, kind, field, from, to) {
1399
+ const result = { scenes: [], values: 0 };
1400
+ if (to.trim() === "" || from === to) return result;
1401
+ for (const scene of scenes) {
1402
+ let changed = false;
1403
+ const visit = (holder) => {
1404
+ if (holder.gameData?.[field] !== from) return;
1405
+ holder.gameData[field] = to;
1406
+ result.values++;
1407
+ changed = true;
1408
+ };
1409
+ if (kind === "scene") visit(scene);
1436
1410
  for (const block of scene.blocks) {
1411
+ if (kind === "block") visit(block);
1437
1412
  walkNodes(block.children, (node) => {
1438
- if (node.type === "group") {
1439
- const p = node.prompt;
1440
- if (p) push(p.id, scene.id, lineContext(p));
1413
+ if (node.type !== "snippet") {
1414
+ if (kind === "option" && node.selector === "choice") {
1415
+ for (const option of node.children) if (option.type === "group") visit(option);
1416
+ }
1441
1417
  return;
1442
1418
  }
1443
- for (const beat of node.beats ?? []) {
1444
- if (beat.kind === "gameEvent" || action && beat.kind === "text") continue;
1445
- push(beat.id, scene.id, lineContext(beat));
1446
- }
1419
+ if (kind === "snippet") visit(node);
1420
+ for (const beat of node.beats ?? []) if (beat.kind === kind) visit(beat);
1447
1421
  });
1448
1422
  }
1423
+ if (changed) result.scenes.push(scene);
1449
1424
  }
1450
- for (const c of loaded.project.cast ?? []) {
1451
- if (!c.displayName) continue;
1452
- const id = castStringKey(c.name);
1453
- const src = projectSources[id];
1454
- const translation = isTemplate ? "" : target[id] ?? "";
1455
- entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: withGender({ character: c.name }), stale: staleFor(id, translation) });
1456
- }
1457
- for (const q of usedQualifiers(loaded.project, loaded.scenes)) {
1458
- const id = qualifierStringKey(q.gameId);
1459
- const src = projectSources[id] ?? q.name;
1460
- const translation = isTemplate ? "" : target[id] ?? "";
1461
- entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: { kind: "qualifier" }, stale: staleFor(id, translation) });
1462
- }
1463
- const pairs = usedSpeakerPairs(loaded.scenes);
1464
- if (pairs.length > 0) {
1465
- const id = SPEAKER_PATTERN_KEY;
1466
- const src = projectSources[id];
1467
- const translation = isTemplate ? "" : target[id] ?? "";
1468
- entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: [SPEAKER_PATTERN_NOTE, ...commentsOf(id)], context: { kind: "speaker pattern" }, stale: staleFor(id, translation) });
1469
- const labelsOf = new Map((loaded.project.cast ?? []).map((c) => [c.name, c.qualifierLabels]));
1470
- for (const p of pairs) {
1471
- const label = labelsOf.get(p.character)?.[p.qualifier];
1472
- if (!label) continue;
1473
- const pid = speakerLabelKey(p.character, p.qualifier);
1474
- const psrc = projectSources[pid] ?? label;
1475
- const ptranslation = isTemplate ? "" : target[pid] ?? "";
1476
- const qualifier = qualifierName.get(p.qualifier) ?? p.qualifier;
1477
- entries.push({ id: pid, scene: PROJECT_LOCALE_SCENE, source: psrc, translation: ptranslation, comments: commentsOf(pid), context: withGender({ character: p.character, kind: "speaker", qualifier }), stale: staleFor(pid, ptranslation) });
1478
- }
1479
- }
1480
- return { project: loaded.project.project.id, defaultLocale, locale: targetLocale, entries };
1481
- }
1482
- var SPEAKER_PATTERN_NOTE = "{name} and {qualifier} are filled in with the speaker's name and the qualifier, so keep both as they are. You can put them in any order.";
1483
- var layoutOf = (loaded) => projectLayout2(loaded.project);
1484
- function existingLocPath(loaded, scene, locale) {
1485
- for (let i = 0; i < loaded.locales.length; i++) {
1486
- const l = loaded.locales[i];
1487
- if (l.scene === scene && l.locale === locale) return loaded.localeFiles[i] ?? null;
1488
- }
1489
- return null;
1490
- }
1491
- function targetLocPath(loaded, scene, locale, defaultLocale) {
1492
- const existing = existingLocPath(loaded, scene, locale);
1493
- if (existing) return existing;
1494
- const fromDefault = existingLocPath(loaded, scene, defaultLocale);
1495
- if (fromDefault) {
1496
- const parts = fromDefault.split(sep);
1497
- for (let i = parts.length - 2; i >= 0; i--) {
1498
- if (parts[i] === defaultLocale) {
1499
- parts[i] = locale;
1500
- return parts.join(sep);
1501
- }
1502
- }
1503
- }
1504
- const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
1505
- return join3(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
1506
- }
1507
- function authoringPath(loaded, scene) {
1508
- const dir = join3(loaded.root, layoutOf(loaded).authoring);
1509
- if (scene === PROJECT_LOCALE_SCENE) return join3(dir, "_project.patterx");
1510
- const flow = loaded.sceneFiles[scene];
1511
- const stem = flow ? basename3(flow).replace(/\.patterflow$/, "") : scene;
1512
- return join3(dir, `${stem}.patterx`);
1425
+ return result;
1513
1426
  }
1514
- var findFile = (files, items, path) => {
1515
- const i = files.indexOf(path);
1516
- return i >= 0 ? items[i] : void 0;
1517
- };
1518
- function applyLoc(loaded, catalog, opts = {}) {
1519
- const locale = catalog.locale;
1520
- if (!locale || locale === loaded.project.locales.default) {
1521
- return { writes: [], stats: { updated: 0, files: 0, dropped: 0 } };
1522
- }
1523
- if (!loaded.project.locales.all.includes(locale)) {
1524
- throw new Error(`'${locale}' is not one of the project's languages (${loaded.project.locales.all.join(", ")}); add it to the project first`);
1525
- }
1526
- const known = /* @__PURE__ */ new Set([...loaded.scenes.map((sc) => sc.id), PROJECT_LOCALE_SCENE]);
1527
- const unknown = [...new Set(catalog.entries.filter((e) => e.translation.trim() && !known.has(e.scene)).map((e) => e.scene))];
1528
- if (unknown.length) {
1529
- throw new Error(`the file names scene(s) this project does not have: ${unknown.map((u) => `'${u}'`).join(", ")}; nothing was imported`);
1530
- }
1531
- const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
1532
- const action = actionTextIds(loaded);
1533
- const bySceneText = /* @__PURE__ */ new Map();
1534
- const bySceneFresh = /* @__PURE__ */ new Map();
1535
- for (const e of catalog.entries) {
1536
- if (!e.translation.trim()) continue;
1537
- if (action.get(e.scene)?.has(e.id)) continue;
1538
- (bySceneText.get(e.scene) ?? bySceneText.set(e.scene, /* @__PURE__ */ new Map()).get(e.scene)).set(e.id, e.translation);
1539
- if (!e.stale) (bySceneFresh.get(e.scene) ?? bySceneFresh.set(e.scene, /* @__PURE__ */ new Set()).get(e.scene)).add(e.id);
1540
- }
1541
- const writes = [];
1542
- let updated = 0;
1543
- let dropped = 0;
1544
- let files = 0;
1545
- const scenes = new Set(bySceneText.keys());
1546
- for (const l of loaded.locales) if (l.locale === locale && l.scene !== PROJECT_LOCALE_SCENE && known.has(l.scene)) scenes.add(l.scene);
1547
- for (const scene of scenes) {
1548
- const strings = bySceneText.get(scene) ?? /* @__PURE__ */ new Map();
1549
- const path = targetLocPath(loaded, scene, locale, loaded.project.locales.default);
1550
- const existing = findFile(loaded.localeFiles, loaded.locales, path);
1551
- const merged = { ...existing?.strings ?? {} };
1552
- for (const [id, text] of strings) {
1553
- if (merged[id] !== text) updated++;
1554
- merged[id] = text;
1555
- }
1556
- const actionIds = action.get(scene);
1557
- for (const id of actionIds ?? []) if (id in merged) {
1558
- delete merged[id];
1559
- dropped++;
1560
- }
1561
- const file = {
1562
- schema: existing?.schema ?? STRINGS_SCHEMA,
1563
- scene,
1564
- locale,
1565
- ...actionIds ? { textAction: true } : {},
1566
- strings: merged
1567
- };
1568
- const content = canonicalStringify3(file);
1569
- if (!existing || strings.size > 0 || canonicalStringify3(existing) !== content) {
1570
- writes.push({ path, content });
1571
- files++;
1572
- }
1573
- const fresh = bySceneFresh.get(scene);
1574
- if (!fresh?.size) continue;
1575
- const aPath = authoringPath(loaded, scene);
1576
- const aExisting = findFile(loaded.authoringFiles, loaded.authoring, aPath);
1577
- const authoring = aExisting ? { ...aExisting, edits: { ...aExisting.edits } } : { schema: AUTHORING_SCHEMA };
1578
- const editsMap = { ...authoring.edits ?? {} };
1579
- for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
1580
- authoring.edits = editsMap;
1581
- writes.push({ path: aPath, content: canonicalStringify3(authoring) });
1427
+
1428
+ // src/script-doc.ts
1429
+ var TOKENS = {
1430
+ ink: "26221c",
1431
+ // near-black warm ink (headings)
1432
+ inkRead: "3a352d",
1433
+ // dialogue body (ink ~84% + muted)
1434
+ inkSoft: "5c554b",
1435
+ // text (ink ~68% + muted) - a step softer than dialogue
1436
+ muted: "8c8479",
1437
+ // labels, quiet tags
1438
+ accent: "8a5a34",
1439
+ // structural marks: conditions, options, jumps, game events
1440
+ line: "e4dfd5"
1441
+ // hairlines / rules / the group rail
1442
+ };
1443
+ var CHAR_PALETTE = [
1444
+ "b23b3b",
1445
+ "b5642b",
1446
+ "927524",
1447
+ "6b7a31",
1448
+ "3f7d49",
1449
+ "2c7d6e",
1450
+ "2d7791",
1451
+ "3a6aa8",
1452
+ "5a5ba6",
1453
+ "7e4e9c",
1454
+ "9c4486",
1455
+ "a83e5c"
1456
+ ];
1457
+ function colourIndex(name) {
1458
+ let h = 2166136261;
1459
+ for (let i = 0; i < name.length; i++) {
1460
+ h ^= name.charCodeAt(i);
1461
+ h = Math.imul(h, 16777619);
1582
1462
  }
1583
- return { writes, stats: { updated, files, dropped } };
1463
+ h ^= h >>> 16;
1464
+ h = Math.imul(h, 2246822507);
1465
+ h ^= h >>> 13;
1466
+ h = Math.imul(h, 3266489909);
1467
+ h ^= h >>> 16;
1468
+ return (h >>> 0) % 12;
1584
1469
  }
1585
-
1586
- // src/pack.ts
1587
- import JSZip from "jszip";
1588
- import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
1589
- import { dirname as dirname5, join as join5, relative as relative2, sep as sep2 } from "path";
1590
- import { parseSource as parseSource3 } from "@patterkit/core";
1591
- import { EXTENSION_SHARD as EXTENSION_SHARD2, EXTENSIONS_DIR as EXTENSIONS_DIR2, MANIFEST_EXTENSION as MANIFEST_EXTENSION2 } from "@patterkit/model";
1592
- import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
1593
-
1594
- // src/handoff.ts
1595
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1596
- import { basename as basename4, join as join4 } from "path";
1597
- import { HANDOFF_SCHEMA } from "@patterkit/model";
1598
- var HANDOFF_DIR = "handoffs";
1599
- var HANDOFF_EXT = ".json";
1600
- function handoffPath(root, id) {
1601
- return join4(root, HANDOFF_DIR, `${id}${HANDOFF_EXT}`);
1470
+ function isColourSlot(v) {
1471
+ return typeof v === "number" && Number.isInteger(v) && v >= 0 && v < CHAR_PALETTE.length;
1602
1472
  }
1603
- function serialiseHandoff(handoff) {
1604
- return JSON.stringify(handoff, null, 2) + "\n";
1473
+ function characterColour(name, stored) {
1474
+ return CHAR_PALETTE[isColourSlot(stored) ? stored : colourIndex(name)];
1605
1475
  }
1606
- function handoffWrite(root, handoff) {
1607
- return { path: handoffPath(root, handoff.id), content: serialiseHandoff(handoff) };
1476
+ function castColours(cast) {
1477
+ return new Map((cast ?? []).filter((c) => isColourSlot(c.colour)).map((c) => [c.name, c.colour]));
1608
1478
  }
1609
- function parseHandoff(text) {
1610
- const h = JSON.parse(text);
1611
- if (h.schema !== HANDOFF_SCHEMA) throw new Error(`not a handoff record (schema ${JSON.stringify(h.schema)})`);
1612
- if (typeof h.id !== "string" || !h.lines || !Array.isArray(h.skeleton)) throw new Error("handoff record is missing its id, lines, or skeleton");
1613
- return h;
1479
+ function cueLabel(character, qualifierName) {
1480
+ return qualifierName ? `${character.toUpperCase()} (${qualifierName.toUpperCase()})` : character.toUpperCase();
1614
1481
  }
1615
- function readHandoffs(root) {
1616
- const handoffs = [];
1617
- const issues = [];
1618
- for (const file of walkFiles(join4(root, HANDOFF_DIR), HANDOFF_EXT)) {
1619
- try {
1620
- handoffs.push(parseHandoff(readFileSync3(file, "utf8")));
1621
- } catch (e) {
1622
- issues.push({ file, message: `${basename4(file)}: ${e instanceof Error ? e.message : String(e)}` });
1482
+ function qualifierNamer(project) {
1483
+ const names = new Map(projectQualifiers(project).map((q) => [q.gameId, q.name]));
1484
+ return (gameId) => gameId ? names.get(gameId) ?? gameId : void 0;
1485
+ }
1486
+ var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
1487
+ var INTERP_RE = /\{@[^}]*\}/g;
1488
+ function textRuns(text) {
1489
+ const marked = [];
1490
+ let last = 0;
1491
+ let m;
1492
+ MARKUP_RE.lastIndex = 0;
1493
+ while ((m = MARKUP_RE.exec(text)) !== null) {
1494
+ if (m.index > last) marked.push({ text: text.slice(last, m.index), bold: false, italic: false });
1495
+ const tag = m[1];
1496
+ marked.push({ text: m[2], bold: tag === "b" || tag === "bi", italic: tag === "i" || tag === "bi" });
1497
+ last = MARKUP_RE.lastIndex;
1498
+ }
1499
+ if (last < text.length) marked.push({ text: text.slice(last), bold: false, italic: false });
1500
+ if (!marked.length) marked.push({ text, bold: false, italic: false });
1501
+ const out = [];
1502
+ for (const r of marked) {
1503
+ let li = 0;
1504
+ let im;
1505
+ INTERP_RE.lastIndex = 0;
1506
+ while ((im = INTERP_RE.exec(r.text)) !== null) {
1507
+ if (im.index > li) out.push({ text: r.text.slice(li, im.index), bold: r.bold, italic: r.italic, code: false });
1508
+ out.push({ text: im[0], bold: r.bold, italic: r.italic, code: true });
1509
+ li = INTERP_RE.lastIndex;
1623
1510
  }
1511
+ if (li < r.text.length) out.push({ text: r.text.slice(li), bold: r.bold, italic: r.italic, code: false });
1624
1512
  }
1625
- handoffs.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
1626
- return { handoffs, issues };
1627
- }
1628
- function readHandoff(root, id) {
1629
- const path = handoffPath(root, id);
1630
- if (!existsSync3(path)) return void 0;
1631
- return parseHandoff(readFileSync3(path, "utf8"));
1632
- }
1633
- var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1634
- function checkChar(body) {
1635
- let sum = 0;
1636
- for (let i = 0; i < body.length; i++) sum += (i + 1) * ALPHABET.indexOf(body[i]);
1637
- return ALPHABET[sum % 31];
1638
- }
1639
- function randomBody(length, random) {
1640
- let s = "";
1641
- for (let i = 0; i < length; i++) s += ALPHABET[Math.floor(random() * ALPHABET.length)];
1642
- return s;
1513
+ return out.length ? out : [{ text, bold: false, italic: false, code: false }];
1643
1514
  }
1644
- function markerLength(count) {
1645
- let length = 3;
1646
- while (ALPHABET.length ** length < Math.max(1, count) * 1e3) length++;
1647
- return length;
1515
+ function targetLabel(to, sceneOf, blockTrail) {
1516
+ if (to === "END") return "END";
1517
+ return blockTrail.get(to) ?? sceneOf.get(to) ?? to;
1648
1518
  }
1649
- function issueMarkerCodes(count, random = Math.random) {
1650
- const length = markerLength(count);
1651
- const codes = /* @__PURE__ */ new Set();
1652
- while (codes.size < count) {
1653
- const body = randomBody(length, random);
1654
- codes.add(body + checkChar(body));
1519
+ function gameEventLabel(beat) {
1520
+ const gd = beat.gameData;
1521
+ if (gd && typeof gd === "object") {
1522
+ const parts = Object.entries(gd).map(([k, v]) => {
1523
+ const val = v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
1524
+ return val === "" ? k : `${k}: ${val}`;
1525
+ });
1526
+ if (parts.length) return `game event: ${parts.join(", ")}`;
1655
1527
  }
1656
- return [...codes];
1528
+ return "game event";
1657
1529
  }
1658
- function formatMarker(code) {
1659
- return `[#${code}]`;
1530
+ function optionTag(node) {
1531
+ const parts = [];
1532
+ if (node.fallback) parts.push("fallback");
1533
+ parts.push(node.sticky ? "repeatable" : "once only");
1534
+ if (node.secretUntilEligible) parts.push("hidden");
1535
+ return parts.join(", ");
1660
1536
  }
1661
- function newHandoffId(taken = [], random = Math.random) {
1662
- const used = new Set(taken);
1663
- for (; ; ) {
1664
- const id = `H-${randomBody(4, random)}`;
1665
- if (!used.has(id)) return id;
1666
- }
1537
+ function sequenceLabel(node) {
1538
+ const o = node.options ?? {};
1539
+ const bits = [];
1540
+ if ((o.order ?? "sequential") === "shuffle") bits.push("shuffled");
1541
+ const ex = o.exhaust ?? "once";
1542
+ if (ex === "repeat") bits.push("repeating");
1543
+ else if (ex === "stick") bits.push("holds on the last");
1544
+ return bits.length ? `In sequence (${bits.join(", ")})` : "In sequence";
1545
+ }
1546
+ var EFFECT_PHASE_LABEL = { begin: "On begin", end: "On end" };
1547
+ var REF_RE = /@[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?/g;
1548
+ var andList = (xs) => xs.length <= 2 ? xs.join(" and ") : `${xs.slice(0, -1).join(", ")}, and ${xs[xs.length - 1]}`;
1549
+ function effectDescriber(dialect, nodeLabel = (id) => id) {
1550
+ const scopes = new Set(dialect.scopes.map((s) => s.token));
1551
+ const isScope = (t) => scopes.has(t);
1552
+ const refText = (ref) => {
1553
+ const body = ref.slice(1);
1554
+ const dot = body.indexOf(".");
1555
+ if (dot < 0) return body;
1556
+ return body.slice(0, dot) === "patter" ? body.slice(dot + 1) : ref;
1557
+ };
1558
+ const prose = (src) => {
1559
+ let out = "", i = 0;
1560
+ while (i < src.length) {
1561
+ const q = src[i];
1562
+ if (q === '"' || q === "'") {
1563
+ let j2 = i + 1;
1564
+ while (j2 < src.length && src[j2] !== q) j2 += src[j2] === "\\" ? 2 : 1;
1565
+ out += src.slice(i, j2 + 1);
1566
+ i = j2 + 1;
1567
+ continue;
1568
+ }
1569
+ let j = i;
1570
+ while (j < src.length && src[j] !== '"' && src[j] !== "'") j++;
1571
+ out += src.slice(i, j).replace(REF_RE, refText);
1572
+ i = j;
1573
+ }
1574
+ return humanizeNodeRefs(out.trim(), nodeLabel);
1575
+ };
1576
+ const sameRef = (n, target) => n?.kind === "scopedvar" && n.scope === target.scope && n.name.toLowerCase() === target.name;
1577
+ return (effect) => {
1578
+ const shown2 = refText(effect.target);
1579
+ let ast;
1580
+ try {
1581
+ ast = parse2(effect.value, dialect);
1582
+ } catch {
1583
+ ast = void 0;
1584
+ }
1585
+ const target = splitRef(effect.target, isScope);
1586
+ if (ast?.kind === "call" && ast.name === "advance" && ast.args.length === 1 && sameRef(ast.args[0], target)) return `Advances ${shown2}`;
1587
+ if (ast?.kind === "call" && ast.name === "set_flags" && sameRef(ast.args[0], target) && ast.args.length > 1 && ast.args.slice(1).every((a) => a.kind === "flagdelta")) {
1588
+ const deltas = ast.args.slice(1);
1589
+ const adds = deltas.filter((d) => d.sign === "+").map((d) => d.name), removes = deltas.filter((d) => d.sign === "-").map((d) => d.name);
1590
+ if (adds.length && removes.length) return `Adds ${andList(adds)} to ${shown2} and removes ${andList(removes)}`;
1591
+ return adds.length ? `Adds ${andList(adds)} to ${shown2}` : `Removes ${andList(removes)} from ${shown2}`;
1592
+ }
1593
+ if (ast?.kind === "string") return `Sets ${shown2} to ${ast.value}`;
1594
+ return `Sets ${shown2} to ${prose(effect.value)}`;
1595
+ };
1667
1596
  }
1668
- function readMarker(text) {
1669
- const m = /\[\s*#?\s*([0-9A-Za-z]{2,9})\s*\]/.exec(text);
1670
- if (!m) return void 0;
1671
- const code = m[1].toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1");
1672
- if ([...code].some((c) => !ALPHABET.includes(c))) return { code, valid: false };
1673
- const body = code.slice(0, -1);
1674
- return { code, valid: body.length >= 2 && checkChar(body) === code.slice(-1) };
1597
+ function effectLine(phase, effects, describe) {
1598
+ const clauses = effects.map((e) => {
1599
+ const t = describe(e);
1600
+ return t.charAt(0).toLowerCase() + t.slice(1);
1601
+ });
1602
+ return `${EFFECT_PHASE_LABEL[phase]}: ${clauses.join("; ")}`;
1675
1603
  }
1676
-
1677
- // src/pack.ts
1678
- var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj", EXTENSION_SHARD2];
1679
- var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
1680
- async function runPack(startPath) {
1681
- const projectFile = findProjectFile(startPath);
1682
- const root = dirname5(projectFile);
1683
- const project = parseSource3(readFileSync4(projectFile, "utf8"));
1684
- const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
1685
- if (unresolved.length) {
1686
- throw new Error(
1687
- `${unresolved.length} unresolved merge conflict(s) - resolve them before packing, or the recipient gets your side of a disagreement with no sign of it:
1688
- ` + unresolved.map((i) => ` ${i.file}`).join("\n")
1689
- );
1690
- }
1604
+ function runScriptDoc(loaded, opts = {}) {
1605
+ const { project } = loaded;
1606
+ const source = sourceStrings(loaded);
1607
+ const { cut, documentation } = mergeAuthoring2(loaded);
1608
+ const noteClasses = opts.notes === void 0 || opts.notes === "all" ? void 0 : classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, opts.notes.channel);
1609
+ const ownNotes = (id) => opts.notes === void 0 ? [] : (documentation.get(id) ?? []).filter((l) => l.type !== void 0 && (noteClasses === void 0 || noteClasses.has(l.type)));
1610
+ const sceneOf = /* @__PURE__ */ new Map();
1611
+ const blockTrail = /* @__PURE__ */ new Map();
1612
+ const blockName = /* @__PURE__ */ new Map();
1613
+ for (const scene of loaded.scenes) {
1614
+ sceneOf.set(scene.id, scene.name);
1615
+ for (const block of scene.blocks) {
1616
+ blockTrail.set(block.id, `${scene.name} \u203A ${block.name}`);
1617
+ blockName.set(block.id, block.name);
1618
+ }
1619
+ }
1620
+ const humanize = (cond) => humanizeNodeRefs(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
1621
+ const describeEffect = effectDescriber(stageDialect(project, loaded.gameScopes), (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
1622
+ const els = [];
1623
+ const nameOf = qualifierNamer(project);
1624
+ const colours = castColours(project.cast);
1625
+ const speaker = (beat) => ({
1626
+ character: beat.character ?? "",
1627
+ ...colours.has(beat.character ?? "") ? { colour: colours.get(beat.character ?? "") } : {},
1628
+ ...beat.qualifier ? { qualifier: beat.qualifier, qualifierName: nameOf(beat.qualifier) } : {},
1629
+ direction: beat.direction
1630
+ });
1631
+ const textOf = (id) => source[id] ?? "";
1632
+ const notes = (id, indent, sid) => {
1633
+ for (const l of ownNotes(id)) els.push({ kind: "note", id, indent, snippet: sid, text: l.text, class: l.type });
1634
+ };
1635
+ const jumpText = (j) => {
1636
+ const dest = targetLabel(j.to, sceneOf, blockTrail);
1637
+ return j.mode === "call" ? `${dest} (and return)` : dest;
1638
+ };
1639
+ let snippetSeq = 0;
1640
+ const nextSid = () => ++snippetSeq;
1641
+ const effects = (id, phase, list, indent, sid) => {
1642
+ if (list?.length) els.push({ kind: "effect", id, phase, indent, snippet: sid, text: effectLine(phase, list, describeEffect) });
1643
+ };
1644
+ const emitBeats = (node, indent, sid) => {
1645
+ effects(node.id, "begin", node.onEnter, indent, sid);
1646
+ for (const beat of node.beats ?? []) {
1647
+ if (cut.has(beat.id)) continue;
1648
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent, snippet: sid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
1649
+ else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
1650
+ else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent, snippet: sid, text: gameEventLabel(beat) });
1651
+ else continue;
1652
+ notes(beat.id, indent, sid);
1653
+ }
1654
+ effects(node.id, "end", node.onExit, indent, sid);
1655
+ if (node.jump) els.push({ kind: "jump", id: node.id, to: node.jump.to, indent, snippet: sid, text: jumpText(node.jump) });
1656
+ };
1657
+ const walk = (node, indent, sid) => {
1658
+ if (cut.has(node.id)) return;
1659
+ if (node.type === "snippet") {
1660
+ if (node.condition) els.push({ kind: "condition", id: node.id, indent, snippet: sid, text: `if ${humanize(node.condition)}` });
1661
+ notes(node.id, indent, sid);
1662
+ emitBeats(node, indent, sid);
1663
+ return;
1664
+ }
1665
+ if (node.condition) els.push({ kind: "condition", id: node.id, indent, snippet: sid, text: `if ${humanize(node.condition)}` });
1666
+ if (node.selector === "choice" || node.selector === "branch" || node.selector === "sequence") {
1667
+ } else notes(node.id, indent, sid);
1668
+ if (node.selector === "choice") {
1669
+ els.push({ kind: "group", id: node.id, indent, label: "Choose" });
1670
+ notes(node.id, indent, sid);
1671
+ for (const child2 of node.children ?? []) {
1672
+ if (cut.has(child2.id)) continue;
1673
+ const cid = nextSid();
1674
+ if (child2.type === "group" && child2.prompt) {
1675
+ if (child2.condition) els.push({ kind: "condition", id: child2.id, indent, snippet: cid, text: `if ${humanize(child2.condition)}` });
1676
+ els.push({ kind: "option", id: child2.prompt.id, node: child2.id, indent, snippet: cid, runs: textRuns(textOf(child2.prompt.id) || "(option)"), tag: optionTag(child2) });
1677
+ notes(child2.id, indent, cid);
1678
+ notes(child2.prompt.id, indent, cid);
1679
+ for (const c of child2.children ?? []) walk(c, indent + 1, cid);
1680
+ } else if (child2.type === "snippet") {
1681
+ const beats = child2.beats ?? [];
1682
+ const first = beats.find((b) => b.kind === "line" || b.kind === "text");
1683
+ if (child2.condition) els.push({ kind: "condition", id: child2.id, indent, snippet: cid, text: `if ${humanize(child2.condition)}` });
1684
+ els.push({ kind: "option", id: first?.id ?? child2.id, node: child2.id, indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child2) });
1685
+ notes(child2.id, indent, cid);
1686
+ if (first) notes(first.id, indent, cid);
1687
+ effects(child2.id, "begin", child2.onEnter, indent + 1, cid);
1688
+ let seenPrompt = false;
1689
+ for (const beat of beats) {
1690
+ if (!seenPrompt && beat === first) {
1691
+ seenPrompt = true;
1692
+ continue;
1693
+ }
1694
+ if (cut.has(beat.id)) continue;
1695
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent: indent + 1, snippet: cid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
1696
+ else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
1697
+ else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
1698
+ else continue;
1699
+ notes(beat.id, indent + 1, cid);
1700
+ }
1701
+ effects(child2.id, "end", child2.onExit, indent + 1, cid);
1702
+ if (child2.jump) els.push({ kind: "jump", id: child2.id, to: child2.jump.to, indent: indent + 1, snippet: cid, text: jumpText(child2.jump) });
1703
+ } else {
1704
+ walk(child2, indent + 1, cid);
1705
+ }
1706
+ }
1707
+ return;
1708
+ }
1709
+ if (node.selector === "branch") {
1710
+ els.push({ kind: "group", id: node.id, indent, label: "One of (first match wins)" });
1711
+ notes(node.id, indent, sid);
1712
+ const kids2 = (node.children ?? []).filter((c) => !cut.has(c.id));
1713
+ kids2.forEach((child2, i) => {
1714
+ const isCatchAll = i === kids2.length - 1 && kids2.length > 1 && !("condition" in child2 && child2.condition);
1715
+ if (isCatchAll) els.push({ kind: "else", id: child2.id, indent });
1716
+ walk(child2, indent, nextSid());
1717
+ });
1718
+ return;
1719
+ }
1720
+ if (node.selector === "sequence") {
1721
+ els.push({ kind: "group", id: node.id, indent, label: sequenceLabel(node) });
1722
+ notes(node.id, indent, sid);
1723
+ for (const child2 of node.children ?? []) walk(child2, indent, nextSid());
1724
+ return;
1725
+ }
1726
+ for (const child2 of node.children ?? []) walk(child2, indent, sid);
1727
+ };
1728
+ const only = opts.scenes ? new Set(opts.scenes) : void 0;
1729
+ for (const scene of loaded.scenes) {
1730
+ if (cut.has(scene.id) || only && !only.has(scene.id)) continue;
1731
+ els.push({ kind: "scene", id: scene.id, text: scene.name });
1732
+ notes(scene.id, 0, void 0);
1733
+ effects(scene.id, "begin", scene.onEntry, 0, void 0);
1734
+ for (const block of scene.blocks) {
1735
+ if (cut.has(block.id)) continue;
1736
+ els.push({ kind: "block", id: block.id, text: block.name });
1737
+ notes(block.id, 0, void 0);
1738
+ for (const child2 of block.children ?? []) walk(child2, 0, void 0);
1739
+ }
1740
+ }
1741
+ return { project: project.project.name, elements: els };
1742
+ }
1743
+
1744
+ // src/localisation.ts
1745
+ var SETTING_NAME = `"Localise text lines in dialogue scenes" (Project Settings, Language; \`localiseDialogueText\` in the project file)`;
1746
+ function describeLocExport(loaded, opts = {}) {
1747
+ const source = tableFor(loaded, loaded.project.locales.default);
1748
+ let lines = 0, scenes = 0;
1749
+ for (const ids of actionTextIds(loaded).values()) {
1750
+ const n = [...ids].filter((id) => source[id]).length;
1751
+ if (n) {
1752
+ lines += n;
1753
+ scenes++;
1754
+ }
1755
+ }
1756
+ const out = [];
1757
+ if (lines) {
1758
+ out.push(`Left out ${lines} text line${lines === 1 ? "" : "s"} in ${scenes} dialogue scene${scenes === 1 ? "" : "s"}: ${lines === 1 ? "it's" : "they're"} action, a direction for whoever stages the scene, because ${SETTING_NAME} is off. Turn it on to translate ${lines === 1 ? "it" : "them"}.`);
1759
+ }
1760
+ return [...out, ...describeActionTextChanges(loaded, actionTextChanges(loaded, opts))];
1761
+ }
1762
+ function describeActionTextChanges(loaded, changes) {
1763
+ const nameOf = new Map(loaded.scenes.map((sc) => [sc.id, sc.name]));
1764
+ const out = [];
1765
+ const groups = /* @__PURE__ */ new Map();
1766
+ for (const c of changes) {
1767
+ const key = `${c.locale}|${c.change}`;
1768
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(c);
1769
+ }
1770
+ for (const [key, list] of groups) {
1771
+ const [locale, change] = key.split("|");
1772
+ const n = `${list.length} text line${list.length === 1 ? "" : "s"}`;
1773
+ out.push(change === "dropped" ? `${n} now action, so left out of '${locale}': the next loc import drops ${list.length === 1 ? "its translation" : "their translations"} (turn on ${SETTING_NAME} to keep ${list.length === 1 ? "it" : "them"})` : `${n} no longer action, so new to '${locale}' and untranslated`);
1774
+ for (const c of list) out.push(` ${c.id} in '${nameOf.get(c.scene) ?? c.scene}'`);
1775
+ }
1776
+ return out;
1777
+ }
1778
+ function sceneTextIds(scene) {
1779
+ const out = [];
1780
+ for (const block of scene.blocks) {
1781
+ walkNodes2(block.children, (node) => {
1782
+ if (node.type === "snippet") {
1783
+ for (const beat of node.beats ?? []) if (beat.kind === "text") out.push(beat.id);
1784
+ }
1785
+ });
1786
+ }
1787
+ return out;
1788
+ }
1789
+ function actionTextIds(loaded) {
1790
+ const out = /* @__PURE__ */ new Map();
1791
+ for (const scene of loaded.scenes) if (textIsAction(loaded.project, scene)) out.set(scene.id, new Set(sceneTextIds(scene)));
1792
+ return out;
1793
+ }
1794
+ function actionTextChanges(loaded, opts = {}) {
1795
+ const defaultLocale = loaded.project.locales.default;
1796
+ const locales = opts.locale !== void 0 ? opts.locale === defaultLocale ? [] : [opts.locale] : loaded.project.locales.all.filter((l) => l !== defaultLocale);
1797
+ const source = tableFor(loaded, defaultLocale);
1798
+ const out = [];
1799
+ for (const locale of locales) {
1800
+ for (const scene of loaded.scenes) {
1801
+ const path = existingLocPath(loaded, scene.id, locale);
1802
+ const shard = path ? findFile(loaded.localeFiles, loaded.locales, path) : void 0;
1803
+ if (!shard) continue;
1804
+ const action = textIsAction(loaded.project, scene);
1805
+ for (const id of sceneTextIds(scene)) {
1806
+ if (action && shard.strings[id]) out.push({ id, scene: scene.id, locale, change: "dropped" });
1807
+ else if (!action && shard.textAction && source[id]) out.push({ id, scene: scene.id, locale, change: "added" });
1808
+ }
1809
+ }
1810
+ }
1811
+ return out;
1812
+ }
1813
+ function projectStringSources(project, sourceTable) {
1814
+ const out = {};
1815
+ const put = (id, authored) => {
1816
+ if (authored) out[id] = sourceTable[id] ?? authored;
1817
+ };
1818
+ for (const c of project.cast ?? []) {
1819
+ put(castStringKey(c.name), c.displayName);
1820
+ for (const [q, label] of Object.entries(c.qualifierLabels ?? {})) put(speakerLabelKey(c.name, q), label);
1821
+ }
1822
+ for (const q of projectQualifiers2(project)) put(qualifierStringKey(q.gameId), q.name);
1823
+ put(SPEAKER_PATTERN_KEY, project.speakerPattern ?? DEFAULT_SPEAKER_PATTERN);
1824
+ return out;
1825
+ }
1826
+ function extractLoc(loaded, opts = {}) {
1827
+ const defaultLocale = loaded.project.locales.default;
1828
+ const isTemplate = opts.locale === void 0 || opts.locale === defaultLocale;
1829
+ const targetLocale = isTemplate ? void 0 : opts.locale;
1830
+ const source = tableFor(loaded, defaultLocale);
1831
+ const target = targetLocale ? tableFor(loaded, targetLocale) : {};
1832
+ const edits = mergeAuthoring2(loaded).edits;
1833
+ const projectSources = projectStringSources(loaded.project, source);
1834
+ const docs = resolveDocumentation(loaded, "loc");
1835
+ const commentsOf = (id) => (docs.get(id) ?? []).map((d) => d.text);
1836
+ const staleFor = (id, translation) => {
1837
+ if (isTemplate || !translation) return false;
1838
+ const e = edits.get(id);
1839
+ const localised = e?.localisedAt?.[targetLocale];
1840
+ return !!(e?.modifiedAt && localised && e.modifiedAt > localised);
1841
+ };
1842
+ const genderOf = /* @__PURE__ */ new Map();
1843
+ for (const c of loaded.project.cast ?? []) if (c.gender) genderOf.set(c.name, c.gender);
1844
+ const withGender = (context) => {
1845
+ const g = context?.character ? genderOf.get(context.character) : void 0;
1846
+ return g ? { ...context, gender: g } : context;
1847
+ };
1848
+ const qualifierName = new Map(projectQualifiers2(loaded.project).map((q) => [q.gameId, q.name]));
1849
+ const lineContext = (b) => {
1850
+ const q = b.kind === "line" && b.qualifier ? qualifierName.get(b.qualifier) ?? b.qualifier : void 0;
1851
+ return { character: b.kind === "line" ? b.character : void 0, kind: b.kind, ...q ? { qualifier: q } : {} };
1852
+ };
1853
+ const entries = [];
1854
+ const push = (id, scene, context) => {
1855
+ const src = source[id];
1856
+ if (src === void 0) return;
1857
+ const translation = isTemplate ? "" : target[id] ?? "";
1858
+ entries.push({ id, scene, source: src, translation, comments: commentsOf(id), context: withGender(context), stale: staleFor(id, translation) });
1859
+ };
1860
+ for (const scene of loaded.scenes) {
1861
+ const action = textIsAction(loaded.project, scene);
1862
+ for (const block of scene.blocks) {
1863
+ walkNodes2(block.children, (node) => {
1864
+ if (node.type === "group") {
1865
+ const p = node.prompt;
1866
+ if (p) push(p.id, scene.id, lineContext(p));
1867
+ return;
1868
+ }
1869
+ for (const beat of node.beats ?? []) {
1870
+ if (beat.kind === "gameEvent" || action && beat.kind === "text") continue;
1871
+ push(beat.id, scene.id, lineContext(beat));
1872
+ }
1873
+ });
1874
+ }
1875
+ }
1876
+ for (const c of loaded.project.cast ?? []) {
1877
+ if (!c.displayName) continue;
1878
+ const id = castStringKey(c.name);
1879
+ const src = projectSources[id];
1880
+ const translation = isTemplate ? "" : target[id] ?? "";
1881
+ entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: withGender({ character: c.name }), stale: staleFor(id, translation) });
1882
+ }
1883
+ for (const q of usedQualifiers(loaded.project, loaded.scenes)) {
1884
+ const id = qualifierStringKey(q.gameId);
1885
+ const src = projectSources[id] ?? q.name;
1886
+ const translation = isTemplate ? "" : target[id] ?? "";
1887
+ entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: commentsOf(id), context: { kind: "qualifier" }, stale: staleFor(id, translation) });
1888
+ }
1889
+ const pairs = usedSpeakerPairs(loaded.scenes);
1890
+ if (pairs.length > 0) {
1891
+ const id = SPEAKER_PATTERN_KEY;
1892
+ const src = projectSources[id];
1893
+ const translation = isTemplate ? "" : target[id] ?? "";
1894
+ entries.push({ id, scene: PROJECT_LOCALE_SCENE, source: src, translation, comments: [SPEAKER_PATTERN_NOTE, ...commentsOf(id)], context: { kind: "speaker pattern" }, stale: staleFor(id, translation) });
1895
+ const labelsOf = new Map((loaded.project.cast ?? []).map((c) => [c.name, c.qualifierLabels]));
1896
+ for (const p of pairs) {
1897
+ const label = labelsOf.get(p.character)?.[p.qualifier];
1898
+ if (!label) continue;
1899
+ const pid = speakerLabelKey(p.character, p.qualifier);
1900
+ const psrc = projectSources[pid] ?? label;
1901
+ const ptranslation = isTemplate ? "" : target[pid] ?? "";
1902
+ const qualifier = qualifierName.get(p.qualifier) ?? p.qualifier;
1903
+ entries.push({ id: pid, scene: PROJECT_LOCALE_SCENE, source: psrc, translation: ptranslation, comments: commentsOf(pid), context: withGender({ character: p.character, kind: "speaker", qualifier }), stale: staleFor(pid, ptranslation) });
1904
+ }
1905
+ }
1906
+ return { project: loaded.project.project.id, defaultLocale, locale: targetLocale, entries };
1907
+ }
1908
+ var SPEAKER_PATTERN_NOTE = "{name} and {qualifier} are filled in with the speaker's name and the qualifier, so keep both as they are. You can put them in any order.";
1909
+ var layoutOf = (loaded) => projectLayout2(loaded.project);
1910
+ function existingLocPath(loaded, scene, locale) {
1911
+ for (let i = 0; i < loaded.locales.length; i++) {
1912
+ const l = loaded.locales[i];
1913
+ if (l.scene === scene && l.locale === locale) return loaded.localeFiles[i] ?? null;
1914
+ }
1915
+ return null;
1916
+ }
1917
+ function targetLocPath(loaded, scene, locale, defaultLocale) {
1918
+ const existing = existingLocPath(loaded, scene, locale);
1919
+ if (existing) return existing;
1920
+ const fromDefault = existingLocPath(loaded, scene, defaultLocale);
1921
+ if (fromDefault) {
1922
+ const parts = fromDefault.split(sep);
1923
+ for (let i = parts.length - 2; i >= 0; i--) {
1924
+ if (parts[i] === defaultLocale) {
1925
+ parts[i] = locale;
1926
+ return parts.join(sep);
1927
+ }
1928
+ }
1929
+ }
1930
+ const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
1931
+ return join3(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
1932
+ }
1933
+ function authoringPath(loaded, scene) {
1934
+ const dir = join3(loaded.root, layoutOf(loaded).authoring);
1935
+ if (scene === PROJECT_LOCALE_SCENE) return join3(dir, "_project.patterx");
1936
+ const flow = loaded.sceneFiles[scene];
1937
+ const stem = flow ? basename3(flow).replace(/\.patterflow$/, "") : scene;
1938
+ return join3(dir, `${stem}.patterx`);
1939
+ }
1940
+ var findFile = (files, items, path) => {
1941
+ const i = files.indexOf(path);
1942
+ return i >= 0 ? items[i] : void 0;
1943
+ };
1944
+ function locLineHomes(loaded, sourceTable) {
1945
+ const out = /* @__PURE__ */ new Map();
1946
+ for (const scene of loaded.scenes) {
1947
+ for (const block of scene.blocks) {
1948
+ walkNodes2(block.children, (node) => {
1949
+ if (node.type === "group") {
1950
+ if (node.prompt) out.set(node.prompt.id, scene.id);
1951
+ return;
1952
+ }
1953
+ for (const beat of node.beats ?? []) if (beat.kind !== "gameEvent") out.set(beat.id, scene.id);
1954
+ });
1955
+ }
1956
+ }
1957
+ for (const id of Object.keys(projectStringSources(loaded.project, sourceTable))) out.set(id, PROJECT_LOCALE_SCENE);
1958
+ return out;
1959
+ }
1960
+ function textTokens(text) {
1961
+ const out = [...text.matchAll(INTERP_RE)].map((m) => m[0]);
1962
+ for (const m of text.matchAll(MARKUP_RE)) out.push(`<${m[1]}>`);
1963
+ return out;
1964
+ }
1965
+ function compareLocTokens(source, translation) {
1966
+ const count = (list) => {
1967
+ const m = /* @__PURE__ */ new Map();
1968
+ for (const t of list) m.set(t, (m.get(t) ?? 0) + 1);
1969
+ return m;
1970
+ };
1971
+ const a = count(textTokens(source)), b = count(textTokens(translation));
1972
+ const short = (x, y) => [...x].flatMap(([t, n]) => Array(Math.max(0, n - (y.get(t) ?? 0))).fill(t)).sort();
1973
+ const missing = short(a, b), extra = short(b, a);
1974
+ return missing.length || extra.length ? { missing, extra } : null;
1975
+ }
1976
+ function importLocale(fileLocale, chosen, opts = {}) {
1977
+ if (fileLocale && chosen && fileLocale !== chosen && !opts.force) {
1978
+ return { ok: false, reason: "mismatch", error: `This file is for '${fileLocale}', not '${chosen}', so nothing was imported.` };
1979
+ }
1980
+ const locale = chosen ?? fileLocale;
1981
+ if (!locale) return { ok: false, reason: "none", error: "This file doesn't say which language it's for, so nothing was imported." };
1982
+ if (!fileLocale) return { ok: true, locale, warning: `This file doesn't say which language it's for, so it was imported as '${locale}'. Check that's the right language.` };
1983
+ return { ok: true, locale };
1984
+ }
1985
+ function describeLocImport(loaded, report) {
1986
+ const nameOf = new Map(loaded.scenes.map((sc) => [sc.id, sc.name]));
1987
+ const where = (scene) => scene === PROJECT_LOCALE_SCENE ? "the project's strings" : `'${nameOf.get(scene) ?? scene}'`;
1988
+ const out = [];
1989
+ const u = report.unknown.length;
1990
+ if (u) {
1991
+ out.push(`Skipped ${u} line${u === 1 ? "" : "s"} this project doesn't have, so ${u === 1 ? "its translation wasn't" : "their translations weren't"} imported:`);
1992
+ for (const l of report.unknown) out.push(` ${l.id} in ${where(l.scene)}`);
1993
+ }
1994
+ const m = report.mismatched.length;
1995
+ if (m) {
1996
+ out.push(`${m} translation${m === 1 ? "" : "s"} ${m === 1 ? "doesn't" : "don't"} match the source's placeholders or formatting. ${m === 1 ? "It was" : "They were"} imported, but check ${m === 1 ? "it" : "them"}:`);
1997
+ for (const l of report.mismatched) {
1998
+ const parts = [l.missing.length ? `missing ${l.missing.join(", ")}` : "", l.extra.length ? `extra ${l.extra.join(", ")}` : ""].filter(Boolean);
1999
+ out.push(` ${l.id} in ${where(l.scene)}: ${parts.join("; ")}`);
2000
+ }
2001
+ }
2002
+ return out;
2003
+ }
2004
+ function applyLoc(loaded, catalog, opts = {}) {
2005
+ const report = { unknown: [], mismatched: [] };
2006
+ const locale = catalog.locale;
2007
+ if (!locale || locale === loaded.project.locales.default) {
2008
+ return { writes: [], stats: { updated: 0, files: 0, dropped: 0 }, report };
2009
+ }
2010
+ if (!loaded.project.locales.all.includes(locale)) {
2011
+ throw new Error(`'${locale}' is not one of the project's languages (${loaded.project.locales.all.join(", ")}); add it to the project first`);
2012
+ }
2013
+ const known = /* @__PURE__ */ new Set([...loaded.scenes.map((sc) => sc.id), PROJECT_LOCALE_SCENE]);
2014
+ const unknown = [...new Set(catalog.entries.filter((e) => e.translation.trim() && !known.has(e.scene)).map((e) => e.scene))];
2015
+ if (unknown.length) {
2016
+ throw new Error(`the file names scene(s) this project does not have: ${unknown.map((u) => `'${u}'`).join(", ")}; nothing was imported`);
2017
+ }
2018
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
2019
+ const sourceTable = tableFor(loaded, loaded.project.locales.default);
2020
+ const projectSources = projectStringSources(loaded.project, sourceTable);
2021
+ const homes = locLineHomes(loaded, sourceTable);
2022
+ const action = actionTextIds(loaded);
2023
+ const bySceneText = /* @__PURE__ */ new Map();
2024
+ const bySceneFresh = /* @__PURE__ */ new Map();
2025
+ for (const e of catalog.entries) {
2026
+ if (!e.translation.trim()) continue;
2027
+ const scene = homes.get(e.id);
2028
+ if (scene === void 0) {
2029
+ report.unknown.push({ id: e.id, scene: e.scene });
2030
+ continue;
2031
+ }
2032
+ if (action.get(scene)?.has(e.id)) continue;
2033
+ const source = sourceTable[e.id] ?? projectSources[e.id];
2034
+ const diff = source === void 0 ? null : compareLocTokens(source, e.translation);
2035
+ if (diff) report.mismatched.push({ id: e.id, scene, ...diff });
2036
+ (bySceneText.get(scene) ?? bySceneText.set(scene, /* @__PURE__ */ new Map()).get(scene)).set(e.id, e.translation);
2037
+ if (!e.stale) (bySceneFresh.get(scene) ?? bySceneFresh.set(scene, /* @__PURE__ */ new Set()).get(scene)).add(e.id);
2038
+ }
2039
+ const writes = [];
2040
+ let updated = 0;
2041
+ let dropped = 0;
2042
+ let files = 0;
2043
+ const scenes = new Set(bySceneText.keys());
2044
+ for (const l of loaded.locales) if (l.locale === locale && l.scene !== PROJECT_LOCALE_SCENE && known.has(l.scene)) scenes.add(l.scene);
2045
+ for (const scene of scenes) {
2046
+ const strings = bySceneText.get(scene) ?? /* @__PURE__ */ new Map();
2047
+ const path = targetLocPath(loaded, scene, locale, loaded.project.locales.default);
2048
+ const existing = findFile(loaded.localeFiles, loaded.locales, path);
2049
+ const merged = { ...existing?.strings ?? {} };
2050
+ for (const [id, text] of strings) {
2051
+ if (merged[id] !== text) updated++;
2052
+ merged[id] = text;
2053
+ }
2054
+ const actionIds = action.get(scene);
2055
+ for (const id of actionIds ?? []) if (id in merged) {
2056
+ delete merged[id];
2057
+ dropped++;
2058
+ }
2059
+ const file = {
2060
+ schema: existing?.schema ?? STRINGS_SCHEMA,
2061
+ scene,
2062
+ locale,
2063
+ ...actionIds ? { textAction: true } : {},
2064
+ strings: merged
2065
+ };
2066
+ const content = canonicalStringify3(file);
2067
+ if (!existing || strings.size > 0 || canonicalStringify3(existing) !== content) {
2068
+ writes.push({ path, content });
2069
+ files++;
2070
+ }
2071
+ const fresh = bySceneFresh.get(scene);
2072
+ if (!fresh?.size) continue;
2073
+ const aPath = authoringPath(loaded, scene);
2074
+ const aExisting = findFile(loaded.authoringFiles, loaded.authoring, aPath);
2075
+ const authoring = aExisting ? { ...aExisting, edits: { ...aExisting.edits } } : { schema: AUTHORING_SCHEMA };
2076
+ const editsMap = { ...authoring.edits ?? {} };
2077
+ for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
2078
+ authoring.edits = editsMap;
2079
+ writes.push({ path: aPath, content: canonicalStringify3(authoring) });
2080
+ }
2081
+ return { writes, stats: { updated, files, dropped }, report };
2082
+ }
2083
+
2084
+ // src/pack.ts
2085
+ import JSZip from "jszip";
2086
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2087
+ import { dirname as dirname5, join as join5, relative as relative2, sep as sep2 } from "path";
2088
+ import { parseSource as parseSource3 } from "@patterkit/core";
2089
+ import { EXTENSION_SHARD as EXTENSION_SHARD2, EXTENSIONS_DIR as EXTENSIONS_DIR2, MANIFEST_EXTENSION as MANIFEST_EXTENSION2 } from "@patterkit/model";
2090
+ import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
2091
+
2092
+ // src/handoff.ts
2093
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2094
+ import { basename as basename4, join as join4 } from "path";
2095
+ import { HANDOFF_SCHEMA } from "@patterkit/model";
2096
+ var HANDOFF_DIR = "handoffs";
2097
+ var HANDOFF_EXT = ".json";
2098
+ function handoffPath(root, id) {
2099
+ return join4(root, HANDOFF_DIR, `${id}${HANDOFF_EXT}`);
2100
+ }
2101
+ function serialiseHandoff(handoff) {
2102
+ return JSON.stringify(handoff, null, 2) + "\n";
2103
+ }
2104
+ function handoffWrite(root, handoff) {
2105
+ return { path: handoffPath(root, handoff.id), content: serialiseHandoff(handoff) };
2106
+ }
2107
+ function parseHandoff(text) {
2108
+ const h = JSON.parse(text);
2109
+ if (h.schema !== HANDOFF_SCHEMA) throw new Error(`not a handoff record (schema ${JSON.stringify(h.schema)})`);
2110
+ if (typeof h.id !== "string" || !h.lines || !Array.isArray(h.skeleton)) throw new Error("handoff record is missing its id, lines, or skeleton");
2111
+ return h;
2112
+ }
2113
+ function readHandoffs(root) {
2114
+ const handoffs = [];
2115
+ const issues = [];
2116
+ for (const file of walkFiles(join4(root, HANDOFF_DIR), HANDOFF_EXT)) {
2117
+ try {
2118
+ handoffs.push(parseHandoff(readFileSync3(file, "utf8")));
2119
+ } catch (e) {
2120
+ issues.push({ file, message: `${basename4(file)}: ${e instanceof Error ? e.message : String(e)}` });
2121
+ }
2122
+ }
2123
+ handoffs.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
2124
+ return { handoffs, issues };
2125
+ }
2126
+ function readHandoff(root, id) {
2127
+ const path = handoffPath(root, id);
2128
+ if (!existsSync3(path)) return void 0;
2129
+ return parseHandoff(readFileSync3(path, "utf8"));
2130
+ }
2131
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2132
+ function checkChar(body) {
2133
+ let sum = 0;
2134
+ for (let i = 0; i < body.length; i++) sum += (i + 1) * ALPHABET.indexOf(body[i]);
2135
+ return ALPHABET[sum % 31];
2136
+ }
2137
+ function randomBody(length, random) {
2138
+ let s = "";
2139
+ for (let i = 0; i < length; i++) s += ALPHABET[Math.floor(random() * ALPHABET.length)];
2140
+ return s;
2141
+ }
2142
+ function markerLength(count) {
2143
+ let length = 3;
2144
+ while (ALPHABET.length ** length < Math.max(1, count) * 1e3) length++;
2145
+ return length;
2146
+ }
2147
+ function issueMarkerCodes(count, random = Math.random) {
2148
+ const length = markerLength(count);
2149
+ const codes = /* @__PURE__ */ new Set();
2150
+ while (codes.size < count) {
2151
+ const body = randomBody(length, random);
2152
+ codes.add(body + checkChar(body));
2153
+ }
2154
+ return [...codes];
2155
+ }
2156
+ function formatMarker(code) {
2157
+ return `[#${code}]`;
2158
+ }
2159
+ function newHandoffId(taken = [], random = Math.random) {
2160
+ const used = new Set(taken);
2161
+ for (; ; ) {
2162
+ const id = `H-${randomBody(4, random)}`;
2163
+ if (!used.has(id)) return id;
2164
+ }
2165
+ }
2166
+ function readMarker(text) {
2167
+ const m = /\[\s*#?\s*([0-9A-Za-z]{2,9})\s*\]/.exec(text);
2168
+ if (!m) return void 0;
2169
+ const code = m[1].toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1");
2170
+ if ([...code].some((c) => !ALPHABET.includes(c))) return { code, valid: false };
2171
+ const body = code.slice(0, -1);
2172
+ return { code, valid: body.length >= 2 && checkChar(body) === code.slice(-1) };
2173
+ }
2174
+
2175
+ // src/pack.ts
2176
+ var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj", EXTENSION_SHARD2];
2177
+ var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
2178
+ async function runPack(startPath) {
2179
+ const projectFile = findProjectFile(startPath);
2180
+ const root = dirname5(projectFile);
2181
+ const project = parseSource3(readFileSync4(projectFile, "utf8"));
2182
+ const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
2183
+ if (unresolved.length) {
2184
+ throw new Error(
2185
+ `${unresolved.length} unresolved merge conflict(s) - resolve them before packing, or the recipient gets your side of a disagreement with no sign of it:
2186
+ ` + unresolved.map((i) => ` ${i.file}`).join("\n")
2187
+ );
2188
+ }
1691
2189
  const shardFiles = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative2(root, abs).split(sep2).join("/") }));
1692
2190
  const marked = [...new Set(unresolvedMarkIssues(shardFiles.map((f) => ({ file: f.rel, text: readFileSync4(f.abs, "utf8") }))).map((i) => i.file))];
1693
2191
  if (marked.length) {
@@ -1738,8 +2236,8 @@ function runValidate(loaded) {
1738
2236
  const { project, scenes, locales } = loaded;
1739
2237
  const structural = validateProject({ project, scenes, authoring: loaded.authoring });
1740
2238
  const merged = loaded.gameScopes?.merged;
1741
- const scopes = projectScopes(project, merged);
1742
- const foreignScopes = hostScopesToSpec(scopes.host);
2239
+ const scopes = projectScopes2(project, merged);
2240
+ const foreignScopes = hostScopesToSpec2(scopes.host);
1743
2241
  const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged });
1744
2242
  const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
1745
2243
  const gameScopes = gameScopesIssues(loaded, scopes.notes);
@@ -1778,7 +2276,7 @@ var isError = (i) => i.severity === "error";
1778
2276
  function exportBlockers(loaded) {
1779
2277
  const { project, scenes, locales } = loaded;
1780
2278
  const merged = loaded.gameScopes?.merged;
1781
- const foreignScopes = hostScopesToSpec(projectScopes(project, merged).host);
2279
+ const foreignScopes = hostScopesToSpec2(projectScopes2(project, merged).host);
1782
2280
  const structural = validateProject({ project, scenes, authoring: loaded.authoring }).filter((i) => i.severity !== "warning");
1783
2281
  const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged }).filter(isError);
1784
2282
  const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged }).filter(isError);
@@ -2131,7 +2629,7 @@ import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
2131
2629
  import { DEFAULT_PAD_AFTER } from "@patterkit/model";
2132
2630
 
2133
2631
  // src/playable-runtime.ts
2134
- var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var oe=Object.defineProperty;var ft=Object.getOwnPropertyDescriptor;var ht=Object.getOwnPropertyNames;var mt=Object.prototype.hasOwnProperty;var yt=(r,e)=>{for(var t in e)oe(r,t,{get:e[t],enumerable:!0})},St=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ht(e))!mt.call(r,s)&&s!==t&&oe(r,s,{get:()=>e[s],enumerable:!(n=ft(e,s))||n.enumerable});return r};var wt=r=>St(oe({},"__esModule",{value:!0}),r);var Ht={};yt(Ht,{Checkpoint:()=>H,DEFAULT_PAD_AFTER:()=>E,Engine:()=>re,Flow:()=>O,buildTagIndex:()=>ne,compareStages:()=>N,describeBundle:()=>ot,effectiveGameData:()=>ct,extensionDecls:()=>lt,extensionsOf:()=>dt,gameDataFields:()=>at,gameDataValue:()=>we,nextStage:()=>B,resolveSceneRef:()=>Z,sceneAddressIndex:()=>fe,stageIndex:()=>P});function F(r,e){return r!==void 0&&Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0}function R(r){switch(r[0]){case"b":return{kind:"bool",value:r[1]};case"n":return{kind:"number",value:r[1]};case"s":return{kind:"string",value:r[1]};case"sv":return{kind:"scopedvar",scope:r[1],name:r[2]};case"u":return{kind:"unary",op:r[1],operand:R(r[2])};case"bin":return{kind:"binary",op:r[1],left:R(r[2]),right:R(r[3])};case"call":{let e=r.slice(2).map(R);return{kind:"call",name:r[1],args:e}}case"fd":return{kind:"flagdelta",sign:r[1],name:r[2]}}}var u=class extends Error{constructor(e){super(e),this.name="EvalError"}};function J(r,e,t){let n=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=F(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):F(o,i.name);if(a===void 0){if(n.get(i.scope)==="throw")throw new u(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!F(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new u(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=ae(a,e);if(c===void 0)throw new u("advance() needs a quality reference (@scope.name of a quality property)");let l=v(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=F(t.functions,i.name);if(!o)throw new u(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new u("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new u(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new u(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let h=s(i.left);if(typeof h!="boolean")throw new u(`\'and\' requires boolean operands, left is ${typeof h}`);if(!h)return!1;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'and\' requires boolean operands, right is ${typeof f}`);return f}if(i.op==="or"){let h=s(i.left);if(typeof h!="boolean")throw new u(`\'or\' requires boolean operands, left is ${typeof h}`);if(h)return!0;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'or\' requires boolean operands, right is ${typeof f}`);return f}let o=s(i.left),a=s(i.right),c=ae(i.left,e),l=ae(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!vt(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new u(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return v(o,d,">")>v(a,d,">");case">=":return v(o,d,">=")>=v(a,d,">=");case"<":return v(o,d,"<")<v(a,d,"<");case"<=":return v(o,d,"<=")<=v(a,d,"<=");case"+":case"-":case"*":case"/":throw new u(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return Ce(o,a);case"!=":return!Ce(o,a);case">":return D(o,a,">"),o>a;case">=":return D(o,a,">="),o>=a;case"<":return D(o,a,"<"),o<a;case"<=":return D(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new u(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return D(o,a,"-"),o-a;case"*":return D(o,a,"*"),o*a;case"/":if(D(o,a,"/"),a===0)throw new u("division by zero");return o/a}}}};return s(r)}function Ce(r,e){if(Array.isArray(r)||Array.isArray(e)){if(!Array.isArray(r)||!Array.isArray(e)||r.length!==e.length)return!1;let t=[...r].sort(),n=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==n[s])return!1;return!0}return r===e}function D(r,e,t){if(typeof r!="number"||typeof e!="number")throw new u(`\'${t}\' requires numeric operands, got ${typeof r} and ${typeof e}`)}function ae(r,e){if(!(r.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(r.scope,r.name)}function v(r,e,t){if(typeof r!="string")throw new u(`\'${t}\' on a quality compares stages, got ${typeof r}`);let n=e.indexOf(r);if(n<0)throw new u(`"${r}" is not a stage of this quality (stages: ${e.join(", ")})`);return n}var vt=(r,e)=>r.length===e.length&&r.every((t,n)=>t===e[n]);function T(r){if(Number.isNaN(r)||!Number.isFinite(r))return 0;let e=Math.trunc(r)%4294967296;return e<0?e+4294967296:e}function ce(r){let e=T(r);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 de=r=>Array.isArray(r)?r:r.stages??[],bt=r=>Array.isArray(r)?"flags":r==null?"nothing":typeof r;function P(r,e){return typeof e=="string"?de(r).indexOf(e):-1}function le(r,e){if(typeof e!="string")throw new u(`a stage is a name, got ${bt(e)}`);let t=r.indexOf(e);if(t<0)throw new u(`"${e}" is not a stage of this quality (stages: ${r.join(", ")})`);return t}function N(r,e,t){let n=de(r),s=le(n,e)-le(n,t);return s<0?-1:s>0?1:0}function B(r,e){let t=de(r);return t[Math.min(le(t,e)+1,t.length-1)]}function pe(r,e,t){switch(r.type){case"boolean":return typeof e=="boolean";case"number":return typeof e=="number";case"string":case"text":case"multiline":return typeof e=="string";case"enum":return typeof e!="string"?!1:e===""&&t?.noneChosen?!0:!Array.isArray(r.values)||r.values.includes(e);case"flags":return Array.isArray(e)&&e.every(n=>typeof n=="string"&&(!Array.isArray(r.values)||r.values.includes(n)));case"quality":return typeof e=="string"&&(!Array.isArray(r.stages)||r.stages.includes(e));default:return!0}}var xt={name:"check_flags",count:r=>Math.max(1,r.args.length-1)},kt=[xt];function Ie(r,e,t){let n=t?.countingCalls??kt;return Q(r,t?.want??!0,e,n)}function Q(r,e,t,n){if(r.kind==="binary"&&(r.op==="and"||r.op==="or")){let s=Q(r.left,e,t,n),i=Q(r.right,e,t,n);return r.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(r.kind==="unary"&&r.op==="not")return Q(r.operand,!e,t,n);if(r.kind==="call"){let s=n.find(i=>i.name===r.name);if(s){let i=s.count(r),o=t(r);return e?o?i:0:o?0:1}}return t(r)===e?1:0}var k=class r{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(n=>n.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let n=this.norm(t.name);this.decls.set(n,t),this.values[n]=structuredClone(t.default??A(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,n){let s=this.norm(e);if(!n?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:n?.silent??!1,reason:n?.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])=>Pe(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new r([],{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,t){let n=new Map;for(let[i,o]of Object.entries(e)){let a=this.norm(i);this.values[a]=structuredClone(o),n.set(a,o)}let s=[];if(!t?.refit)return s;for(let[i,o]of this.decls){if(!n.has(i))continue;let a=n.get(i);pe(o,a)||(this.values[i]=structuredClone(A(o)),s.push({name:i,value:a}))}return s}};function Pe(r,e,t,n,s=""){let i=n??r.name.toLowerCase();return{name:i,path:s+i,type:r.type,value:e,default:r.default??A(r),...r.values!==void 0?{values:r.values}:{},...r.stages!==void 0?{stages:r.stages}:{},writable:t??r.writable??!0}}var Et=r=>r.toLowerCase(),ue=1,z=class{scopes=new Map;parked=new Map;parkedRefit=new Set;misfits=[];rev=0;get revision(){return this.rev}defineOwned(e,t,n){let s=typeof n=="string"?{pathPrefix:n}:n??{},i=new k(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,n){this.assertFree(e,n?.owner),this.scopes.set(e,{kind:"owned",bag:t,...n?.owner!==void 0?{owner:n.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(this.note(e,t.load(s,{refit:this.parkedRefit.has(e)})),this.parked.delete(e),this.parkedRefit.delete(e)),this}remove(e,t){let n=this.scopes.get(e);if(!n)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&n.kind==="owned"&&(this.parked.set(e,n.bag.save()),this.parkedRefit.delete(e)),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear(),this.parkedRefit.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&(this.parked.delete(t),this.parkedRefit.delete(t));return this}takeMisfits(e){if(!e){let n=this.misfits;return this.misfits=[],n}let t=this.misfits.filter(n=>e(n.scope));return this.misfits=this.misfits.filter(n=>!e(n.scope)),t}note(e,t){for(let n of t)this.misfits.push({scope:e,name:n.name,value:n.value})}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,n=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??Et,a=new Map;for(let c of n)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 n=this.scopes.get(e);if(n)return n.kind==="owned"?n.bag.get(t):n.resolver.get(n.norm(t))}set(e,t,n,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,n,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,n)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,n]of this.scopes){let s=n.owner!==void 0?{owner:n.owner}:{};if(n.kind==="owned")for(let i of n.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of n.decls)e.push({scope:t,...s,...Pe(o,n.resolver.get(i),this.foreignWritable(n,i),i,`${t}.`)})}return e}toEvalContext(e,t){let n=this.view(t?.aliases),s={};for(let[o,a]of n)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(n);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=n.get(o);return c?i.get(o)?.get(Rt(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[n,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${n}\' names \'${s}\', which is not registered`);t.set(n,i)}return t}qualityLadders(e){let t=new Map;for(let[n,s]of e)for(let[i,o]of Oe(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(n);a||(a=new Map,t.set(n,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[n,s]of this.view(e?.aliases)){let i=Oe(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(n,o)}return{properties:t}}save(){let e={};for(let[t,n]of this.scopes)n.kind==="owned"&&(e[t]=n.bag.save());for(let[t,n]of this.parked)e[t]=structuredClone(n);return e}load(e,t){t?.keepParked||(this.parked.clear(),this.parkedRefit.clear());let n=t?.refit===!0;for(let[s,i]of Object.entries(e)){let o=this.scopes.get(s);o?.kind==="owned"?this.note(s,o.bag.load(i,{refit:n})):o||(this.parked.set(s,structuredClone(i)),n?this.parkedRefit.add(s):this.parkedRefit.delete(s))}}saveFragment(){return{version:ue,scopes:this.save()}}loadFragment(e){if(e.version!==ue)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${ue})`);this.load(e.scopes)}assertFree(e,t){let n=this.scopes.get(e);if(!n)return;let s=n.owner!==void 0?` by ${n.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function Oe(r){return r.kind==="foreign"?[...r.decls.entries()]:r.bag.declarations().map(e=>[r.bag.normalise(e.name),e])}function Rt(r){return r.kind==="foreign"?r.norm:e=>r.bag.normalise(e)}function A(r){if(r.default!==void 0)return r.default;switch(r.type){case"boolean":return!1;case"number":return 0;case"string":case"text":case"multiline":return"";case"enum":return r.values?.[0]??"";case"flags":return[];case"quality":return r.stages?.[0]??"";default:return!1}}var Ne=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function L(r){return r.ctx.host??{}}var ge={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(r,e){if(r.length!==2)throw new u("random(a, b) requires exactly 2 arguments");let t=L(e).nextRandom;if(!t)throw new u("random() called without a PRNG in context");let n=e.evaluate(r[0]),s=e.evaluate(r[1]);if(typeof n!="number"||typeof s!="number")throw new u("random(a, b) arguments must be numbers");if(!Number.isInteger(n)||!Number.isInteger(s))throw new u("random(a, b) arguments must be integers");let i=Math.min(n,s),o=Math.max(n,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:Ae("check_flags"),eval(r,e){let t=Be(r[0],e,"check_flags");for(let n=1;n<r.length;n++){let s=r[n];if(s.kind!=="flagdelta")throw new u("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:Ae("set_flags"),eval(r,e){let t=[...Be(r[0],e,"set_flags")];for(let n=1;n<r.length;n++){let s=r[n];if(s.kind!=="flagdelta")throw new u("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:X("visits"),eval:(r,e)=>L(e).visits?.(Y(r,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:X("seen"),eval:(r,e)=>(L(e).visits?.(Y(r,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:X("patter_visits"),eval:(r,e)=>L(e).patterVisits?.(Y(r,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:X("patter_seen"),eval:(r,e)=>(L(e).patterVisits?.(Y(r,e,"patter_seen"))??0)>0}}},wn=Ne.map(r=>r.token).filter(r=>r!=="patter"&&r!=="scene");function Fe(r,e){let t=r.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function Y(r,e,t){let n=e.evaluate(r[0]);if(typeof n!="string")throw new u(`${t}(id) requires a string node id`);return n}function X(r){return(e,t)=>{let n=e[0];n&&n.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(id): the argument must be a string id literal (a scene / block / node id)`})}}function Be(r,e,t){if(!r)throw new u(`${t}() requires at least one argument (the flags variable)`);let n=e.evaluate(r);if(Array.isArray(n))return n;if(n===!1||n===null||n===void 0)return[];throw new u(`${t}() first argument must be a flags property`)}function Ae(r){return(e,t)=>{if(e.length===0)return;let n=e[0];if(n.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(n.scope)?.get(n.name);if(s&&s.type!=="flags"){let i=n.scope===t.defaultScope?n.name:`${n.scope}.${n.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(): \'@${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:`${r}(): 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:`${r}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var Dt=/^@[A-Za-z0-9_.]+$/;function*Ct(r){let e="",t=0;for(;t<r.length;){let n=r[t];if(n==="{"&&r[t+1]==="{"){e+="{",t+=2;continue}if(n==="}"&&r[t+1]==="}"){e+="}",t+=2;continue}if(n==="{"){let s=r.indexOf("}",t+1);if(s!==-1){let i=r.slice(t,s+1),o=r.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:Dt.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=n,t+=1}e&&(yield{kind:"text",value:e})}function It(r){return Array.isArray(r)?r.join(", "):typeof r=="boolean"?r?"true":"false":String(r)}function Ot(r){return r===" "||r===" "||r===`\n`||r==="\\r"||r==="\\f"||r==="\\v"}function Pt(r){let e="",t=!1;for(let n of r){if(Ot(n)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=n}return e}function Te(r,e,t){if(e.length===0||r.indexOf(e)<0)return r;let n="",s=0,i=!1;for(;s<r.length;){if(r.startsWith(e,s)){let o=r.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}n+=r.slice(s);break}n+=r[s],s+=1}return i?Pt(n):r}function Le(r,e){if(r.indexOf("{")<0)return r;let t="";for(let n of Ct(r)){if(n.kind==="text"){t+=n.value;continue}if(!n.ref){t+=n.raw;continue}let s=e(n.ref);t+=s===void 0?"":It(s)}return t}function C(r,e){for(let t of r){e(t);let n=t.children;n&&C(n,e)}}function Nt(r){return r.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function m(r){let e=r.gameId?.trim();return e||Nt(r.name)}function fe(r){let e=new Map;for(let[t,n]of Object.entries(r))e.set(m(n),t);return e}function Z(r,e,t){return(t??fe(r)).get(e)??(Object.hasOwn(r,e)?e:void 0)}var E=.6;function ee(r){return`cast:${r}`}function te(r){return`qualifier:${r}`}function je(r,e){return`speaker:${r}:${e}`}var Me="speaker:pattern",Ve="{name} ({qualifier})";function Ge(r,e,t){return r.split("{name}").map(n=>n.split("{qualifier}").join(t)).join(e)}var he={open:"[",close:"]"},qe="SFX";var j=3;function M(r){let e=new Set,t=[];for(let n of r)e.has(n)||(e.add(n),t.push(n));return t}function ne(r){let e=new Map,t=(n,s)=>{let i=M([...s,...n.tags??[]]);if(e.set(n.id,i),n.type==="group"&&n.prompt&&e.set(n.prompt.id,M([...i,...n.prompt.tags??[]])),n.type==="group")for(let o of n.children)t(o,i);else for(let o of n.beats??[])e.set(o.id,M([...i,...o.tags??[]]))};for(let n of Object.values(r.scenes)){let s=M(n.tags??[]);e.set(n.id,s);for(let i of n.blocks){let o=M([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var $e=r=>r.kind==="line"||r.kind==="text",V=r=>typeof r=="number"&&Number.isFinite(r)?r:void 0;function _e(r){let e=new Map,t=(i,o,a)=>{let c=i.kind==="gameEvent"?void 0:V(i.padAfter),l=c??o;a&&l<0&&(l=0),e.set(i.id,c!==void 0?{own:c,resolved:l}:{resolved:l})},n=(i,o)=>{let a=V(i.padAfterDefault)??o;if(i.type==="group"){i.prompt&&t(i.prompt,a,!1);for(let d of i.children)n(d,a);return}let c=i.beats??[],l=-1;c.forEach((d,h)=>{$e(d)&&(l=h)}),c.forEach((d,h)=>{$e(d)&&t(d,a,h===l)})},s=V(r.padAfterDefault)??E;for(let i of Object.values(r.scenes)){let o=V(i.padAfterDefault)??s;for(let a of i.blocks){let c=V(a.padAfterDefault)??o;for(let l of a.children)n(l,c)}}return e}var He=new WeakMap,Ue=new WeakMap;function Bt(r){let e=Ue.get(r);return e||(e=ne(r),Ue.set(r,e)),e}var We=new WeakMap;function At(r){let e=We.get(r);return e||(e=_e(r),We.set(r,e)),e}var Ke=new WeakMap;function Ft(r){let e=Ke.get(r);if(e)return e;e=new Map;for(let t of Object.values(r.scenes))for(let n of t.blocks)C(n.children,s=>{if(s.type==="group"){s.prompt&&e.set(s.prompt.id,s.prompt);return}for(let i of s.beats??[])e.set(i.id,i)});return Ke.set(r,e),e}function I(r,e){return r.tagIndex.get(e)?.slice()}var $="Patter",G=r=>r.replace(/%/g,"%25").replace(/\\//g,"%2F"),Tt=r=>r.replace(/%2F/g,"/").replace(/%25/g,"%"),Je=Symbol("ownsRegistry"),w={stage:r=>`patter/scene/${G(r)}`,flow:r=>`patter/flow/${G(r)}/`,flowGlobals:r=>`patter/flow/${G(r)}/patter`,flowScene:(r,e)=>`patter/flow/${G(r)}/scene/${G(e)}`},H=class{constructor(e){this.journal=e}journal},re=class r{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 n=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=n;let i=s[n]??{},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 p of e.cast??[])p.displayName&&l.set(p.name,p.displayName);let d=new Map;for(let p of e.qualifiers??[])p?.gameId&&d.set(p.gameId,p.name);let h=new Map;for(let p of e.cast??[])for(let[g,S]of Object.entries(p.qualifierLabels??{}))typeof S=="string"&&h.set(`${p.name}\\0${g}`,S);let f=e.speakerPattern??Ve;this.defaultSeed=(t.seed??2654435769)>>>0;let ve=new Map,be=new Map,xe=new Map;for(let[p,g]of Object.entries(e.scenes)){this.sceneGameIdToId.set(m(g),p);let S=new Map;for(let y of g.blocks)be.set(y.id,{sceneId:p}),xe.set(y.id,y),S.set(m(y),y.id),C(y.children,K=>ve.set(K.id,K));this.blockGameIdToId.set(p,S)}let ke=e.properties??[],se=ke.filter(p=>p.shared??!0).map(Xe),pt=ke.filter(p=>!(p.shared??!0)).map(Xe),ut=new Set(se.map(p=>p.name.toLowerCase())),x=t.registry??new z,Ee=!t.registry||t[Je]===!0,Re=new k(se,{pathPrefix:"@patter."}),ie=[],W=[];try{x.mountOwned("patter",Re,{owner:$}),W.push("patter");let p=new Set;for(let[g,S]of Object.entries(Ze(t))){let y=e.scopeRegistry?.scopes.find(gt=>gt.token===g),K=(y?.declarations??[]).map(rt);x.defineForeign(g,S,K,{writable:y?.writable??!0,owner:$}),W.push(g),p.add(g)}for(let g of Ee?e.scopeRegistry?.scopes??[]:[]){if(p.has(g.token)||x.has(g.token))continue;let S=(g.declarations??[]).map(y=>Mt(y,g.writable));x.defineOwned(g.token,S,{owner:$}),W.push(g.token),ie.push(g.token)}}catch(p){for(let g of W)x.remove(g,{keep:!0});throw p}let De=new Map;for(let[p,g]of Object.entries(e.scenes)){let S=new Set((g.sceneProps??[]).filter(y=>y.shared??!1).map(y=>y.name.toLowerCase()));De.set(p,S)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(p,g,S)=>this.emitEngine(p,g,S),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,qualifierDisplay:d,speakerLabels:h,speakerPattern:f,nodeIndex:ve,blockIndex:be,blockById:xe,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:Bt(e),padIndex:At(e),registry:x,ownsRegistry:Ee,patterBag:Re,hostScopes:ie,patterSharedDecls:se,patterLocalDecls:pt,patterSharedNames:ut,sceneSharedNames:De,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(p=>console.warn(`Patterplay: ${p.kind} on ${p.node} in flow \'${p.flow}\' failed, played through: ${p.message}`)),reportRefits:()=>{let p=x.takeMisfits(g=>g==="patter"||g.startsWith("patter/")||ie.includes(g));if(p.length!==0)if(t.onRefit)t.onRefit(p);else for(let g of p)console.warn(`Patterplay: the save\'s ${g.scope}.${g.name} was ${JSON.stringify(g.value)}, which no longer fits its declaration; it is back at its default`)},replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??he).open,captionClose:(e.closedCaptions??he).close,captionCharacter:e.closedCaptions?.character||qe,refSplitCache:new Map,refSplitRevision:x.revision},this.host.reportRefits()}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(),n=c=>(c.setLocale(this.currentLocale),c.setClosedCaptions(this.host.captionsOn),c),s=()=>new r(e,{...this.creationOptions,registry:this.host.registry,[Je]:this.host.ownsRegistry}),i=new Set(["patter",...this.host.hostScopes]),o=this.host.ownsRegistry?void 0:Object.fromEntries(Object.entries(this.host.registry.save()).filter(([c])=>i.has(c)||c.startsWith("patter/")));this.release(!0),o&&this.host.registry.load(o,{keepParked:!0,refit:!0});let a=s();try{return a.loadGame(t),n(a)}catch{a.release(!0);let c=this.host.ownsRegistry?t.registry:o;c&&this.host.registry.load(c,{keepParked:!0,refit:!0});let l=s();for(let[d,h]of Object.entries(t.flows)){let f=h.cursor.currentSceneId;try{l.openFlow(d,f!==null?{scene:f}:{})}catch{}}return n(l)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:n,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 O(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(n,s),a}runFlow(e,t,n){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:n}).advanceToStop().played;if(!s.goto(t,n))throw new Error(`runFlow: address not found: ${t}${n===void 0?"":` / ${n}`}`);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 n=me(this.host,e);if(n===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:n};let s=ye(this.host,n,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:n,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 me(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:ye(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?m(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?m(t):void 0}tagsForBeat(e){return I(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?I(this.host,t):void 0)??[]}tagsForBlock(e,t){let n=this.resolveSceneRef(e),s=this.resolveBlockRef(n,t);return(s!=null?I(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let n=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(n!=null?this.host.blockById.get(n):void 0)?.gameData}}extensionsForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.extensions}}extensionsForBlock(e,t){let n=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(n!=null?this.host.blockById.get(n):void 0)?.extensions}}extensionsForSnippet(e){let t=this.host.nodeIndex.get(e);return t?.type==="snippet"?{...t.extensions}:{}}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),n=t!=null?this.host.bundle.scenes[t]:void 0;if(!n)return[];let s=new Set;for(let i of n.blocks)Qe(i.children,s);return[...s]}castForBlock(e,t){let n=this.resolveSceneRef(e),s=this.resolveBlockRef(n,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Qe(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...m(e)?{gameId:m(e)}:{},name:e.name,...ze(e.gameData),...b(e.extensions),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...m(t)?{gameId:m(t)}:{},name:t.name,...ze(t.gameData),...b(t.extensions),...this.tagsField(t.id),children:t.children.map(n=>this.outlineNode(n))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let n of t.blocks)C(n.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:n.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=I(this.host,e.id),n={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){n.character=e.character;let i=this.host.defaultStrings[ee(e.character)]??this.host.castDisplay.get(e.character);i!==void 0&&(n.characterName=i)}if(e.direction!==void 0&&(n.direction=e.direction),e.qualifier!==void 0){n.qualifier=e.qualifier;let i=this.host.defaultStrings[te(e.qualifier)]??this.host.qualifierDisplay.get(e.qualifier);i!==void 0&&(n.qualifierName=i)}let s=q(this.host,e.character,e.qualifier,!0);s!==void 0&&(n.speakerLabel=s)}if(e.kind==="line"||e.kind==="text"){let s=this.host.padIndex.get(e.id);n.padAfter=s?.resolved??E,s?.own!==void 0&&(n.ownPadAfter=s.own);let i=this.host.defaultStrings[e.id];i!==void 0&&(n.text=i)}return e.gameData&&Object.keys(e.gameData).length&&(n.gameData=e.gameData),e.extensions&&Object.keys(e.extensions).length&&(n.extensions=e.extensions),t&&t.length&&(n.tags=t),n}tagsField(e){let t=I(this.host,e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,n]of this.host.stageBags)e.push({bag:n,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 H(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let n=t.length-1;n>=0;n--)t[n]();r.forget(e.journal)}commit(e){this.endCheckpoint(e),r.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}captureFlow(e){let t=this.flowsById.get(e);if(!t)throw new Error(`no open flow \'${e}\' to capture`);return{version:j,...t.snapshot(),...Gt(e,this.host.registry.save())}}restoreFlow(e,t){this.refuseInCheckpoint("restoreFlow");let n=t.version;if(n!==2&&n!==j)throw new Error(`unsupported flow state version: ${String(n)}`);let s=t.cursor;if(typeof s!="object"||s===null)throw new Error("malformed flow state: no cursor");if(this.flowsById.has(e))throw new Error(`a flow \'${e}\' is already open; close it before restoring another under that id`);this.assertExternalScopes();let i=w.flow(e);this.host.registry.discardParked(i),this.host.registry.load(it(e,t),{keepParked:!0,refit:!0});let o=new O(e,this.host,this.defaultSeed);try{o.restore(t)}catch(a){throw o.close(),this.host.registry.discardParked(i),a}return this.flowsById.set(e,o),this.host.reportRefits(),o}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(w.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let n of this.flowsById.values())n.releaseBags(e),n.close();this.flowsById.clear();let t=this.host.registry;for(let n of this.host.stageBags.keys())t.remove(w.stage(n),{keep:e});this.host.stageBags.clear();for(let n of["patter",...this.host.hostScopes])t.has(n)&&t.remove(n,{keep:e});for(let n of Object.keys(Ze(this.creationOptions)))t.has(n)&&t.remove(n)}getProperty(e){let{scope:t,name:n}=this.splitShared(e);return this.host.registry.get(t,n)}setProperty(e,t){let{scope:n,name:s}=this.splitShared(e),i=this.host.journal?this.host.registry.get(n,s):void 0;this.host.registry.set(n,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(n,s,i,{host:!0}))}stageIndex(e,t){return P(this.ladderOf(e),t)}compareStages(e,t,n){return N(this.ladderOf(e),t,n)}nextStage(e,t){return B(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:n}=this.splitShared(e),s=jt(this.host,t,n);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}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,n){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...n?{scene:n}:{}})}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:A(e),writable:e.writable??!0}))}splitShared(e){let t=st(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,n]of this.flowsById)e[t]=n.snapshot();return{version:j,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:tt(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==j)throw new Error(`unsupported save version: ${String(t)}`);let n=e.flows;if(typeof n!="object"||n===null||Array.isArray(n))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(w.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.version===2?Vt(e):e.registry;i&&(this.host.ownsRegistry?s.load(i,{refit:!0}):s.load(i,{keepParked:!0,refit:!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 nt(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new O(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}this.host.reportRefits()}},O=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 n=this.currentSceneId;if(n===null)return;let s=this.sceneBagFor(n,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,n){this.id=e,this.host=t,this.rngState=T(n),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(),n=this.evalCtx.scopes;for(let s of Object.keys(n))delete n[s];Object.assign(n,t.scopes),n.patter=this.patterResolver,n.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let n=t.toLowerCase();if(e==="patter")return _(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(n);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:_(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(n)}return this.registryQualities?.(e,t)??_(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(n)}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(w.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 n=this.host.blockIndex.get(t);if(!n)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(n.sceneId),this.stack=[{sceneId:n.sceneId,containerId:t,index:0}],this.enter(t)}else{let n=e??Object.keys(this.host.bundle.scenes)[0],s=n?this.host.bundle.scenes[n]:void 0;if(!s)throw new Error(n?`unknown scene: ${n}`:"no scenes in bundle");this.enterSceneSetup(n);let i=s.blocks[0];i&&(this.stack=[{sceneId:n,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 n=me(this.host,e);if(n===void 0)return!1;let s;return t!==void 0&&(s=ye(this.host,n,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??n,"jump"),this.settle(),!0):(this.begin(n,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:n,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:d}=this,h=this.stack.map(f=>({...f}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:n,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:d,stack:h})})}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 n=this.childrenOf(t.containerId);if(!n){this.stack.pop();continue}let s=t.index;for(;t.index<n.length&&!this.eligible(n[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:n.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:n[t.index]?.id??null}),t.index>=n.length){this.stack.pop();continue}this.enterChild(n[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,n]of this.sceneBags)e.push({bag:n,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)}stepFor(e){let t=Ft(this.host.bundle).get(e);return t?this.beatResult(t):void 0}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let n=t.options.find(o=>o.id===e);if(!n)throw new Error(`unknown choice option: ${e}`);if(!n.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&&n.prompt?{beat:i,shown:{...n.prompt}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:n}=this.splitRef(e);return t==="patter"?this.patterResolver.get(n):t==="scene"?this.sceneResolver.get(n):this.host.registry.get(t,n)}setProperty(e,t){this.writeProperty(e,t,!0)}stageIndex(e,t){return P(this.ladderOf(e),t)}compareStages(e,t,n){return N(this.ladderOf(e),t,n)}nextStage(e,t){return B(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:n}=this.splitRef(e);this.context();let s=this.stagesFor(t,n);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}writeProperty(e,t,n){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,n?{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(({extensions:e,...t})=>({...t}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:Lt(this.pendingPrompt.shown)}:{},selectors:tt(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(n=>n.sceneId)]))t!==null&&this.host.bundle.scenes[t]&&this.ensureSceneBags(t)}restoreCursor(e){this.rngState=T(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(n=>{let{nextId:s,...i}=n;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 n=this.host.nodeIndex.get(t.activeSnippetId);n&&n.type==="snippet"&&(this.activeSnippet=n)}if(this.selectors=nt(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let n=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);if(!o)continue;n.set(i.id,o);let{prompt:a,extensions:c,...l}=i,d=a?.kind==="line"&&a.speakerLabel===void 0?q(this.host,a.character,a.qualifier):void 0;s.push({...l,...a?{prompt:{...a,padAfter:a.padAfter??this.promptPadOf(o),...d!==void 0?{speakerLabel:d}:{}}}:{},...b(o.extensions)})}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:n})}if(this.pendingPrompt=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let n=this.host.nodeIndex.get(this.pendingPromptOwnerId),s=n?this.authoredPromptOf(n):void 0,i=t.pendingPrompt,o=i?.kind==="line"&&i.speakerLabel===void 0?q(this.host,i.character,i.qualifier):void 0;s?this.pendingPrompt={beat:s,...i?{shown:{...i,padAfter:i.padAfter??this.padOf(s.id),...o!==void 0?{speakerLabel:o}:{}}}:{}}: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 n=this.selectChild(e);n&&this.enterChild(n)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let n=this.host.nodeIndex.get(e);if(n&&n.type==="group")return n.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],n=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,...b(o.extensions)}),n.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:n};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 n,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}n=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),n=a.sceneId,s=e}this.enter(s);let o={sceneId:n,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)})),n=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(n.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(n[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(n,i,a):s==="specificity"?this.pickSpecificity(n,i,a):this.pickSequential(n,i,a));default:return null}}pickSequential(e,t,n){let s=e.length,i=n.seq??0;return n.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,n){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(f=>f.id),a=new Set(e.map(f=>f.id));if(n.bag===void 0&&(n.bag=o()),!n.bag.some(f=>a.has(f))){if(t==="once")return null;if(i){let f=e[s-1];return n.last=f.id,f}if(n.bag=o(),n.bag.length===0)return null}let c=n.bag.filter(f=>a.has(f)),l=n.last!==void 0&&c.length>1?c.indexOf(n.last):-1,d=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&d>=l&&d++;let h=c[d];return n.bag.splice(n.bag.indexOf(h),1),n.last=h,e.find(f=>f.id===h)}pickSpecificity(e,t,n){let s=e;if(t!=="repeat"){n.bag===void 0&&(n.bag=e.map(d=>d.id));let l=new Set(n.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&n.last!==void 0?e.find(d=>d.id===n.last)??null:null}let i=-1,a=s.map(l=>{let d=this.specScore(l);return d>i&&(i=d),{c:l,s:d}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=n.last!==void 0?a.findIndex(h=>h.id===n.last):-1,d=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&d>=l&&d++,c=a[d]}return t!=="repeat"&&(n.bag=n.bag.filter(l=>l!==c.id)),n.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,n){return Ie(e,i=>{try{return et(J(i,this.context(),ge))}catch(o){return this.reportError("best-match",n.id,n.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,n=this.host.journal,s;if(n&&(s=e.shared?n.selectors:n.flowSelectors.get(this),s||(s=new Set,n.flowSelectors.set(this,s))),n&&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;n.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 n of e??[]){let s;try{s=this.evalExpr(n.value);let i=this.host.tracing?this.getProperty(n.target):void 0;this.writeProperty(n.target,s,!1),this.host.tracing&&this.emit({type:"write",target:n.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,n.value,i)}}}eligible(e){if(!e.condition)return!0;try{return et(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,n,s){let i=s instanceof Error?s.message:String(s),o=n?.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 J(this.conditionAst(e),this.context(),ge)}conditionAst(e){let t=He.get(e);return t||(t=R(e.ast),He.set(e,t)),t}enter(e){let t=this.visitCounts,n=t.get(e);t.set(e,(n??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(()=>{n===void 0?t.delete(e):t.set(e,n),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=ce(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=I(this.host,e.id),n=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...b(e.extensions),...n};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),padAfter:this.padOf(e.id),...e.action?{action:!0}:{},gameData:e.gameData,...b(e.extensions),...n};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,qualifier:c?void 0:e.qualifier,qualifierName:c?void 0:this.resolveQualifierName(e.qualifier),speakerLabel:c?void 0:q(this.host,e.character,e.qualifier),padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...n}}}}interpolate(e){return Le(e,t=>this.getProperty(t))}stripCaptions(e){return Te(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 n=this.interpolate(this.resolveString(t.id)),s=this.padOf(t.id);return t.kind==="line"?{kind:"line",text:this.captionLine(n),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction,qualifier:t.qualifier,qualifierName:this.resolveQualifierName(t.qualifier),speakerLabel:q(this.host,t.character,t.qualifier),padAfter:s}:{kind:"text",text:n,padAfter:s}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let n=I(this.host,e.id),s=n&&n.length?{tags:n}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,qualifier:t.qualifier,qualifierName:t.qualifierName,speakerLabel:t.speakerLabel,padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(n=>n.kind==="line"||n.kind==="text")}firstTextSnippetIn(e){let t;return C(e,n=>{!t&&n.type==="snippet"&&(n.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=n)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let n=this.host.defaultStrings[e];return n!==void 0?`<Untranslated: ${e}> ${n}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=ee(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}promptPadOf(e){let t=this.promptBeatOf(e);return t?this.padOf(t.id):E}padOf(e){return this.host.padIndex.get(e)?.resolved??E}resolveQualifierName(e){if(e===void 0||this.host.emitIds)return;let t=te(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.qualifierDisplay.get(e)}splitRef(e){return st(this.host,e)}newLocal(){return new k(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=w.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:$}),this.registered.add(e),this.host.reportRefits()}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,n=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal,i=!this.sceneBags.has(e)||!this.host.stageBags.has(e);if(!this.sceneBags.has(e)){let o=new k(n.filter(l=>!t.has(l.name.toLowerCase())),{pathPrefix:"@scene."}),a=w.flowScene(this.id,e),c=Ye(this.host.registry,a,o,s!==null);this.registered.add(a),this.sceneBags.set(e,o),s&&s.undo.push(()=>{this.sceneBags.get(e)===o&&(this.host.registry.remove(a,c?{keep:!0}:void 0),this.registered.delete(a),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let o=new k(n.filter(c=>t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),a=Ye(this.host.registry,w.stage(e),o,s!==null);this.host.stageBags.set(e,o),s&&s.undo.push(()=>{this.host.stageBags.get(e)===o&&(this.host.registry.remove(w.stage(e),a?{keep:!0}:void 0),this.host.stageBags.delete(e))})}i&&this.host.reportRefits()}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let n of e.sceneProps??[]){if(!n.temporary)continue;let s=n.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,A(n))}}};function Qe(r,e){C(r,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let n of t.beats??[])n.kind==="line"&&n.character&&e.add(n.character)})}function q(r,e,t,n=!1){if(!e||r.emitIds)return;let s=c=>(n?void 0:r.strings[c])??r.defaultStrings[c],i=s(ee(e))??r.castDisplay.get(e)??e;if(t===void 0)return i;let o=r.speakerLabels.get(`${e}\\0${t}`);if(o!==void 0)return s(je(e,t))??o;let a=s(te(t))??r.qualifierDisplay.get(t)??t;return Ge(s(Me)??r.speakerPattern,i,a)}function ze(r){return r&&Object.keys(r).length?{gameData:r}:{}}function b(r){return r&&Object.keys(r).length?{extensions:r}:{}}function Lt(r){let e={kind:r.kind,text:r.text};return r.character!==void 0&&(e.character=r.character),r.characterName!==void 0&&(e.characterName=r.characterName),r.direction!==void 0&&(e.direction=r.direction),r.qualifier!==void 0&&(e.qualifier=r.qualifier),r.qualifierName!==void 0&&(e.qualifierName=r.qualifierName),r.speakerLabel!==void 0&&(e.speakerLabel=r.speakerLabel),typeof r.padAfter=="number"&&(e.padAfter=r.padAfter),e}function tt(r){let e={};for(let[t,n]of r){let s={};n.seq!==void 0&&(s.seq=n.seq),n.bag&&(s.bag=[...n.bag]),n.last!==void 0&&(s.last=n.last),e[t]=s}return e}function nt(r){let e=new Map;for(let[t,n]of Object.entries(r??{})){let s={};n.seq!==void 0&&(s.seq=n.seq),n.bag&&(s.bag=[...n.bag]),n.last!==void 0&&(s.last=n.last),e.set(t,s)}return e}function Ye(r,e,t,n){let s=n?JSON.stringify(t.save()):"";return r.mountOwned(e,t,{owner:$}),n&&JSON.stringify(t.save())!==s}function Xe(r){return{name:r.name,type:r.type,values:r.values,stages:r.stages,default:r.default}}function Ze(r){return{...r.world?{world:r.world}:{},...r.hostScopes}}function _(r,e,t){let n=r.qualityLadders.get(e);if(!n){n=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!n.has(i)&&n.set(i,s.stages)}r.qualityLadders.set(e,n)}return n}function jt(r,e,t){let n=t.toLowerCase();return e==="patter"?_(r,"patter",()=>[...r.patterSharedDecls,...r.patterLocalDecls]).get(n):r.registry.toEvalContext().qualities?.(e,t)??_(r,`host/${e}`,()=>r.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(n)}function rt(r){return{name:r.name,type:r.type,values:r.values,stages:r.stages,default:r.default,writable:r.writable}}function Mt(r,e){let t=rt(r),n=r.writable??e;return n===void 0?t:{...t,writable:n}}function me(r,e){return Z(r.bundle.scenes,e,r.sceneGameIdToId)}function ye(r,e,t){return r.blockGameIdToId.get(e)?.get(t)??(r.blockIndex.get(t)?.sceneId===e?t:void 0)}function st(r,e){r.refSplitRevision!==r.registry.revision&&(r.refSplitCache.clear(),r.refSplitRevision=r.registry.revision);let t=r.refSplitCache.get(e);return t||(t=Fe(e,n=>n==="scene"||r.registry.has(n)||(r.bundle.externalScopes?.includes(n)??!1)),r.refSplitCache.set(e,t)),t}function Vt(r){let e={};r.shared?.patter&&(e.patter=r.shared.patter);for(let[t,n]of Object.entries(r.stageBags??{}))e[w.stage(t)]=n;for(let[t,n]of Object.entries(r.flows??{}))Object.assign(e,it(t,n));return e}function it(r,e){let t={};e.scopes?.patter&&(t[w.flowGlobals(r)]=e.scopes.patter);for(let[n,s]of Object.entries(e.sceneBags??{}))t[w.flowScene(r,n)]=s;return t}function Gt(r,e){let t={scopes:{},sceneBags:{}},n=w.flow(r);for(let[s,i]of Object.entries(e)){if(!s.startsWith(n))continue;let o=s.slice(n.length);o==="patter"?t.scopes.patter=i:o.startsWith("scene/")&&(t.sceneBags[Tt(o.slice(6))]=i)}return t}function et(r){return typeof r=="boolean"?r:typeof r=="number"?r!==0:typeof r=="string"?r!=="":r.length>0}var qt=(r,e)=>r.shared??e;function Se(r,e){return{name:r.name,type:r.type,hasDefault:r.default!==void 0,...r.default!==void 0?{default:r.default}:{},shared:qt(r,e)}}function $t(r){return{name:r.name,type:r.type,hasDefault:r.default!==void 0,...r.values?{values:[...r.values]}:{},...r.purpose?{purpose:r.purpose}:{}}}function _t(r,e){e.blocks++;let t=[...r.children];for(;t.length;){let n=t.pop();if(n.type==="group"){e.groups++,n.prompt&&e.prompts++,t.push(...n.children);continue}e.snippets++;for(let s of n.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function ot(r){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:r.cast?.length??0},t=[],n=[];for(let o of Object.values(r.scenes)){e.scenes++;let a=m(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:m(c),name:c.name}))});for(let c of o.blocks)_t(c,e);o.sceneProps?.length&&n.push({gameId:a,properties:o.sceneProps.map(c=>Se(c,!1))})}let s=(r.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>Se(a,!0))})),i=Object.entries(r.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map($t)}));return{identity:{schema:r.schema,project:r.content.project,...r.content.version!==void 0?{version:r.content.version}:{},...r.content.hash!==void 0?{hash:r.content.hash}:{},...r.content.structureHash!==void 0?{structureHash:r.content.structureHash}:{},voiced:r.voiced,defaultLocale:r.locales.default,locales:[...r.locales.included],localisation:r.localisation?.mode??"embedded",sourceDebug:r.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(r.properties??[]).map(o=>Se(o,!0)),scene:n},gameData:i,counts:e}}var U=(r,e)=>Object.prototype.hasOwnProperty.call(r,e);function at(r,e){return r.gameDataFields?.[e]??[]}function we(r,e,t){return e&&U(e,t)?e[t]:r.find(n=>n.name===t)?.default}function ct(r,e){let t={};for(let n of r){let s=we(r,e,n.name);s!==void 0&&(t[n.name]=s)}for(let[n,s]of Object.entries(e??{}))U(t,n)||(t[n]=s);return t}function lt(r,e,t){let n=r.extensions!==void 0&&U(r.extensions,e)?r.extensions[e]:void 0;return n!==void 0&&U(n.fields,t)?n.fields[t]??[]:[]}function dt(r,e,t=""){let n=o=>({...o?.extensions??{}}),s=Object.values(r.scenes??{}),i=o=>(U(r.scenes??{},o)?r.scenes[o]:void 0)??s.find(a=>m(a)===o);switch(e){case"project":{let o={};for(let[a,c]of Object.entries(r.extensions??{}))c.project!==void 0&&(o[a]=c.project);return o}case"scene":return n(i(t));case"block":{let o=t.indexOf("/");if(o<0)return{};let a=i(t.slice(0,o))?.blocks??[],c=t.slice(o+1);return n(a.find(l=>l.id===c)??a.find(l=>m(l)===c))}case"snippet":case"option":case"line":case"text":case"gameEvent":{let o,a=(c,l)=>{for(let d of c??[]){if(o!==void 0)return;if(d.type==="group"){if(l&&d.id===t&&e==="option"){o=d;return}if(l&&d.prompt?.id===t&&d.prompt.kind===e){o=d.prompt;return}a(d.children??[],d.selector==="choice");continue}if(d.id===t&&e==="snippet"){o=d;return}let h=(d.beats??[]).find(f=>f.id===t);if(h!==void 0){h.kind===e&&(o=h);return}}};for(let c of s)for(let l of c.blocks??[])o===void 0&&a(l.children??[],!1);return n(o)}default:return{}}}return wt(Ht);})();\n';
2632
+ var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var de=Object.defineProperty;var vt=Object.getOwnPropertyDescriptor;var xt=Object.getOwnPropertyNames;var kt=Object.prototype.hasOwnProperty;var Et=(r,e)=>{for(var t in e)de(r,t,{get:e[t],enumerable:!0})},Ct=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of xt(e))!kt.call(r,s)&&s!==t&&de(r,s,{get:()=>e[s],enumerable:!(n=vt(e,s))||n.enumerable});return r};var Dt=r=>Ct(de({},"__esModule",{value:!0}),r);var Zt={};Et(Zt,{Checkpoint:()=>K,DEFAULT_PAD_AFTER:()=>O,Engine:()=>ae,Flow:()=>N,buildTagIndex:()=>ie,compareStages:()=>B,describeBundle:()=>ut,effectiveGameData:()=>ft,extensionDecls:()=>ht,extensionsOf:()=>mt,gameDataFields:()=>gt,gameDataValue:()=>ke,nextStage:()=>F,resolveSceneRef:()=>ne,sceneAddressIndex:()=>ye,stageIndex:()=>A});function j(r,e){return r!==void 0&&Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0}function I(r){switch(r[0]){case"b":return{kind:"bool",value:r[1]};case"n":return{kind:"number",value:r[1]};case"s":return{kind:"string",value:r[1]};case"sv":return{kind:"scopedvar",scope:r[1],name:r[2]};case"u":return{kind:"unary",op:r[1],operand:I(r[2])};case"bin":return{kind:"binary",op:r[1],left:I(r[2]),right:I(r[3])};case"call":{let e=r.slice(2).map(I);return{kind:"call",name:r[1],args:e}}case"fd":return{kind:"flagdelta",sign:r[1],name:r[2]}}}var u=class extends Error{constructor(e){super(e),this.name="EvalError"}};function Y(r,e,t){let n=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=j(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):j(o,i.name);if(a===void 0){if(n.get(i.scope)==="throw")throw new u(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!j(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new u(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=pe(a,e);if(c===void 0)throw new u("advance() needs a quality reference (@scope.name of a quality property)");let l=v(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=j(t.functions,i.name);if(!o)throw new u(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new u("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new u(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new u(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let g=s(i.left);if(typeof g!="boolean")throw new u(`\'and\' requires boolean operands, left is ${typeof g}`);if(!g)return!1;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'and\' requires boolean operands, right is ${typeof f}`);return f}if(i.op==="or"){let g=s(i.left);if(typeof g!="boolean")throw new u(`\'or\' requires boolean operands, left is ${typeof g}`);if(g)return!0;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'or\' requires boolean operands, right is ${typeof f}`);return f}let o=s(i.left),a=s(i.right),c=pe(i.left,e),l=pe(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!It(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new u(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return v(o,d,">")>v(a,d,">");case">=":return v(o,d,">=")>=v(a,d,">=");case"<":return v(o,d,"<")<v(a,d,"<");case"<=":return v(o,d,"<=")<=v(a,d,"<=");case"+":case"-":case"*":case"/":throw new u(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return Pe(o,a);case"!=":return!Pe(o,a);case">":return R(o,a,">"),o>a;case">=":return R(o,a,">="),o>=a;case"<":return R(o,a,"<"),o<a;case"<=":return R(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new u(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return R(o,a,"-"),o-a;case"*":return R(o,a,"*"),o*a;case"/":if(R(o,a,"/"),a===0)throw new u("division by zero");return o/a}}}};return s(r)}function Pe(r,e){if(Array.isArray(r)||Array.isArray(e)){if(!Array.isArray(r)||!Array.isArray(e)||r.length!==e.length)return!1;let t=[...r].sort(),n=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==n[s])return!1;return!0}return r===e}function R(r,e,t){if(typeof r!="number"||typeof e!="number")throw new u(`\'${t}\' requires numeric operands, got ${typeof r} and ${typeof e}`)}function pe(r,e){if(!(r.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(r.scope,r.name)}function v(r,e,t){if(typeof r!="string")throw new u(`\'${t}\' on a quality compares stages, got ${typeof r}`);let n=e.indexOf(r);if(n<0)throw new u(`"${r}" is not a stage of this quality (stages: ${e.join(", ")})`);return n}var It=(r,e)=>r.length===e.length&&r.every((t,n)=>t===e[n]);function M(r){if(Number.isNaN(r)||!Number.isFinite(r))return 0;let e=Math.trunc(r)%4294967296;return e<0?e+4294967296:e}function ue(r){let e=M(r);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 fe=r=>Array.isArray(r)?r:r.stages??[],Rt=r=>Array.isArray(r)?"flags":r==null?"nothing":typeof r;function A(r,e){return typeof e=="string"?fe(r).indexOf(e):-1}function ge(r,e){if(typeof e!="string")throw new u(`a stage is a name, got ${Rt(e)}`);let t=r.indexOf(e);if(t<0)throw new u(`"${e}" is not a stage of this quality (stages: ${r.join(", ")})`);return t}function B(r,e,t){let n=fe(r),s=ge(n,e)-ge(n,t);return s<0?-1:s>0?1:0}function F(r,e){let t=fe(r);return t[Math.min(ge(t,e)+1,t.length-1)]}function $(r,e,t){switch(r.type){case"boolean":return typeof e=="boolean";case"number":return typeof e=="number";case"string":case"text":case"multiline":return typeof e=="string";case"enum":return typeof e!="string"?!1:e===""&&t?.noneChosen?!0:!Array.isArray(r.values)||r.values.includes(e);case"flags":return Array.isArray(e)&&e.every(n=>typeof n=="string"&&(!Array.isArray(r.values)||r.values.includes(n)));case"quality":return typeof e=="string"&&(!Array.isArray(r.stages)||r.stages.includes(e));default:return!0}}var Ot={name:"check_flags",count:r=>Math.max(1,r.args.length-1)},Pt=[Ot];function Ne(r,e,t){let n=t?.countingCalls??Pt;return X(r,t?.want??!0,e,n)}function X(r,e,t,n){if(r.kind==="binary"&&(r.op==="and"||r.op==="or")){let s=X(r.left,e,t,n),i=X(r.right,e,t,n);return r.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(r.kind==="unary"&&r.op==="not")return X(r.operand,!e,t,n);if(r.kind==="call"){let s=n.find(i=>i.name===r.name);if(s){let i=s.count(r),o=t(r);return e?o?i:0:o?0:1}}return t(r)===e?1:0}var E=class r{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(n=>n.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let n=this.norm(t.name);this.decls.set(n,t),this.values[n]=structuredClone(t.default??L(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,n){let s=this.norm(e);if(!n?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:n?.silent??!1,reason:n?.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])=>Be(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new r([],{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,t){let n=new Map;for(let[i,o]of Object.entries(e)){let a=this.norm(i);this.values[a]=structuredClone(o),n.set(a,o)}let s=[];if(!t?.refit)return s;for(let[i,o]of this.decls){if(!n.has(i))continue;let a=n.get(i);$(o,a,{noneChosen:o.type==="enum"&&o.default===""})||(this.values[i]=structuredClone(L(o)),s.push({name:i,value:a}))}return s}};function Be(r,e,t,n,s=""){let i=n??r.name.toLowerCase();return{name:i,path:s+i,type:r.type,value:e,default:r.default??L(r),...r.values!==void 0?{values:r.values}:{},...r.stages!==void 0?{stages:r.stages}:{},writable:t??r.writable??!0}}var Nt=r=>r.toLowerCase(),he=1,Z=class{scopes=new Map;parked=new Map;parkedRefit=new Set;misfits=[];rev=0;get revision(){return this.rev}defineOwned(e,t,n){let s=typeof n=="string"?{pathPrefix:n}:n??{},i=new E(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,n){this.assertFree(e,n?.owner),this.scopes.set(e,{kind:"owned",bag:t,...n?.owner!==void 0?{owner:n.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(this.note(e,t.load(s,{refit:this.parkedRefit.has(e)})),this.parked.delete(e),this.parkedRefit.delete(e)),this}remove(e,t){let n=this.scopes.get(e);if(!n)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&n.kind==="owned"&&(this.parked.set(e,n.bag.save()),this.parkedRefit.delete(e)),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear(),this.parkedRefit.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&(this.parked.delete(t),this.parkedRefit.delete(t));return this}takeMisfits(e){if(!e){let n=this.misfits;return this.misfits=[],n}let t=this.misfits.filter(n=>e(n.scope));return this.misfits=this.misfits.filter(n=>!e(n.scope)),t}note(e,t){for(let n of t)this.misfits.push({scope:e,name:n.name,value:n.value})}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,n=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??Nt,a=new Map;for(let c of n)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 n=this.scopes.get(e);if(n)return n.kind==="owned"?n.bag.get(t):n.resolver.get(n.norm(t))}set(e,t,n,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,n,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,n)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,n]of this.scopes){let s=n.owner!==void 0?{owner:n.owner}:{};if(n.kind==="owned")for(let i of n.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of n.decls)e.push({scope:t,...s,...Be(o,n.resolver.get(i),this.foreignWritable(n,i),i,`${t}.`)})}return e}toEvalContext(e,t){let n=this.view(t?.aliases),s={};for(let[o,a]of n)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(n);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=n.get(o);return c?i.get(o)?.get(At(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[n,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${n}\' names \'${s}\', which is not registered`);t.set(n,i)}return t}qualityLadders(e){let t=new Map;for(let[n,s]of e)for(let[i,o]of Ae(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(n);a||(a=new Map,t.set(n,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[n,s]of this.view(e?.aliases)){let i=Ae(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(n,o)}return{properties:t}}save(){let e={};for(let[t,n]of this.scopes)n.kind==="owned"&&(e[t]=n.bag.save());for(let[t,n]of this.parked)e[t]=structuredClone(n);return e}load(e,t){t?.keepParked||(this.parked.clear(),this.parkedRefit.clear());let n=t?.refit===!0;for(let[s,i]of Object.entries(e)){let o=this.scopes.get(s);o?.kind==="owned"?this.note(s,o.bag.load(i,{refit:n})):o||(this.parked.set(s,structuredClone(i)),n?this.parkedRefit.add(s):this.parkedRefit.delete(s))}}saveFragment(){return{version:he,scopes:this.save()}}loadFragment(e){if(e.version!==he)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${he})`);this.load(e.scopes)}assertFree(e,t){let n=this.scopes.get(e);if(!n)return;let s=n.owner!==void 0?` by ${n.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function Ae(r){return r.kind==="foreign"?[...r.decls.entries()]:r.bag.declarations().map(e=>[r.bag.normalise(e.name),e])}function At(r){return r.kind==="foreign"?r.norm:e=>r.bag.normalise(e)}function L(r){if(r.default!==void 0)return r.default;switch(r.type){case"boolean":return!1;case"number":return 0;case"string":case"text":case"multiline":return"";case"enum":return r.values?.[0]??"";case"flags":return[];case"quality":return r.stages?.[0]??"";default:return!1}}var Fe=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function G(r){return r.ctx.host??{}}var me={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(r,e){if(r.length!==2)throw new u("random(a, b) requires exactly 2 arguments");let t=G(e).nextRandom;if(!t)throw new u("random() called without a PRNG in context");let n=e.evaluate(r[0]),s=e.evaluate(r[1]);if(typeof n!="number"||typeof s!="number")throw new u("random(a, b) arguments must be numbers");if(!Number.isInteger(n)||!Number.isInteger(s))throw new u("random(a, b) arguments must be integers");let i=Math.min(n,s),o=Math.max(n,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:Te("check_flags"),eval(r,e){let t=Le(r[0],e,"check_flags");for(let n=1;n<r.length;n++){let s=r[n];if(s.kind!=="flagdelta")throw new u("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:Te("set_flags"),eval(r,e){let t=[...Le(r[0],e,"set_flags")];for(let n=1;n<r.length;n++){let s=r[n];if(s.kind!=="flagdelta")throw new u("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:te("visits"),eval:(r,e)=>G(e).visits?.(ee(r,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:te("seen"),eval:(r,e)=>(G(e).visits?.(ee(r,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:te("patter_visits"),eval:(r,e)=>G(e).patterVisits?.(ee(r,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:te("patter_seen"),eval:(r,e)=>(G(e).patterVisits?.(ee(r,e,"patter_seen"))??0)>0}}},Rn=Fe.map(r=>r.token).filter(r=>r!=="patter"&&r!=="scene");function je(r,e){let t=r.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function ee(r,e,t){let n=e.evaluate(r[0]);if(typeof n!="string")throw new u(`${t}(id) requires a string node id`);return n}function te(r){return(e,t)=>{let n=e[0];n&&n.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(id): the argument must be a string id literal (a scene / block / node id)`})}}function Le(r,e,t){if(!r)throw new u(`${t}() requires at least one argument (the flags variable)`);let n=e.evaluate(r);if(Array.isArray(n))return n;if(n===!1||n===null||n===void 0)return[];throw new u(`${t}() first argument must be a flags property`)}function Te(r){return(e,t)=>{if(e.length===0)return;let n=e[0];if(n.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(n.scope)?.get(n.name);if(s&&s.type!=="flags"){let i=n.scope===t.defaultScope?n.name:`${n.scope}.${n.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(): \'@${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:`${r}(): 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:`${r}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var Bt=/^@[A-Za-z0-9_.]+$/;function*Ft(r){let e="",t=0;for(;t<r.length;){let n=r[t];if(n==="{"&&r[t+1]==="{"){e+="{",t+=2;continue}if(n==="}"&&r[t+1]==="}"){e+="}",t+=2;continue}if(n==="{"){let s=r.indexOf("}",t+1);if(s!==-1){let i=r.slice(t,s+1),o=r.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:Bt.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=n,t+=1}e&&(yield{kind:"text",value:e})}function Lt(r){return Array.isArray(r)?r.join(", "):typeof r=="boolean"?r?"true":"false":String(r)}function Tt(r){return r===" "||r===" "||r===`\n`||r==="\\r"||r==="\\f"||r==="\\v"}function jt(r){let e="",t=!1;for(let n of r){if(Tt(n)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=n}return e}function Me(r,e,t){if(e.length===0||r.indexOf(e)<0)return r;let n="",s=0,i=!1;for(;s<r.length;){if(r.startsWith(e,s)){let o=r.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}n+=r.slice(s);break}n+=r[s],s+=1}return i?jt(n):r}function $e(r,e){if(r.indexOf("{")<0)return r;let t="";for(let n of Ft(r)){if(n.kind==="text"){t+=n.value;continue}if(!n.ref){t+=n.raw;continue}let s=e(n.ref);t+=s===void 0?"":Lt(s)}return t}function Ge(r){return r.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function C(r,e){for(let t of r){e(t);let n=t.children;n&&C(n,e)}}function y(r){let e=r.gameId?.trim();return e||Ge(r.name)}function ye(r){let e=new Map;for(let[t,n]of Object.entries(r))e.set(y(n),t);return e}function ne(r,e,t){return(t??ye(r)).get(e)??(Object.hasOwn(r,e)?e:void 0)}var O=.6;function re(r){return`cast:${r}`}function se(r){return`qualifier:${r}`}function Ve(r,e){return`speaker:${r}:${e}`}var qe="speaker:pattern",_e="{name} ({qualifier})";function He(r,e,t){return r.split("{name}").map(n=>n.split("{qualifier}").join(t)).join(e)}var Se={open:"[",close:"]"},Ue="SFX";var V=4;function q(r){let e=new Set,t=[];for(let n of r)e.has(n)||(e.add(n),t.push(n));return t}function ie(r){let e=new Map,t=(n,s)=>{let i=q([...s,...n.tags??[]]);if(e.set(n.id,i),n.type==="group"&&n.prompt&&e.set(n.prompt.id,q([...i,...n.prompt.tags??[]])),n.type==="group")for(let o of n.children)t(o,i);else for(let o of n.beats??[])e.set(o.id,q([...i,...o.tags??[]]))};for(let n of Object.values(r.scenes)){let s=q(n.tags??[]);e.set(n.id,s);for(let i of n.blocks){let o=q([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var We=r=>r.kind==="line"||r.kind==="text",_=r=>typeof r=="number"&&Number.isFinite(r)?r:void 0;function Ke(r){let e=new Map,t=(i,o,a)=>{let c=i.kind==="gameEvent"?void 0:_(i.padAfter),l=c??o;a&&l<0&&(l=0),e.set(i.id,c!==void 0?{own:c,resolved:l}:{resolved:l})},n=(i,o)=>{let a=_(i.padAfterDefault)??o;if(i.type==="group"){i.prompt&&t(i.prompt,a,!1);for(let d of i.children)n(d,a);return}let c=i.beats??[],l=-1;c.forEach((d,g)=>{We(d)&&(l=g)}),c.forEach((d,g)=>{We(d)&&t(d,a,g===l)})},s=_(r.padAfterDefault)??O;for(let i of Object.values(r.scenes)){let o=_(i.padAfterDefault)??s;for(let a of i.blocks){let c=_(a.padAfterDefault)??o;for(let l of a.children)n(l,c)}}return e}var Je=new WeakMap,Qe=new WeakMap;function Mt(r){let e=Qe.get(r);return e||(e=ie(r),Qe.set(r,e)),e}var ze=new WeakMap;function $t(r){let e=ze.get(r);return e||(e=Ke(r),ze.set(r,e)),e}var Ye=new WeakMap;function Gt(r){let e=Ye.get(r);if(e)return e;e=new Map;for(let t of Object.values(r.scenes))for(let n of t.blocks)C(n.children,s=>{if(s.type==="group"&&s.selector==="choice")for(let i of s.children)e.set(i.id,s.id)});return Ye.set(r,e),e}var Xe=new WeakMap;function Vt(r){let e=Xe.get(r);if(e)return e;e=new Map;for(let t of Object.values(r.scenes))for(let n of t.blocks)C(n.children,s=>{if(s.type==="group"){s.prompt&&e.set(s.prompt.id,s.prompt);return}for(let i of s.beats??[])e.set(i.id,i)});return Xe.set(r,e),e}function b(r){if(r===void 0)return;let e={};for(let[t,n]of Object.entries(r))e[t]=Array.isArray(n)?[...n]:n;return e}function P(r,e){return r.tagIndex.get(e)?.slice()}var U="Patter",H=r=>r.replace(/%/g,"%25").replace(/\\//g,"%2F"),qt=r=>r.replace(/%2F/g,"/").replace(/%25/g,"%"),Ze=Symbol("ownsRegistry"),et=Symbol("setupReported"),w={stage:r=>`patter/scene/${H(r)}`,flow:r=>`patter/flow/${H(r)}/`,flowGlobals:r=>`patter/flow/${H(r)}/patter`,flowScene:(r,e)=>`patter/flow/${H(r)}/scene/${H(e)}`},K=class{constructor(e){this.journal=e}journal},ae=class r{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;requestedLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineSeq=0;engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let n=[];for(let p of Object.keys(t))Ht.has(p)||n.push({node:p,message:p==="world"?\'the engine option "world" has gone: bind the scope as hostScopes: { world }\':`"${p}" isn\'t an engine option, so it\'s ignored`});let s=e.strings;this.allStrings=s,this.requestedLocale=t.locale,this.currentLocale=we(e,t.locale),this.currentLocale.problem&&n.push({node:t.locale,message:this.currentLocale.problem});let i=it(s,this.currentLocale),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 p of e.cast??[])p.displayName&&l.set(p.name,p.displayName);let d=new Map;for(let p of e.qualifiers??[])p?.gameId&&d.set(p.gameId,p.name);let g=new Map;for(let p of e.cast??[])for(let[m,h]of Object.entries(p.qualifierLabels??{}))typeof h=="string"&&g.set(`${p.name}\\0${m}`,h);let f=e.speakerPattern??_e;this.defaultSeed=(t.seed??2654435769)>>>0;let T=new Map,Ee=new Map,Ce=new Map;for(let[p,m]of Object.entries(e.scenes)){this.sceneGameIdToId.set(y(m),p);let h=new Map;for(let S of m.blocks)Ee.set(S.id,{sceneId:p}),Ce.set(S.id,S),h.set(y(S),S.id),C(S.children,k=>T.set(k.id,k));this.blockGameIdToId.set(p,h)}let De=e.properties??[],ce=De.filter(p=>p.shared??!0).map(st),yt=De.filter(p=>!(p.shared??!0)).map(st),St=new Set(ce.map(p=>p.name.toLowerCase())),x=t.registry??new Z,Ie=!t.registry||t[Ze]===!0,Re=new E(ce,{pathPrefix:"@patter."}),le=[],Q=[];try{x.mountOwned("patter",Re,{owner:U}),Q.push("patter");let p=new Set,m=e.scopeRegistry?.scopes??[];for(let[h,S]of Object.entries(t.hostScopes??{})){let k=m.find(z=>z.token===h);if(!k&&!(e.externalScopes??[]).includes(h)){let z=m.find(bt=>bt.token.toLowerCase()===h.toLowerCase());n.push({node:h,message:z?`hostScopes binds "${h}", which this bundle doesn\'t declare: it declares "${z.token}", and a scope\'s token is case-sensitive`:`hostScopes binds "${h}", which this bundle doesn\'t declare`})}let wt=(k?.declarations??[]).map(dt);x.defineForeign(h,S,wt,{writable:k?.writable??!0,owner:U}),Q.push(h),p.add(h)}for(let h of Ie?e.scopeRegistry?.scopes??[]:[]){if(p.has(h.token)||x.has(h.token))continue;let S=(h.declarations??[]).map(k=>Kt(k,h.writable));x.defineOwned(h.token,S,{owner:U}),Q.push(h.token),le.push(h.token)}}catch(p){for(let m of Q)x.remove(m,{keep:!0});throw p}let Oe=new Map;for(let[p,m]of Object.entries(e.scenes)){let h=new Set((m.sceneProps??[]).filter(S=>S.shared??!1).map(S=>S.name.toLowerCase()));Oe.set(p,h)}if(this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(p,m,h)=>this.emitEngine(p,m,h),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,qualifierDisplay:d,speakerLabels:g,speakerPattern:f,nodeIndex:T,blockIndex:Ee,blockById:Ce,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:Mt(e),padIndex:$t(e),registry:x,ownsRegistry:Ie,patterBag:Re,hostScopes:le,patterSharedDecls:ce,patterLocalDecls:yt,patterSharedNames:St,sceneSharedNames:Oe,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(p=>console.warn(p.kind==="setup"?`Patterplay: ${p.message}`:p.kind==="save"?`Patterplay: flow \'${p.flow}\': ${p.message}`:`Patterplay: ${p.kind} on ${p.node} in flow \'${p.flow}\' failed, played through: ${p.message}`)),reportRefits:()=>{let p=x.takeMisfits(m=>m==="patter"||m.startsWith("patter/")||le.includes(m));if(p.length!==0)if(t.onRefit)t.onRefit(p);else for(let m of p)console.warn(`Patterplay: the save\'s ${m.scope}.${m.name} was ${JSON.stringify(m.value)}, which no longer fits its declaration; it is back at its default`)},replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??Se).open,captionClose:(e.closedCaptions??Se).close,captionCharacter:e.closedCaptions?.character||Ue,refSplitCache:new Map,refSplitRevision:x.revision},this.host.reportRefits(),t[et]!==!0)for(let p of n)this.reportSetup(p.node,p.message)}reportSetup(e,t){this.host.onError({flow:"",kind:"setup",node:e,message:t}),this.host.tracing&&this.emitEngine("",{type:"diagnostic",kind:"setup",node:e,message:t})}get locale(){return this.currentLocale.locale}get locales(){let{default:e,included:t}=this.host.bundle.locales;return{default:e,included:[...t]}}get isSourceDebug(){return this.sourceDebug}get buildId(){return this.host.bundle.content?.hash??""}setLocale(e){this.requestedLocale=e,this.useLocale(we(this.host.bundle,e)),this.currentLocale.problem&&this.reportSetup(e,this.currentLocale.problem)}useLocale(e){this.currentLocale=e,this.host.strings=it(this.allStrings,e)}replaceStrings(e){this.allStrings=e.strings;let t=this.currentLocale.problem;this.useLocale(we(e,this.requestedLocale)),this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{};let n=this.currentLocale.problem;n&&n!==t&&this.reportSetup(this.requestedLocale,n)}hotSwap(e){this.refuseInCheckpoint("hotSwap");let t=this.saveGame(),n=l=>(l.setClosedCaptions(this.host.captionsOn),l),s=this.requestedLocale,i=(l=!1)=>new r(e,{...this.creationOptions,...s!==void 0?{locale:s}:{},registry:this.host.registry,[Ze]:this.host.ownsRegistry,[et]:l}),o=new Set(["patter",...this.host.hostScopes]),a=this.host.ownsRegistry?void 0:Object.fromEntries(Object.entries(this.host.registry.save()).filter(([l])=>o.has(l)||l.startsWith("patter/")));this.release(!0),a&&this.host.registry.load(a,{keepParked:!0,refit:!0});let c=i();try{return c.loadGame(t),n(c)}catch{c.release(!0);let l=this.host.ownsRegistry?t.registry:a;l&&this.host.registry.load(l,{keepParked:!0,refit:!0});let d=i(!0);for(let[g,f]of Object.entries(t.flows)){let T=f.cursor.currentSceneId;try{d.openFlow(g,T!==null?{scene:T}:{})}catch{}}return n(d)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:n,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 N(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(n,s),a}runFlow(e,t,n){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:n}).advanceToStop().played;if(!s.goto(t,n))throw new Error(`runFlow: address not found: ${t}${n===void 0?"":` / ${n}`}`);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 n=be(this.host,e);if(n===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:n};let s=ve(this.host,n,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:n,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 be(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:ve(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?y(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?y(t):void 0}tagsForBeat(e){return P(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?P(this.host,t):void 0)??[]}tagsForBlock(e,t){let n=this.resolveSceneRef(e),s=this.resolveBlockRef(n,t);return(s!=null?P(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e),n=t!=null?this.host.bundle.scenes[t]:void 0;return b(n?.gameData)??{}}gameDataForBlock(e,t){let n=this.resolveBlockRef(this.resolveSceneRef(e),t),s=n!=null?this.host.blockById.get(n):void 0;return b(s?.gameData)??{}}extensionsForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.extensions}}extensionsForBlock(e,t){let n=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(n!=null?this.host.blockById.get(n):void 0)?.extensions}}extensionsForSnippet(e){let t=this.host.nodeIndex.get(e);return t?.type==="snippet"?{...t.extensions}:{}}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),n=t!=null?this.host.bundle.scenes[t]:void 0;if(!n)return[];let s=new Set;for(let i of n.blocks)tt(i.children,s);return[...s]}castForBlock(e,t){let n=this.resolveSceneRef(e),s=this.resolveBlockRef(n,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return tt(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...y(e)?{gameId:y(e)}:{},name:e.name,...nt(e.gameData),...D(e.extensions),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...y(t)?{gameId:y(t)}:{},name:t.name,...nt(t.gameData),...D(t.extensions),...this.tagsField(t.id),children:t.children.map(n=>this.outlineNode(n))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let n of t.blocks)C(n.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:n.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=P(this.host,e.id),n={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){n.character=e.character;let i=this.host.defaultStrings[re(e.character)]??this.host.castDisplay.get(e.character);i!==void 0&&(n.characterName=i)}if(e.direction!==void 0&&(n.direction=e.direction),e.qualifier!==void 0){n.qualifier=e.qualifier;let i=this.host.defaultStrings[se(e.qualifier)]??this.host.qualifierDisplay.get(e.qualifier);i!==void 0&&(n.qualifierName=i)}let s=oe(this.host,e.character,e.qualifier,!0);s!==void 0&&(n.speakerLabel=s)}if(e.kind==="line"||e.kind==="text"){let s=this.host.padIndex.get(e.id);n.padAfter=s?.resolved??O,s?.own!==void 0&&(n.ownPadAfter=s.own);let i=this.host.defaultStrings[e.id];i!==void 0&&(n.text=i)}return e.gameData&&Object.keys(e.gameData).length&&(n.gameData=b(e.gameData)),e.extensions&&Object.keys(e.extensions).length&&(n.extensions=e.extensions),t&&t.length&&(n.tags=t),n}tagsField(e){let t=P(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,n]of this.host.stageBags)e.push({bag:n,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 K(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let n=t.length-1;n>=0;n--)t[n]();r.forget(e.journal)}commit(e){this.endCheckpoint(e),r.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}captureFlow(e){let t=this.flowsById.get(e);if(!t)throw new Error(`no open flow \'${e}\' to capture`);return{version:V,...t.snapshot(),...Qt(e,this.host.registry.save())}}restoreFlow(e,t){this.refuseInCheckpoint("restoreFlow");let n=t.version;if(n!==3&&n!==V)throw new Error(`unsupported flow state version: ${String(n)}`);let s=t.cursor;if(typeof s!="object"||s===null)throw new Error("malformed flow state: no cursor");if(this.flowsById.has(e))throw new Error(`a flow \'${e}\' is already open; close it before restoring another under that id`);this.assertExternalScopes();let i=w.flow(e);this.host.registry.discardParked(i),this.host.registry.load(Jt(e,t),{keepParked:!0,refit:!0});let o=new N(e,this.host,this.defaultSeed);try{o.restore(t)}catch(a){throw o.close(),this.host.registry.discardParked(i),a}return this.flowsById.set(e,o),this.host.reportRefits(),o}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(w.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let n of this.flowsById.values())n.releaseBags(e),n.close();this.flowsById.clear();let t=this.host.registry;for(let n of this.host.stageBags.keys())t.remove(w.stage(n),{keep:e});this.host.stageBags.clear();for(let n of["patter",...this.host.hostScopes])t.has(n)&&t.remove(n,{keep:e});for(let n of Object.keys(this.creationOptions.hostScopes??{}))t.has(n)&&t.remove(n)}getProperty(e){let{scope:t,name:n}=this.splitShared(e);return this.host.registry.get(t,n)}setProperty(e,t){let{scope:n,name:s}=this.splitShared(e);lt(this.host,n,s,t,null);let i=this.host.journal?this.host.registry.get(n,s):void 0;this.host.registry.set(n,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(n,s,i,{host:!0}))}stageIndex(e,t){return A(this.ladderOf(e),t)}compareStages(e,t,n){return B(this.ladderOf(e),t,n)}nextStage(e,t){return F(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:n}=this.splitShared(e),s=Wt(this.host,t,n);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}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,n){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...n?{scene:n}:{}})}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:L(e),writable:e.writable??!0}))}splitShared(e){let t=pt(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}saveGame(){let e={};for(let[t,n]of this.flowsById)e[t]=n.snapshot();return{version:V,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:at(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==3&&t!==V)throw new Error(`unsupported save version: ${String(t)}`);let n=e.flows;if(typeof n!="object"||n===null||Array.isArray(n))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(w.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.registry;i&&(this.host.ownsRegistry?s.load(i,{refit:!0}):s.load(i,{keepParked:!0,refit:!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 ct(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new N(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}this.host.reportRefits()}},N=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 n=this.currentSceneId;if(n===null)return;let s=this.sceneBagFor(n,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,n){this.id=e,this.host=t,this.rngState=M(n),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(),n=this.evalCtx.scopes;for(let s of Object.keys(n))delete n[s];Object.assign(n,t.scopes),n.patter=this.patterResolver,n.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let n=t.toLowerCase();if(e==="patter")return W(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(n);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:W(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(n)}return this.registryQualities?.(e,t)??W(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(n)}begin(e,t){let n=this.host.bundle.start;if(e===void 0&&t===void 0&&n&&this.host.bundle.scenes[n.scene]&&(e=n.scene,n.block!==void 0&&this.host.blockIndex.get(n.block)?.sceneId===n.scene&&(t=n.block)),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(w.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 s=this.host.blockIndex.get(t);if(!s)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(s.sceneId),this.stack=[{sceneId:s.sceneId,containerId:t,index:0}],this.enter(t)}else{let s=e??Object.keys(this.host.bundle.scenes)[0],i=s?this.host.bundle.scenes[s]:void 0;if(!i)throw new Error(s?`unknown scene: ${s}`:"no scenes in bundle");this.enterSceneSetup(s);let o=i.blocks[0];o&&(this.stack=[{sceneId:s,containerId:o.id,index:0}],this.enter(o.id))}this.settle()}reset(e,t){this.begin(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 n=be(this.host,e);if(n===void 0)return!1;let s;return t!==void 0&&(s=ve(this.host,n,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??n,"jump"),this.settle(),!0):(this.begin(n,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:n,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:d}=this,g=this.stack.map(f=>({...f}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:n,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:d,stack:g})})}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.offer(this.pendingChoice)}: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 n=this.childrenOf(t.containerId);if(!n){this.stack.pop();continue}let s=t.index;for(;t.index<n.length&&!this.eligible(n[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:n.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:n[t.index]?.id??null}),t.index>=n.length){this.stack.pop();continue}this.enterChild(n[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,n]of this.sceneBags)e.push({bag:n,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)}stepFor(e){let t=Vt(this.host.bundle).get(e);return t?this.beatResult(t):void 0}getChoices(){return this.pendingChoice?this.offer(this.pendingChoice):[]}offer(e){return e.options.map(t=>{let n=e.byId.get(t.id),s=this.promptFor(n);return e.shown.set(t.id,s),{id:t.id,prompt:s,eligible:t.eligible,gameData:b(t.gameData),...D(n.extensions)}})}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let n=t.options.find(a=>a.id===e);if(!n)throw new Error(`unknown choice option: ${e}`);if(!n.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,o=i?t.shown.has(e)?t.shown.get(e):this.promptFor(s):void 0;this.pendingPrompt=i&&o?{beat:i,shown:{...o}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:n}=this.splitRef(e);return t==="patter"?this.patterResolver.get(n):t==="scene"?this.sceneResolver.get(n):this.host.registry.get(t,n)}setProperty(e,t){this.writeProperty(e,t,!0)}stageIndex(e,t){return A(this.ladderOf(e),t)}compareStages(e,t,n){return B(this.ladderOf(e),t,n)}nextStage(e,t){return F(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:n}=this.splitRef(e);this.context();let s=this.stagesFor(t,n);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}writeProperty(e,t,n){let{scope:s,name:i}=this.splitRef(e);if(n){if(s==="scene"&&this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);lt(this.host,s,i,t,this.currentSceneId)}let 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,n?{host:!0}:void 0),o&&a!==void 0&&o.undo.push(()=>this.host.registry.set(s,i,a,{host:!0}))}}snapshot(){let e=Gt(this.host.bundle),t=this.activeSnippet?.beats?.[this.beatIndex]?.id;return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(n=>{let s=this.childrenOf(n.containerId)?.[n.index],i=e.get(n.containerId);return{...n,...s?{nextId:s.id}:{},...i!==void 0?{choiceId:i}:{}}}),activeSnippetId:this.activeSnippet?.id??null,...t!==void 0?{beatId:t}:{},pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(n=>({id:n.id,eligible:n.eligible,...n.gameData!==void 0?{gameData:b(n.gameData)}:{}}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:_t(this.pendingPrompt.shown)}:{},selectors:at(this.selectors)}}}restore(e){if(this.host.journal)throw new Error("a flow can\'t be restored while a checkpoint is open");let t=this.restoreCursor(e);this.releaseBags(!0),this.mountLocal();for(let n of new Set([this.currentSceneId,...this.stack.map(s=>s.sceneId)]))n!==null&&this.host.bundle.scenes[n]&&this.ensureSceneBags(n);t&&(this.report({flow:this.id,kind:"save",node:t.node,message:`the save\'s place has gone from this build: ${t.instead}`}),t.reoffer&&this.setupChoice(t.reoffer))}restoreCursor(e){this.rngState=M(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;this.started=!0,this.flowEnded=t.flowEnded,this.currentSceneId=t.currentSceneId;let n=null;this.stack=[];for(let s of t.stack){let{nextId:i,choiceId:o,...a}=s,c=this.childrenOf(a.containerId),l=!c&&o!==void 0?this.host.nodeIndex.get(o):void 0;if(l?.type==="group"&&l.selector==="choice"){n={node:t.activeSnippetId??t.pendingChoice?.groupId??a.containerId,instead:`option ${a.containerId} has gone, so choice ${l.id} is offered again`,reoffer:l};break}let d=i!==void 0?c?.findIndex(g=>g.id===i)??-1:-1;this.stack.push(d>=0?{...a,index:d}:{...a})}if(this.activeSnippet=null,this.beatIndex=0,!n?.reoffer&&t.activeSnippetId!==null){let s=this.host.nodeIndex.get(t.activeSnippetId);if(s&&s.type==="snippet"){let i=s.beats??[];if(this.activeSnippet=s,t.beatId!==void 0){let o=i.findIndex(a=>a.id===t.beatId);this.beatIndex=o>=0?o:i.length,o<0&&(n={node:t.activeSnippetId,instead:`beat ${t.beatId} has gone from snippet ${t.activeSnippetId}, so the snippet ends there`})}else this.beatIndex=t.beatIndex??i.length}else n={node:t.activeSnippetId,instead:`snippet ${t.activeSnippetId} has gone, so play carries on after it`}}if(this.selectors=ct(t.selectors),this.pendingChoice=null,!n?.reoffer&&t.pendingChoice!==null){let s=new Map,i=[];for(let o of t.pendingChoice.options){let a=this.host.nodeIndex.get(o.id);a&&(s.set(o.id,a),i.push({id:o.id,eligible:o.eligible,...o.gameData!==void 0?{gameData:o.gameData}:{}}))}if(i.length>0)this.pendingChoice={groupId:t.pendingChoice.groupId,options:i,byId:s,shown:new Map};else{let o=this.host.nodeIndex.get(t.pendingChoice.groupId);n=o?.type==="group"&&o.selector==="choice"?{node:t.pendingChoice.groupId,instead:`none of choice ${o.id}\'s options is left, so it\'s offered again`,reoffer:o}:{node:t.pendingChoice.groupId,instead:`choice ${t.pendingChoice.groupId} has gone, so play carries on after it`}}}if(this.pendingPrompt=null,this.pendingPromptOwnerId=n?.reoffer?null:t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let s=this.host.nodeIndex.get(this.pendingPromptOwnerId),i=s?this.authoredPromptOf(s):void 0,o=t.pendingPrompt,a=o?.kind==="line"&&o.speakerLabel===void 0?oe(this.host,o.character,o.qualifier):void 0;i?this.pendingPrompt={beat:i,...o?{shown:{...o,padAfter:o.padAfter??this.padOf(i.id),...a!==void 0?{speakerLabel:a}:{}}}:{}}:this.pendingPromptOwnerId=null}return n}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 n=this.selectChild(e);n&&this.enterChild(n)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let n=this.host.nodeIndex.get(e);if(n&&n.type==="group")return n.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],n=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,eligible:a,...o.gameData!==void 0?{gameData:o.gameData}:{}}),n.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:n,shown:new Map};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 n,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}n=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),n=a.sceneId,s=e}this.enter(s);let o={sceneId:n,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)})),n=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(n.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(n[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(n,i,a):s==="specificity"?this.pickSpecificity(n,i,a):this.pickSequential(n,i,a));default:return null}}pickSequential(e,t,n){let s=e.length,i=n.seq??0;return n.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,n){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(f=>f.id),a=new Set(e.map(f=>f.id));if(n.bag===void 0&&(n.bag=o()),!n.bag.some(f=>a.has(f))){if(t==="once")return null;if(i){let f=e[s-1];return n.last=f.id,f}if(n.bag=o(),n.bag.length===0)return null}let c=n.bag.filter(f=>a.has(f)),l=n.last!==void 0&&c.length>1?c.indexOf(n.last):-1,d=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&d>=l&&d++;let g=c[d];return n.bag.splice(n.bag.indexOf(g),1),n.last=g,e.find(f=>f.id===g)}pickSpecificity(e,t,n){let s=e;if(t!=="repeat"){n.bag===void 0&&(n.bag=e.map(d=>d.id));let l=new Set(n.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&n.last!==void 0?e.find(d=>d.id===n.last)??null:null}let i=-1,a=s.map(l=>{let d=this.specScore(l);return d>i&&(i=d),{c:l,s:d}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=n.last!==void 0?a.findIndex(g=>g.id===n.last):-1,d=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&d>=l&&d++,c=a[d]}return t!=="repeat"&&(n.bag=n.bag.filter(l=>l!==c.id)),n.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,n){return Ne(e,i=>{try{return ot(Y(i,this.context(),me))}catch(o){return this.reportError("best-match",n.id,n.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,n=this.host.journal,s;if(n&&(s=e.shared?n.selectors:n.flowSelectors.get(this),s||(s=new Set,n.flowSelectors.set(this,s))),n&&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;n.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 n of e??[]){let s;try{s=this.evalExpr(n.value);let i=this.host.tracing?this.getProperty(n.target):void 0;this.writeProperty(n.target,s,!1),this.host.tracing&&this.emit({type:"write",target:n.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,n.value,i)}}}eligible(e){if(!e.condition)return!0;try{return ot(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,n,s){let i=s instanceof Error?s.message:String(s),o=n?.src;this.report({flow:this.id,kind:e,node:t,...o?{source:o}:{},message:i})}report(e){this.host.onError(e),this.host.tracing&&this.emit({type:"diagnostic",kind:e.kind,node:e.node,...e.source?{source:e.source}:{},message:e.message})}evalExpr(e){return Y(this.conditionAst(e),this.context(),me)}conditionAst(e){let t=Je.get(e);return t||(t=I(e.ast),Je.set(e,t)),t}enter(e){let t=this.visitCounts,n=t.get(e);t.set(e,(n??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(()=>{n===void 0?t.delete(e):t.set(e,n),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=ue(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=P(this.host,e.id),n=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:b(e.gameData),...D(e.extensions),...n};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),padAfter:this.padOf(e.id),...e.action?{action:!0}:{},gameData:b(e.gameData),...D(e.extensions),...n};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,qualifier:c?void 0:e.qualifier,qualifierName:c?void 0:this.resolveQualifierName(e.qualifier),speakerLabel:c?void 0:oe(this.host,e.character,e.qualifier),padAfter:this.padOf(e.id),gameData:b(e.gameData),...D(e.extensions),...n}}}}interpolate(e){return $e(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 n=this.interpolate(this.resolveString(t.id)),s=this.padOf(t.id);return t.kind==="line"?{kind:"line",text:this.captionLine(n),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction,qualifier:t.qualifier,qualifierName:this.resolveQualifierName(t.qualifier),speakerLabel:oe(this.host,t.character,t.qualifier),padAfter:s}:{kind:"text",text:n,padAfter:s}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let n=P(this.host,e.id),s=n&&n.length?{tags:n}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,padAfter:this.padOf(e.id),gameData:b(e.gameData),...D(e.extensions),...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,qualifier:t.qualifier,qualifierName:t.qualifierName,speakerLabel:t.speakerLabel,padAfter:this.padOf(e.id),gameData:b(e.gameData),...D(e.extensions),...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(n=>n.kind==="line"||n.kind==="text")}firstTextSnippetIn(e){let t;return C(e,n=>{!t&&n.type==="snippet"&&(n.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=n)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let n=this.host.defaultStrings[e];return n!==void 0?`<Untranslated: ${e}> ${n}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=re(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}padOf(e){return this.host.padIndex.get(e)?.resolved??O}resolveQualifierName(e){if(e===void 0||this.host.emitIds)return;let t=se(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.qualifierDisplay.get(e)}splitRef(e){return pt(this.host,e)}newLocal(){return new E(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=w.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:U}),this.registered.add(e),this.host.reportRefits()}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,n=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal,i=!this.sceneBags.has(e)||!this.host.stageBags.has(e);if(!this.sceneBags.has(e)){let o=new E(n.filter(l=>!t.has(l.name.toLowerCase())),{pathPrefix:"@scene."}),a=w.flowScene(this.id,e),c=rt(this.host.registry,a,o,s!==null);this.registered.add(a),this.sceneBags.set(e,o),s&&s.undo.push(()=>{this.sceneBags.get(e)===o&&(this.host.registry.remove(a,c?{keep:!0}:void 0),this.registered.delete(a),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let o=new E(n.filter(c=>t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),a=rt(this.host.registry,w.stage(e),o,s!==null);this.host.stageBags.set(e,o),s&&s.undo.push(()=>{this.host.stageBags.get(e)===o&&(this.host.registry.remove(w.stage(e),a?{keep:!0}:void 0),this.host.stageBags.delete(e))})}i&&this.host.reportRefits()}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let n of e.sceneProps??[]){if(!n.temporary)continue;let s=n.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,L(n))}}};function tt(r,e){C(r,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let n of t.beats??[])n.kind==="line"&&n.character&&e.add(n.character)})}function oe(r,e,t,n=!1){if(!e||r.emitIds)return;let s=c=>(n?void 0:r.strings[c])??r.defaultStrings[c],i=s(re(e))??r.castDisplay.get(e)??e;if(t===void 0)return i;let o=r.speakerLabels.get(`${e}\\0${t}`);if(o!==void 0)return s(Ve(e,t))??o;let a=s(se(t))??r.qualifierDisplay.get(t)??t;return He(s(qe)??r.speakerPattern,i,a)}function nt(r){return r&&Object.keys(r).length?{gameData:b(r)}:{}}function D(r){return r&&Object.keys(r).length?{extensions:r}:{}}function _t(r){let e={kind:r.kind,text:r.text};return r.character!==void 0&&(e.character=r.character),r.characterName!==void 0&&(e.characterName=r.characterName),r.direction!==void 0&&(e.direction=r.direction),r.qualifier!==void 0&&(e.qualifier=r.qualifier),r.qualifierName!==void 0&&(e.qualifierName=r.qualifierName),r.speakerLabel!==void 0&&(e.speakerLabel=r.speakerLabel),typeof r.padAfter=="number"&&(e.padAfter=r.padAfter),e}function at(r){let e={};for(let[t,n]of r){let s={};n.seq!==void 0&&(s.seq=n.seq),n.bag&&(s.bag=[...n.bag]),n.last!==void 0&&(s.last=n.last),e[t]=s}return e}function ct(r){let e=new Map;for(let[t,n]of Object.entries(r??{})){let s={};n.seq!==void 0&&(s.seq=n.seq),n.bag&&(s.bag=[...n.bag]),n.last!==void 0&&(s.last=n.last),e.set(t,s)}return e}function rt(r,e,t,n){let s=n?JSON.stringify(t.save()):"";return r.mountOwned(e,t,{owner:U}),n&&JSON.stringify(t.save())!==s}function st(r){return{name:r.name,type:r.type,values:r.values,stages:r.stages,default:r.default}}var Ht=new Set(["rng","seed","locale","hostScopes","registry","replayPromptOnChoose","closedCaptions","onDryChoice","onError","onRefit","log"]);function we(r,e){let t=r.locales.default;if(e===void 0)return{locale:t,table:t};if(r.localisation?.mode==="ids")return{locale:e,table:e};let n=[...new Set([t,...r.locales.included,...Object.keys(r.strings)])];if(n.includes(e))return{locale:e,table:e};let s=c=>c.toLowerCase().replace(/_/g,"-"),i=n.find(c=>s(c)===s(e));if(i!==void 0)return{locale:i,table:i,problem:`the locale "${e}" is spelled "${i}" in this bundle, so it\'s using "${i}"`};let o=s(e).split("-")[0],a=n.find(c=>s(c)===o);return a!==void 0?{locale:a,table:a,problem:`this bundle has no locale "${e}", so it\'s using its language, "${a}"`}:{locale:t,problem:`this bundle has no locale "${e}", so it\'s using the default, "${t}", with every string marked untranslated`}}function it(r,e){return e.table!==void 0?r[e.table]??{}:{}}function lt(r,e,t,n,s){let i=c=>c?.find(l=>l.name.toLowerCase()===t),o;if(e==="patter")o=i(r.bundle.properties);else if(e==="scene")o=i(s!==null?r.bundle.scenes[s]?.sceneProps:void 0);else{let c=r.bundle.scopeRegistry?.scopes.find(l=>l.token===e);if(!c||c.declarations===void 0)return;o=i(c.declarations)}let a=`@${e}.${t}`;if(o===void 0)throw new Error(`no property at "${a}": the content declares no ${a}`);if(!$(o,n,{noneChosen:o.type==="enum"&&o.default===""}))throw new Error(`"${a}" takes ${Ut(o)}; ${JSON.stringify(n)} doesn\'t fit`)}function Ut(r){let e=t=>(t??[]).map(n=>JSON.stringify(n)).join(", ");switch(r.type){case"number":return"a number";case"boolean":return"true or false";case"enum":return`one of ${e(r.values)}`;case"flags":return r.values!==void 0&&r.values.length>0?`a list of flags from ${e(r.values)}`:"a list of flags";case"quality":return`one of its stages ${e(r.stages)}`;default:return"text"}}function W(r,e,t){let n=r.qualityLadders.get(e);if(!n){n=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!n.has(i)&&n.set(i,s.stages)}r.qualityLadders.set(e,n)}return n}function Wt(r,e,t){let n=t.toLowerCase();return e==="patter"?W(r,"patter",()=>[...r.patterSharedDecls,...r.patterLocalDecls]).get(n):r.registry.toEvalContext().qualities?.(e,t)??W(r,`host/${e}`,()=>r.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(n)}function dt(r){return{name:r.name,type:r.type,values:r.values,stages:r.stages,default:r.default,writable:r.writable}}function Kt(r,e){let t=dt(r),n=r.writable??e;return n===void 0?t:{...t,writable:n}}function be(r,e){return ne(r.bundle.scenes,e,r.sceneGameIdToId)}function ve(r,e,t){return r.blockGameIdToId.get(e)?.get(t)??(r.blockIndex.get(t)?.sceneId===e?t:void 0)}function pt(r,e){r.refSplitRevision!==r.registry.revision&&(r.refSplitCache.clear(),r.refSplitRevision=r.registry.revision);let t=r.refSplitCache.get(e);return t||(t=je(e,n=>n==="scene"||r.registry.has(n)||(r.bundle.externalScopes?.includes(n)??!1)),r.refSplitCache.set(e,t)),t}function Jt(r,e){let t={};e.scopes?.patter&&(t[w.flowGlobals(r)]=e.scopes.patter);for(let[n,s]of Object.entries(e.sceneBags??{}))t[w.flowScene(r,n)]=s;return t}function Qt(r,e){let t={scopes:{},sceneBags:{}},n=w.flow(r);for(let[s,i]of Object.entries(e)){if(!s.startsWith(n))continue;let o=s.slice(n.length);o==="patter"?t.scopes.patter=i:o.startsWith("scene/")&&(t.sceneBags[qt(o.slice(6))]=i)}return t}function ot(r){return typeof r=="boolean"?r:typeof r=="number"?r!==0:typeof r=="string"?r!=="":r.length>0}var zt=(r,e)=>r.shared??e;function xe(r,e){return{name:r.name,type:r.type,hasDefault:r.default!==void 0,...r.default!==void 0?{default:r.default}:{},shared:zt(r,e)}}function Yt(r){return{name:r.name,type:r.type,hasDefault:r.default!==void 0,...r.values?{values:[...r.values]}:{},...r.purpose?{purpose:r.purpose}:{}}}function Xt(r,e){e.blocks++;let t=[...r.children];for(;t.length;){let n=t.pop();if(n.type==="group"){e.groups++,n.prompt&&e.prompts++,t.push(...n.children);continue}e.snippets++;for(let s of n.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function ut(r){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:r.cast?.length??0},t=[],n=[];for(let o of Object.values(r.scenes)){e.scenes++;let a=y(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:y(c),name:c.name}))});for(let c of o.blocks)Xt(c,e);o.sceneProps?.length&&n.push({gameId:a,properties:o.sceneProps.map(c=>xe(c,!1))})}let s=(r.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>xe(a,!0))})),i=Object.entries(r.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(Yt)}));return{identity:{schema:r.schema,project:r.content.project,...r.content.version!==void 0?{version:r.content.version}:{},...r.content.hash!==void 0?{hash:r.content.hash}:{},...r.content.structureHash!==void 0?{structureHash:r.content.structureHash}:{},voiced:r.voiced,defaultLocale:r.locales.default,locales:[...r.locales.included],localisation:r.localisation?.mode??"embedded",sourceDebug:r.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(r.properties??[]).map(o=>xe(o,!0)),scene:n},gameData:i,counts:e}}var J=(r,e)=>Object.prototype.hasOwnProperty.call(r,e);function gt(r,e){return r.gameDataFields?.[e]??[]}function ke(r,e,t){return e&&J(e,t)?e[t]:r.find(n=>n.name===t)?.default}function ft(r,e){let t={};for(let n of r){let s=ke(r,e,n.name);s!==void 0&&(t[n.name]=s)}for(let[n,s]of Object.entries(e??{}))J(t,n)||(t[n]=s);return t}function ht(r,e,t){let n=r.extensions!==void 0&&J(r.extensions,e)?r.extensions[e]:void 0;return n!==void 0&&J(n.fields,t)?n.fields[t]??[]:[]}function mt(r,e,t=""){let n=o=>({...o?.extensions??{}}),s=Object.values(r.scenes??{}),i=o=>(J(r.scenes??{},o)?r.scenes[o]:void 0)??s.find(a=>y(a)===o);switch(e){case"project":{let o={};for(let[a,c]of Object.entries(r.extensions??{}))c.project!==void 0&&(o[a]=c.project);return o}case"scene":return n(i(t));case"block":{let o=t.indexOf("/");if(o<0)return{};let a=i(t.slice(0,o))?.blocks??[],c=t.slice(o+1);return n(a.find(l=>l.id===c)??a.find(l=>y(l)===c))}case"snippet":case"option":case"line":case"text":case"gameEvent":{let o,a=(c,l)=>{for(let d of c??[]){if(o!==void 0)return;if(d.type==="group"){if(l&&d.id===t&&e==="option"){o=d;return}if(l&&d.prompt?.id===t&&d.prompt.kind===e){o=d.prompt;return}a(d.children??[],d.selector==="choice");continue}if(d.id===t&&e==="snippet"){o=d;return}let g=(d.beats??[]).find(f=>f.id===t);if(g!==void 0){g.kind===e&&(o=g);return}}};for(let c of s)for(let l of c.blocks??[])o===void 0&&a(l.children??[],!1);return n(o)}default:return{}}}return Dt(Zt);})();\n';
2135
2633
  var PLAYABLE_TIMING_JS = 'var GameSubtitles=(()=>{var l=Object.defineProperty;var a=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var u=(t,e)=>{for(var s in e)l(t,s,{get:e[s],enumerable:!0})},f=(t,e,s,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of c(e))!_.call(t,i)&&i!==s&&l(t,i,{get:()=>e[i],enumerable:!(n=a(e,i))||n.enumerable});return t};var p=t=>f(l({},"__esModule",{value:!0}),t);var d={};u(d,{estimateDuration:()=>h});function h(t,{charsPerSecond:e=14,minSeconds:s=1.5,maxSeconds:n=18}={}){let i=0;for(let o of t??"")o!=="\\xAD"&&i++;let r=e>0?e:14;return Math.min(Math.max(i/r,s),n)}return p(d);})();\n';
2136
2634
 
2137
2635
  // src/play-timeline.ts
@@ -2187,7 +2685,6 @@ var PLAYER_JS = String.raw`
2187
2685
  (function () {
2188
2686
  var Engine = window.Patterplay.Engine;
2189
2687
  var BUNDLE = window.PATTER_BUNDLE;
2190
- var startScene = (BUNDLE.start && BUNDLE.start.scene) || Object.keys(BUNDLE.scenes)[0];
2191
2688
  var SAVE_KEY = "patter.play." + ((BUNDLE.content && BUNDLE.content.hash) || "x");
2192
2689
 
2193
2690
  var stage = document.getElementById("stage");
@@ -2242,7 +2739,7 @@ var PLAYER_JS = String.raw`
2242
2739
  };
2243
2740
  }
2244
2741
 
2245
- function build() { engine = new Engine(BUNDLE); flow = engine.openFlow("main", { scene: startScene }); }
2742
+ function build() { engine = new Engine(BUNDLE); flow = engine.openFlow("main"); } // the project's start, or the first scene
2246
2743
 
2247
2744
  function newGame() { stage.innerHTML = ""; controls.innerHTML = ""; build(); run(); }
2248
2745
 
@@ -2513,8 +3010,9 @@ function renderPlay(result) {
2513
3010
 
2514
3011
  // src/coverage.ts
2515
3012
  import { Engine as Engine2 } from "@patterkit/runtime";
2516
- import { isContentlessBeat as isContentlessBeat2, walkNodes as walkNodes2 } from "@patterkit/model";
2517
- import { deserialiseAst, makePrng, stagesEitherSide } from "@wildwinter/expr";
3013
+ import { isContentlessBeat as isContentlessBeat2, walkNodes as walkNodes3 } from "@patterkit/model";
3014
+ import { neverRunNodes } from "@patterkit/core";
3015
+ import { deserialiseAst, makePrng, stagesEitherSide, valueFits } from "@wildwinter/expr";
2518
3016
  var CADENCE_PROB = {
2519
3017
  rarely: 0.15,
2520
3018
  sometimes: 0.4,
@@ -2640,15 +3138,13 @@ function analyzeHostScopes(bundle, hostTokens) {
2640
3138
  refsIn(e.value);
2641
3139
  }
2642
3140
  };
2643
- const walk = (nodes, blockId, gate, fine, branch, dead) => {
2644
- let won = false;
3141
+ const walk = (nodes, blockId, gate, fine, dead, never) => {
2645
3142
  for (const node of nodes) {
2646
- const shadow = dead || won;
2647
- if (branch && !node.condition) won = true;
3143
+ const shadow = dead || never.has(node.id);
2648
3144
  const here = /* @__PURE__ */ new Set([...gate, ...refsIn(node.condition)]);
2649
3145
  const hereFine = /* @__PURE__ */ new Set([...fine, ...fineIn(node.condition)]);
2650
3146
  if (node.type === "group") {
2651
- walk(node.children, blockId, here, hereFine, node.selector === "branch", shadow);
3147
+ walk(node.children, blockId, here, hereFine, shadow, never);
2652
3148
  } else {
2653
3149
  const beats = (node.beats ?? []).map((b) => b.id);
2654
3150
  if (beats.length) siteBeats.set(node.id, beats);
@@ -2666,7 +3162,10 @@ function analyzeHostScopes(bundle, hostTokens) {
2666
3162
  };
2667
3163
  for (const scene of Object.values(bundle.scenes)) {
2668
3164
  scanEffects(scene.onEntry, scene.id);
2669
- for (const block of scene.blocks) walk(block.children, block.id, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false, false);
3165
+ for (const block of scene.blocks) {
3166
+ const never = new Set(neverRunNodes(block.children).map((n) => n.id));
3167
+ walk(block.children, block.id, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false, never);
3168
+ }
2670
3169
  }
2671
3170
  return empty;
2672
3171
  }
@@ -2722,7 +3221,7 @@ function proposeCoverageDrivers(loaded) {
2722
3221
  const { written, proposals } = analyzeHostScopes(bundle, hostTokens);
2723
3222
  const declByRef = /* @__PURE__ */ new Map();
2724
3223
  for (const s of hostScopes) {
2725
- for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages });
3224
+ for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages, default: d.default });
2726
3225
  }
2727
3226
  const drivers = [];
2728
3227
  for (const [ref, pool] of proposals) {
@@ -2734,6 +3233,7 @@ function proposeCoverageDrivers(loaded) {
2734
3233
  else if (decl.type === "enum" && decl.values) values = [...decl.values];
2735
3234
  else if (decl.type === "quality" && decl.stages) values = [...decl.stages];
2736
3235
  }
3236
+ if (decl) values = values.filter((v) => valueFits(decl, v, { noneChosen: decl.type === "enum" && decl.default === "" }));
2737
3237
  if (values.length === 0) continue;
2738
3238
  if (decl?.type === "quality" && decl.stages) {
2739
3239
  const near = new Set(values.flatMap((v) => stagesEitherSide(decl, v)));
@@ -2764,7 +3264,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
2764
3264
  for (const scene of loaded.scenes) {
2765
3265
  nodeScene.set(scene.id, scene.id);
2766
3266
  for (const block of scene.blocks) {
2767
- walkNodes2(block.children, (node) => {
3267
+ walkNodes3(block.children, (node) => {
2768
3268
  nodeScene.set(node.id, scene.id);
2769
3269
  if (node.type === "group") {
2770
3270
  if (node.selector === "choice") choiceScene.set(node.id, scene.id);
@@ -2801,6 +3301,13 @@ function* sweep(loaded, options = {}, hooks = {}) {
2801
3301
  const recurringDrivers = drivers.filter((d) => d.kind === "recurring");
2802
3302
  const drivenRefs = new Set(drivers.map((d) => d.ref));
2803
3303
  const pick = (vals) => vals[Math.floor(rng() * vals.length)];
3304
+ const drive = (engine, d) => {
3305
+ const value = pick(d.values);
3306
+ try {
3307
+ engine.setProperty(d.ref, value);
3308
+ } catch {
3309
+ }
3310
+ };
2804
3311
  const sites = [...new Set([...analysis.writerSites.values()].flat())];
2805
3312
  const entered = entryGates(bundle, analysis.routes, start);
2806
3313
  let executed = 0;
@@ -2822,7 +3329,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
2822
3329
  },
2823
3330
  ...registry ? { registry } : {}
2824
3331
  });
2825
- for (const d of initialDrivers) engine.setProperty(d.ref, pick(d.values));
3332
+ for (const d of initialDrivers) drive(engine, d);
2826
3333
  const flow = engine.openFlow("cov", { scene: start.scene, block: start.block });
2827
3334
  const seenThisRun = /* @__PURE__ */ new Set();
2828
3335
  let term = "capped";
@@ -2835,7 +3342,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
2835
3342
  }
2836
3343
  if (r.type === "choice") {
2837
3344
  for (const d of recurringDrivers) {
2838
- if (rng() < CADENCE_PROB[d.cadence ?? "sometimes"]) engine.setProperty(d.ref, pick(d.values));
3345
+ if (rng() < CADENCE_PROB[d.cadence ?? "sometimes"]) drive(engine, d);
2839
3346
  }
2840
3347
  const eligible = r.options.filter((o) => o.eligible);
2841
3348
  if (eligible.length === 0) {
@@ -3787,454 +4294,237 @@ function poToCatalog(text) {
3787
4294
  // src/loc-xlsx.ts
3788
4295
  var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender", "Scene", "Qualifier"];
3789
4296
  var SCENE_COLUMN = HEADERS.indexOf("Scene") + 1;
4297
+ var INFO_SHEET = "Patter";
4298
+ var INFO_MARKER = "Patter localisation file";
4299
+ var INFO_LANGUAGE = "Language";
4300
+ var INFO_SOURCE = "Source language";
4301
+ var INFO_PROJECT = "Project";
3790
4302
  var SHEET_NAME_MAX = 31;
3791
4303
  function uniqueSheetName(scene, used) {
3792
4304
  const tidy2 = (s) => s.replace(/^'+|'+$/g, "");
3793
4305
  const base = tidy2(scene.replace(/[:\\/?*[\]]/g, "-").slice(0, SHEET_NAME_MAX)) || "Scene";
3794
4306
  let name = base;
3795
- for (let n = 2; used.has(name.toLowerCase()) || name.toLowerCase() === "history"; n++) {
3796
- const suffix = ` (${n})`;
3797
- name = tidy2(base.slice(0, SHEET_NAME_MAX - suffix.length)) + suffix;
3798
- }
3799
- used.add(name.toLowerCase());
3800
- return name;
3801
- }
3802
- function cellText(v) {
3803
- if (v == null) return "";
3804
- if (typeof v === "string") return v;
3805
- if (typeof v === "number" || typeof v === "boolean") return String(v);
3806
- if (v instanceof Date) return v.toISOString();
3807
- if (typeof v !== "object") return "";
3808
- const o = v;
3809
- if (Array.isArray(o.richText)) return o.richText.map((run) => cellText(run?.text)).join("");
3810
- if ("text" in o) return cellText(o.text);
3811
- if ("result" in o) return cellText(o.result);
3812
- return "";
3813
- }
3814
- async function catalogToXlsx(catalog) {
3815
- const { default: ExcelJS } = await import("exceljs");
3816
- const wb = new ExcelJS.Workbook();
3817
- wb.creator = "patter";
3818
- const byScene = /* @__PURE__ */ new Map();
3819
- for (const e of catalog.entries) (byScene.get(e.scene) ?? byScene.set(e.scene, []).get(e.scene)).push(e);
3820
- const used = /* @__PURE__ */ new Set();
3821
- for (const [scene, entries] of byScene) {
3822
- const ws = wb.addWorksheet(uniqueSheetName(scene, used));
3823
- ws.columns = [
3824
- { header: "ID", key: "id", width: 22 },
3825
- { header: "Source", key: "source", width: 40 },
3826
- { header: "Translation", key: "translation", width: 40 },
3827
- { header: "Comments", key: "comments", width: 30 },
3828
- { header: "Status", key: "status", width: 10 },
3829
- { header: "Gender", key: "gender", width: 12 },
3830
- // Hidden: the translator has no use for it, and it is the import's only faithful record of the scene.
3831
- { header: "Scene", key: "scene", width: 22, hidden: true },
3832
- // After the hidden Scene column, so the columns an older reader finds by position stay where they were.
3833
- { header: "Qualifier", key: "qualifier", width: 11 }
3834
- ];
3835
- ws.getRow(1).font = { bold: true };
3836
- ws.views = [{ state: "frozen", ySplit: 1 }];
3837
- for (const e of entries) {
3838
- ws.addRow({
3839
- id: e.id,
3840
- source: e.source,
3841
- translation: e.translation,
3842
- comments: e.comments.join("\n"),
3843
- status: e.stale ? "stale" : e.translation ? "translated" : "",
3844
- gender: e.context?.gender ?? "",
3845
- scene,
3846
- qualifier: e.context?.qualifier ?? ""
3847
- });
3848
- }
3849
- }
3850
- return Buffer.from(await wb.xlsx.writeBuffer());
3851
- }
3852
- async function xlsxToCatalog(buffer) {
3853
- const { default: ExcelJS } = await import("exceljs");
3854
- const wb = new ExcelJS.Workbook();
3855
- await wb.xlsx.load(buffer);
3856
- const entries = [];
3857
- for (const ws of wb.worksheets) {
3858
- const header = ws.getRow(1).values.slice(1).map(cellText);
3859
- if (header[0] !== HEADERS[0]) continue;
3860
- const hasSceneColumn = header[SCENE_COLUMN - 1] === "Scene";
3861
- const sceneOf = (row) => hasSceneColumn ? cellText(row.getCell(SCENE_COLUMN).value).trim() : "";
3862
- let sheetScene = "";
3863
- ws.eachRow((row, n) => {
3864
- if (n > 1 && !sheetScene) sheetScene = sceneOf(row);
3865
- });
3866
- ws.eachRow((row, n) => {
3867
- if (n === 1) return;
3868
- const cell = (c) => cellText(row.getCell(c).value);
3869
- const id = cell(1).trim();
3870
- if (!id) return;
3871
- const status = cell(5).trim().toLowerCase();
3872
- const scene = checkCatalogField("spreadsheet", "scene", sceneOf(row) || sheetScene || ws.name);
3873
- entries.push({
3874
- id,
3875
- scene,
3876
- source: cell(2),
3877
- translation: cell(3),
3878
- comments: cell(4) ? cell(4).split("\n").filter(Boolean) : [],
3879
- stale: status === "stale"
3880
- });
3881
- });
3882
- }
3883
- return { project: "", defaultLocale: "", locale: void 0, entries };
3884
- }
3885
-
3886
- // src/voice-script.ts
3887
- import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES2, projectQualifiers as projectQualifiers2 } from "@patterkit/model";
3888
- function plainVoice(text) {
3889
- return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
3890
- }
3891
- function groupLabel(node, source) {
3892
- if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
3893
- if (node.selector === "choice") return "";
3894
- return node.selector ?? "group";
3895
- }
3896
- function runVoiceScript(loaded, opts = {}) {
3897
- const { project } = loaded;
3898
- const everything = !!opts.everything;
3899
- const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
3900
- const writingLadder = ladderDecls.map((s) => s.name);
3901
- const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3).map((s) => s.name);
3902
- const stub = writingLadder[0];
3903
- const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
3904
- const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
3905
- const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring2(loaded);
3906
- const recordingBase = opts.recordingOverride ?? manualRecordingOf;
3907
- const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
3908
- const source = sourceStrings(loaded);
3909
- const actorOf = /* @__PURE__ */ new Map();
3910
- for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
3911
- const qualifierName = new Map(projectQualifiers2(project).map((q) => [q.gameId, q.name]));
3912
- const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, "vo");
3913
- const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
3914
- const lines = [];
3915
- const emitLine = (beat, scope, leadingVo) => {
3916
- if (beat.kind !== "line" || cutSet.has(beat.id)) return false;
3917
- const ws = writingOf.get(beat.id) ?? stub;
3918
- if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) return false;
3919
- const own = ownVo(beat.id);
3920
- lines.push({
3921
- scope: scope.join(" \u203A "),
3922
- id: beat.id,
3923
- character: beat.character ?? "",
3924
- ...beat.qualifier ? { qualifier: qualifierName.get(beat.qualifier) ?? beat.qualifier } : {},
3925
- actor: beat.character ? actorOf.get(beat.character) : void 0,
3926
- text: plainVoice(source[beat.id] ?? ""),
3927
- comments: leadingVo ? [...leadingVo, ...own] : own,
3928
- // first line of the run gets the enclosing context
3929
- recordingStatus: recordingOf(beat.id)
3930
- });
3931
- return true;
3932
- };
3933
- const walkNode = (node, scope, ancestorVo) => {
3934
- if (node.type === "group") {
3935
- if (cutSet.has(node.id)) return;
3936
- const label = groupLabel(node, source);
3937
- const childScope = label ? [...scope, label] : scope;
3938
- const childVo = [...ancestorVo, ...ownVo(node.id)];
3939
- if (node.prompt) emitLine(node.prompt, childScope, childVo);
3940
- for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
3941
- return;
3942
- }
3943
- if (cutSet.has(node.id)) return;
3944
- let leading = true;
3945
- for (const beat of node.beats ?? []) {
3946
- if (emitLine(beat, scope, leading ? ancestorVo : void 0)) leading = false;
3947
- }
3948
- };
3949
- for (const scene of loaded.scenes) {
3950
- if (cutSet.has(scene.id)) continue;
3951
- const sceneVo = ownVo(scene.id);
3952
- for (const block of scene.blocks) {
3953
- if (cutSet.has(block.id)) continue;
3954
- const scope = [scene.name, block.name].filter(Boolean);
3955
- const blockVo = [...sceneVo, ...ownVo(block.id)];
3956
- for (const child2 of block.children ?? []) walkNode(child2, scope, blockVo);
3957
- }
3958
- }
3959
- return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
3960
- }
3961
-
3962
- // src/voice-script-xlsx.ts
3963
- async function voiceScriptToXlsx(data) {
3964
- const { default: ExcelJS } = await import("exceljs");
3965
- const wb = new ExcelJS.Workbook();
3966
- wb.creator = "patter";
3967
- const ws = wb.addWorksheet("Voice Script");
3968
- ws.columns = [
3969
- { header: "Scope", key: "scope", width: 28 },
3970
- { header: "Line ID", key: "id", width: 18 },
3971
- { header: "Character", key: "character", width: 16 },
3972
- { header: "Qualifier", key: "qualifier", width: 11 },
3973
- { header: "Actor", key: "actor", width: 16 },
3974
- { header: "Text", key: "text", width: 60 },
3975
- { header: "Comments", key: "comments", width: 36 },
3976
- { header: "Status", key: "status", width: 12 }
3977
- ];
3978
- ws.getRow(1).font = { bold: true };
3979
- ws.views = [{ state: "frozen", ySplit: 1 }];
3980
- for (const l of data.lines) {
3981
- const row = ws.addRow({
3982
- scope: l.scope,
3983
- id: l.id,
3984
- character: l.character,
3985
- qualifier: l.qualifier ?? "",
3986
- actor: l.actor ?? "",
3987
- text: l.text,
3988
- comments: l.comments.join("\n"),
3989
- status: l.recordingStatus
3990
- });
3991
- row.getCell("text").alignment = { wrapText: true, vertical: "top" };
3992
- row.getCell("comments").alignment = { wrapText: true, vertical: "top" };
3993
- }
3994
- return Buffer.from(await wb.xlsx.writeBuffer());
3995
- }
3996
-
3997
- // src/script-doc.ts
3998
- import { DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3, projectQualifiers as projectQualifiers3 } from "@patterkit/model";
3999
- import { humanizeNodeRefs } from "@patterkit/core";
4000
- var TOKENS = {
4001
- ink: "26221c",
4002
- // near-black warm ink (headings)
4003
- inkRead: "3a352d",
4004
- // dialogue body (ink ~84% + muted)
4005
- inkSoft: "5c554b",
4006
- // text (ink ~68% + muted) - a step softer than dialogue
4007
- muted: "8c8479",
4008
- // labels, quiet tags
4009
- accent: "8a5a34",
4010
- // structural marks: conditions, options, jumps, game events
4011
- line: "e4dfd5"
4012
- // hairlines / rules / the group rail
4013
- };
4014
- var CHAR_PALETTE = [
4015
- "b23b3b",
4016
- "b5642b",
4017
- "927524",
4018
- "6b7a31",
4019
- "3f7d49",
4020
- "2c7d6e",
4021
- "2d7791",
4022
- "3a6aa8",
4023
- "5a5ba6",
4024
- "7e4e9c",
4025
- "9c4486",
4026
- "a83e5c"
4027
- ];
4028
- function colourIndex(name) {
4029
- let h = 2166136261;
4030
- for (let i = 0; i < name.length; i++) {
4031
- h ^= name.charCodeAt(i);
4032
- h = Math.imul(h, 16777619);
4033
- }
4034
- h ^= h >>> 16;
4035
- h = Math.imul(h, 2246822507);
4036
- h ^= h >>> 13;
4037
- h = Math.imul(h, 3266489909);
4038
- h ^= h >>> 16;
4039
- return (h >>> 0) % 12;
4040
- }
4041
- function characterColour(name) {
4042
- return CHAR_PALETTE[colourIndex(name)];
4043
- }
4044
- function cueLabel(character, qualifierName) {
4045
- return qualifierName ? `${character.toUpperCase()} (${qualifierName.toUpperCase()})` : character.toUpperCase();
4307
+ for (let n = 2; used.has(name.toLowerCase()) || name.toLowerCase() === "history"; n++) {
4308
+ const suffix = ` (${n})`;
4309
+ name = tidy2(base.slice(0, SHEET_NAME_MAX - suffix.length)) + suffix;
4310
+ }
4311
+ used.add(name.toLowerCase());
4312
+ return name;
4046
4313
  }
4047
- function qualifierNamer(project) {
4048
- const names = new Map(projectQualifiers3(project).map((q) => [q.gameId, q.name]));
4049
- return (gameId) => gameId ? names.get(gameId) ?? gameId : void 0;
4314
+ function cellText(v) {
4315
+ if (v == null) return "";
4316
+ if (typeof v === "string") return v;
4317
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
4318
+ if (v instanceof Date) return v.toISOString();
4319
+ if (typeof v !== "object") return "";
4320
+ const o = v;
4321
+ if (Array.isArray(o.richText)) return o.richText.map((run) => cellText(run?.text)).join("");
4322
+ if ("text" in o) return cellText(o.text);
4323
+ if ("result" in o) return cellText(o.result);
4324
+ return "";
4050
4325
  }
4051
- var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
4052
- var INTERP_RE = /\{@[^}]*\}/g;
4053
- function textRuns(text) {
4054
- const marked = [];
4055
- let last = 0;
4056
- let m;
4057
- MARKUP_RE.lastIndex = 0;
4058
- while ((m = MARKUP_RE.exec(text)) !== null) {
4059
- if (m.index > last) marked.push({ text: text.slice(last, m.index), bold: false, italic: false });
4060
- const tag = m[1];
4061
- marked.push({ text: m[2], bold: tag === "b" || tag === "bi", italic: tag === "i" || tag === "bi" });
4062
- last = MARKUP_RE.lastIndex;
4063
- }
4064
- if (last < text.length) marked.push({ text: text.slice(last), bold: false, italic: false });
4065
- if (!marked.length) marked.push({ text, bold: false, italic: false });
4066
- const out = [];
4067
- for (const r of marked) {
4068
- let li = 0;
4069
- let im;
4070
- INTERP_RE.lastIndex = 0;
4071
- while ((im = INTERP_RE.exec(r.text)) !== null) {
4072
- if (im.index > li) out.push({ text: r.text.slice(li, im.index), bold: r.bold, italic: r.italic, code: false });
4073
- out.push({ text: im[0], bold: r.bold, italic: r.italic, code: true });
4074
- li = INTERP_RE.lastIndex;
4326
+ async function catalogToXlsx(catalog) {
4327
+ const { default: ExcelJS } = await import("exceljs");
4328
+ const wb = new ExcelJS.Workbook();
4329
+ wb.creator = "patter";
4330
+ const byScene = /* @__PURE__ */ new Map();
4331
+ for (const e of catalog.entries) (byScene.get(e.scene) ?? byScene.set(e.scene, []).get(e.scene)).push(e);
4332
+ const used = /* @__PURE__ */ new Set([INFO_SHEET.toLowerCase()]);
4333
+ for (const [scene, entries] of byScene) {
4334
+ const ws = wb.addWorksheet(uniqueSheetName(scene, used));
4335
+ ws.columns = [
4336
+ { header: "ID", key: "id", width: 22 },
4337
+ { header: "Source", key: "source", width: 40 },
4338
+ { header: "Translation", key: "translation", width: 40 },
4339
+ { header: "Comments", key: "comments", width: 30 },
4340
+ { header: "Status", key: "status", width: 10 },
4341
+ { header: "Gender", key: "gender", width: 12 },
4342
+ // Hidden: the translator has no use for it, and it is the import's only faithful record of the scene.
4343
+ { header: "Scene", key: "scene", width: 22, hidden: true },
4344
+ // After the hidden Scene column, so the columns an older reader finds by position stay where they were.
4345
+ { header: "Qualifier", key: "qualifier", width: 11 }
4346
+ ];
4347
+ ws.getRow(1).font = { bold: true };
4348
+ ws.views = [{ state: "frozen", ySplit: 1 }];
4349
+ for (const e of entries) {
4350
+ ws.addRow({
4351
+ id: e.id,
4352
+ source: e.source,
4353
+ translation: e.translation,
4354
+ comments: e.comments.join("\n"),
4355
+ status: e.stale ? "stale" : e.translation ? "translated" : "",
4356
+ gender: e.context?.gender ?? "",
4357
+ scene,
4358
+ qualifier: e.context?.qualifier ?? ""
4359
+ });
4075
4360
  }
4076
- if (li < r.text.length) out.push({ text: r.text.slice(li), bold: r.bold, italic: r.italic, code: false });
4077
4361
  }
4078
- return out.length ? out : [{ text, bold: false, italic: false, code: false }];
4079
- }
4080
- function targetLabel(to, sceneOf, blockTrail) {
4081
- if (to === "END") return "END";
4082
- return blockTrail.get(to) ?? sceneOf.get(to) ?? to;
4362
+ const info = wb.addWorksheet(INFO_SHEET, { state: byScene.size ? "hidden" : "visible" });
4363
+ info.columns = [{ width: 18 }, { width: 40 }];
4364
+ info.addRow([INFO_MARKER]);
4365
+ info.addRow(["Patter reads this sheet to check which language the file is for. Please leave it as it is."]);
4366
+ info.addRow([INFO_LANGUAGE, catalog.locale ?? ""]);
4367
+ info.addRow([INFO_SOURCE, catalog.defaultLocale]);
4368
+ info.addRow([INFO_PROJECT, catalog.project]);
4369
+ return Buffer.from(await wb.xlsx.writeBuffer());
4083
4370
  }
4084
- function gameEventLabel(beat) {
4085
- const gd = beat.gameData;
4086
- if (gd && typeof gd === "object") {
4087
- const parts = Object.entries(gd).map(([k, v]) => {
4088
- const val = v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
4089
- return val === "" ? k : `${k}: ${val}`;
4371
+ async function xlsxToCatalog(buffer) {
4372
+ const { default: ExcelJS } = await import("exceljs");
4373
+ const wb = new ExcelJS.Workbook();
4374
+ await wb.xlsx.load(buffer);
4375
+ const entries = [];
4376
+ let locale, defaultLocale = "", project = "";
4377
+ for (const ws of wb.worksheets) {
4378
+ if (cellText(ws.getCell(1, 1).value).trim() === INFO_MARKER) {
4379
+ ws.eachRow((row) => {
4380
+ const label = cellText(row.getCell(1).value).trim(), value = cellText(row.getCell(2).value).trim();
4381
+ if (label === INFO_LANGUAGE && value) locale = checkCatalogField("spreadsheet", "locale", value);
4382
+ else if (label === INFO_SOURCE) defaultLocale = value;
4383
+ else if (label === INFO_PROJECT) project = value;
4384
+ });
4385
+ continue;
4386
+ }
4387
+ const header = ws.getRow(1).values.slice(1).map(cellText);
4388
+ if (header[0] !== HEADERS[0]) continue;
4389
+ const hasSceneColumn = header[SCENE_COLUMN - 1] === "Scene";
4390
+ const sceneOf = (row) => hasSceneColumn ? cellText(row.getCell(SCENE_COLUMN).value).trim() : "";
4391
+ let sheetScene = "";
4392
+ ws.eachRow((row, n) => {
4393
+ if (n > 1 && !sheetScene) sheetScene = sceneOf(row);
4394
+ });
4395
+ ws.eachRow((row, n) => {
4396
+ if (n === 1) return;
4397
+ const cell = (c) => cellText(row.getCell(c).value);
4398
+ const id = cell(1).trim();
4399
+ if (!id) return;
4400
+ const status = cell(5).trim().toLowerCase();
4401
+ const scene = checkCatalogField("spreadsheet", "scene", sceneOf(row) || sheetScene || ws.name);
4402
+ entries.push({
4403
+ id,
4404
+ scene,
4405
+ source: cell(2),
4406
+ translation: cell(3),
4407
+ comments: cell(4) ? cell(4).split("\n").filter(Boolean) : [],
4408
+ stale: status === "stale"
4409
+ });
4090
4410
  });
4091
- if (parts.length) return `game event \xB7 ${parts.join(", ")}`;
4092
4411
  }
4093
- return "game event";
4412
+ return { project, defaultLocale, locale, entries };
4094
4413
  }
4095
- function optionTag(node) {
4096
- const parts = [];
4097
- if (node.fallback) parts.push("fallback");
4098
- parts.push(node.sticky ? "repeatable" : "once only");
4099
- if (node.secretUntilEligible) parts.push("hidden");
4100
- return parts.join(" \xB7 ");
4414
+
4415
+ // src/voice-script.ts
4416
+ import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3, projectQualifiers as projectQualifiers3 } from "@patterkit/model";
4417
+ function plainVoice(text) {
4418
+ return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
4101
4419
  }
4102
- function sequenceLabel(node) {
4103
- const o = node.options ?? {};
4104
- const bits = [];
4105
- if ((o.order ?? "sequential") === "shuffle") bits.push("shuffled");
4106
- const ex = o.exhaust ?? "once";
4107
- if (ex === "repeat") bits.push("repeating");
4108
- else if (ex === "stick") bits.push("holds on the last");
4109
- return bits.length ? `In sequence \xB7 ${bits.join(", ")}` : "In sequence";
4420
+ function groupLabel(node, source) {
4421
+ if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
4422
+ if (node.selector === "choice") return "";
4423
+ return node.selector ?? "group";
4110
4424
  }
4111
- function runScriptDoc(loaded, opts = {}) {
4425
+ function runVoiceScript(loaded, opts = {}) {
4112
4426
  const { project } = loaded;
4427
+ const everything = !!opts.everything;
4428
+ const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
4429
+ const writingLadder = ladderDecls.map((s) => s.name);
4430
+ const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3).map((s) => s.name);
4431
+ const stub = writingLadder[0];
4432
+ const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
4433
+ const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
4434
+ const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring2(loaded);
4435
+ const recordingBase = opts.recordingOverride ?? manualRecordingOf;
4436
+ const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
4113
4437
  const source = sourceStrings(loaded);
4114
- const { cut, documentation } = mergeAuthoring2(loaded);
4115
- const noteClasses = opts.notes === void 0 || opts.notes === "all" ? void 0 : classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES3, opts.notes.channel);
4116
- const ownNotes = (id) => opts.notes === void 0 ? [] : (documentation.get(id) ?? []).filter((l) => l.type !== void 0 && (noteClasses === void 0 || noteClasses.has(l.type)));
4117
- const sceneOf = /* @__PURE__ */ new Map();
4118
- const blockTrail = /* @__PURE__ */ new Map();
4119
- const blockName = /* @__PURE__ */ new Map();
4120
- for (const scene of loaded.scenes) {
4121
- sceneOf.set(scene.id, scene.name);
4122
- for (const block of scene.blocks) {
4123
- blockTrail.set(block.id, `${scene.name} \u203A ${block.name}`);
4124
- blockName.set(block.id, block.name);
4125
- }
4126
- }
4127
- const humanize = (cond) => humanizeNodeRefs(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
4128
- const els = [];
4129
- const nameOf = qualifierNamer(project);
4130
- const speaker = (beat) => ({
4131
- character: beat.character ?? "",
4132
- ...beat.qualifier ? { qualifier: beat.qualifier, qualifierName: nameOf(beat.qualifier) } : {},
4133
- direction: beat.direction
4134
- });
4135
- const textOf = (id) => source[id] ?? "";
4136
- const notes = (id, indent, sid) => {
4137
- for (const l of ownNotes(id)) els.push({ kind: "note", id, indent, snippet: sid, text: l.text, class: l.type });
4138
- };
4139
- const jumpText = (j) => {
4140
- const dest = targetLabel(j.to, sceneOf, blockTrail);
4141
- return j.mode === "call" ? `${dest} (and return)` : dest;
4142
- };
4143
- let snippetSeq = 0;
4144
- const nextSid = () => ++snippetSeq;
4145
- const emitBeats = (node, indent, sid) => {
4146
- for (const beat of node.beats ?? []) {
4147
- if (cut.has(beat.id)) continue;
4148
- if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent, snippet: sid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
4149
- else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
4150
- else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent, snippet: sid, text: gameEventLabel(beat) });
4151
- else continue;
4152
- notes(beat.id, indent, sid);
4153
- }
4154
- if (node.jump) els.push({ kind: "jump", id: node.id, to: node.jump.to, indent, snippet: sid, text: jumpText(node.jump) });
4155
- };
4156
- const walk = (node, indent, sid) => {
4157
- if (cut.has(node.id)) return;
4158
- if (node.type === "snippet") {
4159
- if (node.condition) els.push({ kind: "condition", id: node.id, indent, snippet: sid, text: `if ${humanize(node.condition)}` });
4160
- notes(node.id, indent, sid);
4161
- emitBeats(node, indent, sid);
4162
- return;
4163
- }
4164
- if (node.condition) els.push({ kind: "condition", id: node.id, indent, snippet: sid, text: `if ${humanize(node.condition)}` });
4165
- if (node.selector === "choice" || node.selector === "branch" || node.selector === "sequence") {
4166
- } else notes(node.id, indent, sid);
4167
- if (node.selector === "choice") {
4168
- els.push({ kind: "group", id: node.id, indent, label: "Choose" });
4169
- notes(node.id, indent, sid);
4170
- for (const child2 of node.children ?? []) {
4171
- if (cut.has(child2.id)) continue;
4172
- const cid = nextSid();
4173
- if (child2.type === "group" && child2.prompt) {
4174
- if (child2.condition) els.push({ kind: "condition", id: child2.id, indent, snippet: cid, text: `if ${humanize(child2.condition)}` });
4175
- els.push({ kind: "option", id: child2.prompt.id, node: child2.id, indent, snippet: cid, runs: textRuns(textOf(child2.prompt.id) || "(option)"), tag: optionTag(child2) });
4176
- notes(child2.id, indent, cid);
4177
- notes(child2.prompt.id, indent, cid);
4178
- for (const c of child2.children ?? []) walk(c, indent + 1, cid);
4179
- } else if (child2.type === "snippet") {
4180
- const beats = child2.beats ?? [];
4181
- const first = beats.find((b) => b.kind === "line" || b.kind === "text");
4182
- if (child2.condition) els.push({ kind: "condition", id: child2.id, indent, snippet: cid, text: `if ${humanize(child2.condition)}` });
4183
- els.push({ kind: "option", id: first?.id ?? child2.id, node: child2.id, indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child2) });
4184
- notes(child2.id, indent, cid);
4185
- if (first) notes(first.id, indent, cid);
4186
- let seenPrompt = false;
4187
- for (const beat of beats) {
4188
- if (!seenPrompt && beat === first) {
4189
- seenPrompt = true;
4190
- continue;
4191
- }
4192
- if (cut.has(beat.id)) continue;
4193
- if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent: indent + 1, snippet: cid, ...speaker(beat), runs: textRuns(textOf(beat.id)) });
4194
- else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
4195
- else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
4196
- else continue;
4197
- notes(beat.id, indent + 1, cid);
4198
- }
4199
- if (child2.jump) els.push({ kind: "jump", id: child2.id, to: child2.jump.to, indent: indent + 1, snippet: cid, text: jumpText(child2.jump) });
4200
- } else {
4201
- walk(child2, indent + 1, cid);
4202
- }
4203
- }
4204
- return;
4205
- }
4206
- if (node.selector === "branch") {
4207
- els.push({ kind: "group", id: node.id, indent, label: "One of \xB7 first match wins" });
4208
- notes(node.id, indent, sid);
4209
- const kids2 = (node.children ?? []).filter((c) => !cut.has(c.id));
4210
- kids2.forEach((child2, i) => {
4211
- const isCatchAll = i === kids2.length - 1 && kids2.length > 1 && !("condition" in child2 && child2.condition);
4212
- if (isCatchAll) els.push({ kind: "else", id: child2.id, indent });
4213
- walk(child2, indent, nextSid());
4214
- });
4438
+ const actorOf = /* @__PURE__ */ new Map();
4439
+ for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
4440
+ const qualifierName = new Map(projectQualifiers3(project).map((q) => [q.gameId, q.name]));
4441
+ const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES3, "vo");
4442
+ const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
4443
+ const lines = [];
4444
+ const emitLine = (beat, scope, leadingVo) => {
4445
+ if (beat.kind !== "line" || cutSet.has(beat.id)) return false;
4446
+ const ws = writingOf.get(beat.id) ?? stub;
4447
+ if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) return false;
4448
+ const own = ownVo(beat.id);
4449
+ lines.push({
4450
+ scope: scope.join(" \u203A "),
4451
+ id: beat.id,
4452
+ character: beat.character ?? "",
4453
+ ...beat.qualifier ? { qualifier: qualifierName.get(beat.qualifier) ?? beat.qualifier } : {},
4454
+ actor: beat.character ? actorOf.get(beat.character) : void 0,
4455
+ ...beat.direction?.trim() ? { direction: plainVoice(beat.direction.trim()) } : {},
4456
+ text: plainVoice(source[beat.id] ?? ""),
4457
+ comments: leadingVo ? [...leadingVo, ...own] : own,
4458
+ // first line of the run gets the enclosing context
4459
+ recordingStatus: recordingOf(beat.id)
4460
+ });
4461
+ return true;
4462
+ };
4463
+ const walkNode = (node, scope, ancestorVo) => {
4464
+ if (node.type === "group") {
4465
+ if (cutSet.has(node.id)) return;
4466
+ const label = groupLabel(node, source);
4467
+ const childScope = label ? [...scope, label] : scope;
4468
+ const childVo = [...ancestorVo, ...ownVo(node.id)];
4469
+ if (node.prompt) emitLine(node.prompt, childScope, childVo);
4470
+ for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
4215
4471
  return;
4216
4472
  }
4217
- if (node.selector === "sequence") {
4218
- els.push({ kind: "group", id: node.id, indent, label: sequenceLabel(node) });
4219
- notes(node.id, indent, sid);
4220
- for (const child2 of node.children ?? []) walk(child2, indent, nextSid());
4221
- return;
4473
+ if (cutSet.has(node.id)) return;
4474
+ let leading = true;
4475
+ for (const beat of node.beats ?? []) {
4476
+ if (emitLine(beat, scope, leading ? ancestorVo : void 0)) leading = false;
4222
4477
  }
4223
- for (const child2 of node.children ?? []) walk(child2, indent, sid);
4224
4478
  };
4225
- const only = opts.scenes ? new Set(opts.scenes) : void 0;
4226
4479
  for (const scene of loaded.scenes) {
4227
- if (cut.has(scene.id) || only && !only.has(scene.id)) continue;
4228
- els.push({ kind: "scene", id: scene.id, text: scene.name });
4229
- notes(scene.id, 0, void 0);
4480
+ if (cutSet.has(scene.id)) continue;
4481
+ const sceneVo = ownVo(scene.id);
4230
4482
  for (const block of scene.blocks) {
4231
- if (cut.has(block.id)) continue;
4232
- els.push({ kind: "block", id: block.id, text: block.name });
4233
- notes(block.id, 0, void 0);
4234
- for (const child2 of block.children ?? []) walk(child2, 0, void 0);
4483
+ if (cutSet.has(block.id)) continue;
4484
+ const scope = [scene.name, block.name].filter(Boolean);
4485
+ const blockVo = [...sceneVo, ...ownVo(block.id)];
4486
+ for (const child2 of block.children ?? []) walkNode(child2, scope, blockVo);
4235
4487
  }
4236
4488
  }
4237
- return { project: project.project.name, elements: els };
4489
+ return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
4490
+ }
4491
+
4492
+ // src/voice-script-xlsx.ts
4493
+ async function voiceScriptToXlsx(data) {
4494
+ const { default: ExcelJS } = await import("exceljs");
4495
+ const wb = new ExcelJS.Workbook();
4496
+ wb.creator = "patter";
4497
+ const ws = wb.addWorksheet("Voice Script");
4498
+ ws.columns = [
4499
+ { header: "Scope", key: "scope", width: 28 },
4500
+ { header: "Line ID", key: "id", width: 18 },
4501
+ { header: "Character", key: "character", width: 16 },
4502
+ { header: "Qualifier", key: "qualifier", width: 11 },
4503
+ { header: "Actor", key: "actor", width: 16 },
4504
+ { header: "Direction", key: "direction", width: 24 },
4505
+ { header: "Text", key: "text", width: 60 },
4506
+ { header: "Comments", key: "comments", width: 36 },
4507
+ { header: "Status", key: "status", width: 12 }
4508
+ ];
4509
+ ws.getRow(1).font = { bold: true };
4510
+ ws.views = [{ state: "frozen", ySplit: 1 }];
4511
+ for (const l of data.lines) {
4512
+ const row = ws.addRow({
4513
+ scope: l.scope,
4514
+ id: l.id,
4515
+ character: l.character,
4516
+ qualifier: l.qualifier ?? "",
4517
+ actor: l.actor ?? "",
4518
+ direction: l.direction ?? "",
4519
+ text: l.text,
4520
+ comments: l.comments.join("\n"),
4521
+ status: l.recordingStatus
4522
+ });
4523
+ row.getCell("direction").alignment = { wrapText: true, vertical: "top" };
4524
+ row.getCell("text").alignment = { wrapText: true, vertical: "top" };
4525
+ row.getCell("comments").alignment = { wrapText: true, vertical: "top" };
4526
+ }
4527
+ return Buffer.from(await wb.xlsx.writeBuffer());
4238
4528
  }
4239
4529
 
4240
4530
  // src/script-docx.ts
@@ -4249,6 +4539,8 @@ var HANG = 340;
4249
4539
  var RIGHT_TAB = 9020;
4250
4540
  var SNIP_GAP = 170;
4251
4541
  var WITHIN = 30;
4542
+ var EFFECT_STYLE = "PatterEffect";
4543
+ var EFFECT_STYLE_DEF = { id: EFFECT_STYLE, name: "Patter effect", basedOn: "Normal", quickFormat: false };
4252
4544
  var S = { h1: 32, h2: 26, body: 22, cue: 18, label: 16, tag: 15, mech: 18 };
4253
4545
  function bodyRuns(runs, color, base = {}) {
4254
4546
  return runs.map((r) => r.code ? new TextRun({ text: r.text, font: MONO, color: TOKENS.accent, size: S.body - 2 }) : new TextRun({ text: r.text, font: SERIF, color, size: S.body, bold: base.bold || r.bold || void 0, italics: base.italic || r.italic || void 0 }));
@@ -4277,7 +4569,7 @@ function paragraph(el, prev) {
4277
4569
  });
4278
4570
  case "line": {
4279
4571
  const kids2 = [];
4280
- if (el.character) kids2.push(new TextRun({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
4572
+ if (el.character) kids2.push(new TextRun({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, color: characterColour(el.character, el.colour), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
4281
4573
  if (el.direction) kids2.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
4282
4574
  kids2.push(...bodyRuns(el.runs, TOKENS.inkRead));
4283
4575
  return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids2 });
@@ -4286,10 +4578,17 @@ function paragraph(el, prev) {
4286
4578
  return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: bodyRuns(el.runs, TOKENS.inkSoft) });
4287
4579
  case "condition":
4288
4580
  return new Paragraph({ keepNext: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: [new TextRun({ text: `\u2039 ${el.text} \u203A`, font: MONO, color: TOKENS.accent, size: S.mech - 1 })] });
4581
+ case "effect": {
4582
+ const [label, ...rest] = el.text.split(": ");
4583
+ return new Paragraph({ style: EFFECT_STYLE, keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: [
4584
+ new TextRun({ text: `${label}: `, font: SANS, italics: true, color: TOKENS.muted, size: S.mech - 1 }),
4585
+ new TextRun({ text: rest.join(": "), font: SANS, color: TOKENS.muted, size: S.mech - 1 })
4586
+ ] });
4587
+ }
4289
4588
  case "group":
4290
4589
  return new Paragraph({ keepNext: true, spacing: { before: 260, after: 60 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: el.label.toUpperCase(), font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: S.label, characterSpacing: 14 })] });
4291
4590
  case "else":
4292
- return new Paragraph({ keepNext: true, spacing: { before: 140, after: 20 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: "else \xB7 catch-all", font: SANS, allCaps: true, color: TOKENS.muted, size: S.label, characterSpacing: 12 })] });
4591
+ return new Paragraph({ keepNext: true, spacing: { before: 140, after: 20 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: "else (catch-all)", font: SANS, allCaps: true, color: TOKENS.muted, size: S.label, characterSpacing: 12 })] });
4293
4592
  case "option": {
4294
4593
  const kids2 = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
4295
4594
  if (el.tag) kids2.push(new TextRun({ text: " ", size: S.body }), new TextRun({ text: el.tag, font: SANS, smallCaps: true, color: TOKENS.muted, size: S.tag }));
@@ -4309,7 +4608,7 @@ async function scriptToDocx(doc) {
4309
4608
  new Paragraph({ spacing: { after: 560 }, children: [new TextRun({ text: doc.project, font: SERIF, bold: true, color: TOKENS.ink, size: 52 })] }),
4310
4609
  ...doc.elements.map((el, i) => paragraph(el, doc.elements[i - 1]))
4311
4610
  ];
4312
- const document = new Document({ creator: "Patter", title: doc.project, sections: [{ children: body }] });
4611
+ const document = new Document({ creator: "Patter", title: doc.project, styles: { paragraphStyles: [EFFECT_STYLE_DEF] }, sections: [{ children: body }] });
4313
4612
  return Packer.toBuffer(document);
4314
4613
  }
4315
4614
 
@@ -4560,7 +4859,7 @@ async function scriptToPdf(doc) {
4560
4859
  }
4561
4860
  const yTop = pdf.y;
4562
4861
  if (cue) {
4563
- pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character))).text(cue, cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
4862
+ pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character, el.colour))).text(cue, cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
4564
4863
  pdf.y = yTop;
4565
4864
  }
4566
4865
  pdf.fontSize(BODY);
@@ -4575,13 +4874,19 @@ async function scriptToPdf(doc) {
4575
4874
  pdf.fontSize(9.5);
4576
4875
  draw(split(`\u2039 ${el.text} \u203A`, "Mono", ACCENT), cx, { width: w });
4577
4876
  break;
4877
+ case "effect": {
4878
+ const at = el.text.indexOf(": ");
4879
+ pdf.fontSize(8.5);
4880
+ draw([...split(el.text.slice(0, at + 2), "Serif-Italic", MUTED), ...split(el.text.slice(at + 2), "Sans", MUTED)], cx, { width: w, lineGap: 2 });
4881
+ break;
4882
+ }
4578
4883
  case "group":
4579
4884
  pdf.fontSize(8);
4580
4885
  draw(split(el.label.toUpperCase(), "Sans-Bold", ACCENT), cx, { width: w, characterSpacing: 1.2 });
4581
4886
  break;
4582
4887
  case "else":
4583
4888
  pdf.fontSize(8);
4584
- draw(split("ELSE \xB7 CATCH-ALL", "Sans", MUTED), cx, { width: w, characterSpacing: 1 });
4889
+ draw(split("ELSE (CATCH-ALL)", "Sans", MUTED), cx, { width: w, characterSpacing: 1 });
4585
4890
  break;
4586
4891
  case "option": {
4587
4892
  const ox = cx + HANG2;
@@ -4684,6 +4989,8 @@ function runInit(opts) {
4684
4989
  schema: PROJECT_SCHEMA,
4685
4990
  project: { id: newId("proj"), name },
4686
4991
  locales: { default: "en", all: ["en"] },
4992
+ start: { scene: sceneId },
4993
+ // the one scene is where the story starts
4687
4994
  ...opts.vcs ? { vcs: opts.vcs } : {},
4688
4995
  // record the chosen VCS so it can be read / switched later
4689
4996
  voiced: true
@@ -4758,7 +5065,7 @@ indent_size = 2
4758
5065
  `;
4759
5066
 
4760
5067
  // src/resolve.ts
4761
- import { walkNodes as walkNodes3, effectiveGameId as effectiveGameId2, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
5068
+ import { walkNodes as walkNodes4, effectiveGameId as effectiveGameId2, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
4762
5069
  import { defaultFor } from "@wildwinter/scoperegistry";
4763
5070
  import { parseFieldQuery, fieldNameMatches, fieldValueMatches, fieldValueText } from "@wildwinter/toolkit";
4764
5071
  function runResolve(loaded, query) {
@@ -4814,7 +5121,7 @@ function runStatusBrowse(loaded, status, dimension = "writing", focus, recording
4814
5121
  if (statusAt(beat.id) !== status) return;
4815
5122
  out.push({ id: beat.id, kind: "beat", text: strings[beat.id], location: segments, sceneId: scene.id, file });
4816
5123
  };
4817
- walkNodes3(block.children, (node) => {
5124
+ walkNodes4(block.children, (node) => {
4818
5125
  if (node.type === "group") {
4819
5126
  if (node.prompt) consider(node.prompt);
4820
5127
  return;
@@ -4835,7 +5142,7 @@ function listProjectTags(loaded) {
4835
5142
  add(scene.tags);
4836
5143
  for (const block of scene.blocks) {
4837
5144
  add(block.tags);
4838
- walkNodes3(block.children, (node) => {
5145
+ walkNodes4(block.children, (node) => {
4839
5146
  add(node.tags);
4840
5147
  if (node.type === "group") {
4841
5148
  add(node.prompt?.tags);
@@ -4858,7 +5165,7 @@ function runTagBrowse(loaded, tag, focus) {
4858
5165
  for (const block of scene.blocks) {
4859
5166
  const segments = [scene.name, block.name];
4860
5167
  if (has(block.tags)) out.push({ id: block.id, kind: "block", name: block.name, gameId: effectiveGameId2(block), location: segments, sceneId: scene.id, file });
4861
- walkNodes3(block.children, (node) => {
5168
+ walkNodes4(block.children, (node) => {
4862
5169
  if (has(node.tags)) {
4863
5170
  const preview = node.type === "group" ? node.prompt ? strings[node.prompt.id] : void 0 : strings[node.beats?.[0]?.id ?? ""];
4864
5171
  out.push({ id: node.id, kind: node.type, text: preview, location: segments, sceneId: scene.id, file });
@@ -4893,7 +5200,7 @@ function runPropertyUsage(loaded, query, focus) {
4893
5200
  }
4894
5201
  for (const block of scene.blocks) {
4895
5202
  const segments = [scene.name, block.name];
4896
- walkNodes3(block.children, (node) => {
5203
+ walkNodes4(block.children, (node) => {
4897
5204
  if (refs(node.condition) && valueOk(node.condition)) push(node.id, node.type, segments, `if ${node.condition}`);
4898
5205
  if (node.type === "group") {
4899
5206
  if (node.prompt && refs(strings[node.prompt.id]) && valueOk(strings[node.prompt.id])) push(node.prompt.id, "beat", segments, strings[node.prompt.id]);
@@ -4949,7 +5256,7 @@ function runFieldBrowse(loaded, query, focus) {
4949
5256
  const segments = [scene.name, block.name];
4950
5257
  visit("block", block.gameData, { id: block.id, kind: "block", name: block.name, gameId: effectiveGameId2(block), location: segments });
4951
5258
  const options = /* @__PURE__ */ new Set();
4952
- walkNodes3(block.children, (node) => {
5259
+ walkNodes4(block.children, (node) => {
4953
5260
  if (node.type === "group") {
4954
5261
  if (node.selector === "choice") {
4955
5262
  for (const c of node.children) if (c.type === "group") options.add(c.id);
@@ -5018,7 +5325,7 @@ function buildIndex(loaded) {
5018
5325
  sceneId: scene.id,
5019
5326
  file
5020
5327
  });
5021
- walkNodes3(block.children, (node) => {
5328
+ walkNodes4(block.children, (node) => {
5022
5329
  out.push({
5023
5330
  id: node.id,
5024
5331
  kind: node.type,
@@ -5056,7 +5363,7 @@ function buildIndex(loaded) {
5056
5363
 
5057
5364
  // src/replace.ts
5058
5365
  import { canonicalStringify as canonicalStringify9 } from "@patterkit/core";
5059
- import { walkNodes as walkNodes4, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE3, AUTHORING_SCHEMA as AUTHORING_SCHEMA2, FLOW_SCHEMA as FLOW_SCHEMA2 } from "@patterkit/model";
5366
+ import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE3, AUTHORING_SCHEMA as AUTHORING_SCHEMA2, FLOW_SCHEMA as FLOW_SCHEMA2 } from "@patterkit/model";
5060
5367
  import { findMatcher } from "@wildwinter/toolkit";
5061
5368
  function runReplace(loaded, opts) {
5062
5369
  const plan = { hits: [], writes: [], shards: [], authoring: [], flows: [], scenes: 0 };
@@ -5072,7 +5379,7 @@ function runReplace(loaded, opts) {
5072
5379
  sceneName.set(scene.id, scene.name);
5073
5380
  for (const block of scene.blocks) {
5074
5381
  const segs = [scene.name, block.name];
5075
- walkNodes4(block.children, (node) => {
5382
+ walkNodes5(block.children, (node) => {
5076
5383
  if (node.type === "group") {
5077
5384
  if (node.prompt) locOf.set(node.prompt.id, segs);
5078
5385
  return;
@@ -5154,7 +5461,7 @@ function runReplace(loaded, opts) {
5154
5461
  for (const block of scene.blocks) {
5155
5462
  const segs = [scene.name, block.name];
5156
5463
  visit("block", block, segs);
5157
- walkNodes4(block.children, (node) => {
5464
+ walkNodes5(block.children, (node) => {
5158
5465
  if (node.type === "group") {
5159
5466
  if (node.selector === "choice") {
5160
5467
  for (const option of node.children) if (option.type === "group") visit("option", option, segs);
@@ -5177,110 +5484,6 @@ function runReplace(loaded, opts) {
5177
5484
  return plan;
5178
5485
  }
5179
5486
 
5180
- // src/stages.ts
5181
- import { parse, renameStageInExpression, stageGateCount as gatesIn } from "@wildwinter/expr";
5182
- import { dialectWithForeignScopes } from "@patterkit/dialect";
5183
- import { hostScopesToSpec as hostScopesToSpec2, projectScopes as projectScopes2 } from "@patterkit/compiler";
5184
- import { walkNodes as walkNodes5 } from "@patterkit/model";
5185
- function stageDialect(project, gameScopes) {
5186
- return dialectWithForeignScopes(hostScopesToSpec2(projectScopes2(project, gameScopes?.merged).host));
5187
- }
5188
- var refersTo = (target) => (scope, name) => scope === target.scope && name === target.name;
5189
- function effectTarget(ref, dialect) {
5190
- try {
5191
- const n = parse(ref, dialect);
5192
- return n.kind === "scopedvar" ? { scope: n.scope, name: n.name } : void 0;
5193
- } catch {
5194
- return void 0;
5195
- }
5196
- }
5197
- function sceneSites(scene) {
5198
- const sites = [];
5199
- const effects = (list) => {
5200
- for (const e of list ?? []) {
5201
- sites.push({ get: () => e.value, set: (src) => {
5202
- e.value = src;
5203
- }, assignedTo: e.target });
5204
- }
5205
- };
5206
- effects(scene.onEntry);
5207
- for (const block of scene.blocks) {
5208
- walkNodes5(block.children, (node) => {
5209
- if (typeof node.condition === "string") {
5210
- sites.push({ get: () => node.condition, set: (src) => {
5211
- node.condition = src;
5212
- } });
5213
- }
5214
- if (node.type === "snippet") {
5215
- effects(node.onEnter);
5216
- effects(node.onExit);
5217
- }
5218
- });
5219
- }
5220
- return sites;
5221
- }
5222
- var reach = (scenes, target) => target.scope === "scene" ? scenes.filter((s) => s.id === target.scene) : scenes;
5223
- function renameStage(scenes, target, from, to, dialect) {
5224
- const result = { scenes: [], expressions: 0 };
5225
- if (to.trim() === "" || from === to) return result;
5226
- const isTarget = refersTo(target);
5227
- for (const scene of reach(scenes, target)) {
5228
- let changed = false;
5229
- for (const site of sceneSites(scene)) {
5230
- const src = site.get();
5231
- if (src === void 0) continue;
5232
- const t = site.assignedTo !== void 0 ? effectTarget(site.assignedTo, dialect) : void 0;
5233
- const next = renameStageInExpression(src, dialect, isTarget, from, to, t !== void 0 && isTarget(t.scope, t.name));
5234
- if (next === void 0) continue;
5235
- site.set(next);
5236
- result.expressions++;
5237
- changed = true;
5238
- }
5239
- if (changed) result.scenes.push(scene);
5240
- }
5241
- return result;
5242
- }
5243
- function stageGateCount(scenes, target, dialect) {
5244
- const isTarget = refersTo(target);
5245
- let count = 0;
5246
- for (const scene of reach(scenes, target)) {
5247
- for (const site of sceneSites(scene)) {
5248
- const src = site.get();
5249
- if (src !== void 0) count += gatesIn(src, dialect, isTarget);
5250
- }
5251
- }
5252
- return count;
5253
- }
5254
- function renameGameDataStage(scenes, kind, field, from, to) {
5255
- const result = { scenes: [], values: 0 };
5256
- if (to.trim() === "" || from === to) return result;
5257
- for (const scene of scenes) {
5258
- let changed = false;
5259
- const visit = (holder) => {
5260
- if (holder.gameData?.[field] !== from) return;
5261
- holder.gameData[field] = to;
5262
- result.values++;
5263
- changed = true;
5264
- };
5265
- if (kind === "scene") visit(scene);
5266
- for (const block of scene.blocks) {
5267
- if (kind === "block") visit(block);
5268
- walkNodes5(block.children, (node) => {
5269
- if (node.type !== "snippet") {
5270
- if (kind === "option" && node.selector === "choice") {
5271
- for (const option of node.children) if (option.type === "group") visit(option);
5272
- }
5273
- return;
5274
- }
5275
- if (kind === "snippet") visit(node);
5276
- for (const beat of node.beats ?? []) if (beat.kind === kind) visit(beat);
5277
- });
5278
- }
5279
- if (changed) result.scenes.push(scene);
5280
- }
5281
- return result;
5282
- }
5283
-
5284
5487
  // src/pin.ts
5285
5488
  import { readFileSync as readFileSync11 } from "fs";
5286
5489
  import { canonicalStringify as canonicalStringify10, parseSource as parseSource7 } from "@patterkit/core";
@@ -5548,8 +5751,190 @@ function containedWrite(dir, name) {
5548
5751
  return full;
5549
5752
  }
5550
5753
 
5754
+ // src/resolutions.ts
5755
+ import { readFileSync as readFileSync13 } from "fs";
5756
+ import { canonicalStringify as canonicalStringify12, parseSource as parseSource9 } from "@patterkit/core";
5757
+ import { DEFAULT_QUALIFIERS as DEFAULT_QUALIFIERS2 } from "@patterkit/model";
5758
+ import { clearUnresolved, unresolvedOf } from "@wildwinter/toolkit/extensions";
5759
+ var ResolutionError = class extends Error {
5760
+ };
5761
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5762
+ var UNSETTABLE_KINDS = /* @__PURE__ */ new Set(["moved", "structural"]);
5763
+ function isSettableClash(clash) {
5764
+ if (typeof clash.kind !== "string" || UNSETTABLE_KINDS.has(clash.kind)) return false;
5765
+ return !clash.path.endsWith(":order");
5766
+ }
5767
+ function unresolvedClashes(shard) {
5768
+ const out = [];
5769
+ for (const e of unresolvedOf(shard) ?? []) {
5770
+ if (!isObj2(e) || typeof e["path"] !== "string") continue;
5771
+ const c = { path: e["path"], kind: typeof e["kind"] === "string" ? e["kind"] : "" };
5772
+ if ("base" in e) c.base = e["base"];
5773
+ if ("ours" in e) c.ours = e["ours"];
5774
+ if ("theirs" in e) c.theirs = e["theirs"];
5775
+ out.push(c);
5776
+ }
5777
+ return out;
5778
+ }
5779
+ function clashPathSegments(path) {
5780
+ const p = path.endsWith(":order") ? path.slice(0, -":order".length) : path;
5781
+ const out = [];
5782
+ let i = 0;
5783
+ while (i < p.length) {
5784
+ if (p[i] === ".") {
5785
+ i++;
5786
+ continue;
5787
+ }
5788
+ if (p[i] === "[") {
5789
+ if (p[i + 1] === '"') {
5790
+ let j2 = i + 2;
5791
+ while (j2 < p.length && !(p[j2] === '"' && p[j2 - 1] !== "\\")) j2++;
5792
+ let key;
5793
+ try {
5794
+ key = JSON.parse(p.slice(i + 1, j2 + 1));
5795
+ } catch {
5796
+ key = p.slice(i + 2, j2);
5797
+ }
5798
+ out.push({ key, bracket: false });
5799
+ i = p.indexOf("]", j2) + 1 || p.length;
5800
+ continue;
5801
+ }
5802
+ const end = p.indexOf("]", i);
5803
+ out.push({ key: p.slice(i + 1, end < 0 ? p.length : end), bracket: true });
5804
+ i = end < 0 ? p.length : end + 1;
5805
+ continue;
5806
+ }
5807
+ let j = i;
5808
+ while (j < p.length && p[j] !== "." && p[j] !== "[") j++;
5809
+ out.push({ key: p.slice(i, j), bracket: false });
5810
+ i = j;
5811
+ }
5812
+ return out;
5813
+ }
5814
+ function elementMatches(el, key) {
5815
+ if (!isObj2(el)) return false;
5816
+ if (el["id"] === key || el["name"] === key || el["gameId"] === key) return true;
5817
+ return "ts" in el && `${String(el["author"] ?? "")}|${String(el["ts"] ?? "")}` === key;
5818
+ }
5819
+ function valueAtClashPath(shard, path) {
5820
+ let at = shard;
5821
+ for (const { key } of clashPathSegments(path)) {
5822
+ if (Array.isArray(at)) at = at.find((el) => elementMatches(el, key));
5823
+ else if (isObj2(at)) at = at[key];
5824
+ else return void 0;
5825
+ }
5826
+ return at;
5827
+ }
5828
+ var DOTTED_LISTS = /* @__PURE__ */ new Set(["qualifiers", "suggestions"]);
5829
+ function emptyContainer(key, next, depth) {
5830
+ if (depth === 0 && key === "qualifiers") return structuredClone(DEFAULT_QUALIFIERS2);
5831
+ return next.bracket || depth === 0 && DOTTED_LISTS.has(key) ? [] : {};
5832
+ }
5833
+ function setAtPath(root, path, value) {
5834
+ const segs = clashPathSegments(path);
5835
+ if (segs.length === 0) throw new ResolutionError(`"${path}" doesn't name a place in the shard`);
5836
+ const chain = [];
5837
+ let at = root;
5838
+ for (let i = 0; i < segs.length - 1; i++) {
5839
+ const key = segs[i].key;
5840
+ let next;
5841
+ if (Array.isArray(at)) {
5842
+ next = at.find((el) => elementMatches(el, key));
5843
+ if (next === void 0) {
5844
+ if (value === void 0) return;
5845
+ throw new ResolutionError(`"${path}": nothing in the shard is "${key}", so their value has nowhere to go`);
5846
+ }
5847
+ } else {
5848
+ next = at[key];
5849
+ if (next === void 0) {
5850
+ if (value === void 0) return;
5851
+ next = emptyContainer(key, segs[i + 1], i);
5852
+ at[key] = next;
5853
+ }
5854
+ }
5855
+ if (!isObj2(next) && !Array.isArray(next)) throw new ResolutionError(`"${path}": "${key}" isn't an object or a list`);
5856
+ chain.push({ parent: at, key });
5857
+ at = next;
5858
+ }
5859
+ const last = segs[segs.length - 1].key;
5860
+ if (Array.isArray(at)) {
5861
+ const i = at.findIndex((el) => elementMatches(el, last));
5862
+ if (value === void 0) {
5863
+ if (i >= 0) at.splice(i, 1);
5864
+ } else if (i >= 0) at[i] = value;
5865
+ else at.push(value);
5866
+ } else if (value === void 0) {
5867
+ delete at[last];
5868
+ } else {
5869
+ at[last] = value;
5870
+ }
5871
+ if (value !== void 0) return;
5872
+ let child2 = at;
5873
+ for (let i = chain.length - 1; i >= 0; i--) {
5874
+ if (!isObj2(child2) || Object.keys(child2).length > 0) break;
5875
+ const { parent, key } = chain[i];
5876
+ if (Array.isArray(parent)) break;
5877
+ if (parent === root) break;
5878
+ delete parent[key];
5879
+ child2 = parent;
5880
+ }
5881
+ }
5882
+ function applyResolutions(shardText, decisions) {
5883
+ const shard = parseSource9(shardText);
5884
+ if (!isObj2(shard)) throw new ResolutionError("the shard isn't an object");
5885
+ const clashes = new Map(unresolvedClashes(shard).map((c) => [c.path, c]));
5886
+ const out = structuredClone(shard);
5887
+ for (const { path, decision } of decisions) {
5888
+ const clash = clashes.get(path);
5889
+ if (!clash) throw new ResolutionError(`no unresolved clash at "${path}"`);
5890
+ if (decision !== "theirs") continue;
5891
+ if (!isSettableClash(clash)) throw new ResolutionError(`the clash at "${path}" (${clash.kind}) isn't one value, so theirs can't be written: keep ours, or edit it and mark it resolved`);
5892
+ setAtPath(out, path, structuredClone(clash.theirs));
5893
+ }
5894
+ return canonicalStringify12(clearUnresolved(out, decisions.map((d) => d.path)));
5895
+ }
5896
+ function findMarkedShards(root) {
5897
+ const tree = walkFilesByExt(root, [...SHARD_EXTENSIONS, CONFLICT_SIDECAR]);
5898
+ const sidecars = new Set(tree.get(CONFLICT_SIDECAR));
5899
+ const out = [];
5900
+ for (const file of SHARD_EXTENSIONS.flatMap((ext) => tree.get(ext)).sort()) {
5901
+ let text;
5902
+ try {
5903
+ text = readFileSync13(file, "utf8");
5904
+ } catch {
5905
+ continue;
5906
+ }
5907
+ if (!text.includes("unresolved")) continue;
5908
+ let shard;
5909
+ try {
5910
+ shard = parseSource9(text);
5911
+ } catch {
5912
+ continue;
5913
+ }
5914
+ if (!isObj2(shard) || unresolvedOf(shard) === void 0) continue;
5915
+ const found = { file, shard, clashes: unresolvedClashes(shard) };
5916
+ const sidecarPath = `${file}${CONFLICT_SIDECAR}`;
5917
+ if (sidecars.has(sidecarPath)) {
5918
+ found.sidecarPath = sidecarPath;
5919
+ try {
5920
+ const s = JSON.parse(readFileSync13(sidecarPath, "utf8"));
5921
+ found.sidecar = { ...typeof s.type === "string" ? { type: s.type } : {}, conflicts: Array.isArray(s.conflicts) ? s.conflicts : [], warnings: Array.isArray(s.warnings) ? s.warnings : [] };
5922
+ } catch {
5923
+ }
5924
+ }
5925
+ out.push(found);
5926
+ }
5927
+ return out;
5928
+ }
5929
+
5551
5930
  // src/cast-rename.ts
5552
5931
  import { castStringKey as castStringKey2, speakerLabelKey as speakerLabelKey2, walkNodes as walkNodes6 } from "@patterkit/model";
5932
+ function keepCastColours(cast, renames) {
5933
+ for (const [from, to] of renames) {
5934
+ const m = cast.find((c) => c.name === to);
5935
+ if (m && !isColourSlot(m.colour)) m.colour = colourIndex(from);
5936
+ }
5937
+ }
5553
5938
  function cleanCastRenames(renames) {
5554
5939
  const out = /* @__PURE__ */ new Map();
5555
5940
  for (const [from, to] of Object.entries(renames ?? {})) if (from && to && from !== to) out.set(from, to);
@@ -5607,8 +5992,8 @@ function renameCastStrings(loc, renames) {
5607
5992
  }
5608
5993
 
5609
5994
  // src/suggestions.ts
5610
- import { readFileSync as readFileSync13 } from "fs";
5611
- import { canonicalStringify as canonicalStringify12, parseSource as parseSource9 } from "@patterkit/core";
5995
+ import { readFileSync as readFileSync14 } from "fs";
5996
+ import { canonicalStringify as canonicalStringify13, parseSource as parseSource10 } from "@patterkit/core";
5612
5997
  import { walkNodes as walkNodes7, AUTHORING_SCHEMA as AUTHORING_SCHEMA3 } from "@patterkit/model";
5613
5998
  function staleParts(s, liveText, line) {
5614
5999
  const stale = [];
@@ -5694,7 +6079,7 @@ var Working = class {
5694
6079
  if (!path) return void 0;
5695
6080
  let file = this.flows.get(path);
5696
6081
  if (!file) {
5697
- const onDisk = parseSource9(readFileSync13(path, "utf8"));
6082
+ const onDisk = parseSource10(readFileSync14(path, "utf8"));
5698
6083
  const scene = this.loaded.scenes.find((s) => s.id === sceneId);
5699
6084
  file = { ...onDisk, scene: structuredClone(scene ?? onDisk.scene) };
5700
6085
  this.flows.set(path, file);
@@ -5702,7 +6087,7 @@ var Working = class {
5702
6087
  return { path, file };
5703
6088
  }
5704
6089
  writes() {
5705
- return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify12(file) })).sort((a, b) => a.path.localeCompare(b.path));
6090
+ return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify13(file) })).sort((a, b) => a.path.localeCompare(b.path));
5706
6091
  }
5707
6092
  };
5708
6093
  function beatIn(scene, id) {
@@ -5866,6 +6251,7 @@ async function exportEditableScript(loaded, opts) {
5866
6251
  const lines = {};
5867
6252
  const skeleton = [];
5868
6253
  for (const el of doc.elements) {
6254
+ if (el.kind === "effect") continue;
5869
6255
  if (isEditable(el)) {
5870
6256
  const code = markerOf.get(el);
5871
6257
  const baseline = source[el.id] ?? "";
@@ -5910,10 +6296,11 @@ async function exportEditableScript(loaded, opts) {
5910
6296
  const document = new Document2({
5911
6297
  creator: "Patter",
5912
6298
  title: `${loaded.project.project.name} (editable script)`,
6299
+ styles: { paragraphStyles: [EFFECT_STYLE_DEF] },
5913
6300
  sections: [{
5914
6301
  properties: { page: { size: { width: 11906, height: 16838 } } },
5915
6302
  headers: { default: new Header({ children: [new Paragraph2({ children: [
5916
- new TextRun2({ text: `${loaded.project.project.name} \xB7 Editable script \xB7 Handoff ${id} \xB7 exported ${date} \xB7 Please keep the grey [#\u2026] markers`, font: SANS, size: 15, color: TOKENS.muted })
6303
+ new TextRun2({ text: `${loaded.project.project.name}: editable script. Handoff ${id}, exported ${date}. Please keep the grey [#\u2026] markers`, font: SANS, size: 15, color: TOKENS.muted })
5917
6304
  ] })] }) },
5918
6305
  children: body
5919
6306
  }]
@@ -5926,7 +6313,7 @@ function contextText(el) {
5926
6313
  case "group":
5927
6314
  return el.label;
5928
6315
  case "else":
5929
- return "else \xB7 catch-all";
6316
+ return "else (catch-all)";
5930
6317
  default:
5931
6318
  return el.text;
5932
6319
  }
@@ -5936,7 +6323,7 @@ function box(el, code, status) {
5936
6323
  const textW = Math.max(MIN_TEXT, FULL - ind - LEAD2 - MARGIN2);
5937
6324
  const cell = (children, width, extra = {}) => new TableCell({ width: { size: width, type: WidthType.DXA }, borders: { top: NONE, bottom: NONE, left: NONE, right: NONE }, margins: { top: 60, bottom: 60, left: 100, right: 100 }, children, ...extra });
5938
6325
  const lead = [];
5939
- if (el.kind === "line" && el.character) lead.push(new TextRun2({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character) }));
6326
+ if (el.kind === "line" && el.character) lead.push(new TextRun2({ text: cueLabel(el.character, el.qualifierName), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character, el.colour) }));
5940
6327
  if (el.kind === "line" && el.direction) lead.push(new TextRun2({ text: `(${el.direction})`, font: SERIF, italics: true, size: S.cue, color: TOKENS.muted, break: el.character ? 1 : 0 }));
5941
6328
  if (el.kind === "option") lead.push(new TextRun2({ text: "\u25C7", color: TOKENS.accent, size: S.body }));
5942
6329
  const ink = el.kind === "narration" ? TOKENS.inkSoft : el.kind === "option" ? TOKENS.ink : TOKENS.inkRead;
@@ -6000,7 +6387,7 @@ function frontPage(loaded, handoff, opts, now) {
6000
6387
  p("How to edit this script", { bold: true, size: 24, color: TOKENS.ink, after: 120 }),
6001
6388
  item("1.", "Change the words inside the shaded boxes. That's all that can be edited."),
6002
6389
  item("2.", "Don't delete, copy, or move the boxes, or change the grey markers like [#K7Q2M] beside them. If you want a line cut, delete its words but leave the box, or leave a comment."),
6003
- item("3.", "Everything outside the boxes (scene and block headings, speaker names, conditions, choices, jumps, and game events) is there to show you how the story flows. Comment on it rather than editing it."),
6390
+ item("3.", "Everything outside the boxes (scene and block headings, speaker names, conditions, choices, jumps, game events, and effects) is there to show you how the story flows. Comment on it rather than editing it."),
6004
6391
  item("4.", "Use comments freely. You can also type a note inside a box like [[this]]."),
6005
6392
  p("How to read the structure", { bold: true, size: 24, color: TOKENS.ink, before: 280, after: 120 }),
6006
6393
  item("CHOOSE", "The player picks one of the options marked \u25C7."),
@@ -6009,6 +6396,7 @@ function frontPage(loaded, handoff, opts, now) {
6009
6396
  item("\u2039 if \u2026 \u203A", "This only happens when the condition holds."),
6010
6397
  item("\u21AA", "The story carries on somewhere else."),
6011
6398
  item("\u2699", "Something happens in the game here."),
6399
+ item("On begin / On end", "What the story changes as it passes through, before or after the lines it sits with."),
6012
6400
  item("once only", "An option that disappears once it has been picked; repeatable ones stay."),
6013
6401
  p("Words like {@name} in a different typeface are filled in by the game. Keep them exactly as they are, braces included, though they can move within a line.", { before: 160 }),
6014
6402
  new Paragraph2({ children: [new PageBreak()] })
@@ -6021,7 +6409,7 @@ function castPage(loaded, elements) {
6021
6409
  return [
6022
6410
  new Paragraph2({ spacing: { after: 200 }, children: [new TextRun2({ text: "Cast", font: SERIF, bold: true, size: S.h1, color: TOKENS.ink })] }),
6023
6411
  ...cast.map((c) => new Paragraph2({ spacing: { after: 100 }, children: [
6024
- new TextRun2({ text: c.name.toUpperCase(), font: SANS, bold: true, size: S.cue, color: characterColour(c.name) }),
6412
+ new TextRun2({ text: c.name.toUpperCase(), font: SANS, bold: true, size: S.cue, color: characterColour(c.name, c.colour) }),
6025
6413
  ...c.notes ? [new TextRun2({ text: ` ${c.notes}`, font: SERIF, size: S.body, color: TOKENS.inkRead })] : []
6026
6414
  ] })),
6027
6415
  new Paragraph2({ children: [new PageBreak()] })
@@ -6221,6 +6609,7 @@ function readRow(tr, indent) {
6221
6609
  ...indent !== void 0 ? { indent } : {}
6222
6610
  };
6223
6611
  }
6612
+ var EFFECT_LABEL = /^\s*On (begin|end):\s/i;
6224
6613
  function readBody(body) {
6225
6614
  const items = [];
6226
6615
  for (const e of kids(body)) {
@@ -6229,7 +6618,10 @@ function readBody(body) {
6229
6618
  const c = new Collector();
6230
6619
  c.walk(e, "both");
6231
6620
  const style = attr(child(child(e, "pPr") ?? e, "pStyle"), "val");
6232
- items.push({ kind: "paragraph", text: c.read(), ...style ? { style } : {} });
6621
+ const text = c.read();
6622
+ const phase = EFFECT_LABEL.exec(text.original.replace(/<\/?(?:b|i|bi)>/g, ""))?.[1]?.toLowerCase();
6623
+ if (style === EFFECT_STYLE || phase) items.push({ kind: "effect", phase: phase === "end" ? "end" : "begin", text });
6624
+ else items.push({ kind: "paragraph", text, ...style ? { style } : {} });
6233
6625
  } else if (name === "tbl") {
6234
6626
  const tblPr = child(e, "tblPr");
6235
6627
  const ind = tblPr ? attr(child(tblPr, "tblInd"), "w") : void 0;
@@ -6308,7 +6700,7 @@ async function readEditableDocx(bytes) {
6308
6700
  }
6309
6701
 
6310
6702
  // src/editable-import.ts
6311
- import { canonicalStringify as canonicalStringify13 } from "@patterkit/core";
6703
+ import { canonicalStringify as canonicalStringify14 } from "@patterkit/core";
6312
6704
  import { AUTHORING_SCHEMA as AUTHORING_SCHEMA4, projectQualifiers as projectQualifiers4 } from "@patterkit/model";
6313
6705
  var REFUSE_MISSING_SHARE = 0.25;
6314
6706
  var tidy = (s) => s.replace(/[\s   ]+/g, " ").trim();
@@ -6576,7 +6968,7 @@ function planEditableImport(loaded, returned, opts) {
6576
6968
  const af = shardFor(c.anchor);
6577
6969
  af.comments = [...af.comments ?? [], c];
6578
6970
  }
6579
- let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify13(af) }));
6971
+ let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify14(af) }));
6580
6972
  if (opts.direct && suggestions.length) {
6581
6973
  const warned = new Set(problems.filter((p) => p.severity === "warning" && p.anchor).map((p) => p.anchor));
6582
6974
  const clean = suggestions.filter((s) => (live[s.anchor] ?? "") === s.baseline && !warned.has(s.anchor));
@@ -6689,6 +7081,7 @@ function alignParagraphs(items, skeleton, codeOfBox, onEdited, onAdded, lineIdOf
6689
7081
  return rows;
6690
7082
  };
6691
7083
  let boxIndex = -1, gap = [], lastLine;
7084
+ let beforeNext = [];
6692
7085
  const settle = (to, near) => {
6693
7086
  const rows = rowsBefore(to);
6694
7087
  const unmatchedItems = [];
@@ -6722,14 +7115,21 @@ function alignParagraphs(items, skeleton, codeOfBox, onEdited, onAdded, lineIdOf
6722
7115
  const line = code ? lineIdOf(code) : void 0;
6723
7116
  if (line) {
6724
7117
  lastLine = line;
6725
- note([...item.text.comments, ...item.lead.comments, ...item.margin.comments], line);
7118
+ note([...beforeNext, ...item.text.comments, ...item.lead.comments, ...item.margin.comments], line);
7119
+ beforeNext = [];
6726
7120
  }
6727
7121
  continue;
6728
7122
  }
7123
+ if (item.kind === "effect") {
7124
+ if (item.phase === "end") note(item.text.comments, lastLine);
7125
+ else beforeNext.push(...item.text.comments);
7126
+ continue;
7127
+ }
6729
7128
  if (item.text.proposed.trim() === "" && item.text.original.trim() === "" && !item.text.comments.length) continue;
6730
7129
  gap.push(item);
6731
7130
  }
6732
7131
  settle(segments.length - 1, boxIndex);
7132
+ note(beforeNext, lastLine);
6733
7133
  return anchors;
6734
7134
  }
6735
7135
  export {
@@ -6748,6 +7148,7 @@ export {
6748
7148
  PROJECT_HANDLED,
6749
7149
  RARE_REACH_PCT,
6750
7150
  REFUSE_MISSING_SHARE,
7151
+ ResolutionError,
6751
7152
  SCRIPT_FONT_FILES,
6752
7153
  SCRIPT_FONT_PACKAGES,
6753
7154
  SHARD_EXTENSIONS,
@@ -6756,6 +7157,7 @@ export {
6756
7157
  actionTextChanges,
6757
7158
  actionTextIds,
6758
7159
  applyLoc,
7160
+ applyResolutions,
6759
7161
  applySceneOrder,
6760
7162
  applySuggestionDecisions,
6761
7163
  applyWrites,
@@ -6766,23 +7168,30 @@ export {
6766
7168
  authoringPath,
6767
7169
  besideBundlePath,
6768
7170
  bundleOutputPath,
7171
+ castColours,
6769
7172
  catalogToJson,
6770
7173
  catalogToPo,
6771
7174
  catalogToXlsx,
7175
+ characterColour,
7176
+ clashPathSegments,
6772
7177
  classesForChannel,
6773
7178
  cleanCastRenames,
7179
+ compareLocTokens,
6774
7180
  compileLoaded,
6775
7181
  cueLabel,
6776
7182
  currentBundlePosture,
6777
7183
  defaultBundlePath,
6778
7184
  defaultGameScopesDir,
6779
7185
  describeLocExport,
7186
+ describeLocImport,
6780
7187
  detectMergeType,
6781
7188
  discoverGameScopes,
6782
7189
  drivableScopes,
7190
+ effectDescriber,
6783
7191
  exportBlockers,
6784
7192
  exportEditableScript,
6785
7193
  extractLoc,
7194
+ findMarkedShards,
6786
7195
  findProjectFile,
6787
7196
  formatMarker,
6788
7197
  gameScopeTokens,
@@ -6791,9 +7200,12 @@ export {
6791
7200
  handoffPath,
6792
7201
  handoffWrite,
6793
7202
  hostScopeTokens,
7203
+ importLocale,
7204
+ isSettableClash,
6794
7205
  isUnsafeEntry2 as isUnsafeEntry,
6795
7206
  issueMarkerCodes,
6796
7207
  jsonToCatalog,
7208
+ keepCastColours,
6797
7209
  leastReachedFirst,
6798
7210
  listGameDataFields,
6799
7211
  listOpenSuggestions,
@@ -6801,6 +7213,7 @@ export {
6801
7213
  loadProject,
6802
7214
  loadProjectLanding,
6803
7215
  loadedExtensions,
7216
+ locLineHomes,
6804
7217
  manifestPath,
6805
7218
  markerLength,
6806
7219
  moveCastKeys,
@@ -6882,8 +7295,10 @@ export {
6882
7295
  stageDialect,
6883
7296
  stageGateCount,
6884
7297
  staleParts,
7298
+ unresolvedClashes,
6885
7299
  unresolvedMarkIssues,
6886
7300
  useScriptFontFiles,
7301
+ valueAtClashPath,
6887
7302
  vcsConfigWrites,
6888
7303
  voiceScriptToXlsx,
6889
7304
  worldSettingsScopes,