@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.
- package/dist/index.d.ts +186 -10
- package/dist/index.js +1326 -911
- 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/
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
|
1353
|
-
const
|
|
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
|
-
|
|
1359
|
+
effects(node.onEnter);
|
|
1360
|
+
effects(node.onExit);
|
|
1358
1361
|
}
|
|
1359
1362
|
});
|
|
1360
1363
|
}
|
|
1361
|
-
return
|
|
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
|
-
|
|
1369
|
-
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
for (const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const
|
|
1377
|
-
if (
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
|
1385
|
+
return result;
|
|
1386
1386
|
}
|
|
1387
|
-
function
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1397
|
-
put(SPEAKER_PATTERN_KEY, project.speakerPattern ?? DEFAULT_SPEAKER_PATTERN);
|
|
1398
|
-
return out;
|
|
1396
|
+
return count;
|
|
1399
1397
|
}
|
|
1400
|
-
function
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
if (
|
|
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
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
for (
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1604
|
-
return
|
|
1473
|
+
function characterColour(name, stored) {
|
|
1474
|
+
return CHAR_PALETTE[isColourSlot(stored) ? stored : colourIndex(name)];
|
|
1605
1475
|
}
|
|
1606
|
-
function
|
|
1607
|
-
return
|
|
1476
|
+
function castColours(cast) {
|
|
1477
|
+
return new Map((cast ?? []).filter((c) => isColourSlot(c.colour)).map((c) => [c.name, c.colour]));
|
|
1608
1478
|
}
|
|
1609
|
-
function
|
|
1610
|
-
|
|
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
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
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
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
|
1528
|
+
return "game event";
|
|
1657
1529
|
}
|
|
1658
|
-
function
|
|
1659
|
-
|
|
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
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
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
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
const
|
|
1682
|
-
const
|
|
1683
|
-
const
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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 =
|
|
1742
|
-
const foreignScopes =
|
|
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 =
|
|
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",
|
|
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
|
|
2517
|
-
import {
|
|
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,
|
|
2644
|
-
let won = false;
|
|
3141
|
+
const walk = (nodes, blockId, gate, fine, dead, never) => {
|
|
2645
3142
|
for (const node of nodes) {
|
|
2646
|
-
const shadow = dead ||
|
|
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,
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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(/</g, "<").replace(/>/g, ">").replace(/&/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
|
|
4048
|
-
|
|
4049
|
-
|
|
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
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
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
|
-
|
|
4079
|
-
}
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
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
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
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
|
|
4412
|
+
return { project, defaultLocale, locale, entries };
|
|
4094
4413
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
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(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
4101
4419
|
}
|
|
4102
|
-
function
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
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
|
|
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
|
|
4115
|
-
const
|
|
4116
|
-
const
|
|
4117
|
-
const
|
|
4118
|
-
const
|
|
4119
|
-
const
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
};
|
|
4139
|
-
const
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
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.
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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 (
|
|
4228
|
-
|
|
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 (
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
for (const child2 of block.children ?? [])
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5611
|
-
import { canonicalStringify as
|
|
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 =
|
|
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:
|
|
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}
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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:
|
|
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,
|