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