@patterkit/ops 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +25 -5
  2. package/dist/index.js +1870 -1797
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ function readDirSafe(dir) {
13
13
  function walkFiles(dir, ext) {
14
14
  const out = [];
15
15
  for (const e of readDirSafe(dir)) {
16
+ if (e.name.startsWith(".")) continue;
16
17
  const p = join(dir, e.name);
17
18
  if (e.isDirectory()) out.push(...walkFiles(p, ext));
18
19
  else if (e.isFile() && e.name.endsWith(ext)) out.push(p);
@@ -225,250 +226,625 @@ function applyWrites(writes) {
225
226
 
226
227
  // src/validate.ts
227
228
  import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
228
- import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
229
- import { validateConditions, validateInterpolation, exportBundle, hostScopesToSpec } from "@patterkit/compiler";
230
- function runValidate(loaded) {
231
- const { project, scenes, locales } = loaded;
232
- const structural = validateProject({ project, scenes, authoring: loaded.authoring });
233
- const foreignScopes = hostScopesToSpec(project.scopeRegistry);
234
- const conditions = validateConditions({ project, scenes }, { foreignScopes });
235
- const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes });
236
- const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
237
- const staleBundles = checkBundles(loaded);
238
- const unresolvedMerges = walkFiles(loaded.root, ".patterconflict").map((file) => ({
229
+
230
+ // src/merge.ts
231
+ import { canonicalStringify } from "@patterkit/core";
232
+ var UnsupportedMergeError = class extends Error {
233
+ };
234
+ var eq = (a, b) => canonicalStringify(a) === canonicalStringify(b);
235
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
236
+ var CONFLICT_SIDECAR = ".patterconflict";
237
+ function sidecarIssues(sidecarPaths) {
238
+ return sidecarPaths.map((file) => ({
239
239
  file,
240
240
  message: "unresolved merge conflict - resolve it and delete the .patterconflict sidecar before committing"
241
241
  }));
242
- return {
243
- structural,
244
- conditions,
245
- interpolation,
246
- hygiene,
247
- staleBundles,
248
- unresolvedMerges,
249
- ok: structural.length === 0 && conditions.length === 0 && interpolation.length === 0 && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0
250
- };
251
242
  }
252
- function checkBundles(loaded) {
253
- const issues = [];
254
- const bundles = walkFiles(loaded.root, ".patterc");
255
- if (bundles.length === 0) return issues;
256
- let fresh;
257
- try {
258
- fresh = exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales }).content.hash;
259
- } catch {
260
- return issues;
243
+ function detectMergeType(file) {
244
+ const s = typeof file.schema === "string" ? file.schema : "";
245
+ if (s.startsWith("patter/flow")) return "flow";
246
+ if (s.startsWith("patter/strings")) return "loc";
247
+ if (s.startsWith("patter/authoring")) return "authoring";
248
+ if (s.startsWith("patter/project")) return "project";
249
+ throw new UnsupportedMergeError(`cannot detect a Patter merge type from schema '${s}'`);
250
+ }
251
+ function runMerge(base, ours, theirs, opts) {
252
+ const type = opts?.type ?? detectMergeType(ours);
253
+ switch (type) {
254
+ case "loc":
255
+ return mergeLoc(base, ours, theirs);
256
+ case "authoring":
257
+ return mergeAuthoring(base, ours, theirs);
258
+ case "flow":
259
+ return mergeFlow(base, ours, theirs);
260
+ case "project":
261
+ return mergeProject(base, ours, theirs);
261
262
  }
262
- for (const file of bundles) {
263
- let hash;
264
- try {
265
- const parsed = parseSource2(readFileSync2(file, "utf8"));
266
- hash = parsed?.content?.hash;
267
- } catch {
268
- issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
269
- continue;
270
- }
271
- if (hash !== fresh) {
272
- issues.push({ file, message: "compiled bundle is stale (does not match current source) - run `patter export`" });
263
+ }
264
+ function deletedKind(base, ours, theirs) {
265
+ const deleted = base !== void 0 && (ours === void 0 || theirs === void 0);
266
+ return deleted ? "delete-vs-edit" : "both-changed";
267
+ }
268
+ function merge3(base, ours, theirs, id, path, conflicts) {
269
+ if (eq(ours, theirs)) return ours;
270
+ if (eq(base, ours)) return theirs;
271
+ if (eq(base, theirs)) return ours;
272
+ conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
273
+ return ours;
274
+ }
275
+ function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
276
+ const out = {};
277
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
278
+ const b = base[k], o = ours[k], t = theirs[k];
279
+ let v;
280
+ if (eq(o, t)) v = o;
281
+ else if (eq(b, o)) v = t;
282
+ else if (eq(b, t)) v = o;
283
+ else {
284
+ const r = resolve5?.(k, b, o, t);
285
+ if (r && r.resolved) v = r.value;
286
+ else {
287
+ conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
288
+ v = o;
289
+ }
273
290
  }
291
+ if (v !== void 0) out[k] = v;
274
292
  }
275
- return issues;
293
+ return out;
276
294
  }
277
- var hygieneCache = /* @__PURE__ */ new Map();
278
- function checkHygiene(files) {
279
- const issues = [];
280
- for (const file of files) {
281
- let mtimeMs;
282
- try {
283
- mtimeMs = statSync2(file).mtimeMs;
284
- } catch {
295
+ var asMap = (v) => isObj(v) ? v : {};
296
+ var asArr = (v) => Array.isArray(v) ? v : [];
297
+ function setIf(obj, key, val) {
298
+ if (Array.isArray(val) ? val.length > 0 : Object.keys(val).length > 0) obj[key] = val;
299
+ }
300
+ function mergeLoc(base, ours, theirs) {
301
+ const conflicts = [];
302
+ const merged = {};
303
+ for (const f of ["schema", "scene", "locale", "default"]) {
304
+ const v = merge3(base[f], ours[f], theirs[f], "", f, conflicts);
305
+ if (v !== void 0) merged[f] = v;
306
+ }
307
+ merged.strings = mergeMap(asMap(base.strings), asMap(ours.strings), asMap(theirs.strings), "strings", conflicts);
308
+ return { type: "loc", merged, conflicts, warnings: [] };
309
+ }
310
+ function mergeAuthoring(base, ours, theirs) {
311
+ const conflicts = [];
312
+ const merged = {};
313
+ merged.schema = merge3(base.schema, ours.schema, theirs.schema, "", "schema", conflicts) ?? ours.schema;
314
+ setIf(merged, "comments", mergeComments(asArr(base.comments), asArr(ours.comments), asArr(theirs.comments)));
315
+ const edits = mergeEdits(asMap(base.edits), asMap(ours.edits), asMap(theirs.edits));
316
+ setIf(merged, "edits", edits);
317
+ const lww = lastWriterWins(asMap(ours.edits), asMap(theirs.edits));
318
+ for (const f of ["writing", "recording", "audio"]) {
319
+ setIf(merged, f, mergeMap(asMap(base[f]), asMap(ours[f]), asMap(theirs[f]), f, conflicts, lww));
320
+ }
321
+ setIf(merged, "documentation", mergeMap(asMap(base.documentation), asMap(ours.documentation), asMap(theirs.documentation), "documentation", conflicts));
322
+ setIf(merged, "cut", mergeMap(asMap(base.cut), asMap(ours.cut), asMap(theirs.cut), "cut", conflicts, void 0));
323
+ setIf(merged, "suggestions", mergeById(asArr(base.suggestions), asArr(ours.suggestions), asArr(theirs.suggestions), "suggestions", conflicts));
324
+ setIf(merged, "rerecord", mergeMap(asMap(base.rerecord), asMap(ours.rerecord), asMap(theirs.rerecord), "rerecord", conflicts, void 0));
325
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
326
+ if (k in merged || AUTHORING_HANDLED.has(k)) continue;
327
+ const v = merge3(base[k], ours[k], theirs[k], "", k, conflicts);
328
+ if (v !== void 0) merged[k] = v;
329
+ }
330
+ return { type: "authoring", merged, conflicts, warnings: [] };
331
+ }
332
+ var AUTHORING_HANDLED = /* @__PURE__ */ new Set(["schema", "comments", "edits", "writing", "recording", "audio", "documentation", "cut", "suggestions", "rerecord"]);
333
+ function mergeById(base, ours, theirs, path, conflicts) {
334
+ const index = (arr) => {
335
+ const m = {};
336
+ for (const r of arr) if (isObj(r) && typeof r.id === "string") m[r.id] = r;
337
+ return m;
338
+ };
339
+ const merged = mergeMap(index(base), index(ours), index(theirs), path, conflicts);
340
+ return Object.values(merged).sort((a, b) => String(asMap(a).ts ?? "").localeCompare(String(asMap(b).ts ?? "")));
341
+ }
342
+ function mergeComments(base, ours, theirs) {
343
+ const byId = /* @__PURE__ */ new Map();
344
+ for (const c of [...base, ...ours, ...theirs]) {
345
+ if (isObj(c) && typeof c.id === "string" && !byId.has(c.id)) byId.set(c.id, c);
346
+ }
347
+ return [...byId.values()].sort((a, b) => String(a.ts ?? "").localeCompare(String(b.ts ?? "")));
348
+ }
349
+ function lastWriterWins(oursEdits, theirsEdits) {
350
+ return (id, _b, o, t) => {
351
+ const om = asMap(oursEdits[id]).modifiedAt, tm = asMap(theirsEdits[id]).modifiedAt;
352
+ if (typeof om === "string" && typeof tm === "string") return { resolved: true, value: om >= tm ? o : t };
353
+ return { resolved: false, kind: "no-timestamp" };
354
+ };
355
+ }
356
+ function mergeEdits(base, ours, theirs) {
357
+ const out = {};
358
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
359
+ const o = ours[id], t = theirs[id];
360
+ if (eq(o, t)) {
361
+ if (o !== void 0) out[id] = o;
285
362
  continue;
286
363
  }
287
- const hit = hygieneCache.get(file);
288
- if (hit && hit.mtimeMs === mtimeMs) {
289
- issues.push(...hit.issues);
364
+ if (o === void 0) {
365
+ if (t !== void 0) out[id] = t;
290
366
  continue;
291
367
  }
292
- const fileIssues = [];
293
- try {
294
- const text = readFileSync2(file, "utf8");
295
- if (text.charCodeAt(0) === 65279) {
296
- fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
297
- }
298
- if (text.includes("\r")) {
299
- fileIssues.push({ file, message: "file contains CRLF line endings (canonical form is LF - run `patter format`)" });
300
- }
301
- } catch {
368
+ if (t === void 0) {
369
+ out[id] = o;
302
370
  continue;
303
371
  }
304
- hygieneCache.set(file, { mtimeMs, issues: fileIssues });
305
- issues.push(...fileIssues);
372
+ out[id] = mergeEditRecord(asMap(o), asMap(t));
306
373
  }
307
- return issues;
374
+ return out;
308
375
  }
309
-
310
- // src/export.ts
311
- import { basename as basename2, isAbsolute, join as join2 } from "path";
312
- import { exportBundle as exportBundle2 } from "@patterkit/compiler";
313
- import { isContentlessBeat } from "@patterkit/model";
314
- function compileScenes(loaded) {
315
- const def = loaded.project.locales.default;
316
- const written = /* @__PURE__ */ new Set();
317
- for (const l of loaded.locales) if (l.locale === def) {
318
- for (const [k, v] of Object.entries(l.strings)) if (v) written.add(k);
376
+ function mergeEditRecord(o, t) {
377
+ const om = typeof o.modifiedAt === "string" ? o.modifiedAt : "";
378
+ const tm = typeof t.modifiedAt === "string" ? t.modifiedAt : "";
379
+ const newer = om >= tm ? o : t;
380
+ const ol = asMap(o.localisedAt), tl = asMap(t.localisedAt);
381
+ const loc = {};
382
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(ol), ...Object.keys(tl)])) {
383
+ const a = typeof ol[k] === "string" ? ol[k] : "";
384
+ const b = typeof tl[k] === "string" ? tl[k] : "";
385
+ loc[k] = a >= b ? ol[k] ?? tl[k] : tl[k] ?? ol[k];
319
386
  }
320
- const clean = (n) => {
321
- if (n.type === "group") return { ...n, children: (n.children ?? []).map(clean) };
322
- if (!n.beats) return n;
323
- const kept = n.beats.filter((b) => !isContentlessBeat(b, written.has(b.id)));
324
- if (kept.length === n.beats.length) return n;
325
- const { beats, ...rest } = n;
326
- return kept.length ? { ...rest, beats: kept } : rest;
387
+ const merged = { ...newer };
388
+ if (Object.keys(loc).length > 0) merged.localisedAt = loc;
389
+ return merged;
390
+ }
391
+ function mergeKeyedByName(base, ours, theirs, path, conflicts) {
392
+ const byName = (arr) => {
393
+ const m = {};
394
+ for (const p of arr) if (isObj(p) && typeof p.name === "string") m[p.name] = p;
395
+ return m;
327
396
  };
328
- return loaded.scenes.map((s) => ({
329
- ...s,
330
- blocks: s.blocks.map((b) => ({ ...b, children: b.children.map(clean) }))
331
- }));
397
+ const merged = mergeMap(byName(base), byName(ours), byName(theirs), path, conflicts);
398
+ return Object.keys(merged).sort().map((name) => merged[name]);
332
399
  }
333
- function runExport(loaded) {
334
- const { project, locales } = loaded;
335
- const full = exportBundle2({ project, scenes: compileScenes(loaded), locales });
336
- const loc = project.export?.localisation;
337
- if (!loc || loc.mode === "embedded") return full;
338
- const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
339
- return { ...full, strings, localisation: { mode: "ids", ...loc.sourceDebug ? { sourceDebug: true } : {} } };
400
+ function mergeProject(base, ours, theirs) {
401
+ const conflicts = [];
402
+ const merged = {};
403
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
404
+ if (k === "properties" || k === "cast") {
405
+ const v = mergeKeyedByName(asArr(base[k]), asArr(ours[k]), asArr(theirs[k]), k, conflicts);
406
+ if (v.length > 0) merged[k] = v;
407
+ } else if (k === "gameDataFields") {
408
+ const v = mergeGameDataFields(asMap(base[k]), asMap(ours[k]), asMap(theirs[k]), conflicts);
409
+ if (Object.keys(v).length > 0) merged[k] = v;
410
+ } else if (k === "locales") {
411
+ merged.locales = mergeLocales(asMap(base.locales), asMap(ours.locales), asMap(theirs.locales), conflicts);
412
+ } else {
413
+ const v = merge3(base[k], ours[k], theirs[k], "", k, conflicts);
414
+ if (v !== void 0) merged[k] = v;
415
+ }
416
+ }
417
+ return { type: "project", merged, conflicts, warnings: [] };
340
418
  }
341
- function runExportFull(loaded) {
342
- const { project, locales } = loaded;
343
- return exportBundle2({ project, scenes: compileScenes(loaded), locales });
419
+ function mergeGameDataFields(b, o, t, conflicts) {
420
+ const out = {};
421
+ for (const kind of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
422
+ const v = mergeKeyedByName(asArr(b[kind]), asArr(o[kind]), asArr(t[kind]), `gameDataFields.${kind}`, conflicts);
423
+ if (v.length > 0) out[kind] = v;
424
+ }
425
+ return out;
344
426
  }
345
- function bundleOutputPath(loaded) {
346
- const override = loaded.project.export?.bundle;
347
- if (override) return isAbsolute(override) ? override : join2(loaded.root, override);
348
- const stem = basename2(loaded.projectFile).replace(/\.patterproj$/, "");
349
- return join2(loaded.root, "dist", `${stem}.patterc`);
427
+ function mergeLocales(b, o, t, conflicts) {
428
+ const out = {};
429
+ const def = merge3(b.default, o.default, t.default, "", "locales.default", conflicts);
430
+ if (def !== void 0) out.default = def;
431
+ const all = [];
432
+ for (const arr of [asArr(b.all), asArr(o.all), asArr(t.all)]) {
433
+ for (const x of arr) if (typeof x === "string" && !all.includes(x)) all.push(x);
434
+ }
435
+ out.all = all;
436
+ return out;
350
437
  }
351
-
352
- // src/export-html.ts
353
- import { canonicalStringify } from "@patterkit/core";
354
-
355
- // src/playable-runtime.ts
356
- var PLAYABLE_RUNTIME_JS = '"use strict";var Patterplay=(()=>{var G=Object.defineProperty;var Se=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var ve=Object.prototype.hasOwnProperty;var be=(n,e)=>{for(var t in e)G(n,t,{get:e[t],enumerable:!0})},we=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ye(e))!ve.call(n,s)&&s!==t&&G(n,s,{get:()=>e[s],enumerable:!(r=Se(e,s))||r.enumerable});return n};var xe=n=>we(G({},"__esModule",{value:!0}),n);var Me={};be(Me,{Engine:()=>N,Flow:()=>k,buildTagIndex:()=>P,describeBundle:()=>ue,effectiveGameData:()=>fe,gameDataFields:()=>ge,gameDataValue:()=>U});function v(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:v(n[2])};case"bin":return{kind:"binary",op:n[1],left:v(n[2]),right:v(n[3])};case"call":{let e=n.slice(2).map(v);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var l=class extends Error{constructor(e){super(e),this.name="EvalError"}};function D(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=e.scopes[i.scope];if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):o[i.name];if(a===void 0){if(r.get(i.scope)==="throw")throw new l(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!t.functions[i.name]){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new l(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=T(a,e);if(c===void 0)throw new l("advance() needs a quality reference (@scope.name of a quality property)");let d=S(s(a),c,"advance");return c[Math.min(d+1,c.length-1)]}let o=t.functions[i.name];if(!o)throw new l(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new l("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 l(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new l(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let g=s(i.left);if(typeof g!="boolean")throw new l(`\'and\' requires boolean operands, left is ${typeof g}`);if(!g)return!1;let h=s(i.right);if(typeof h!="boolean")throw new l(`\'and\' requires boolean operands, right is ${typeof h}`);return h}if(i.op==="or"){let g=s(i.left);if(typeof g!="boolean")throw new l(`\'or\' requires boolean operands, left is ${typeof g}`);if(g)return!0;let h=s(i.right);if(typeof h!="boolean")throw new l(`\'or\' requires boolean operands, right is ${typeof h}`);return h}let o=s(i.left),a=s(i.right),c=T(i.left,e),d=T(i.right,e),p=c??d;if(p!==void 0){if(c&&d&&!ke(c,d)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new l(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return S(o,p,">")>S(a,p,">");case">=":return S(o,p,">=")>=S(a,p,">=");case"<":return S(o,p,"<")<S(a,p,"<");case"<=":return S(o,p,"<=")<=S(a,p,"<=");case"+":case"-":case"*":case"/":throw new l(`\'${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 X(o,a);case"!=":return!X(o,a);case">":return b(o,a,">"),o>a;case">=":return b(o,a,">="),o>=a;case"<":return b(o,a,"<"),o<a;case"<=":return b(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new l(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return b(o,a,"-"),o-a;case"*":return b(o,a,"*"),o*a;case"/":if(b(o,a,"/"),a===0)throw new l("division by zero");return o/a}}}};return s(n)}function X(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;for(let t=0;t<n.length;t++)if(n[t]!==e[t])return!1;return!0}return n===e}function b(n,e,t){if(typeof n!="number"||typeof e!="number")throw new l(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function T(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function S(n,e,t){if(typeof n!="string")throw new l(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new l(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var ke=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);var Ce={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ee=[Ce];function Y(n,e,t){let r=t?.countingCalls??Ee;return I(n,t?.want??!0,e,r)}function I(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=I(n.left,e,t,r),i=I(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 I(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 M=class n{values={};decls=new Map;subscribers=new Set;auditors=new Set;norm;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),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??Q(t))}}get(e){return this.values[this.norm(e)]}set(e,t,r){let s=this.norm(e);if(this.decls.get(s)?.writable===!1)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])=>Z(t,this.get(e),void 0,e))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm});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 Z(n,e,t,r){return{name:r??n.name.toLowerCase(),type:n.type,value:e,default:n.default??Q(n),...n.values!==void 0?{values:n.values}:{},writable:t??n.writable??!0}}var V=1,C=class{scopes=new Map;defineOwned(e,t){return this.mountOwned(e,new M(t))}mountOwned(e,t){return this.assertFree(e),this.scopes.set(e,{kind:"owned",bag:t}),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){this.assertFree(e);let i=new Map;for(let o of r)i.set(o.name.toLowerCase(),o);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:i,scopeWritable:s}),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(t.toLowerCase())}set(e,t,r){let s=this.scopes.get(e);if(!s)throw new Error(`unknown scope \'@${e}\'`);if(s.kind==="owned"){try{s.bag.set(t,r)}catch{throw new Error(`\'@${e}.${t}\' is read-only`)}return}let i=t.toLowerCase();if(!this.foreignWritable(s,i))throw new Error(`\'@${e}.${t}\' is read-only`);s.resolver.set(i,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)if(r.kind==="owned")for(let s of r.bag.rows())e.push({scope:t,...s});else for(let s of r.decls.values())e.push({scope:t,...Z(s,r.resolver.get(s.name.toLowerCase()),this.foreignWritable(r,s.name.toLowerCase()))});return e}toEvalContext(e){let t={};for(let[s,i]of this.scopes)t[s]=i.kind==="owned"?i.bag.values:i.resolver;let r=this.qualityLadders();return r.size===0?{scopes:t,host:e}:{scopes:t,host:e,qualities:(s,i)=>r.get(s)?.get(i.toLowerCase())}}qualityLadders(){let e=new Map;for(let[t,r]of this.scopes){let s=r.kind==="owned"?r.bag.declarations():[...r.decls.values()];for(let i of s){if(i.type!=="quality"||i.stages===void 0)continue;let o=e.get(t);o||(o=new Map,e.set(t,o)),o.set(i.name.toLowerCase(),i.stages)}}return e}toSchema(){let e=new Map;for(let[t,r]of this.scopes){let s=r.kind==="owned"?r.bag.declarations():[...r.decls.values()];if(s.length===0)continue;let i=new Map;for(let o of s)i.set(o.name.toLowerCase(),{type:o.type,enumValues:o.values,...o.stages!==void 0?{stages:o.stages}:{}});e.set(t,i)}return{properties:e}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());return e}load(e){for(let[t,r]of Object.entries(e)){let s=this.scopes.get(t);s?.kind==="owned"&&s.bag.load(r)}}saveFragment(){return{version:V,scopes:this.save()}}loadFragment(e){if(e.version!==V)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${V})`);this.load(e.scopes)}assertFree(e){if(this.scopes.has(e))throw new Error(`scope \'@${e}\' is already registered`)}};function Q(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]??""}}function E(n){return n.ctx.host??{}}var L={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new l("random(a, b) requires exactly 2 arguments");let t=E(e).nextRandom;if(!t)throw new l("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 l("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new l("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:te("check_flags"),eval(n,e){let t=ee(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new l("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:te("set_flags"),eval(n,e){let t=[...ee(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new l("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:B("visits"),eval:(n,e)=>E(e).visits?.(R(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:B("seen"),eval:(n,e)=>(E(e).visits?.(R(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:B("patter_visits"),eval:(n,e)=>E(e).patterVisits?.(R(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:B("patter_seen"),eval:(n,e)=>(E(e).patterVisits?.(R(n,e,"patter_seen"))??0)>0}}};function j(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 R(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new l(`${t}(id) requires a string node id`);return r}function B(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 ee(n,e,t){if(!n)throw new l(`${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 l(`${t}() first argument must be a flags property`)}function te(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 De=/^@[A-Za-z0-9_.]+$/;function*Ie(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:De.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 Re(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function Be(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function Oe(n){let e="",t=!1;for(let r of n){if(Be(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function re(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?Oe(r):n}function ne(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of Ie(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?"":Re(s)}return t}function x(n,e){for(let t of n){e(t);let r=t.children;r&&x(r,e)}}function Pe(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function f(n){let e=n.gameId?.trim();return e||Pe(n.name)}function $(n){return`cast:${n}`}var _={open:"[",close:"]"},se="SFX";function O(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function P(n){let e=new Map,t=(r,s)=>{let i=O([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,O([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=O(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=O([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var ie=new WeakMap,N=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;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 d=new Map;for(let u of e.cast??[])u.displayName&&d.set(u.name,u.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let p=new Map,g=new Map,h=new Map;for(let[u,y]of Object.entries(e.scenes)){this.sceneGameIdToId.set(f(y),u);let w=new Map;for(let m of y.blocks)g.set(m.id,{sceneId:u}),h.set(m.id,m),w.set(f(m),m.id),x(m.children,J=>p.set(J.id,J));this.blockGameIdToId.set(u,w)}let W=e.properties??[],A=W.filter(u=>u.shared??!0).map(ae),he=W.filter(u=>!(u.shared??!0)).map(ae),me=new Set(A.map(u=>u.name.toLowerCase())),F=new C().defineOwned("patter",A),K=new Set;if(t.world){let u=e.scopeRegistry?.scopes.find(w=>w.token==="world"),y=(u?.declarations??[]).map(ce);F.defineForeign("world",t.world,y,u?.writable??!0),K.add("world")}for(let u of e.scopeRegistry?.scopes??[]){if(K.has(u.token))continue;let y=(u.declarations??[]).map(ce);F.defineForeign(u.token,Fe(u.declarations??[]),y,u.writable??!0)}let z=new Map;for(let[u,y]of Object.entries(e.scenes)){let w=new Set((y.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));z.set(u,w)}this.host={bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:d,nodeIndex:p,blockIndex:g,blockById:h,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:P(e),shared:F,patterSharedDecls:A,patterLocalDecls:he,patterSharedNames:me,sceneSharedNames:z,sharedVisits:new Map,sharedSelectors:new Map,stageBags:new Map,customRng:t.rng,onDryChoice:t.onDryChoice,replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??_).open,captionClose:(e.closedCaptions??_).close,captionCharacter:e.closedCaptions?.character||se,refSplitCache:new Map}}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){let t=this.saveGame(),r=i=>(i.setLocale(this.currentLocale),i.setClosedCaptions(this.host.captionsOn),i),s=new n(e,this.creationOptions);try{return s.loadGame(t),r(s)}catch{let i=new n(e,this.creationOptions);for(let[o,a]of Object.entries(t.flows)){let c=a.cursor.currentSceneId;try{i.openFlow(o,c!==null?{scene:c}:{})}catch{}}return r(i)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){let r=this.resolveSceneRef(t.scene),s=this.resolveBlockRef(r,t.block);this.flowsById.get(e)?.close();let i=new k(e,this.host,t.seed??this.defaultSeed);return this.flowsById.set(e,i),i.start(r,s),i}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}resolveSceneRef(e){if(e!=null)return this.host.bundle.scenes[e]?e:this.sceneGameIdToId.get(e)??e}resolveBlockRef(e,t){if(t!=null){if(this.host.blockById.has(t))return t;if(e!=null){let r=this.blockGameIdToId.get(e)?.get(t);if(r)return r}return t}}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?f(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?f(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)??[]}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)oe(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 oe(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...f(e)?{gameId:f(e)}:{},name:e.name,...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...f(t)?{gameId:f(t)}:{},name:t.name,...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)x(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[$(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()]}closeFlow(e){this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.shared.reseedOwned("patter",this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear(),this.host.stageBags.clear()}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.shared.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e);this.host.shared.set(r,s,t)}listProperties(){return this.host.patterSharedDecls.map(e=>({ref:`@${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:Ne(e)}))}splitShared(e){let t=this.host.refSplitCache.get(e);if(t||(t=j(e,r=>r==="scene"||this.host.shared.has(r)),this.host.refSplitCache.set(e,t)),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 this.host.shared.save()}load(e){this.host.shared.load(e)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:2,shared:this.host.shared.save(),sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:pe(this.host.sharedSelectors),stageBags:Object.fromEntries([...this.host.stageBags].map(([t,r])=>[t,{...r}])),flows:e}}loadGame(e){if(e.version!==2)throw new Error(`unsupported save version: ${e.version}`);this.host.shared.load(e.shared),this.host.sharedVisits.clear();for(let[t,r]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(t,r);this.host.sharedSelectors.clear();for(let[t,r]of de(e.sharedSelectors))this.host.sharedSelectors.set(t,r);this.host.stageBags.clear();for(let[t,r]of Object.entries(e.stageBags??{}))this.host.stageBags.set(t,{...r});this.flowsById.clear();for(let[t,r]of Object.entries(e.flows)){let s=new k(t,this.host,this.defaultSeed);s.restore(r),this.flowsById.set(t,s)}}},k=class{id;host;local;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPromptBeat=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.shared.get("patter",e):this.local.get("patter",e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.shared.set("patter",e,t):this.local.set("patter",e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;return t===null?void 0:(this.host.sceneSharedNames.get(t)?.has(e)?this.host.stageBags.get(t):this.sceneBags.get(t))?.[e]},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.host.sceneSharedNames.get(r)?.has(e)?this.host.stageBags.get(r):this.sceneBags.get(r);s&&(s[e]=t)}};evalCtx;constructor(e,t,r){this.id=e,this.host=t,this.rngState=r>>>0,this.local=this.freshLocal();let s={...t.shared.toEvalContext().scopes};s.patter=this.patterResolver,s.scene=this.sceneResolver,this.evalCtx={scopes:s,host:{nextRandom:this.rng,visits:i=>this.visitCounts.get(i)??0,patterVisits:i=>this.host.sharedVisits.get(i)??0},qualities:(i,o)=>this.stagesFor(i,o)}}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 s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(e,t){if(this.sceneBags.clear(),this.local=this.freshLocal(),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(e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=this.host.sceneGameIdToId.get(e)??(this.host.bundle.scenes[e]?e:void 0);if(r===void 0)return!1;let s;return t!==void 0&&(s=this.host.blockGameIdToId.get(r)?.get(t)??(this.host.blockIndex.get(t)?.sceneId===r?t:void 0),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPromptBeat=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.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null}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.pendingPromptBeat){let e=this.pendingPromptBeat;return this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.beatResult(e)}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);let s=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(s);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}for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}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(i=>i.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);let s=t.byId.get(e);this.pendingChoice=null,this.pendingPromptBeat=this.host.replayPromptOnChoose?this.promptBeatOf(s)??null:null,this.pendingPromptOwnerId=this.pendingPromptBeat?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.shared.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitRef(e);if(r==="patter")this.patterResolver.set(s,t);else if(r==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);this.sceneResolver.set(s,t)}else this.host.shared.set(r,s,t)}snapshot(){return{scopes:this.local.save(),sceneBags:Object.fromEntries([...this.sceneBags].map(([e,t])=>[e,{...t}])),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,selectors:pe(this.selectors)}}}restore(e){this.rngState=e.rngState>>>0,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.sceneBags=new Map(Object.entries(e.sceneBags??{}).map(([r,s])=>[r,{...s}])),this.local=this.freshLocal(),this.local.load(e.scopes),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.pendingPromptBeat=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId);this.pendingPromptBeat=r?this.promptBeatOf(r)??null:null,this.pendingPromptBeat||(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)}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),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.length>0){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.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(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.filter(s=>this.eligible(s));if(t.length===0)return null;let r=this.selectorState(e);switch(e.selector){case"branch":return t[0];case"sequence":{let s=e.options?.order??"sequential",i=e.options?.exhaust??"once";return s==="shuffle"?this.pickShuffle(t,i,r):s==="specificity"?this.pickSpecificity(t,i,r):this.pickSequential(t,i,r)}default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(g=>g.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let g=e[s-1];return r.last=g.id,g}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,d=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&d>=c&&d++;let p=a[d];return a.splice(d,1),r.last=p,e.find(g=>g.id===p)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(p=>p.id));let d=new Set(r.bag);if(s=e.filter(p=>d.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(d=>{let p=this.specScore(d);return p>i&&(i=p),{c:d,s:p}}).filter(d=>d.s===i).map(d=>d.c),c;if(a.length===1)c=a[0];else{let d=r.last!==void 0?a.findIndex(g=>g.id===r.last):-1,p=Math.floor(this.rng()*(d>=0?a.length-1:a.length));d>=0&&p>=d&&p++,c=a[p]}return t!=="repeat"&&(r.bag=r.bag.filter(d=>d!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0):0}matchedSpec(e,t){return Y(e,s=>le(D(s,this.evalCtx,L)),{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=t.get(e.id);return r||(r={},t.set(e.id,r)),r}runEffects(e){for(let t of e??[])this.setProperty(t.target,this.evalExpr(t.value))}eligible(e){return e.condition?le(this.evalExpr(e.condition)):!0}evalExpr(e){return D(this.conditionAst(e),this.evalCtx,L)}conditionAst(e){let t=ie.get(e);return t||(t=v(e.ast),ie.set(e,t)),t}enter(e){this.visitCounts.set(e,(this.visitCounts.get(e)??0)+1),this.host.sharedVisits.set(e,(this.host.sharedVisits.get(e)??0)+1)}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=this.rngState+1831565813|0;this.rngState=e;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};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 ne(e,t=>this.getProperty(t))}stripCaptions(e){return re(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}}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 x(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=$(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){let t=this.host.refSplitCache.get(e);return t||(t=j(e,r=>r==="scene"||this.host.shared.has(r)),this.host.refSplitCache.set(e,t)),t}freshLocal(){return new C().defineOwned("patter",this.host.patterLocalDecls)}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;if(!this.sceneBags.has(e.id)){let r={};for(let s of e.sceneProps??[]){let i=s.name.toLowerCase();t.has(i)||(r[i]=q(s))}this.sceneBags.set(e.id,r)}if(!this.host.stageBags.has(e.id)){let r={};for(let s of e.sceneProps??[]){let i=s.name.toLowerCase();t.has(i)&&(r[i]=q(s))}this.host.stageBags.set(e.id,r)}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);i&&(i[s]=q(r))}}};function oe(n,e){x(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 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 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 ae(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Ne(n){if(n.default!==void 0)return n.default;switch(n.type){case"number":return 0;case"string":return"";case"flags":return[];case"enum":return n.values?.[0]??"";case"quality":return n.stages?.[0]??"";default:return!1}}function ce(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function Ae(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"flags":return[];case"enum":return n.values?.[0]??"";case"quality":return n.stages?.[0]??""}}function Fe(n){let e=r=>r.toLowerCase(),t=new Map;for(let r of n)t.set(e(r.name),Ae(r));return{get:r=>t.get(e(r)),set:(r,s)=>{t.set(e(r),s)}}}function q(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"flags":return[];case"enum":return n.values?.[0]??"";case"quality":return n.stages?.[0]??""}}function le(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Ge=(n,e)=>n.shared??e;function H(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Ge(n,e)}}function Te(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Ve(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 ue(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=f(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:f(c),name:c.name}))});for(let c of o.blocks)Ve(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>H(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=>H(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(Te)}));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=>H(o,!0)),scene:r},gameData:i,counts:e}}function ge(n,e){return n.gameDataFields?.[e]??[]}function U(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function fe(n,e){let t={};for(let r of n){let s=U(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return xe(Me);})();\n//# sourceMappingURL=patterplay.min.js.map';
357
-
358
- // src/export-html.ts
359
- var esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
360
- var PLAYER_JS = String.raw`
361
- (function () {
362
- var Engine = window.Patterplay.Engine;
363
- var BUNDLE = window.PATTER_BUNDLE;
364
- var startScene = (BUNDLE.start && BUNDLE.start.scene) || Object.keys(BUNDLE.scenes)[0];
365
- var SAVE_KEY = "patter.play." + ((BUNDLE.content && BUNDLE.content.hash) || "x");
366
-
367
- var stage = document.getElementById("stage");
368
- var controls = document.getElementById("controls");
369
- var engine, flow;
370
-
371
- // Stable per-character hue (same idea as the editor's colour-by-character), tinted for a light page.
372
- function hueOf(name) { var h = 0; for (var i = 0; i < (name || "").length; i++) h = (h * 31 + name.charCodeAt(i)) % 360; return h; }
373
- function add(cls, html) { var d = document.createElement("div"); d.className = cls; d.innerHTML = html; stage.appendChild(d); return d; }
374
- function esc(s) { return String(s == null ? "" : s).replace(/[&<>]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]; }); }
375
-
376
- // Pacing: a source-only reader carries no audio, so each line is held on screen for a reading-length delay
377
- // (proportional to its word count) before the next appears, and every line fades in (see CSS). The Speed
378
- // control sets the reading pace (words/min); "Instant" drops the delay entirely. The choice is persisted.
379
- // A run token cancels an in-flight paced read the moment the player restarts, loads, or picks a choice.
380
- var SPEED_WPM = { slow: 300, normal: 500, fast: 800, instant: 0 }, runToken = 0;
381
- var speedKey = (function () { try { return localStorage.getItem("patter.playSpeed") || "normal"; } catch (e) { return "normal"; } })();
382
- if (SPEED_WPM[speedKey] === undefined) speedKey = "normal";
383
- function fakeDuration(text) {
384
- var wpm = SPEED_WPM[speedKey];
385
- if (!wpm) return 0; // "Instant" - reveal the next line immediately
386
- var words = String(text == null ? "" : text).trim().split(/\s+/).filter(Boolean).length;
387
- return Math.max(450, Math.round((words / wpm) * 60000)); // a beat needs to land
438
+ var CHILD_KEYS = ["blocks", "children", "beats"];
439
+ function toTree(raw) {
440
+ const childKey = CHILD_KEYS.find((k) => Array.isArray(raw[k]));
441
+ const fields = {};
442
+ for (const [k, v] of Object.entries(raw)) if (k !== childKey) fields[k] = v;
443
+ const children = childKey ? raw[childKey].filter(isObj).map(toTree) : [];
444
+ return { id: typeof raw.id === "string" ? raw.id : "", fields, childKey, children };
445
+ }
446
+ function fromTree(n) {
447
+ const out = { ...n.fields };
448
+ if (n.childKey) out[n.childKey] = n.children.map(fromTree);
449
+ return out;
450
+ }
451
+ function mergeFlow(base, ours, theirs) {
452
+ const conflicts = [];
453
+ const warnings = [];
454
+ const merged = {};
455
+ merged.schema = merge3(base.schema, ours.schema, theirs.schema, "", "schema", conflicts) ?? ours.schema;
456
+ const bScene = isObj(base.scene) ? toTree(base.scene) : void 0;
457
+ const oScene = toTree(asMap(ours.scene));
458
+ const tScene = isObj(theirs.scene) ? toTree(theirs.scene) : void 0;
459
+ const mergedScene = mergeNode(bScene, oScene, tScene, "scene", conflicts, warnings);
460
+ merged.scene = fromTree(mergedScene);
461
+ checkDuplicateIds(mergedScene, conflicts);
462
+ return { type: "flow", merged, conflicts, warnings };
463
+ }
464
+ function mergeNode(b, o, t, path, conflicts, warnings) {
465
+ const fields = mergeFields(b?.fields ?? {}, o.fields, t?.fields ?? {}, o.id, path, conflicts);
466
+ const childKey = o.childKey ?? t?.childKey ?? b?.childKey;
467
+ const children = childKey ? mergeChildren(b?.children ?? [], o.children, t?.children ?? [], `${path}.${childKey}`, conflicts, warnings) : [];
468
+ return { id: o.id, fields, childKey, children };
469
+ }
470
+ function mergeFields(b, o, t, id, path, conflicts) {
471
+ const out = {};
472
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
473
+ const v = merge3(b[k], o[k], t[k], id, `${path}.${k}`, conflicts);
474
+ if (v !== void 0) out[k] = v;
388
475
  }
389
- function scrollDown() { window.scrollTo(0, document.body.scrollHeight); }
390
- var speedSel = document.getElementById("speed");
391
- if (speedSel) {
392
- speedSel.value = speedKey; // a fresh fakeDuration() reads speedKey each line, so a change applies next line
393
- speedSel.onchange = function () { speedKey = speedSel.value; try { localStorage.setItem("patter.playSpeed", speedKey); } catch (e) {} };
476
+ return out;
477
+ }
478
+ function mergeChildren(B, O, T, path, conflicts, warnings) {
479
+ const Bm = idMap(B), Om = idMap(O), Tm = idMap(T);
480
+ const Bset = new Set(Bm.keys()), Oset = new Set(Om.keys()), Tset = new Set(Tm.keys());
481
+ const merged = /* @__PURE__ */ new Map();
482
+ const here = (id) => `${path}[${id}]`;
483
+ for (const id of /* @__PURE__ */ new Set([...Bm.keys(), ...Om.keys(), ...Tm.keys()])) {
484
+ const b = Bm.get(id), o = Om.get(id), t = Tm.get(id);
485
+ const inB = Bset.has(id), inO = Oset.has(id), inT = Tset.has(id);
486
+ if (inO && inT) {
487
+ if (!inB && !eq(fromTree(o), fromTree(t))) {
488
+ conflicts.push({ id, path: here(id), base: void 0, ours: fromTree(o), theirs: fromTree(t), kind: "added-both" });
489
+ merged.set(id, o);
490
+ } else {
491
+ merged.set(id, mergeNode(b, o, t, here(id), conflicts, warnings));
492
+ }
493
+ } else if (inO && !inT) {
494
+ if (inB && !eq(fromTree(o), fromTree(b))) {
495
+ conflicts.push({ id, path: here(id), base: fromTree(b), ours: fromTree(o), theirs: void 0, kind: "delete-vs-edit" });
496
+ merged.set(id, o);
497
+ } else if (!inB) {
498
+ merged.set(id, o);
499
+ }
500
+ } else if (!inO && inT) {
501
+ if (inB && !eq(fromTree(t), fromTree(b))) {
502
+ conflicts.push({ id, path: here(id), base: fromTree(b), ours: void 0, theirs: fromTree(t), kind: "delete-vs-edit" });
503
+ } else if (!inB) {
504
+ merged.set(id, t);
505
+ }
506
+ }
394
507
  }
395
-
396
- function build() { engine = new Engine(BUNDLE); flow = engine.openFlow("main", { scene: startScene }); }
397
-
398
- function newGame() { stage.innerHTML = ""; controls.innerHTML = ""; build(); run(); }
399
-
400
- function run() {
401
- controls.innerHTML = "";
402
- var token = ++runToken;
403
- (function step() {
404
- if (token !== runToken) return; // superseded by a restart / load / choice pick
405
- var s = flow.advance();
406
- if (!s) { add("end", "The End"); return; } // defensive: a step should always be returned; never hard-crash
407
- if (s.type === "line") {
408
- var who = s.characterName || s.character || "";
409
- var dir = s.direction ? '<em class="dir">(' + esc(s.direction) + ')</em> ' : "";
410
- add("line", '<span class="who" style="color:hsl(' + hueOf(s.character || who) + ',55%,38%)">' + esc(who) + '</span>' + dir + esc(s.text));
411
- scrollDown(); setTimeout(step, fakeDuration(s.text));
412
- } else if (s.type === "text") {
413
- add("text", esc(s.text)); scrollDown(); setTimeout(step, fakeDuration(s.text));
414
- } else if (s.type === "choice") { renderChoice(s); }
415
- else if (s.type === "end") { add("end", "The End"); }
416
- else { step(); } // game-event beat: a host cue with no player-facing text - skip it, advance at once
417
- })();
508
+ return orderChildren(merged, B, O, T, path, conflicts, warnings);
509
+ }
510
+ function orderChildren(merged, B, O, T, path, conflicts, warnings) {
511
+ const Bids = B.map((n) => n.id), Oids = O.map((n) => n.id), Tids = T.map((n) => n.id);
512
+ const Oidset = new Set(Oids);
513
+ const common = new Set(Bids.filter((id) => merged.has(id)));
514
+ const restrict = (ids) => ids.filter((id) => common.has(id));
515
+ const relB = restrict(Bids), relO = restrict(Oids), relT = restrict(Tids);
516
+ const fullO = relO.length === common.size, fullT = relT.length === common.size;
517
+ let commonOrder;
518
+ if (!fullO && !fullT) commonOrder = relB;
519
+ else if (!fullO) commonOrder = relT;
520
+ else if (!fullT) commonOrder = relO;
521
+ else if (arrEq(relO, relB)) commonOrder = relT;
522
+ else if (arrEq(relT, relB)) commonOrder = relO;
523
+ else if (arrEq(relO, relT)) commonOrder = relO;
524
+ else {
525
+ conflicts.push({ id: "", path, base: relB, ours: relO, theirs: relT, kind: "moved" });
526
+ commonOrder = relO;
418
527
  }
419
-
420
- function renderChoice(step) {
421
- var opts = step.options || [];
422
- if (!opts.length) { add("end", "The End"); return; }
423
- for (var i = 0; i < opts.length; i++) (function (opt, idx) {
424
- var b = document.createElement("button");
425
- b.className = "choice"; b.textContent = (opt.prompt && opt.prompt.text) || "(continue)";
426
- b.disabled = opt.eligible === false;
427
- b.style.animationDelay = (idx * 60) + "ms"; // gentle stagger so the options fade in one after another
428
- b.onclick = function () { flow.choose(opt.id); scrollDown(); run(); };
429
- controls.appendChild(b);
430
- })(opts[i], i);
431
- scrollDown();
528
+ const anchorIn = (ids, id) => {
529
+ for (let i = ids.indexOf(id) - 1; i >= 0; i--) if (common.has(ids[i])) return ids[i];
530
+ return null;
531
+ };
532
+ const byAnchor = /* @__PURE__ */ new Map();
533
+ const group = (a) => {
534
+ let g = byAnchor.get(a);
535
+ if (!g) {
536
+ g = { ours: [], theirs: [] };
537
+ byAnchor.set(a, g);
538
+ }
539
+ return g;
540
+ };
541
+ for (const n of O) if (merged.has(n.id) && !common.has(n.id)) group(anchorIn(Oids, n.id)).ours.push(n.id);
542
+ for (const n of T) if (merged.has(n.id) && !common.has(n.id) && !Oidset.has(n.id)) group(anchorIn(Tids, n.id)).theirs.push(n.id);
543
+ const out = [];
544
+ const emit = (anchor) => {
545
+ const g = byAnchor.get(anchor);
546
+ if (!g) return;
547
+ if (g.ours.length > 0 && g.theirs.length > 0) warnings.push({ id: anchor ?? "", path, message: "both sides inserted here; kept ours-then-theirs - review the order" });
548
+ out.push(...g.ours, ...g.theirs);
549
+ };
550
+ emit(null);
551
+ for (const id of commonOrder) {
552
+ out.push(id);
553
+ emit(id);
432
554
  }
433
-
434
- document.getElementById("restart").onclick = newGame;
435
- document.getElementById("save").onclick = function () { try { localStorage.setItem(SAVE_KEY, JSON.stringify(engine.saveGame())); flash("Saved"); } catch (e) {} };
436
- document.getElementById("load").onclick = function () {
437
- var blob; try { blob = localStorage.getItem(SAVE_KEY); } catch (e) {}
438
- if (!blob) { flash("No save yet"); return; }
439
- engine = new Engine(BUNDLE);
440
- engine.loadGame(JSON.parse(blob)); flow = engine.getFlow("main");
441
- stage.innerHTML = ""; add("text", "(resumed from save)"); run();
555
+ return out.map((id) => merged.get(id));
556
+ }
557
+ function checkDuplicateIds(root, conflicts) {
558
+ const seen = /* @__PURE__ */ new Set(), dup = /* @__PURE__ */ new Set();
559
+ const walk = (n) => {
560
+ if (n.id) {
561
+ if (seen.has(n.id)) dup.add(n.id);
562
+ else seen.add(n.id);
563
+ }
564
+ n.children.forEach(walk);
442
565
  };
443
- function flash(msg) { var f = document.getElementById("flash"); f.textContent = msg; f.style.opacity = "1"; setTimeout(function () { f.style.opacity = "0"; }, 1100); }
566
+ walk(root);
567
+ for (const id of dup) conflicts.push({ id, path: "scene", base: void 0, ours: void 0, theirs: void 0, kind: "structural" });
568
+ }
569
+ var idMap = (nodes) => new Map(nodes.filter((n) => n.id).map((n) => [n.id, n]));
570
+ var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
444
571
 
445
- newGame();
446
- })();
447
- `;
448
- var STYLE = String.raw`
449
- :root { color-scheme: light; }
450
- * { box-sizing: border-box; }
451
- body { margin: 0; background: #f6f1e7; color: #2b2620; font: 18px/1.65 Georgia, "Times New Roman", serif; }
452
- .wrap { max-width: 42rem; margin: 0 auto; padding: 1.5rem 1.25rem 6rem; }
453
- header { display: flex; align-items: baseline; gap: 0.75rem; border-bottom: 1px solid #e0d7c5; padding-bottom: 0.75rem; margin-bottom: 1.25rem; }
454
- h1 { font-size: 1.15rem; margin: 0; font-weight: 600; }
455
- header .by { color: #9a8f78; font-size: 0.8rem; font-family: ui-sans-serif, system-ui, sans-serif; margin-left: auto; }
456
- #stage > div { margin: 0.55rem 0; animation: fade 0.45s ease-out both; }
457
- @keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
458
- @media (prefers-reduced-motion: reduce) { #stage > div, .choice { animation: none; } }
459
- .line .who { font-weight: 700; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.8rem; letter-spacing: 0.02em; text-transform: uppercase; margin-right: 0.4rem; }
460
- .line .dir { color: #9a8f78; font-style: italic; }
461
- .text { color: #4a4334; }
462
- .end { color: #9a8f78; font-style: italic; text-align: center; margin-top: 1.5rem; }
463
- #controls { margin: 1.25rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
464
- .choice { font: 1rem Georgia, serif; text-align: left; background: #fff; border: 1px solid #d9cfbb; border-radius: 8px; padding: 0.55rem 0.8rem; cursor: pointer; transition: border-color .15s, background .15s; animation: fade 0.4s ease-out both; }
465
- .choice:hover:not(:disabled) { border-color: #b8956a; background: #fffaf0; }
466
- .choice:disabled { color: #b8af9b; cursor: default; }
467
- .bar { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(246,241,231,0.94); border-top: 1px solid #e0d7c5; backdrop-filter: blur(4px); }
468
- .bar .inner { max-width: 42rem; margin: 0 auto; padding: 0.6rem 1.25rem; display: flex; gap: 0.5rem; align-items: center; }
469
- .bar button { font: 0.85rem ui-sans-serif, system-ui, sans-serif; background: #fff; border: 1px solid #d9cfbb; border-radius: 6px; padding: 0.3rem 0.7rem; cursor: pointer; }
470
- .bar button:hover { border-color: #b8956a; }
471
- .bar .speedlabel { font: 0.8rem ui-sans-serif, system-ui, sans-serif; color: #9a8f78; display: flex; align-items: center; gap: 0.3rem; }
572
+ // src/validate.ts
573
+ import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
574
+ import { validateConditions, validateInterpolation, exportBundle, hostScopesToSpec } from "@patterkit/compiler";
575
+ function runValidate(loaded) {
576
+ const { project, scenes, locales } = loaded;
577
+ const structural = validateProject({ project, scenes, authoring: loaded.authoring });
578
+ const foreignScopes = hostScopesToSpec(project.scopeRegistry);
579
+ const conditions = validateConditions({ project, scenes }, { foreignScopes });
580
+ const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes });
581
+ const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
582
+ const staleBundles = checkBundles(loaded);
583
+ const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
584
+ const orphans = orphanShards(loaded);
585
+ return {
586
+ structural,
587
+ conditions,
588
+ interpolation,
589
+ hygiene,
590
+ staleBundles,
591
+ unresolvedMerges,
592
+ orphans,
593
+ ok: structural.length === 0 && conditions.length === 0 && interpolation.length === 0 && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0
594
+ };
595
+ }
596
+ function orphanShards(loaded) {
597
+ const collected = /* @__PURE__ */ new Set([
598
+ loaded.projectFile,
599
+ ...Object.values(loaded.sceneFiles),
600
+ ...loaded.localeFiles,
601
+ ...loaded.authoringFiles
602
+ ]);
603
+ const kind = {
604
+ ".patterflow": "scene",
605
+ ".patterloc": "locale",
606
+ ".patterx": "authoring",
607
+ ".patterproj": "project"
608
+ };
609
+ const out = [];
610
+ for (const [ext, what] of Object.entries(kind)) {
611
+ for (const file of walkFiles(loaded.root, ext)) {
612
+ if (collected.has(file)) continue;
613
+ out.push({ file, message: `a ${what} shard outside the project's layout - nothing loads it, so its content is not in the project (move it under the configured folder, or delete it)` });
614
+ }
615
+ }
616
+ return out;
617
+ }
618
+ function checkBundles(loaded) {
619
+ const issues = [];
620
+ const bundles = walkFiles(loaded.root, ".patterc");
621
+ if (bundles.length === 0) return issues;
622
+ let fresh;
623
+ try {
624
+ fresh = exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales }).content.hash;
625
+ } catch {
626
+ return issues;
627
+ }
628
+ for (const file of bundles) {
629
+ let hash;
630
+ try {
631
+ const parsed = parseSource2(readFileSync2(file, "utf8"));
632
+ hash = parsed?.content?.hash;
633
+ } catch {
634
+ issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
635
+ continue;
636
+ }
637
+ if (hash !== fresh) {
638
+ issues.push({ file, message: "compiled bundle is stale (does not match current source) - run `patter export`" });
639
+ }
640
+ }
641
+ return issues;
642
+ }
643
+ var hygieneCache = /* @__PURE__ */ new Map();
644
+ function checkHygiene(files) {
645
+ const issues = [];
646
+ for (const file of files) {
647
+ let mtimeMs;
648
+ try {
649
+ mtimeMs = statSync2(file).mtimeMs;
650
+ } catch {
651
+ continue;
652
+ }
653
+ const hit = hygieneCache.get(file);
654
+ if (hit && hit.mtimeMs === mtimeMs) {
655
+ issues.push(...hit.issues);
656
+ continue;
657
+ }
658
+ const fileIssues = [];
659
+ try {
660
+ const text = readFileSync2(file, "utf8");
661
+ if (text.charCodeAt(0) === 65279) {
662
+ fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
663
+ }
664
+ if (text.includes("\r")) {
665
+ fileIssues.push({ file, message: "file contains CRLF line endings (canonical form is LF - run `patter format`)" });
666
+ }
667
+ } catch {
668
+ continue;
669
+ }
670
+ hygieneCache.set(file, { mtimeMs, issues: fileIssues });
671
+ issues.push(...fileIssues);
672
+ }
673
+ return issues;
674
+ }
675
+
676
+ // src/export.ts
677
+ import { basename as basename2, isAbsolute, join as join2 } from "path";
678
+ import { exportBundle as exportBundle2 } from "@patterkit/compiler";
679
+ import { isContentlessBeat } from "@patterkit/model";
680
+ function compileScenes(loaded) {
681
+ const def = loaded.project.locales.default;
682
+ const written = /* @__PURE__ */ new Set();
683
+ for (const l of loaded.locales) if (l.locale === def) {
684
+ for (const [k, v] of Object.entries(l.strings)) if (v) written.add(k);
685
+ }
686
+ const clean = (n) => {
687
+ if (n.type === "group") return { ...n, children: (n.children ?? []).map(clean) };
688
+ if (!n.beats) return n;
689
+ const kept = n.beats.filter((b) => !isContentlessBeat(b, written.has(b.id)));
690
+ if (kept.length === n.beats.length) return n;
691
+ const { beats, ...rest } = n;
692
+ return kept.length ? { ...rest, beats: kept } : rest;
693
+ };
694
+ return loaded.scenes.map((s) => ({
695
+ ...s,
696
+ blocks: s.blocks.map((b) => ({ ...b, children: b.children.map(clean) }))
697
+ }));
698
+ }
699
+ function refuseUnresolvedMerge(loaded) {
700
+ const issues = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
701
+ if (!issues.length) return;
702
+ throw new Error(
703
+ `${issues.length} unresolved merge conflict(s) - resolve them and delete the .patterconflict sidecar(s) first:
704
+ ` + issues.map((i) => ` ${i.file}`).join("\n")
705
+ );
706
+ }
707
+ function runExport(loaded) {
708
+ refuseUnresolvedMerge(loaded);
709
+ const { project, locales } = loaded;
710
+ const full = exportBundle2({ project, scenes: compileScenes(loaded), locales });
711
+ const loc = project.export?.localisation;
712
+ if (!loc || loc.mode === "embedded") return full;
713
+ const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
714
+ return { ...full, strings, localisation: { mode: "ids", ...loc.sourceDebug ? { sourceDebug: true } : {} } };
715
+ }
716
+ function runExportFull(loaded) {
717
+ refuseUnresolvedMerge(loaded);
718
+ const { project, locales } = loaded;
719
+ return exportBundle2({ project, scenes: compileScenes(loaded), locales });
720
+ }
721
+ function bundleOutputPath(loaded) {
722
+ const override = loaded.project.export?.bundle;
723
+ if (override) return isAbsolute(override) ? override : join2(loaded.root, override);
724
+ const stem = basename2(loaded.projectFile).replace(/\.patterproj$/, "");
725
+ return join2(loaded.root, "dist", `${stem}.patterc`);
726
+ }
727
+
728
+ // src/export-html.ts
729
+ import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
730
+
731
+ // src/playable-runtime.ts
732
+ var PLAYABLE_RUNTIME_JS = '"use strict";var Patterplay=(()=>{var G=Object.defineProperty;var Se=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var ve=Object.prototype.hasOwnProperty;var be=(n,e)=>{for(var t in e)G(n,t,{get:e[t],enumerable:!0})},we=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ye(e))!ve.call(n,s)&&s!==t&&G(n,s,{get:()=>e[s],enumerable:!(r=Se(e,s))||r.enumerable});return n};var xe=n=>we(G({},"__esModule",{value:!0}),n);var Me={};be(Me,{Engine:()=>N,Flow:()=>k,buildTagIndex:()=>P,describeBundle:()=>ue,effectiveGameData:()=>fe,gameDataFields:()=>ge,gameDataValue:()=>U});function v(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:v(n[2])};case"bin":return{kind:"binary",op:n[1],left:v(n[2]),right:v(n[3])};case"call":{let e=n.slice(2).map(v);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var l=class extends Error{constructor(e){super(e),this.name="EvalError"}};function D(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=e.scopes[i.scope];if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):o[i.name];if(a===void 0){if(r.get(i.scope)==="throw")throw new l(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!t.functions[i.name]){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new l(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=T(a,e);if(c===void 0)throw new l("advance() needs a quality reference (@scope.name of a quality property)");let d=S(s(a),c,"advance");return c[Math.min(d+1,c.length-1)]}let o=t.functions[i.name];if(!o)throw new l(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new l("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 l(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new l(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let g=s(i.left);if(typeof g!="boolean")throw new l(`\'and\' requires boolean operands, left is ${typeof g}`);if(!g)return!1;let h=s(i.right);if(typeof h!="boolean")throw new l(`\'and\' requires boolean operands, right is ${typeof h}`);return h}if(i.op==="or"){let g=s(i.left);if(typeof g!="boolean")throw new l(`\'or\' requires boolean operands, left is ${typeof g}`);if(g)return!0;let h=s(i.right);if(typeof h!="boolean")throw new l(`\'or\' requires boolean operands, right is ${typeof h}`);return h}let o=s(i.left),a=s(i.right),c=T(i.left,e),d=T(i.right,e),p=c??d;if(p!==void 0){if(c&&d&&!ke(c,d)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new l(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return S(o,p,">")>S(a,p,">");case">=":return S(o,p,">=")>=S(a,p,">=");case"<":return S(o,p,"<")<S(a,p,"<");case"<=":return S(o,p,"<=")<=S(a,p,"<=");case"+":case"-":case"*":case"/":throw new l(`\'${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 X(o,a);case"!=":return!X(o,a);case">":return b(o,a,">"),o>a;case">=":return b(o,a,">="),o>=a;case"<":return b(o,a,"<"),o<a;case"<=":return b(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new l(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return b(o,a,"-"),o-a;case"*":return b(o,a,"*"),o*a;case"/":if(b(o,a,"/"),a===0)throw new l("division by zero");return o/a}}}};return s(n)}function X(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;for(let t=0;t<n.length;t++)if(n[t]!==e[t])return!1;return!0}return n===e}function b(n,e,t){if(typeof n!="number"||typeof e!="number")throw new l(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function T(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function S(n,e,t){if(typeof n!="string")throw new l(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new l(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var ke=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);var Ce={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ee=[Ce];function Y(n,e,t){let r=t?.countingCalls??Ee;return I(n,t?.want??!0,e,r)}function I(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=I(n.left,e,t,r),i=I(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 I(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 M=class n{values={};decls=new Map;subscribers=new Set;auditors=new Set;norm;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),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??Q(t))}}get(e){return this.values[this.norm(e)]}set(e,t,r){let s=this.norm(e);if(this.decls.get(s)?.writable===!1)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])=>Z(t,this.get(e),void 0,e))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm});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 Z(n,e,t,r){return{name:r??n.name.toLowerCase(),type:n.type,value:e,default:n.default??Q(n),...n.values!==void 0?{values:n.values}:{},writable:t??n.writable??!0}}var V=1,C=class{scopes=new Map;defineOwned(e,t){return this.mountOwned(e,new M(t))}mountOwned(e,t){return this.assertFree(e),this.scopes.set(e,{kind:"owned",bag:t}),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){this.assertFree(e);let i=new Map;for(let o of r)i.set(o.name.toLowerCase(),o);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:i,scopeWritable:s}),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(t.toLowerCase())}set(e,t,r){let s=this.scopes.get(e);if(!s)throw new Error(`unknown scope \'@${e}\'`);if(s.kind==="owned"){try{s.bag.set(t,r)}catch{throw new Error(`\'@${e}.${t}\' is read-only`)}return}let i=t.toLowerCase();if(!this.foreignWritable(s,i))throw new Error(`\'@${e}.${t}\' is read-only`);s.resolver.set(i,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)if(r.kind==="owned")for(let s of r.bag.rows())e.push({scope:t,...s});else for(let s of r.decls.values())e.push({scope:t,...Z(s,r.resolver.get(s.name.toLowerCase()),this.foreignWritable(r,s.name.toLowerCase()))});return e}toEvalContext(e){let t={};for(let[s,i]of this.scopes)t[s]=i.kind==="owned"?i.bag.values:i.resolver;let r=this.qualityLadders();return r.size===0?{scopes:t,host:e}:{scopes:t,host:e,qualities:(s,i)=>r.get(s)?.get(i.toLowerCase())}}qualityLadders(){let e=new Map;for(let[t,r]of this.scopes){let s=r.kind==="owned"?r.bag.declarations():[...r.decls.values()];for(let i of s){if(i.type!=="quality"||i.stages===void 0)continue;let o=e.get(t);o||(o=new Map,e.set(t,o)),o.set(i.name.toLowerCase(),i.stages)}}return e}toSchema(){let e=new Map;for(let[t,r]of this.scopes){let s=r.kind==="owned"?r.bag.declarations():[...r.decls.values()];if(s.length===0)continue;let i=new Map;for(let o of s)i.set(o.name.toLowerCase(),{type:o.type,enumValues:o.values,...o.stages!==void 0?{stages:o.stages}:{}});e.set(t,i)}return{properties:e}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());return e}load(e){for(let[t,r]of Object.entries(e)){let s=this.scopes.get(t);s?.kind==="owned"&&s.bag.load(r)}}saveFragment(){return{version:V,scopes:this.save()}}loadFragment(e){if(e.version!==V)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${V})`);this.load(e.scopes)}assertFree(e){if(this.scopes.has(e))throw new Error(`scope \'@${e}\' is already registered`)}};function Q(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]??""}}function E(n){return n.ctx.host??{}}var L={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new l("random(a, b) requires exactly 2 arguments");let t=E(e).nextRandom;if(!t)throw new l("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 l("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new l("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:te("check_flags"),eval(n,e){let t=ee(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new l("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:te("set_flags"),eval(n,e){let t=[...ee(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new l("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:B("visits"),eval:(n,e)=>E(e).visits?.(R(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:B("seen"),eval:(n,e)=>(E(e).visits?.(R(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:B("patter_visits"),eval:(n,e)=>E(e).patterVisits?.(R(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:B("patter_seen"),eval:(n,e)=>(E(e).patterVisits?.(R(n,e,"patter_seen"))??0)>0}}};function j(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 R(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new l(`${t}(id) requires a string node id`);return r}function B(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 ee(n,e,t){if(!n)throw new l(`${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 l(`${t}() first argument must be a flags property`)}function te(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 De=/^@[A-Za-z0-9_.]+$/;function*Ie(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:De.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 Re(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function Be(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function Oe(n){let e="",t=!1;for(let r of n){if(Be(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function re(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?Oe(r):n}function ne(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of Ie(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?"":Re(s)}return t}function x(n,e){for(let t of n){e(t);let r=t.children;r&&x(r,e)}}function Pe(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function f(n){let e=n.gameId?.trim();return e||Pe(n.name)}function $(n){return`cast:${n}`}var _={open:"[",close:"]"},se="SFX";function O(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function P(n){let e=new Map,t=(r,s)=>{let i=O([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,O([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=O(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=O([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var ie=new WeakMap,N=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;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 d=new Map;for(let u of e.cast??[])u.displayName&&d.set(u.name,u.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let p=new Map,g=new Map,h=new Map;for(let[u,y]of Object.entries(e.scenes)){this.sceneGameIdToId.set(f(y),u);let w=new Map;for(let m of y.blocks)g.set(m.id,{sceneId:u}),h.set(m.id,m),w.set(f(m),m.id),x(m.children,J=>p.set(J.id,J));this.blockGameIdToId.set(u,w)}let W=e.properties??[],A=W.filter(u=>u.shared??!0).map(ae),he=W.filter(u=>!(u.shared??!0)).map(ae),me=new Set(A.map(u=>u.name.toLowerCase())),F=new C().defineOwned("patter",A),K=new Set;if(t.world){let u=e.scopeRegistry?.scopes.find(w=>w.token==="world"),y=(u?.declarations??[]).map(ce);F.defineForeign("world",t.world,y,u?.writable??!0),K.add("world")}for(let u of e.scopeRegistry?.scopes??[]){if(K.has(u.token))continue;let y=(u.declarations??[]).map(ce);F.defineForeign(u.token,Fe(u.declarations??[]),y,u.writable??!0)}let z=new Map;for(let[u,y]of Object.entries(e.scenes)){let w=new Set((y.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));z.set(u,w)}this.host={bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:d,nodeIndex:p,blockIndex:g,blockById:h,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:P(e),shared:F,patterSharedDecls:A,patterLocalDecls:he,patterSharedNames:me,sceneSharedNames:z,sharedVisits:new Map,sharedSelectors:new Map,stageBags:new Map,customRng:t.rng,onDryChoice:t.onDryChoice,replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??_).open,captionClose:(e.closedCaptions??_).close,captionCharacter:e.closedCaptions?.character||se,refSplitCache:new Map}}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){let t=this.saveGame(),r=i=>(i.setLocale(this.currentLocale),i.setClosedCaptions(this.host.captionsOn),i),s=new n(e,this.creationOptions);try{return s.loadGame(t),r(s)}catch{let i=new n(e,this.creationOptions);for(let[o,a]of Object.entries(t.flows)){let c=a.cursor.currentSceneId;try{i.openFlow(o,c!==null?{scene:c}:{})}catch{}}return r(i)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){let r=this.resolveSceneRef(t.scene),s=this.resolveBlockRef(r,t.block);this.flowsById.get(e)?.close();let i=new k(e,this.host,t.seed??this.defaultSeed);return this.flowsById.set(e,i),i.start(r,s),i}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}resolveSceneRef(e){if(e!=null)return this.host.bundle.scenes[e]?e:this.sceneGameIdToId.get(e)??e}resolveBlockRef(e,t){if(t!=null){if(this.host.blockById.has(t))return t;if(e!=null){let r=this.blockGameIdToId.get(e)?.get(t);if(r)return r}return t}}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?f(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?f(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)??[]}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)oe(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 oe(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...f(e)?{gameId:f(e)}:{},name:e.name,...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...f(t)?{gameId:f(t)}:{},name:t.name,...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)x(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[$(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()]}closeFlow(e){this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.shared.reseedOwned("patter",this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear(),this.host.stageBags.clear()}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.shared.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e);this.host.shared.set(r,s,t)}listProperties(){return this.host.patterSharedDecls.map(e=>({ref:`@${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:Ne(e)}))}splitShared(e){let t=this.host.refSplitCache.get(e);if(t||(t=j(e,r=>r==="scene"||this.host.shared.has(r)),this.host.refSplitCache.set(e,t)),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 this.host.shared.save()}load(e){this.host.shared.load(e)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:2,shared:this.host.shared.save(),sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:pe(this.host.sharedSelectors),stageBags:Object.fromEntries([...this.host.stageBags].map(([t,r])=>[t,{...r}])),flows:e}}loadGame(e){if(e.version!==2)throw new Error(`unsupported save version: ${e.version}`);this.host.shared.load(e.shared),this.host.sharedVisits.clear();for(let[t,r]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(t,r);this.host.sharedSelectors.clear();for(let[t,r]of de(e.sharedSelectors))this.host.sharedSelectors.set(t,r);this.host.stageBags.clear();for(let[t,r]of Object.entries(e.stageBags??{}))this.host.stageBags.set(t,{...r});this.flowsById.clear();for(let[t,r]of Object.entries(e.flows)){let s=new k(t,this.host,this.defaultSeed);s.restore(r),this.flowsById.set(t,s)}}},k=class{id;host;local;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPromptBeat=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.shared.get("patter",e):this.local.get("patter",e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.shared.set("patter",e,t):this.local.set("patter",e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;return t===null?void 0:(this.host.sceneSharedNames.get(t)?.has(e)?this.host.stageBags.get(t):this.sceneBags.get(t))?.[e]},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.host.sceneSharedNames.get(r)?.has(e)?this.host.stageBags.get(r):this.sceneBags.get(r);s&&(s[e]=t)}};evalCtx;constructor(e,t,r){this.id=e,this.host=t,this.rngState=r>>>0,this.local=this.freshLocal();let s={...t.shared.toEvalContext().scopes};s.patter=this.patterResolver,s.scene=this.sceneResolver,this.evalCtx={scopes:s,host:{nextRandom:this.rng,visits:i=>this.visitCounts.get(i)??0,patterVisits:i=>this.host.sharedVisits.get(i)??0},qualities:(i,o)=>this.stagesFor(i,o)}}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 s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(e,t){if(this.sceneBags.clear(),this.local=this.freshLocal(),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(e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=this.host.sceneGameIdToId.get(e)??(this.host.bundle.scenes[e]?e:void 0);if(r===void 0)return!1;let s;return t!==void 0&&(s=this.host.blockGameIdToId.get(r)?.get(t)??(this.host.blockIndex.get(t)?.sceneId===r?t:void 0),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPromptBeat=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.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null}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.pendingPromptBeat){let e=this.pendingPromptBeat;return this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.beatResult(e)}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);let s=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(s);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}for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}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(i=>i.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);let s=t.byId.get(e);this.pendingChoice=null,this.pendingPromptBeat=this.host.replayPromptOnChoose?this.promptBeatOf(s)??null:null,this.pendingPromptOwnerId=this.pendingPromptBeat?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.shared.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitRef(e);if(r==="patter")this.patterResolver.set(s,t);else if(r==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);this.sceneResolver.set(s,t)}else this.host.shared.set(r,s,t)}snapshot(){return{scopes:this.local.save(),sceneBags:Object.fromEntries([...this.sceneBags].map(([e,t])=>[e,{...t}])),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,selectors:pe(this.selectors)}}}restore(e){this.rngState=e.rngState>>>0,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.sceneBags=new Map(Object.entries(e.sceneBags??{}).map(([r,s])=>[r,{...s}])),this.local=this.freshLocal(),this.local.load(e.scopes),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.pendingPromptBeat=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId);this.pendingPromptBeat=r?this.promptBeatOf(r)??null:null,this.pendingPromptBeat||(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)}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),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.length>0){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.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(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.filter(s=>this.eligible(s));if(t.length===0)return null;let r=this.selectorState(e);switch(e.selector){case"branch":return t[0];case"sequence":{let s=e.options?.order??"sequential",i=e.options?.exhaust??"once";return s==="shuffle"?this.pickShuffle(t,i,r):s==="specificity"?this.pickSpecificity(t,i,r):this.pickSequential(t,i,r)}default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(g=>g.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let g=e[s-1];return r.last=g.id,g}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,d=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&d>=c&&d++;let p=a[d];return a.splice(d,1),r.last=p,e.find(g=>g.id===p)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(p=>p.id));let d=new Set(r.bag);if(s=e.filter(p=>d.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(d=>{let p=this.specScore(d);return p>i&&(i=p),{c:d,s:p}}).filter(d=>d.s===i).map(d=>d.c),c;if(a.length===1)c=a[0];else{let d=r.last!==void 0?a.findIndex(g=>g.id===r.last):-1,p=Math.floor(this.rng()*(d>=0?a.length-1:a.length));d>=0&&p>=d&&p++,c=a[p]}return t!=="repeat"&&(r.bag=r.bag.filter(d=>d!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0):0}matchedSpec(e,t){return Y(e,s=>le(D(s,this.evalCtx,L)),{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=t.get(e.id);return r||(r={},t.set(e.id,r)),r}runEffects(e){for(let t of e??[])this.setProperty(t.target,this.evalExpr(t.value))}eligible(e){return e.condition?le(this.evalExpr(e.condition)):!0}evalExpr(e){return D(this.conditionAst(e),this.evalCtx,L)}conditionAst(e){let t=ie.get(e);return t||(t=v(e.ast),ie.set(e,t)),t}enter(e){this.visitCounts.set(e,(this.visitCounts.get(e)??0)+1),this.host.sharedVisits.set(e,(this.host.sharedVisits.get(e)??0)+1)}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=this.rngState+1831565813|0;this.rngState=e;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};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 ne(e,t=>this.getProperty(t))}stripCaptions(e){return re(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}}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 x(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=$(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){let t=this.host.refSplitCache.get(e);return t||(t=j(e,r=>r==="scene"||this.host.shared.has(r)),this.host.refSplitCache.set(e,t)),t}freshLocal(){return new C().defineOwned("patter",this.host.patterLocalDecls)}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;if(!this.sceneBags.has(e.id)){let r={};for(let s of e.sceneProps??[]){let i=s.name.toLowerCase();t.has(i)||(r[i]=q(s))}this.sceneBags.set(e.id,r)}if(!this.host.stageBags.has(e.id)){let r={};for(let s of e.sceneProps??[]){let i=s.name.toLowerCase();t.has(i)&&(r[i]=q(s))}this.host.stageBags.set(e.id,r)}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);i&&(i[s]=q(r))}}};function oe(n,e){x(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 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 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 ae(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Ne(n){if(n.default!==void 0)return n.default;switch(n.type){case"number":return 0;case"string":return"";case"flags":return[];case"enum":return n.values?.[0]??"";case"quality":return n.stages?.[0]??"";default:return!1}}function ce(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function Ae(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"flags":return[];case"enum":return n.values?.[0]??"";case"quality":return n.stages?.[0]??""}}function Fe(n){let e=r=>r.toLowerCase(),t=new Map;for(let r of n)t.set(e(r.name),Ae(r));return{get:r=>t.get(e(r)),set:(r,s)=>{t.set(e(r),s)}}}function q(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"flags":return[];case"enum":return n.values?.[0]??"";case"quality":return n.stages?.[0]??""}}function le(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Ge=(n,e)=>n.shared??e;function H(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Ge(n,e)}}function Te(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Ve(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 ue(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=f(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:f(c),name:c.name}))});for(let c of o.blocks)Ve(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>H(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=>H(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(Te)}));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=>H(o,!0)),scene:r},gameData:i,counts:e}}function ge(n,e){return n.gameDataFields?.[e]??[]}function U(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function fe(n,e){let t={};for(let r of n){let s=U(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return xe(Me);})();\n//# sourceMappingURL=patterplay.min.js.map';
733
+
734
+ // src/export-html.ts
735
+ var esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
736
+ var PLAYER_JS = String.raw`
737
+ (function () {
738
+ var Engine = window.Patterplay.Engine;
739
+ var BUNDLE = window.PATTER_BUNDLE;
740
+ var startScene = (BUNDLE.start && BUNDLE.start.scene) || Object.keys(BUNDLE.scenes)[0];
741
+ var SAVE_KEY = "patter.play." + ((BUNDLE.content && BUNDLE.content.hash) || "x");
742
+
743
+ var stage = document.getElementById("stage");
744
+ var controls = document.getElementById("controls");
745
+ var engine, flow;
746
+
747
+ // Stable per-character hue (same idea as the editor's colour-by-character), tinted for a light page.
748
+ function hueOf(name) { var h = 0; for (var i = 0; i < (name || "").length; i++) h = (h * 31 + name.charCodeAt(i)) % 360; return h; }
749
+ function add(cls, html) { var d = document.createElement("div"); d.className = cls; d.innerHTML = html; stage.appendChild(d); return d; }
750
+ function esc(s) { return String(s == null ? "" : s).replace(/[&<>]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]; }); }
751
+
752
+ // Pacing: a source-only reader carries no audio, so each line is held on screen for a reading-length delay
753
+ // (proportional to its word count) before the next appears, and every line fades in (see CSS). The Speed
754
+ // control sets the reading pace (words/min); "Instant" drops the delay entirely. The choice is persisted.
755
+ // A run token cancels an in-flight paced read the moment the player restarts, loads, or picks a choice.
756
+ var SPEED_WPM = { slow: 300, normal: 500, fast: 800, instant: 0 }, runToken = 0;
757
+ var speedKey = (function () { try { return localStorage.getItem("patter.playSpeed") || "normal"; } catch (e) { return "normal"; } })();
758
+ if (SPEED_WPM[speedKey] === undefined) speedKey = "normal";
759
+ function fakeDuration(text) {
760
+ var wpm = SPEED_WPM[speedKey];
761
+ if (!wpm) return 0; // "Instant" - reveal the next line immediately
762
+ var words = String(text == null ? "" : text).trim().split(/\s+/).filter(Boolean).length;
763
+ return Math.max(450, Math.round((words / wpm) * 60000)); // a beat needs to land
764
+ }
765
+ function scrollDown() { window.scrollTo(0, document.body.scrollHeight); }
766
+ var speedSel = document.getElementById("speed");
767
+ if (speedSel) {
768
+ speedSel.value = speedKey; // a fresh fakeDuration() reads speedKey each line, so a change applies next line
769
+ speedSel.onchange = function () { speedKey = speedSel.value; try { localStorage.setItem("patter.playSpeed", speedKey); } catch (e) {} };
770
+ }
771
+
772
+ function build() { engine = new Engine(BUNDLE); flow = engine.openFlow("main", { scene: startScene }); }
773
+
774
+ function newGame() { stage.innerHTML = ""; controls.innerHTML = ""; build(); run(); }
775
+
776
+ function run() {
777
+ controls.innerHTML = "";
778
+ var token = ++runToken;
779
+ (function step() {
780
+ if (token !== runToken) return; // superseded by a restart / load / choice pick
781
+ var s = flow.advance();
782
+ if (!s) { add("end", "The End"); return; } // defensive: a step should always be returned; never hard-crash
783
+ if (s.type === "line") {
784
+ var who = s.characterName || s.character || "";
785
+ var dir = s.direction ? '<em class="dir">(' + esc(s.direction) + ')</em> ' : "";
786
+ add("line", '<span class="who" style="color:hsl(' + hueOf(s.character || who) + ',55%,38%)">' + esc(who) + '</span>' + dir + esc(s.text));
787
+ scrollDown(); setTimeout(step, fakeDuration(s.text));
788
+ } else if (s.type === "text") {
789
+ add("text", esc(s.text)); scrollDown(); setTimeout(step, fakeDuration(s.text));
790
+ } else if (s.type === "choice") { renderChoice(s); }
791
+ else if (s.type === "end") { add("end", "The End"); }
792
+ else { step(); } // game-event beat: a host cue with no player-facing text - skip it, advance at once
793
+ })();
794
+ }
795
+
796
+ function renderChoice(step) {
797
+ var opts = step.options || [];
798
+ if (!opts.length) { add("end", "The End"); return; }
799
+ for (var i = 0; i < opts.length; i++) (function (opt, idx) {
800
+ var b = document.createElement("button");
801
+ b.className = "choice"; b.textContent = (opt.prompt && opt.prompt.text) || "(continue)";
802
+ b.disabled = opt.eligible === false;
803
+ b.style.animationDelay = (idx * 60) + "ms"; // gentle stagger so the options fade in one after another
804
+ b.onclick = function () { flow.choose(opt.id); scrollDown(); run(); };
805
+ controls.appendChild(b);
806
+ })(opts[i], i);
807
+ scrollDown();
808
+ }
809
+
810
+ document.getElementById("restart").onclick = newGame;
811
+ document.getElementById("save").onclick = function () { try { localStorage.setItem(SAVE_KEY, JSON.stringify(engine.saveGame())); flash("Saved"); } catch (e) {} };
812
+ document.getElementById("load").onclick = function () {
813
+ var blob; try { blob = localStorage.getItem(SAVE_KEY); } catch (e) {}
814
+ if (!blob) { flash("No save yet"); return; }
815
+ engine = new Engine(BUNDLE);
816
+ engine.loadGame(JSON.parse(blob)); flow = engine.getFlow("main");
817
+ stage.innerHTML = ""; add("text", "(resumed from save)"); run();
818
+ };
819
+ function flash(msg) { var f = document.getElementById("flash"); f.textContent = msg; f.style.opacity = "1"; setTimeout(function () { f.style.opacity = "0"; }, 1100); }
820
+
821
+ newGame();
822
+ })();
823
+ `;
824
+ var STYLE = String.raw`
825
+ :root { color-scheme: light; }
826
+ * { box-sizing: border-box; }
827
+ body { margin: 0; background: #f6f1e7; color: #2b2620; font: 18px/1.65 Georgia, "Times New Roman", serif; }
828
+ .wrap { max-width: 42rem; margin: 0 auto; padding: 1.5rem 1.25rem 6rem; }
829
+ header { display: flex; align-items: baseline; gap: 0.75rem; border-bottom: 1px solid #e0d7c5; padding-bottom: 0.75rem; margin-bottom: 1.25rem; }
830
+ h1 { font-size: 1.15rem; margin: 0; font-weight: 600; }
831
+ header .by { color: #9a8f78; font-size: 0.8rem; font-family: ui-sans-serif, system-ui, sans-serif; margin-left: auto; }
832
+ #stage > div { margin: 0.55rem 0; animation: fade 0.45s ease-out both; }
833
+ @keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
834
+ @media (prefers-reduced-motion: reduce) { #stage > div, .choice { animation: none; } }
835
+ .line .who { font-weight: 700; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.8rem; letter-spacing: 0.02em; text-transform: uppercase; margin-right: 0.4rem; }
836
+ .line .dir { color: #9a8f78; font-style: italic; }
837
+ .text { color: #4a4334; }
838
+ .end { color: #9a8f78; font-style: italic; text-align: center; margin-top: 1.5rem; }
839
+ #controls { margin: 1.25rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
840
+ .choice { font: 1rem Georgia, serif; text-align: left; background: #fff; border: 1px solid #d9cfbb; border-radius: 8px; padding: 0.55rem 0.8rem; cursor: pointer; transition: border-color .15s, background .15s; animation: fade 0.4s ease-out both; }
841
+ .choice:hover:not(:disabled) { border-color: #b8956a; background: #fffaf0; }
842
+ .choice:disabled { color: #b8af9b; cursor: default; }
843
+ .bar { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(246,241,231,0.94); border-top: 1px solid #e0d7c5; backdrop-filter: blur(4px); }
844
+ .bar .inner { max-width: 42rem; margin: 0 auto; padding: 0.6rem 1.25rem; display: flex; gap: 0.5rem; align-items: center; }
845
+ .bar button { font: 0.85rem ui-sans-serif, system-ui, sans-serif; background: #fff; border: 1px solid #d9cfbb; border-radius: 6px; padding: 0.3rem 0.7rem; cursor: pointer; }
846
+ .bar button:hover { border-color: #b8956a; }
847
+ .bar .speedlabel { font: 0.8rem ui-sans-serif, system-ui, sans-serif; color: #9a8f78; display: flex; align-items: center; gap: 0.3rem; }
472
848
  .bar #speed { font: 0.85rem ui-sans-serif, system-ui, sans-serif; background: #fff; border: 1px solid #d9cfbb; border-radius: 6px; padding: 0.3rem 0.4rem; cursor: pointer; color: #2b2620; }
473
849
  .bar #speed:hover { border-color: #b8956a; }
474
850
  #flash { margin-left: auto; color: #9a8f78; font: 0.8rem ui-sans-serif, system-ui, sans-serif; opacity: 0; transition: opacity .2s; }
@@ -478,7 +854,7 @@ function sourceStory(loaded) {
478
854
  const def = full.locales?.default ?? Object.keys(full.strings)[0] ?? "en";
479
855
  const bundle = { ...full, locales: { default: def, included: [def] }, strings: { [def]: full.strings[def] ?? {} } };
480
856
  const title = loaded.project.project.name?.trim() || "A Patter story";
481
- const bundleJson = canonicalStringify(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
857
+ const bundleJson = canonicalStringify2(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
482
858
  return { title, lang: def, bundleJson };
483
859
  }
484
860
  function page(title, lang, head, tail) {
@@ -607,7 +983,7 @@ function stringsByLocale(loaded) {
607
983
  }
608
984
  return byLocale;
609
985
  }
610
- function mergeAuthoring(loaded) {
986
+ function mergeAuthoring2(loaded) {
611
987
  const writing = /* @__PURE__ */ new Map();
612
988
  const recording = /* @__PURE__ */ new Map();
613
989
  const cut = /* @__PURE__ */ new Set();
@@ -1021,13 +1397,13 @@ function renderCoverageText(report, sceneName = (id) => id) {
1021
1397
 
1022
1398
  // src/format.ts
1023
1399
  import { readFileSync as readFileSync3 } from "fs";
1024
- import { canonicalStringify as canonicalStringify2, parseSource as parseSource3 } from "@patterkit/core";
1400
+ import { canonicalStringify as canonicalStringify3, parseSource as parseSource3 } from "@patterkit/core";
1025
1401
  function runFormat(files) {
1026
1402
  return files.map((file) => {
1027
1403
  const original = readFileSync3(file, "utf8");
1028
1404
  let canonical;
1029
1405
  try {
1030
- canonical = canonicalStringify2(parseSource3(original));
1406
+ canonical = canonicalStringify3(parseSource3(original));
1031
1407
  } catch (e) {
1032
1408
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
1033
1409
  }
@@ -1067,7 +1443,7 @@ function runReport(loaded, recordingOverride) {
1067
1443
  const thresholdIdx = estimatingOn ? writingIndex.get(est?.thresholdStatus ?? "") ?? 0 : -1;
1068
1444
  const defaultLines = est?.defaultLines ?? 0;
1069
1445
  const tagMap = new Map((est?.tagEstimates ?? []).map((t) => [t.tag, t.lines]));
1070
- const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, edits: editsOf } = mergeAuthoring(loaded);
1446
+ const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, edits: editsOf } = mergeAuthoring2(loaded);
1071
1447
  const recordingBase = recordingOverride ?? manualRecordingOf;
1072
1448
  const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
1073
1449
  const byLocale = stringsByLocale(loaded);
@@ -1412,7 +1788,7 @@ async function runReportXlsx(data) {
1412
1788
 
1413
1789
  // src/localisation.ts
1414
1790
  import { basename as basename4, join as join3, sep } from "path";
1415
- import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
1791
+ import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
1416
1792
  import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
1417
1793
 
1418
1794
  // src/documentation.ts
@@ -1427,7 +1803,7 @@ function classesForChannel(classes, channel) {
1427
1803
  function resolveDocumentation(loaded, channel) {
1428
1804
  const classes = loaded.project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES;
1429
1805
  const allowed = classesForChannel(classes, channel);
1430
- const docsOf = mergeAuthoring(loaded).documentation;
1806
+ const docsOf = mergeAuthoring2(loaded).documentation;
1431
1807
  const own = (id) => (docsOf.get(id) ?? []).filter((l) => l.type !== void 0 && allowed.has(l.type));
1432
1808
  const out = /* @__PURE__ */ new Map();
1433
1809
  const visit = (id, inherited) => {
@@ -1463,7 +1839,7 @@ function extractLoc(loaded, opts = {}) {
1463
1839
  const targetLocale = isTemplate ? void 0 : opts.locale;
1464
1840
  const source = tableFor(loaded, defaultLocale);
1465
1841
  const target = targetLocale ? tableFor(loaded, targetLocale) : {};
1466
- const edits = mergeAuthoring(loaded).edits;
1842
+ const edits = mergeAuthoring2(loaded).edits;
1467
1843
  const docs = resolveDocumentation(loaded, "loc");
1468
1844
  const commentsOf = (id) => (docs.get(id) ?? []).map((d) => d.text);
1469
1845
  const staleFor = (id, translation) => {
@@ -1570,7 +1946,7 @@ function applyLoc(loaded, catalog, opts = {}) {
1570
1946
  merged[id] = text;
1571
1947
  }
1572
1948
  const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
1573
- writes.push({ path, content: canonicalStringify3(file) });
1949
+ writes.push({ path, content: canonicalStringify4(file) });
1574
1950
  const fresh = bySceneFresh.get(scene);
1575
1951
  if (!fresh?.size) continue;
1576
1952
  const aPath = authoringPath(loaded, scene);
@@ -1579,7 +1955,7 @@ function applyLoc(loaded, catalog, opts = {}) {
1579
1955
  const editsMap = { ...authoring.edits ?? {} };
1580
1956
  for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
1581
1957
  authoring.edits = editsMap;
1582
- writes.push({ path: aPath, content: canonicalStringify3(authoring) });
1958
+ writes.push({ path: aPath, content: canonicalStringify4(authoring) });
1583
1959
  }
1584
1960
  return { writes, stats: { updated, files: bySceneText.size } };
1585
1961
  }
@@ -1631,1660 +2007,1347 @@ function catalogToPo(catalog) {
1631
2007
  out.push(`#: ${e.scene}`);
1632
2008
  if (e.stale) out.push("#, fuzzy");
1633
2009
  out.push(poField("msgctxt", e.id), poField("msgid", e.source), poField("msgstr", e.translation), "");
1634
- }
1635
- return out.join("\n");
1636
- }
1637
- function readQuoted(lines, start) {
1638
- const first = lines[start];
1639
- let value = poUnescape(first.slice(first.indexOf('"') + 1, first.lastIndexOf('"')));
1640
- let i = start + 1;
1641
- while (i < lines.length && lines[i].trimStart().startsWith('"')) {
1642
- const l = lines[i].trim();
1643
- value += poUnescape(l.slice(1, l.lastIndexOf('"')));
1644
- i++;
1645
- }
1646
- return { value, next: i };
1647
- }
1648
- function poToCatalog(text) {
1649
- const lines = text.split(/\r?\n/);
1650
- const entries = [];
1651
- let locale;
1652
- let project = "";
1653
- let comments = [];
1654
- let scene = "";
1655
- let stale = false;
1656
- let ctxt;
1657
- let msgid = "";
1658
- let i = 0;
1659
- const flush = (msgstr) => {
1660
- if (ctxt !== void 0) {
1661
- entries.push({ id: ctxt, scene, source: msgid, translation: msgstr, comments, stale });
1662
- } else if (msgid === "") {
1663
- const lang = /Language:\s*([^\\\n]*)/.exec(msgstr);
1664
- if (lang?.[1]?.trim()) locale = lang[1].trim();
1665
- const pid = /Project-Id-Version:\s*([^\\\n]*)/.exec(msgstr);
1666
- if (pid?.[1]?.trim()) project = pid[1].trim();
1667
- }
1668
- comments = [];
1669
- scene = "";
1670
- stale = false;
1671
- ctxt = void 0;
1672
- msgid = "";
1673
- };
1674
- while (i < lines.length) {
1675
- const line = lines[i];
1676
- const t = line.trim();
1677
- if (t === "") {
1678
- i++;
1679
- continue;
1680
- }
1681
- if (t.startsWith("#.")) {
1682
- comments.push(t.slice(2).trim());
1683
- i++;
1684
- continue;
1685
- }
1686
- if (t.startsWith("#:")) {
1687
- scene = t.slice(2).trim().split(/\s+/)[0] ?? "";
1688
- i++;
1689
- continue;
1690
- }
1691
- if (t.startsWith("#,")) {
1692
- if (t.includes("fuzzy")) stale = true;
1693
- i++;
1694
- continue;
1695
- }
1696
- if (t.startsWith("#")) {
1697
- i++;
1698
- continue;
1699
- }
1700
- if (t.startsWith("msgctxt")) {
1701
- const r = readQuoted(lines, i);
1702
- ctxt = r.value;
1703
- i = r.next;
1704
- continue;
1705
- }
1706
- if (t.startsWith("msgid")) {
1707
- const r = readQuoted(lines, i);
1708
- msgid = r.value;
1709
- i = r.next;
1710
- continue;
1711
- }
1712
- if (t.startsWith("msgstr")) {
1713
- const r = readQuoted(lines, i);
1714
- flush(r.value);
1715
- i = r.next;
1716
- continue;
1717
- }
1718
- i++;
1719
- }
1720
- return { project, defaultLocale: locale ?? "", locale, entries };
1721
- }
1722
-
1723
- // src/loc-xlsx.ts
1724
- var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender"];
1725
- var sheetName = (scene) => scene.replace(/[:\\/?*[\]]/g, "-").slice(0, 31);
1726
- async function catalogToXlsx(catalog) {
1727
- const { default: ExcelJS } = await import("exceljs");
1728
- const wb = new ExcelJS.Workbook();
1729
- wb.creator = "patter";
1730
- const byScene = /* @__PURE__ */ new Map();
1731
- for (const e of catalog.entries) (byScene.get(e.scene) ?? byScene.set(e.scene, []).get(e.scene)).push(e);
1732
- for (const [scene, entries] of byScene) {
1733
- const ws = wb.addWorksheet(sheetName(scene));
1734
- ws.columns = [
1735
- { header: "ID", key: "id", width: 22 },
1736
- { header: "Source", key: "source", width: 40 },
1737
- { header: "Translation", key: "translation", width: 40 },
1738
- { header: "Comments", key: "comments", width: 30 },
1739
- { header: "Status", key: "status", width: 10 },
1740
- { header: "Gender", key: "gender", width: 12 }
1741
- ];
1742
- ws.getRow(1).font = { bold: true };
1743
- ws.views = [{ state: "frozen", ySplit: 1 }];
1744
- for (const e of entries) {
1745
- ws.addRow({
1746
- id: e.id,
1747
- source: e.source,
1748
- translation: e.translation,
1749
- comments: e.comments.join("\n"),
1750
- status: e.stale ? "stale" : e.translation ? "translated" : "",
1751
- gender: e.context?.gender ?? ""
1752
- });
1753
- }
1754
- }
1755
- return Buffer.from(await wb.xlsx.writeBuffer());
1756
- }
1757
- async function xlsxToCatalog(buffer) {
1758
- const { default: ExcelJS } = await import("exceljs");
1759
- const wb = new ExcelJS.Workbook();
1760
- await wb.xlsx.load(buffer);
1761
- const entries = [];
1762
- for (const ws of wb.worksheets) {
1763
- const scene = ws.name;
1764
- const header = ws.getRow(1).values.slice(1).map((v) => String(v ?? ""));
1765
- if (header[0] !== HEADERS[0]) continue;
1766
- ws.eachRow((row, n) => {
1767
- if (n === 1) return;
1768
- const cell = (c) => {
1769
- const v = row.getCell(c).value;
1770
- return v == null ? "" : String(typeof v === "object" && "text" in v ? v.text : v);
1771
- };
1772
- const id = cell(1).trim();
1773
- if (!id) return;
1774
- const status = cell(5).trim().toLowerCase();
1775
- entries.push({
1776
- id,
1777
- scene,
1778
- source: cell(2),
1779
- translation: cell(3),
1780
- comments: cell(4) ? cell(4).split("\n").filter(Boolean) : [],
1781
- stale: status === "stale"
1782
- });
1783
- });
1784
- }
1785
- return { project: "", defaultLocale: "", locale: void 0, entries };
1786
- }
1787
-
1788
- // src/voice-script.ts
1789
- import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES2 } from "@patterkit/model";
1790
- function plainVoice(text) {
1791
- return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
1792
- }
1793
- function groupLabel(node, source) {
1794
- if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
1795
- if (node.selector === "choice") return "";
1796
- return node.selector ?? "group";
1797
- }
1798
- function runVoiceScript(loaded, opts = {}) {
1799
- const { project } = loaded;
1800
- const everything = !!opts.everything;
1801
- const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
1802
- const writingLadder = ladderDecls.map((s) => s.name);
1803
- const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3).map((s) => s.name);
1804
- const stub = writingLadder[0];
1805
- const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
1806
- const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
1807
- const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring(loaded);
1808
- const recordingBase = opts.recordingOverride ?? manualRecordingOf;
1809
- const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
1810
- const source = sourceStrings(loaded);
1811
- const actorOf = /* @__PURE__ */ new Map();
1812
- for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
1813
- const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, "vo");
1814
- const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
1815
- const lines = [];
1816
- const walkNode = (node, scope, ancestorVo) => {
1817
- if (node.type === "group") {
1818
- if (cutSet.has(node.id)) return;
1819
- const label = groupLabel(node, source);
1820
- const childScope = label ? [...scope, label] : scope;
1821
- const childVo = [...ancestorVo, ...ownVo(node.id)];
1822
- for (const child of node.children ?? []) walkNode(child, childScope, childVo);
1823
- return;
1824
- }
1825
- if (cutSet.has(node.id)) return;
1826
- let leading = true;
1827
- for (const beat of node.beats ?? []) {
1828
- if (beat.kind !== "line" || cutSet.has(beat.id)) continue;
1829
- const ws = writingOf.get(beat.id) ?? stub;
1830
- if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) continue;
1831
- const own = ownVo(beat.id);
1832
- lines.push({
1833
- scope: scope.join(" \u203A "),
1834
- id: beat.id,
1835
- character: beat.character ?? "",
1836
- actor: beat.character ? actorOf.get(beat.character) : void 0,
1837
- text: plainVoice(source[beat.id] ?? ""),
1838
- comments: leading ? [...ancestorVo, ...own] : own,
1839
- // first line of the run gets the enclosing context
1840
- recordingStatus: recordingOf(beat.id)
1841
- });
1842
- leading = false;
1843
- }
1844
- };
1845
- for (const scene of loaded.scenes) {
1846
- if (cutSet.has(scene.id)) continue;
1847
- const sceneVo = ownVo(scene.id);
1848
- for (const block of scene.blocks) {
1849
- if (cutSet.has(block.id)) continue;
1850
- const scope = [scene.name, block.name].filter(Boolean);
1851
- const blockVo = [...sceneVo, ...ownVo(block.id)];
1852
- for (const child of block.children ?? []) walkNode(child, scope, blockVo);
1853
- }
1854
- }
1855
- return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
1856
- }
1857
-
1858
- // src/voice-script-xlsx.ts
1859
- async function voiceScriptToXlsx(data) {
1860
- const { default: ExcelJS } = await import("exceljs");
1861
- const wb = new ExcelJS.Workbook();
1862
- wb.creator = "patter";
1863
- const ws = wb.addWorksheet("Voice Script");
1864
- ws.columns = [
1865
- { header: "Scope", key: "scope", width: 28 },
1866
- { header: "Line ID", key: "id", width: 18 },
1867
- { header: "Character", key: "character", width: 16 },
1868
- { header: "Actor", key: "actor", width: 16 },
1869
- { header: "Text", key: "text", width: 60 },
1870
- { header: "Comments", key: "comments", width: 36 },
1871
- { header: "Status", key: "status", width: 12 }
1872
- ];
1873
- ws.getRow(1).font = { bold: true };
1874
- ws.views = [{ state: "frozen", ySplit: 1 }];
1875
- for (const l of data.lines) {
1876
- const row = ws.addRow({
1877
- scope: l.scope,
1878
- id: l.id,
1879
- character: l.character,
1880
- actor: l.actor ?? "",
1881
- text: l.text,
1882
- comments: l.comments.join("\n"),
1883
- status: l.recordingStatus
1884
- });
1885
- row.getCell("text").alignment = { wrapText: true, vertical: "top" };
1886
- row.getCell("comments").alignment = { wrapText: true, vertical: "top" };
1887
- }
1888
- return Buffer.from(await wb.xlsx.writeBuffer());
1889
- }
1890
-
1891
- // src/script-doc.ts
1892
- var TOKENS = {
1893
- ink: "26221c",
1894
- // near-black warm ink (headings)
1895
- inkRead: "3a352d",
1896
- // dialogue body (ink ~84% + muted)
1897
- inkSoft: "5c554b",
1898
- // narration (ink ~68% + muted) - a step softer than dialogue
1899
- muted: "8c8479",
1900
- // labels, quiet tags
1901
- accent: "8a5a34",
1902
- // structural marks: conditions, options, jumps, game events
1903
- line: "e4dfd5"
1904
- // hairlines / rules / the group rail
1905
- };
1906
- var CHAR_PALETTE = [
1907
- "b23b3b",
1908
- "b5642b",
1909
- "927524",
1910
- "6b7a31",
1911
- "3f7d49",
1912
- "2c7d6e",
1913
- "2d7791",
1914
- "3a6aa8",
1915
- "5a5ba6",
1916
- "7e4e9c",
1917
- "9c4486",
1918
- "a83e5c"
1919
- ];
1920
- function colourIndex(name) {
1921
- let h = 2166136261;
1922
- for (let i = 0; i < name.length; i++) {
1923
- h ^= name.charCodeAt(i);
1924
- h = Math.imul(h, 16777619);
1925
- }
1926
- h ^= h >>> 16;
1927
- h = Math.imul(h, 2246822507);
1928
- h ^= h >>> 13;
1929
- h = Math.imul(h, 3266489909);
1930
- h ^= h >>> 16;
1931
- return (h >>> 0) % 12;
1932
- }
1933
- function characterColour(name) {
1934
- return CHAR_PALETTE[colourIndex(name)];
1935
- }
1936
- var VISIT_FN_RE = /\b(patter_visits|patter_seen|visits|seen)\s*\(\s*(['"])(.*?)\2\s*\)/g;
1937
- function humanizeCondition(cond, label) {
1938
- return cond.replace(VISIT_FN_RE, (_m, fn, _q, id) => `${fn}(${label(id)})`);
1939
- }
1940
- var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
1941
- var INTERP_RE = /\{@[^}]*\}/g;
1942
- function textRuns(text) {
1943
- const marked = [];
1944
- let last = 0;
1945
- let m;
1946
- MARKUP_RE.lastIndex = 0;
1947
- while ((m = MARKUP_RE.exec(text)) !== null) {
1948
- if (m.index > last) marked.push({ text: text.slice(last, m.index), bold: false, italic: false });
1949
- const tag = m[1];
1950
- marked.push({ text: m[2], bold: tag === "b" || tag === "bi", italic: tag === "i" || tag === "bi" });
1951
- last = MARKUP_RE.lastIndex;
1952
- }
1953
- if (last < text.length) marked.push({ text: text.slice(last), bold: false, italic: false });
1954
- if (!marked.length) marked.push({ text, bold: false, italic: false });
1955
- const out = [];
1956
- for (const r of marked) {
1957
- let li = 0;
1958
- let im;
1959
- INTERP_RE.lastIndex = 0;
1960
- while ((im = INTERP_RE.exec(r.text)) !== null) {
1961
- if (im.index > li) out.push({ text: r.text.slice(li, im.index), bold: r.bold, italic: r.italic, code: false });
1962
- out.push({ text: im[0], bold: r.bold, italic: r.italic, code: true });
1963
- li = INTERP_RE.lastIndex;
1964
- }
1965
- if (li < r.text.length) out.push({ text: r.text.slice(li), bold: r.bold, italic: r.italic, code: false });
1966
- }
1967
- return out.length ? out : [{ text, bold: false, italic: false, code: false }];
1968
- }
1969
- function targetLabel(to, sceneOf, blockTrail) {
1970
- if (to === "END") return "END";
1971
- return blockTrail.get(to) ?? sceneOf.get(to) ?? to;
1972
- }
1973
- function gameEventLabel(beat) {
1974
- const gd = beat.gameData;
1975
- if (gd && typeof gd === "object") {
1976
- const parts = Object.entries(gd).map(([k, v]) => {
1977
- const val = v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
1978
- return val === "" ? k : `${k}: ${val}`;
1979
- });
1980
- if (parts.length) return `game event \xB7 ${parts.join(", ")}`;
1981
- }
1982
- return "game event";
1983
- }
1984
- function optionTag(node) {
1985
- const parts = [];
1986
- if (node.fallback) parts.push("fallback");
1987
- parts.push(node.sticky ? "repeatable" : "once only");
1988
- if (node.secretUntilEligible) parts.push("hidden");
1989
- return parts.join(" \xB7 ");
1990
- }
1991
- function sequenceLabel(node) {
1992
- const o = node.options ?? {};
1993
- const bits = [];
1994
- if ((o.order ?? "sequential") === "shuffle") bits.push("shuffled");
1995
- const ex = o.exhaust ?? "once";
1996
- if (ex === "repeat") bits.push("repeating");
1997
- else if (ex === "stick") bits.push("holds on the last");
1998
- return bits.length ? `In sequence \xB7 ${bits.join(", ")}` : "In sequence";
1999
- }
2000
- function runScriptDoc(loaded) {
2001
- const { project } = loaded;
2002
- const source = sourceStrings(loaded);
2003
- const { cut } = mergeAuthoring(loaded);
2004
- const sceneOf = /* @__PURE__ */ new Map();
2005
- const blockTrail = /* @__PURE__ */ new Map();
2006
- const blockName = /* @__PURE__ */ new Map();
2007
- for (const scene of loaded.scenes) {
2008
- sceneOf.set(scene.id, scene.name);
2009
- for (const block of scene.blocks) {
2010
- blockTrail.set(block.id, `${scene.name} \u203A ${block.name}`);
2011
- blockName.set(block.id, block.name);
2012
- }
2013
- }
2014
- const humanize = (cond) => humanizeCondition(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
2015
- const els = [];
2016
- const textOf = (id) => source[id] ?? "";
2017
- const jumpText = (j) => {
2018
- const dest = targetLabel(j.to, sceneOf, blockTrail);
2019
- return j.mode === "call" ? `${dest} (and return)` : dest;
2020
- };
2021
- let snippetSeq = 0;
2022
- const nextSid = () => ++snippetSeq;
2023
- const emitBeats = (node, indent, sid) => {
2024
- for (const beat of node.beats ?? []) {
2025
- if (cut.has(beat.id)) continue;
2026
- if (beat.kind === "line") els.push({ kind: "line", indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
2027
- else if (beat.kind === "text") els.push({ kind: "narration", indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
2028
- else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent, snippet: sid, text: gameEventLabel(beat) });
2029
- }
2030
- if (node.jump) els.push({ kind: "jump", indent, snippet: sid, text: jumpText(node.jump) });
2031
- };
2032
- const walk = (node, indent, sid) => {
2033
- if (cut.has(node.id)) return;
2034
- if (node.type === "snippet") {
2035
- if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
2036
- emitBeats(node, indent, sid);
2037
- return;
2038
- }
2039
- if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
2040
- if (node.selector === "choice") {
2041
- els.push({ kind: "group", indent, label: "Choose" });
2042
- for (const child of node.children ?? []) {
2043
- if (cut.has(child.id)) continue;
2044
- const cid = nextSid();
2045
- if (child.type === "group" && child.prompt) {
2046
- if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
2047
- els.push({ kind: "option", indent, snippet: cid, runs: textRuns(textOf(child.prompt.id) || "(option)"), tag: optionTag(child) });
2048
- for (const c of child.children ?? []) walk(c, indent + 1, cid);
2049
- } else if (child.type === "snippet") {
2050
- const beats = child.beats ?? [];
2051
- const first = beats.find((b) => b.kind === "line" || b.kind === "text");
2052
- if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
2053
- els.push({ kind: "option", indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child) });
2054
- let seenPrompt = false;
2055
- for (const beat of beats) {
2056
- if (!seenPrompt && beat === first) {
2057
- seenPrompt = true;
2058
- continue;
2059
- }
2060
- if (cut.has(beat.id)) continue;
2061
- if (beat.kind === "line") els.push({ kind: "line", indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
2062
- else if (beat.kind === "text") els.push({ kind: "narration", indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
2063
- else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
2064
- }
2065
- if (child.jump) els.push({ kind: "jump", indent: indent + 1, snippet: cid, text: jumpText(child.jump) });
2066
- } else {
2067
- walk(child, indent + 1, cid);
2068
- }
2069
- }
2070
- return;
2071
- }
2072
- if (node.selector === "branch") {
2073
- els.push({ kind: "group", indent, label: "One of \xB7 first match wins" });
2074
- const kids = (node.children ?? []).filter((c) => !cut.has(c.id));
2075
- kids.forEach((child, i) => {
2076
- const isCatchAll = i === kids.length - 1 && kids.length > 1 && !("condition" in child && child.condition);
2077
- if (isCatchAll) els.push({ kind: "else", indent });
2078
- walk(child, indent, nextSid());
2079
- });
2080
- return;
2081
- }
2082
- if (node.selector === "sequence") {
2083
- els.push({ kind: "group", indent, label: sequenceLabel(node) });
2084
- for (const child of node.children ?? []) walk(child, indent, nextSid());
2085
- return;
2086
- }
2087
- for (const child of node.children ?? []) walk(child, indent, sid);
2088
- };
2089
- for (const scene of loaded.scenes) {
2090
- if (cut.has(scene.id)) continue;
2091
- els.push({ kind: "scene", text: scene.name });
2092
- for (const block of scene.blocks) {
2093
- if (cut.has(block.id)) continue;
2094
- els.push({ kind: "block", text: block.name });
2095
- for (const child of block.children ?? []) walk(child, 0, void 0);
2096
- }
2097
- }
2098
- return { project: project.project.name, elements: els };
2099
- }
2100
-
2101
- // src/script-docx.ts
2102
- import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, TabStopType, TextRun } from "docx";
2103
- var SERIF = "Georgia";
2104
- var SANS = "Calibri";
2105
- var MONO = "Consolas";
2106
- var STEP = 360;
2107
- var SNIPPET_INSET = 220;
2108
- var DIALOGUE_INDENT = 300;
2109
- var HANG = 340;
2110
- var RIGHT_TAB = 9020;
2111
- var SNIP_GAP = 170;
2112
- var WITHIN = 30;
2113
- var S = { h1: 32, h2: 26, body: 22, cue: 18, label: 16, tag: 15, mech: 18 };
2114
- function bodyRuns(runs, color, base = {}) {
2115
- return runs.map((r) => r.code ? new TextRun({ text: r.text, font: MONO, color: TOKENS.accent, size: S.body - 2 }) : new TextRun({ text: r.text, font: SERIF, color, size: S.body, bold: base.bold || r.bold || void 0, italics: base.italic || r.italic || void 0 }));
2116
- }
2117
- var leftOf = (el) => ("indent" in el ? el.indent * STEP : 0) + ("snippet" in el && el.snippet !== void 0 ? SNIPPET_INSET : 0);
2118
- function paragraph(el, prev) {
2119
- const sid = "snippet" in el ? el.snippet : void 0;
2120
- const startsSnippet = sid !== void 0 && (!prev || !("snippet" in prev) || prev.snippet !== sid);
2121
- const before = sid !== void 0 ? startsSnippet ? SNIP_GAP : WITHIN : 100;
2122
- switch (el.kind) {
2123
- case "scene":
2124
- return new Paragraph({
2125
- heading: HeadingLevel.HEADING_1,
2126
- keepNext: true,
2127
- spacing: { before: 480, after: 200 },
2128
- border: { bottom: { style: BorderStyle.SINGLE, size: 14, color: TOKENS.accent, space: 6 } },
2129
- children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h1 })]
2130
- });
2131
- case "block":
2132
- return new Paragraph({
2133
- heading: HeadingLevel.HEADING_2,
2134
- keepNext: true,
2135
- spacing: { before: 360, after: 160 },
2136
- border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: TOKENS.line, space: 4 } },
2137
- children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h2 })]
2138
- });
2139
- case "line": {
2140
- const kids = [];
2141
- if (el.character) kids.push(new TextRun({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
2142
- if (el.direction) kids.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
2143
- kids.push(...bodyRuns(el.runs, TOKENS.inkRead));
2144
- return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids });
2145
- }
2146
- case "narration":
2147
- return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: bodyRuns(el.runs, TOKENS.inkSoft) });
2148
- case "condition":
2149
- return new Paragraph({ keepNext: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: [new TextRun({ text: `\u2039 ${el.text} \u203A`, font: MONO, color: TOKENS.accent, size: S.mech - 1 })] });
2150
- case "group":
2151
- return new Paragraph({ keepNext: true, spacing: { before: 260, after: 60 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: el.label.toUpperCase(), font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: S.label, characterSpacing: 14 })] });
2152
- case "else":
2153
- return new Paragraph({ keepNext: true, spacing: { before: 140, after: 20 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: "else \xB7 catch-all", font: SANS, allCaps: true, color: TOKENS.muted, size: S.label, characterSpacing: 12 })] });
2154
- case "option": {
2155
- const kids = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
2156
- if (el.tag) kids.push(new TextRun({ text: " ", size: S.body }), new TextRun({ text: el.tag, font: SANS, smallCaps: true, color: TOKENS.muted, size: S.tag }));
2157
- return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + HANG, hanging: HANG }, tabStops: el.tag ? [{ type: TabStopType.RIGHT, position: RIGHT_TAB }] : void 0, children: kids });
2158
- }
2159
- case "jump":
2160
- return new Paragraph({ keepLines: true, alignment: AlignmentType.RIGHT, spacing: { before: 40, after: 40 }, children: [new TextRun({ text: `\u21AA ${el.text}`, font: SANS, bold: true, color: TOKENS.accent, size: S.mech })] });
2161
- case "gameEvent":
2162
- return new Paragraph({ keepLines: true, alignment: AlignmentType.RIGHT, spacing: { before: 40, after: 40 }, children: [new TextRun({ text: `\u2699 ${el.text}`, font: MONO, color: TOKENS.accent, size: S.mech - 2 })] });
2163
- }
2164
- }
2165
- async function scriptToDocx(doc) {
2166
- const body = [
2167
- new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: "Readable script", font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: 16, characterSpacing: 30 })] }),
2168
- new Paragraph({ spacing: { after: 560 }, children: [new TextRun({ text: doc.project, font: SERIF, bold: true, color: TOKENS.ink, size: 52 })] }),
2169
- ...doc.elements.map((el, i) => paragraph(el, doc.elements[i - 1]))
2170
- ];
2171
- const document = new Document({ creator: "Patter", title: doc.project, sections: [{ children: body }] });
2172
- return Packer.toBuffer(document);
2173
- }
2174
-
2175
- // src/script-pdf.ts
2176
- import PDFDocument from "pdfkit";
2177
- var MARGIN = 64;
2178
- var INDENT_STEP = 20;
2179
- var EDGE_INSET = 12;
2180
- var EDGE_OFF = 3;
2181
- var DIALOGUE_INDENT2 = 14;
2182
- var HANG2 = 18;
2183
- var CUE_GAP = 7;
2184
- var BODY = 11;
2185
- var CUE = 8.5;
2186
- var LEAD = 3.5;
2187
- var WITHIN2 = 4;
2188
- var BETWEEN = 10;
2189
- var BEAT = 6;
2190
- var TIGHT = 3;
2191
- var hex = (h) => `#${h}`;
2192
- var INK = hex(TOKENS.ink);
2193
- var INK_READ = hex(TOKENS.inkRead);
2194
- var INK_SOFT = hex(TOKENS.inkSoft);
2195
- var MUTED = hex(TOKENS.muted);
2196
- var ACCENT = hex(TOKENS.accent);
2197
- var LINE = hex(TOKENS.line);
2198
- var EDGE = "#d7d0c3";
2199
- var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Serif-Bold" : italic ? "Serif-Italic" : "Serif";
2200
- var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
2201
- async function scriptToPdf(doc) {
2202
- const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
2203
- return new Promise((resolve5, reject) => {
2204
- const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
2205
- pdf.registerFont("Serif", scriptFont("serif"));
2206
- pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
2207
- pdf.registerFont("Serif-Italic", scriptFont("serifItalic"));
2208
- pdf.registerFont("Serif-BoldItalic", scriptFont("serifBoldItalic"));
2209
- pdf.registerFont("Sans", scriptFont("sans"));
2210
- pdf.registerFont("Sans-Bold", scriptFont("sansBold"));
2211
- pdf.registerFont("Mono", scriptFont("mono"));
2212
- pdf.registerFont("Symbol", scriptFont("symbol"));
2213
- pdf.registerFont("Emoji", scriptFont("emoji"));
2214
- const coverage = {};
2215
- const ascender = {};
2216
- for (const name of ["Serif", "Serif-Bold", "Serif-Italic", "Serif-BoldItalic", "Sans", "Sans-Bold", "Mono"]) {
2217
- pdf.font(name);
2218
- const f = pdf._font;
2219
- coverage[name] = (cp) => f.font.hasGlyphForCodePoint(cp);
2220
- ascender[name] = f.ascender;
2221
- }
2222
- const ascentPt = (name, size) => ascender[name] / 1e3 * size;
2223
- const split = (text, primary, color) => {
2224
- const cov = coverage[primary];
2225
- const out = [];
2226
- let buf = "";
2227
- let cur = primary;
2228
- for (const ch of text) {
2229
- const cp = ch.codePointAt(0);
2230
- const f = isEmoji(cp) ? "Emoji" : cov(cp) ? primary : "Symbol";
2231
- if (f !== cur) {
2232
- if (buf) out.push({ text: buf, font: cur, color });
2233
- buf = "";
2234
- cur = f;
2235
- }
2236
- buf += ch;
2237
- }
2238
- if (buf) out.push({ text: buf, font: cur, color });
2239
- return out;
2240
- };
2241
- 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));
2242
- const chunks = [];
2243
- pdf.on("data", (c) => chunks.push(c));
2244
- pdf.on("end", () => resolve5(Buffer.concat(chunks)));
2245
- pdf.on("error", reject);
2246
- const pageRight = pdf.page.width - MARGIN;
2247
- const pageBottom = pdf.page.height - MARGIN;
2248
- const contentW = pdf.page.width - MARGIN * 2;
2249
- const ensure = (space) => {
2250
- if (pdf.y > pageBottom - space) pdf.addPage();
2251
- };
2252
- const draw = (pieces, x, opts) => {
2253
- pieces.forEach((p, i) => {
2254
- pdf.font(p.font).fillColor(p.color);
2255
- const cont = i < pieces.length - 1;
2256
- if (i === 0) pdf.text(p.text, x, pdf.y, { ...opts, continued: cont });
2257
- else pdf.text(p.text, { continued: cont });
2258
- });
2259
- };
2260
- const drawRight = (pieces, size) => {
2261
- let total = 0;
2262
- for (const p of pieces) {
2263
- pdf.font(p.font).fontSize(size);
2264
- total += pdf.widthOfString(p.text);
2265
- }
2266
- const x = Math.max(MARGIN, pageRight - total);
2267
- pieces.forEach((p, i) => {
2268
- pdf.font(p.font).fillColor(p.color).fontSize(size);
2269
- const cont = i < pieces.length - 1;
2270
- if (i === 0) pdf.text(p.text, x, pdf.y, { continued: cont });
2271
- else pdf.text(p.text, { continued: cont });
2272
- });
2273
- };
2274
- pdf.fontSize(9);
2275
- draw(split("READABLE SCRIPT", "Sans-Bold", ACCENT), MARGIN, { characterSpacing: 2 });
2276
- pdf.moveDown(0.4).fontSize(26);
2277
- draw(split(doc.project, "Serif-Bold", INK), MARGIN, { width: contentW });
2278
- pdf.strokeColor(ACCENT).lineWidth(2).moveTo(MARGIN, pdf.y + 10).lineTo(MARGIN + 40, pdf.y + 10).stroke();
2279
- pdf.y += 28;
2280
- const drawContent = (el, cx) => {
2281
- const w = pageRight - cx;
2282
- switch (el.kind) {
2283
- case "scene":
2284
- pdf.fontSize(17);
2285
- draw(split(el.text, "Serif-Bold", INK), MARGIN, { width: contentW });
2286
- pdf.strokeColor(ACCENT).lineWidth(1.5).moveTo(MARGIN, pdf.y + 4).lineTo(MARGIN + 34, pdf.y + 4).stroke();
2287
- break;
2288
- case "block":
2289
- pdf.fontSize(13);
2290
- draw(split(el.text, "Serif-Bold", INK), MARGIN, { width: contentW });
2291
- pdf.strokeColor(LINE).lineWidth(0.75).moveTo(MARGIN, pdf.y + 3).lineTo(pageRight, pdf.y + 3).stroke();
2292
- break;
2293
- case "line": {
2294
- const cueX = cx + DIALOGUE_INDENT2;
2295
- let bodyX = cueX;
2296
- if (el.character) {
2297
- pdf.font("Sans-Bold").fontSize(CUE);
2298
- bodyX = cueX + pdf.widthOfString(el.character.toUpperCase()) + CUE_GAP;
2299
- }
2300
- const yTop = pdf.y;
2301
- if (el.character) {
2302
- pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character))).text(el.character.toUpperCase(), cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
2303
- pdf.y = yTop;
2304
- }
2305
- pdf.fontSize(BODY);
2306
- draw([...el.direction ? split(`(${el.direction}) `, "Serif-Italic", MUTED) : [], ...bodyPieces(el.runs, INK_READ)], bodyX, { width: Math.max(120, pageRight - bodyX), lineGap: LEAD });
2307
- break;
2308
- }
2309
- case "narration":
2310
- pdf.fontSize(BODY);
2311
- draw(bodyPieces(el.runs, INK_SOFT), cx, { width: w, lineGap: LEAD });
2312
- break;
2313
- case "condition":
2314
- pdf.fontSize(9.5);
2315
- draw(split(`\u2039 ${el.text} \u203A`, "Mono", ACCENT), cx, { width: w });
2316
- break;
2317
- case "group":
2318
- pdf.fontSize(8);
2319
- draw(split(el.label.toUpperCase(), "Sans-Bold", ACCENT), cx, { width: w, characterSpacing: 1.2 });
2320
- break;
2321
- case "else":
2322
- pdf.fontSize(8);
2323
- draw(split("ELSE \xB7 CATCH-ALL", "Sans", MUTED), cx, { width: w, characterSpacing: 1 });
2324
- break;
2325
- case "option": {
2326
- const ox = cx + HANG2;
2327
- const tag = el.tag ? el.tag.toUpperCase() : "";
2328
- const tagW = tag ? pdf.font("Sans").fontSize(7.5).widthOfString(tag) + 8 : 0;
2329
- const yStart = pdf.y;
2330
- if (tag) {
2331
- pdf.font("Sans").fontSize(7.5).fillColor(MUTED).text(tag, pageRight - tagW, yStart, { width: tagW, align: "right", lineBreak: false });
2332
- pdf.y = yStart;
2333
- }
2334
- pdf.fontSize(BODY);
2335
- draw([...split("\u25C7 ", "Serif", ACCENT), ...bodyPieces(el.runs, INK)], ox, { width: Math.max(80, pageRight - ox - tagW), indent: -HANG2, lineGap: LEAD });
2336
- break;
2337
- }
2338
- case "jump":
2339
- drawRight(split(`\u21AA ${el.text}`, "Sans-Bold", ACCENT), 9.5);
2340
- break;
2341
- case "gameEvent": {
2342
- const pieces = split(`\u2699 ${el.text}`, "Mono", ACCENT);
2343
- pdf.fontSize(9);
2344
- let total = 0;
2345
- for (const p of pieces) {
2346
- pdf.font(p.font);
2347
- total += pdf.widthOfString(p.text);
2348
- }
2349
- if (total <= pageRight - cx) drawRight(pieces, 9);
2350
- else draw(pieces, cx, { width: pageRight - cx });
2351
- break;
2352
- }
2353
- }
2354
- };
2355
- const leadOf = (k) => k === "scene" ? 24 : k === "block" ? 16 : k === "group" ? 13 : k === "else" ? 9 : 0;
2356
- const els = doc.elements;
2357
- let curSid;
2358
- let curBase = 0;
2359
- for (let i = 0; i < els.length; i++) {
2360
- const el = els[i];
2361
- const next = els[i + 1];
2362
- const sid = "snippet" in el ? el.snippet : void 0;
2363
- const sameNext = sid !== void 0 && next !== void 0 && "snippet" in next && next.snippet === sid;
2364
- if (el.kind === "scene") ensure(90);
2365
- else if (el.kind === "block") ensure(70);
2366
- else ensure(leadOf(el.kind) + BODY + LEAD + 14);
2367
- pdf.y += leadOf(el.kind);
2368
- const base = "indent" in el ? MARGIN + el.indent * INDENT_STEP : MARGIN;
2369
- const cx = base + (sid !== void 0 ? EDGE_INSET : 0);
2370
- const yTop = pdf.y;
2371
- const pageBefore = pdf.page;
2372
- drawContent(el, cx);
2373
- const bottom = pdf.y;
2374
- const trail = el.kind === "condition" ? TIGHT : el.kind === "scene" ? 12 : el.kind === "block" ? 11 : el.kind === "group" ? 7 : el.kind === "else" ? 4 : sid !== void 0 ? sameNext ? WITHIN2 : BETWEEN : BEAT;
2375
- if (sid !== void 0) {
2376
- if (sid !== curSid) {
2377
- curSid = sid;
2378
- curBase = "indent" in el ? el.indent : 0;
2379
- }
2380
- const ex = MARGIN + curBase * INDENT_STEP + EDGE_OFF;
2381
- const top = pdf.page === pageBefore ? yTop : MARGIN;
2382
- pdf.strokeColor(EDGE).lineWidth(0.75).moveTo(ex, top).lineTo(ex, bottom + (sameNext ? trail : 0)).stroke();
2383
- }
2384
- pdf.y = bottom + trail;
2385
- }
2386
- pdf.end();
2387
- });
2010
+ }
2011
+ return out.join("\n");
2388
2012
  }
2389
-
2390
- // src/init.ts
2391
- import { join as join4, basename as basename5, resolve as resolve3 } from "path";
2392
- import { readdirSync as readdirSync3, existsSync as existsSync2 } from "fs";
2393
- import { newId, slug, canonicalStringify as canonicalStringify4 } from "@patterkit/core";
2394
- function runInit(opts) {
2395
- const dir = resolve3(opts.dir);
2396
- const name = opts.name?.trim() || basename5(dir).replace(/\.patter$/, "");
2397
- const fileSlug = slug(name);
2398
- let existing = [];
2399
- try {
2400
- existing = readdirSync3(dir).filter((f) => f.endsWith(".patterproj"));
2401
- } catch {
2013
+ function readQuoted(lines, start) {
2014
+ const first = lines[start];
2015
+ let value = poUnescape(first.slice(first.indexOf('"') + 1, first.lastIndexOf('"')));
2016
+ let i = start + 1;
2017
+ while (i < lines.length && lines[i].trimStart().startsWith('"')) {
2018
+ const l = lines[i].trim();
2019
+ value += poUnescape(l.slice(1, l.lastIndexOf('"')));
2020
+ i++;
2402
2021
  }
2403
- if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
2404
- const sceneId = newId("scn");
2405
- const beatId = newId("T");
2406
- const project = {
2407
- schema: "patter/project@0",
2408
- project: { id: newId("proj"), name },
2409
- locales: { default: "en", all: ["en"] },
2410
- ...opts.vcs ? { vcs: opts.vcs } : {},
2411
- // record the chosen VCS so it can be read / switched later
2412
- voiced: true
2413
- };
2414
- const flow = {
2415
- schema: "patter/flow@0",
2416
- scene: {
2417
- id: sceneId,
2418
- type: "scene",
2419
- name: "Start",
2420
- blocks: [{
2421
- id: newId("blk"),
2422
- type: "block",
2423
- name: "Main",
2424
- children: [{
2425
- id: newId("sn"),
2426
- type: "snippet",
2427
- beats: [{ id: beatId, kind: "text" }],
2428
- jump: { to: "END" }
2429
- }]
2430
- }]
2022
+ return { value, next: i };
2023
+ }
2024
+ function poToCatalog(text) {
2025
+ const lines = text.split(/\r?\n/);
2026
+ const entries = [];
2027
+ let locale;
2028
+ let project = "";
2029
+ let comments = [];
2030
+ let scene = "";
2031
+ let stale = false;
2032
+ let ctxt;
2033
+ let msgid = "";
2034
+ let i = 0;
2035
+ const flush = (msgstr) => {
2036
+ if (ctxt !== void 0) {
2037
+ entries.push({ id: ctxt, scene, source: msgid, translation: msgstr, comments, stale });
2038
+ } else if (msgid === "") {
2039
+ const lang = /Language:\s*([^\\\n]*)/.exec(msgstr);
2040
+ if (lang?.[1]?.trim()) locale = lang[1].trim();
2041
+ const pid = /Project-Id-Version:\s*([^\\\n]*)/.exec(msgstr);
2042
+ if (pid?.[1]?.trim()) project = pid[1].trim();
2431
2043
  }
2044
+ comments = [];
2045
+ scene = "";
2046
+ stale = false;
2047
+ ctxt = void 0;
2048
+ msgid = "";
2432
2049
  };
2433
- const locale = {
2434
- schema: "patter/strings@0",
2435
- scene: sceneId,
2436
- locale: "en",
2437
- default: true,
2438
- // The starter line is STORY content the writer replaces, so it must read the same however the
2439
- // project was made: no file paths, no commands. (Front-ends give their own next step - `patter init`
2440
- // prints "try: patter play <dir>" on the terminal, and Patterpad just opens the scene for editing.)
2441
- strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
2442
- };
2443
- const bundle = opts.bundle ?? "commit";
2444
- const projectFile = join4(dir, `${fileSlug}.patterproj`);
2445
- const writes = [
2446
- { path: projectFile, content: canonicalStringify4(project) },
2447
- { path: join4(dir, "scenes", "start.patterflow"), content: canonicalStringify4(flow) },
2448
- { path: join4(dir, "loc", "en", "start.patterloc"), content: canonicalStringify4(locale) },
2449
- { path: join4(dir, ".editorconfig"), content: EDITORCONFIG },
2450
- ...vcsConfigWrites(dir, opts.vcs, bundle)
2451
- ];
2452
- const collisions = writes.map((w) => w.path).filter((p) => existsSync2(p));
2453
- if (collisions.length > 0) {
2454
- throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
2050
+ while (i < lines.length) {
2051
+ const line = lines[i];
2052
+ const t = line.trim();
2053
+ if (t === "") {
2054
+ i++;
2055
+ continue;
2056
+ }
2057
+ if (t.startsWith("#.")) {
2058
+ comments.push(t.slice(2).trim());
2059
+ i++;
2060
+ continue;
2061
+ }
2062
+ if (t.startsWith("#:")) {
2063
+ scene = t.slice(2).trim().split(/\s+/)[0] ?? "";
2064
+ i++;
2065
+ continue;
2066
+ }
2067
+ if (t.startsWith("#,")) {
2068
+ if (t.includes("fuzzy")) stale = true;
2069
+ i++;
2070
+ continue;
2071
+ }
2072
+ if (t.startsWith("#")) {
2073
+ i++;
2074
+ continue;
2075
+ }
2076
+ if (t.startsWith("msgctxt")) {
2077
+ const r = readQuoted(lines, i);
2078
+ ctxt = r.value;
2079
+ i = r.next;
2080
+ continue;
2081
+ }
2082
+ if (t.startsWith("msgid")) {
2083
+ const r = readQuoted(lines, i);
2084
+ msgid = r.value;
2085
+ i = r.next;
2086
+ continue;
2087
+ }
2088
+ if (t.startsWith("msgstr")) {
2089
+ const r = readQuoted(lines, i);
2090
+ flush(r.value);
2091
+ i = r.next;
2092
+ continue;
2093
+ }
2094
+ i++;
2455
2095
  }
2456
- return { writes, projectFile, name };
2457
- }
2458
- function vcsConfigWrites(dir, vcs, bundle = "commit") {
2459
- const writes = [{ path: join4(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
2460
- if (vcs === "git") writes.push({ path: join4(dir, ".gitattributes"), content: GITATTRIBUTES });
2461
- const ignore = ignoreFileFor(vcs);
2462
- if (ignore) writes.push({ path: join4(dir, ignore), content: ignoreContent(bundle) });
2463
- return writes;
2096
+ return { project, defaultLocale: locale ?? "", locale, entries };
2464
2097
  }
2465
- var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
2466
- root = true
2467
-
2468
- [*]
2469
- charset = utf-8
2470
- end_of_line = lf
2471
- insert_final_newline = true
2472
-
2473
- [*.{patterflow,patterloc,patterx,patterproj}]
2474
- indent_style = space
2475
- indent_size = 2
2476
- `;
2477
- var GITATTRIBUTES = `# Patter source is UTF-8 + LF text (pinned; never let autocrlf touch it).
2478
- *.patterflow text eol=lf
2479
- *.patterloc text eol=lf
2480
- *.patterx text eol=lf
2481
- *.patterproj text eol=lf
2482
-
2483
- # Id-keyed structured merge for Patter source (the 'patter merge' driver).
2484
- # Register it once per clone (see vcs-setup.md); until then git falls back to a
2485
- # normal text merge for these - no harm, just not structure-aware.
2486
- *.patterflow merge=patter
2487
- *.patterloc merge=patter
2488
- *.patterx merge=patter
2489
- *.patterproj merge=patter
2490
2098
 
2491
- # Generated artifacts (spec \xA711). The compiled bundle is committed but
2492
- # REGENERATED, never hand-merged - keep ours on conflict and rebuild (the
2493
- # validate staleness gate catches a stale one). Needs a one-time
2494
- # 'git config merge.ours.driver true' (see vcs-setup.md).
2495
- *.patterc text eol=lf merge=ours
2496
- # The packed document (.patterpack) is a binary zip envelope - kept out of VCS (.gitignore).
2497
- *.patterpack binary
2498
- `;
2499
- function ignoreFileFor(vcs) {
2500
- switch (vcs) {
2501
- case "git":
2502
- return ".gitignore";
2503
- case "perforce":
2504
- return ".p4ignore";
2505
- case "plastic":
2506
- return "ignore.conf";
2507
- default:
2508
- return void 0;
2099
+ // src/loc-xlsx.ts
2100
+ var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender"];
2101
+ var sheetName = (scene) => scene.replace(/[:\\/?*[\]]/g, "-").slice(0, 31);
2102
+ async function catalogToXlsx(catalog) {
2103
+ const { default: ExcelJS } = await import("exceljs");
2104
+ const wb = new ExcelJS.Workbook();
2105
+ wb.creator = "patter";
2106
+ const byScene = /* @__PURE__ */ new Map();
2107
+ for (const e of catalog.entries) (byScene.get(e.scene) ?? byScene.set(e.scene, []).get(e.scene)).push(e);
2108
+ for (const [scene, entries] of byScene) {
2109
+ const ws = wb.addWorksheet(sheetName(scene));
2110
+ ws.columns = [
2111
+ { header: "ID", key: "id", width: 22 },
2112
+ { header: "Source", key: "source", width: 40 },
2113
+ { header: "Translation", key: "translation", width: 40 },
2114
+ { header: "Comments", key: "comments", width: 30 },
2115
+ { header: "Status", key: "status", width: 10 },
2116
+ { header: "Gender", key: "gender", width: 12 }
2117
+ ];
2118
+ ws.getRow(1).font = { bold: true };
2119
+ ws.views = [{ state: "frozen", ySplit: 1 }];
2120
+ for (const e of entries) {
2121
+ ws.addRow({
2122
+ id: e.id,
2123
+ source: e.source,
2124
+ translation: e.translation,
2125
+ comments: e.comments.join("\n"),
2126
+ status: e.stale ? "stale" : e.translation ? "translated" : "",
2127
+ gender: e.context?.gender ?? ""
2128
+ });
2129
+ }
2509
2130
  }
2131
+ return Buffer.from(await wb.xlsx.writeBuffer());
2510
2132
  }
2511
- function ignoreContent(bundle) {
2512
- const lines = [
2513
- "# Patter generated artifacts - not source (see vcs-setup.md).",
2514
- "# Packed document (the send-and-return envelope):",
2515
- "*.patterpack",
2516
- "# Unresolved merge sidecar:",
2517
- "*.patterconflict"
2518
- ];
2519
- if (bundle === "ignore") {
2520
- lines.push("# Compiled bundle (built in CI, not committed under this posture):", "*.patterc");
2133
+ async function xlsxToCatalog(buffer) {
2134
+ const { default: ExcelJS } = await import("exceljs");
2135
+ const wb = new ExcelJS.Workbook();
2136
+ await wb.xlsx.load(buffer);
2137
+ const entries = [];
2138
+ for (const ws of wb.worksheets) {
2139
+ const scene = ws.name;
2140
+ const header = ws.getRow(1).values.slice(1).map((v) => String(v ?? ""));
2141
+ if (header[0] !== HEADERS[0]) continue;
2142
+ ws.eachRow((row, n) => {
2143
+ if (n === 1) return;
2144
+ const cell = (c) => {
2145
+ const v = row.getCell(c).value;
2146
+ return v == null ? "" : String(typeof v === "object" && "text" in v ? v.text : v);
2147
+ };
2148
+ const id = cell(1).trim();
2149
+ if (!id) return;
2150
+ const status = cell(5).trim().toLowerCase();
2151
+ entries.push({
2152
+ id,
2153
+ scene,
2154
+ source: cell(2),
2155
+ translation: cell(3),
2156
+ comments: cell(4) ? cell(4).split("\n").filter(Boolean) : [],
2157
+ stale: status === "stale"
2158
+ });
2159
+ });
2521
2160
  }
2522
- return lines.join("\n") + "\n";
2161
+ return { project: "", defaultLocale: "", locale: void 0, entries };
2523
2162
  }
2524
- function vcsSetup(vcs, bundle) {
2525
- const sections = {
2526
- git: `## git
2527
-
2528
- \`.gitattributes\` (already emitted) pins Patter source shards to UTF-8 + LF text
2529
- and marks the generated artifacts (the \`.patterc\` bundle \`merge=ours\`, the
2530
- \`.patterpack\` document \`binary\`). \`.gitignore\` (already emitted) keeps the document
2531
- (and the bundle, under the "ignore" posture) out of source control.
2532
-
2533
- Recommended pre-commit hook (\`.git/hooks/pre-commit\`, executable):
2534
-
2535
- #!/bin/sh
2536
- patter validate || exit 1
2537
-
2538
- The \`merge=patter\` rules in \`.gitattributes\` are already active; register the
2539
- drivers once per clone (git config is not repo-tracked) - until then git falls
2540
- back to a normal text merge for those files:
2541
-
2542
- git config merge.patter.name "Patter structured merge"
2543
- git config merge.patter.driver "patter merge %O %A %B -o %A"
2544
- git config merge.ours.driver true
2545
-
2546
- git invokes the per-path driver directly (no \`mergetool\` wrapper needed). \`%O %A
2547
- %B\` are base / ours / theirs; the merged result is written back to \`%A\`. On a
2548
- conflict \`patter merge\` exits non-zero and writes a \`.patterconflict\` sidecar
2549
- beside the file, so the merge stays unresolved.
2550
- `,
2551
- perforce: `## Perforce
2552
-
2553
- Add Patter extensions to the typemap (\`p4 typemap\`): source shards and the
2554
- compiled bundle are TEXT with LF; the packed document is BINARY:
2555
-
2556
- text //....patterflow
2557
- text //....patterloc
2558
- text //....patterx
2559
- text //....patterproj
2560
- text //....patterc
2561
- binary //....patterpack
2562
-
2563
- On a unicode-mode server, set \`P4CHARSET=utf8\`. For the lock-based workflow
2564
- (spec: one scene per file = one lock per scene), add \`+l\` to make checkouts
2565
- exclusive. A \`.p4ignore\` (already emitted) keeps the packed document out of the
2566
- depot.
2567
-
2568
- Perforce allows ONE global merge tool, so set \`patter mergetool\` as it - it
2569
- runs the structured merge for Patter source and hands everything else to your
2570
- normal tool. Map Perforce's variables into the order BASE THEIRS OURS OUT:
2571
-
2572
- patter mergetool --fallback "<your merge tool>" %b %t %y %r
2573
-
2574
- (\`%b\` base, \`%t\` theirs, \`%y\` yours/ours, \`%r\` result - adjust to your P4
2575
- client's variable names; the argument ORDER is what matters.)
2576
- `,
2577
- plastic: `## Plastic SCM (Unity VC)
2578
-
2579
- Patter source shards are plain UTF-8 text; Plastic handles them as-is. The
2580
- \`ignore.conf\` (already emitted) keeps the packed document out of the repo. For
2581
- the lock-based workflow, configure exclusive checkout (lock.conf) for the shard
2582
- extensions.
2583
-
2584
- In Preferences > Merge tools, add an external tool (a global entry is fine -
2585
- the wrapper sniffs the path) with the arguments in BASE THEIRS OURS OUT order:
2586
-
2587
- patter mergetool --fallback "<your merge tool>" @basefile @sourcefile @destinationfile @output
2588
- `,
2589
- svn: `## SVN
2590
-
2591
- Set auto-props so Patter source keeps LF (\`~/.subversion/config\` or repo config):
2592
-
2593
- [auto-props]
2594
- *.patterflow = svn:eol-style=LF
2595
- *.patterloc = svn:eol-style=LF
2596
- *.patterx = svn:eol-style=LF
2597
- *.patterproj = svn:eol-style=LF
2598
- *.patterc = svn:eol-style=LF
2599
- *.patterpack = svn:mime-type=application/octet-stream
2600
-
2601
- SVN's ignore is a directory PROPERTY, not a file - set it from the project root:
2602
-
2603
- svn propset svn:ignore "*.patterpack${bundle === "ignore" ? "\\n*.patterc" : ""}\\n*.patterconflict" .
2604
-
2605
- For the lock-based workflow add \`svn:needs-lock\` to the shard patterns.
2606
-
2607
- SVN allows one global merge tool. Point \`[helpers] merge-tool-cmd\` at a small
2608
- wrapper script that forwards SVN's four arguments (base theirs mine merged =
2609
- BASE THEIRS OURS OUT) to \`patter mergetool\`:
2610
2163
 
2611
- #!/bin/sh
2612
- exec patter mergetool --fallback "<your merge tool>" "$1" "$2" "$3" "$4"
2613
- `
2164
+ // src/voice-script.ts
2165
+ import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES2 } from "@patterkit/model";
2166
+ function plainVoice(text) {
2167
+ return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
2168
+ }
2169
+ function groupLabel(node, source) {
2170
+ if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
2171
+ if (node.selector === "choice") return "";
2172
+ return node.selector ?? "group";
2173
+ }
2174
+ function runVoiceScript(loaded, opts = {}) {
2175
+ const { project } = loaded;
2176
+ const everything = !!opts.everything;
2177
+ const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
2178
+ const writingLadder = ladderDecls.map((s) => s.name);
2179
+ const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3).map((s) => s.name);
2180
+ const stub = writingLadder[0];
2181
+ const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
2182
+ const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
2183
+ const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring2(loaded);
2184
+ const recordingBase = opts.recordingOverride ?? manualRecordingOf;
2185
+ const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
2186
+ const source = sourceStrings(loaded);
2187
+ const actorOf = /* @__PURE__ */ new Map();
2188
+ for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
2189
+ const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, "vo");
2190
+ const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
2191
+ const lines = [];
2192
+ const walkNode = (node, scope, ancestorVo) => {
2193
+ if (node.type === "group") {
2194
+ if (cutSet.has(node.id)) return;
2195
+ const label = groupLabel(node, source);
2196
+ const childScope = label ? [...scope, label] : scope;
2197
+ const childVo = [...ancestorVo, ...ownVo(node.id)];
2198
+ for (const child of node.children ?? []) walkNode(child, childScope, childVo);
2199
+ return;
2200
+ }
2201
+ if (cutSet.has(node.id)) return;
2202
+ let leading = true;
2203
+ for (const beat of node.beats ?? []) {
2204
+ if (beat.kind !== "line" || cutSet.has(beat.id)) continue;
2205
+ const ws = writingOf.get(beat.id) ?? stub;
2206
+ if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) continue;
2207
+ const own = ownVo(beat.id);
2208
+ lines.push({
2209
+ scope: scope.join(" \u203A "),
2210
+ id: beat.id,
2211
+ character: beat.character ?? "",
2212
+ actor: beat.character ? actorOf.get(beat.character) : void 0,
2213
+ text: plainVoice(source[beat.id] ?? ""),
2214
+ comments: leading ? [...ancestorVo, ...own] : own,
2215
+ // first line of the run gets the enclosing context
2216
+ recordingStatus: recordingOf(beat.id)
2217
+ });
2218
+ leading = false;
2219
+ }
2614
2220
  };
2615
- const head = vcs ? sections[vcs] : `No VCS selected (\`patter init --vcs git|perforce|plastic|svn\` emits tailored
2616
- config). Whichever you adopt, the rules are the same: Patter source is UTF-8 +
2617
- LF text, one scene per file, and \`patter validate\` belongs in your pre-commit /
2618
- CI. Sections for every supported VCS:
2619
-
2620
- ${Object.keys(sections).map((k) => sections[k]).join("\n")}`;
2621
- return `# Version-control setup for this Patter project
2622
-
2623
- ${head}
2624
- ${bundleSection(bundle)}`;
2221
+ for (const scene of loaded.scenes) {
2222
+ if (cutSet.has(scene.id)) continue;
2223
+ const sceneVo = ownVo(scene.id);
2224
+ for (const block of scene.blocks) {
2225
+ if (cutSet.has(block.id)) continue;
2226
+ const scope = [scene.name, block.name].filter(Boolean);
2227
+ const blockVo = [...sceneVo, ...ownVo(block.id)];
2228
+ for (const child of block.children ?? []) walkNode(child, scope, blockVo);
2229
+ }
2230
+ }
2231
+ return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
2625
2232
  }
2626
- function bundleSection(bundle) {
2627
- const posture = bundle === "commit" ? `This project COMMITS the compiled \`.patterc\` bundle (\`patter init --bundle ignore\`
2628
- to build it in CI instead). The bundle is regenerated, never hand-merged: on a
2629
- conflict, keep ours and re-run \`patter export\`. \`patter validate\` recomputes the
2630
- bundle's embedded hash from source and FAILS if it is stale, so a forgotten
2631
- regenerate cannot ship silently.` : `This project IGNORES the compiled \`.patterc\` bundle (built in CI; \`patter init
2632
- --bundle commit\` to commit it instead). Run \`patter export\` as a build step.`;
2633
- return `## Compiled bundle & packed document
2634
2233
 
2635
- ${posture}
2234
+ // src/voice-script-xlsx.ts
2235
+ async function voiceScriptToXlsx(data) {
2236
+ const { default: ExcelJS } = await import("exceljs");
2237
+ const wb = new ExcelJS.Workbook();
2238
+ wb.creator = "patter";
2239
+ const ws = wb.addWorksheet("Voice Script");
2240
+ ws.columns = [
2241
+ { header: "Scope", key: "scope", width: 28 },
2242
+ { header: "Line ID", key: "id", width: 18 },
2243
+ { header: "Character", key: "character", width: 16 },
2244
+ { header: "Actor", key: "actor", width: 16 },
2245
+ { header: "Text", key: "text", width: 60 },
2246
+ { header: "Comments", key: "comments", width: 36 },
2247
+ { header: "Status", key: "status", width: 12 }
2248
+ ];
2249
+ ws.getRow(1).font = { bold: true };
2250
+ ws.views = [{ state: "frozen", ySplit: 1 }];
2251
+ for (const l of data.lines) {
2252
+ const row = ws.addRow({
2253
+ scope: l.scope,
2254
+ id: l.id,
2255
+ character: l.character,
2256
+ actor: l.actor ?? "",
2257
+ text: l.text,
2258
+ comments: l.comments.join("\n"),
2259
+ status: l.recordingStatus
2260
+ });
2261
+ row.getCell("text").alignment = { wrapText: true, vertical: "top" };
2262
+ row.getCell("comments").alignment = { wrapText: true, vertical: "top" };
2263
+ }
2264
+ return Buffer.from(await wb.xlsx.writeBuffer());
2265
+ }
2636
2266
 
2637
- The packed \`.patterpack\` document (\`patter pack\`) is the send-and-return envelope
2638
- for collaborators without VCS. It is a binary zip and a projection of the
2639
- shards - NOT source - so it stays out of version control (ignored above); edits
2640
- return via \`patter unpack\`.
2641
- `;
2267
+ // src/script-doc.ts
2268
+ var TOKENS = {
2269
+ ink: "26221c",
2270
+ // near-black warm ink (headings)
2271
+ inkRead: "3a352d",
2272
+ // dialogue body (ink ~84% + muted)
2273
+ inkSoft: "5c554b",
2274
+ // narration (ink ~68% + muted) - a step softer than dialogue
2275
+ muted: "8c8479",
2276
+ // labels, quiet tags
2277
+ accent: "8a5a34",
2278
+ // structural marks: conditions, options, jumps, game events
2279
+ line: "e4dfd5"
2280
+ // hairlines / rules / the group rail
2281
+ };
2282
+ var CHAR_PALETTE = [
2283
+ "b23b3b",
2284
+ "b5642b",
2285
+ "927524",
2286
+ "6b7a31",
2287
+ "3f7d49",
2288
+ "2c7d6e",
2289
+ "2d7791",
2290
+ "3a6aa8",
2291
+ "5a5ba6",
2292
+ "7e4e9c",
2293
+ "9c4486",
2294
+ "a83e5c"
2295
+ ];
2296
+ function colourIndex(name) {
2297
+ let h = 2166136261;
2298
+ for (let i = 0; i < name.length; i++) {
2299
+ h ^= name.charCodeAt(i);
2300
+ h = Math.imul(h, 16777619);
2301
+ }
2302
+ h ^= h >>> 16;
2303
+ h = Math.imul(h, 2246822507);
2304
+ h ^= h >>> 13;
2305
+ h = Math.imul(h, 3266489909);
2306
+ h ^= h >>> 16;
2307
+ return (h >>> 0) % 12;
2642
2308
  }
2643
-
2644
- // src/resolve.ts
2645
- import { walkNodes as walkNodes4, effectiveGameId, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
2646
- function runResolve(loaded, query) {
2647
- const entries = indexProject(loaded);
2648
- const q = query.trim();
2649
- const ql = q.toLowerCase();
2650
- const tiers = [
2651
- (e) => e.id === q,
2652
- (e) => e.gameId?.toLowerCase() === ql,
2653
- (e) => e.name?.toLowerCase() === ql,
2654
- (e) => e.id.toLowerCase().includes(ql) || (e.gameId?.toLowerCase().includes(ql) ?? false) || (e.name?.toLowerCase().includes(ql) ?? false)
2655
- ];
2656
- for (const match of tiers) {
2657
- const hits = entries.filter(match);
2658
- if (hits.length > 0) return hits;
2309
+ function characterColour(name) {
2310
+ return CHAR_PALETTE[colourIndex(name)];
2311
+ }
2312
+ var VISIT_FN_RE = /\b(patter_visits|patter_seen|visits|seen)\s*\(\s*(['"])(.*?)\2\s*\)/g;
2313
+ function humanizeCondition(cond, label) {
2314
+ return cond.replace(VISIT_FN_RE, (_m, fn, _q, id) => `${fn}(${label(id)})`);
2315
+ }
2316
+ var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
2317
+ var INTERP_RE = /\{@[^}]*\}/g;
2318
+ function textRuns(text) {
2319
+ const marked = [];
2320
+ let last = 0;
2321
+ let m;
2322
+ MARKUP_RE.lastIndex = 0;
2323
+ while ((m = MARKUP_RE.exec(text)) !== null) {
2324
+ if (m.index > last) marked.push({ text: text.slice(last, m.index), bold: false, italic: false });
2325
+ const tag = m[1];
2326
+ marked.push({ text: m[2], bold: tag === "b" || tag === "bi", italic: tag === "i" || tag === "bi" });
2327
+ last = MARKUP_RE.lastIndex;
2659
2328
  }
2660
- return [];
2329
+ if (last < text.length) marked.push({ text: text.slice(last), bold: false, italic: false });
2330
+ if (!marked.length) marked.push({ text, bold: false, italic: false });
2331
+ const out = [];
2332
+ for (const r of marked) {
2333
+ let li = 0;
2334
+ let im;
2335
+ INTERP_RE.lastIndex = 0;
2336
+ while ((im = INTERP_RE.exec(r.text)) !== null) {
2337
+ if (im.index > li) out.push({ text: r.text.slice(li, im.index), bold: r.bold, italic: r.italic, code: false });
2338
+ out.push({ text: im[0], bold: r.bold, italic: r.italic, code: true });
2339
+ li = INTERP_RE.lastIndex;
2340
+ }
2341
+ if (li < r.text.length) out.push({ text: r.text.slice(li), bold: r.bold, italic: r.italic, code: false });
2342
+ }
2343
+ return out.length ? out : [{ text, bold: false, italic: false, code: false }];
2661
2344
  }
2662
- function runSearch(loaded, query, focus) {
2663
- const ql = query.trim().toLowerCase();
2664
- if (!ql) return [];
2665
- const entries = indexProject(loaded);
2666
- const inMeta = (e) => (e.gameId?.toLowerCase().includes(ql) ?? false) || (e.name?.toLowerCase().includes(ql) ?? false);
2667
- const inText = (e) => e.text?.toLowerCase().includes(ql) ?? false;
2668
- const inTech = (e) => e.id.toLowerCase().includes(ql);
2669
- const matched = entries.map((e, i) => ({ e, i })).filter(({ e }) => inMeta(e) || inText(e) || inTech(e));
2670
- if (!focus) {
2671
- const meta = matched.filter(({ e }) => inMeta(e)).map((m) => m.e);
2672
- const text = matched.filter(({ e }) => !inMeta(e) && inText(e)).map((m) => m.e);
2673
- const tech = matched.filter(({ e }) => !inMeta(e) && !inText(e) && inTech(e)).map((m) => m.e);
2674
- return [...meta, ...text, ...tech];
2345
+ function targetLabel(to, sceneOf, blockTrail) {
2346
+ if (to === "END") return "END";
2347
+ return blockTrail.get(to) ?? sceneOf.get(to) ?? to;
2348
+ }
2349
+ function gameEventLabel(beat) {
2350
+ const gd = beat.gameData;
2351
+ if (gd && typeof gd === "object") {
2352
+ const parts = Object.entries(gd).map(([k, v]) => {
2353
+ const val = v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
2354
+ return val === "" ? k : `${k}: ${val}`;
2355
+ });
2356
+ if (parts.length) return `game event \xB7 ${parts.join(", ")}`;
2675
2357
  }
2676
- const cut = focus.fromBeatId ? entries.findIndex((e) => e.id === focus.fromBeatId) : -1;
2677
- const group = (e, i) => e.sceneId !== focus.sceneId ? 2 : cut < 0 || i >= cut ? 0 : 1;
2678
- return matched.sort((a, b) => group(a.e, a.i) - group(b.e, b.i) || a.i - b.i).map((m) => m.e);
2358
+ return "game event";
2679
2359
  }
2680
- function runStatusBrowse(loaded, status, dimension = "writing", focus, recordingOverride) {
2681
- const strings = sourceStrings(loaded);
2682
- const merged = mergeAuthoring(loaded);
2683
- const recording = dimension === "recording";
2684
- const statusOf = recording ? recordingOverride ?? merged.recording : merged.writing;
2685
- const ladder = recording ? loaded.project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES4 : loaded.project.writingStatuses ?? DEFAULT_WRITING_STATUSES3;
2686
- const lowest = ladder[0]?.name;
2687
- const statusAt = (id) => recording ? effectiveRecording(id, statusOf, merged.rerecord, lowest ?? "") : statusOf.get(id) ?? lowest;
2688
- const out = [];
2360
+ function optionTag(node) {
2361
+ const parts = [];
2362
+ if (node.fallback) parts.push("fallback");
2363
+ parts.push(node.sticky ? "repeatable" : "once only");
2364
+ if (node.secretUntilEligible) parts.push("hidden");
2365
+ return parts.join(" \xB7 ");
2366
+ }
2367
+ function sequenceLabel(node) {
2368
+ const o = node.options ?? {};
2369
+ const bits = [];
2370
+ if ((o.order ?? "sequential") === "shuffle") bits.push("shuffled");
2371
+ const ex = o.exhaust ?? "once";
2372
+ if (ex === "repeat") bits.push("repeating");
2373
+ else if (ex === "stick") bits.push("holds on the last");
2374
+ return bits.length ? `In sequence \xB7 ${bits.join(", ")}` : "In sequence";
2375
+ }
2376
+ function runScriptDoc(loaded) {
2377
+ const { project } = loaded;
2378
+ const source = sourceStrings(loaded);
2379
+ const { cut } = mergeAuthoring2(loaded);
2380
+ const sceneOf = /* @__PURE__ */ new Map();
2381
+ const blockTrail = /* @__PURE__ */ new Map();
2382
+ const blockName = /* @__PURE__ */ new Map();
2689
2383
  for (const scene of loaded.scenes) {
2690
- const file = loaded.sceneFiles[scene.id];
2384
+ sceneOf.set(scene.id, scene.name);
2691
2385
  for (const block of scene.blocks) {
2692
- const segments = [scene.name, block.name];
2693
- const consider = (beat) => {
2694
- const tracked = recording ? beat.kind === "line" : beat.kind === "line" || beat.kind === "text";
2695
- if (!tracked) return;
2696
- if (statusAt(beat.id) !== status) return;
2697
- out.push({ id: beat.id, kind: "beat", text: strings[beat.id], location: segments, sceneId: scene.id, file });
2698
- };
2699
- walkNodes4(block.children, (node) => {
2700
- if (node.type === "group") {
2701
- if (node.prompt) consider(node.prompt);
2702
- return;
2386
+ blockTrail.set(block.id, `${scene.name} \u203A ${block.name}`);
2387
+ blockName.set(block.id, block.name);
2388
+ }
2389
+ }
2390
+ const humanize = (cond) => humanizeCondition(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
2391
+ const els = [];
2392
+ const textOf = (id) => source[id] ?? "";
2393
+ const jumpText = (j) => {
2394
+ const dest = targetLabel(j.to, sceneOf, blockTrail);
2395
+ return j.mode === "call" ? `${dest} (and return)` : dest;
2396
+ };
2397
+ let snippetSeq = 0;
2398
+ const nextSid = () => ++snippetSeq;
2399
+ const emitBeats = (node, indent, sid) => {
2400
+ for (const beat of node.beats ?? []) {
2401
+ if (cut.has(beat.id)) continue;
2402
+ if (beat.kind === "line") els.push({ kind: "line", indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
2403
+ else if (beat.kind === "text") els.push({ kind: "narration", indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
2404
+ else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent, snippet: sid, text: gameEventLabel(beat) });
2405
+ }
2406
+ if (node.jump) els.push({ kind: "jump", indent, snippet: sid, text: jumpText(node.jump) });
2407
+ };
2408
+ const walk = (node, indent, sid) => {
2409
+ if (cut.has(node.id)) return;
2410
+ if (node.type === "snippet") {
2411
+ if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
2412
+ emitBeats(node, indent, sid);
2413
+ return;
2414
+ }
2415
+ if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
2416
+ if (node.selector === "choice") {
2417
+ els.push({ kind: "group", indent, label: "Choose" });
2418
+ for (const child of node.children ?? []) {
2419
+ if (cut.has(child.id)) continue;
2420
+ const cid = nextSid();
2421
+ if (child.type === "group" && child.prompt) {
2422
+ if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
2423
+ els.push({ kind: "option", indent, snippet: cid, runs: textRuns(textOf(child.prompt.id) || "(option)"), tag: optionTag(child) });
2424
+ for (const c of child.children ?? []) walk(c, indent + 1, cid);
2425
+ } else if (child.type === "snippet") {
2426
+ const beats = child.beats ?? [];
2427
+ const first = beats.find((b) => b.kind === "line" || b.kind === "text");
2428
+ if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
2429
+ els.push({ kind: "option", indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child) });
2430
+ let seenPrompt = false;
2431
+ for (const beat of beats) {
2432
+ if (!seenPrompt && beat === first) {
2433
+ seenPrompt = true;
2434
+ continue;
2435
+ }
2436
+ if (cut.has(beat.id)) continue;
2437
+ if (beat.kind === "line") els.push({ kind: "line", indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
2438
+ else if (beat.kind === "text") els.push({ kind: "narration", indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
2439
+ else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
2440
+ }
2441
+ if (child.jump) els.push({ kind: "jump", indent: indent + 1, snippet: cid, text: jumpText(child.jump) });
2442
+ } else {
2443
+ walk(child, indent + 1, cid);
2703
2444
  }
2704
- for (const beat of node.beats ?? []) consider(beat);
2445
+ }
2446
+ return;
2447
+ }
2448
+ if (node.selector === "branch") {
2449
+ els.push({ kind: "group", indent, label: "One of \xB7 first match wins" });
2450
+ const kids = (node.children ?? []).filter((c) => !cut.has(c.id));
2451
+ kids.forEach((child, i) => {
2452
+ const isCatchAll = i === kids.length - 1 && kids.length > 1 && !("condition" in child && child.condition);
2453
+ if (isCatchAll) els.push({ kind: "else", indent });
2454
+ walk(child, indent, nextSid());
2705
2455
  });
2456
+ return;
2706
2457
  }
2707
- }
2708
- if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
2709
- return out;
2710
- }
2711
- function listProjectTags(loaded) {
2712
- const counts = /* @__PURE__ */ new Map();
2713
- const add = (tags) => {
2714
- for (const t of tags ?? []) counts.set(t, (counts.get(t) ?? 0) + 1);
2458
+ if (node.selector === "sequence") {
2459
+ els.push({ kind: "group", indent, label: sequenceLabel(node) });
2460
+ for (const child of node.children ?? []) walk(child, indent, nextSid());
2461
+ return;
2462
+ }
2463
+ for (const child of node.children ?? []) walk(child, indent, sid);
2715
2464
  };
2716
2465
  for (const scene of loaded.scenes) {
2717
- add(scene.tags);
2466
+ if (cut.has(scene.id)) continue;
2467
+ els.push({ kind: "scene", text: scene.name });
2718
2468
  for (const block of scene.blocks) {
2719
- add(block.tags);
2720
- walkNodes4(block.children, (node) => {
2721
- add(node.tags);
2722
- if (node.type === "group") {
2723
- add(node.prompt?.tags);
2724
- return;
2725
- }
2726
- for (const beat of node.beats ?? []) add(beat.tags);
2469
+ if (cut.has(block.id)) continue;
2470
+ els.push({ kind: "block", text: block.name });
2471
+ for (const child of block.children ?? []) walk(child, 0, void 0);
2472
+ }
2473
+ }
2474
+ return { project: project.project.name, elements: els };
2475
+ }
2476
+
2477
+ // src/script-docx.ts
2478
+ import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, TabStopType, TextRun } from "docx";
2479
+ var SERIF = "Georgia";
2480
+ var SANS = "Calibri";
2481
+ var MONO = "Consolas";
2482
+ var STEP = 360;
2483
+ var SNIPPET_INSET = 220;
2484
+ var DIALOGUE_INDENT = 300;
2485
+ var HANG = 340;
2486
+ var RIGHT_TAB = 9020;
2487
+ var SNIP_GAP = 170;
2488
+ var WITHIN = 30;
2489
+ var S = { h1: 32, h2: 26, body: 22, cue: 18, label: 16, tag: 15, mech: 18 };
2490
+ function bodyRuns(runs, color, base = {}) {
2491
+ return runs.map((r) => r.code ? new TextRun({ text: r.text, font: MONO, color: TOKENS.accent, size: S.body - 2 }) : new TextRun({ text: r.text, font: SERIF, color, size: S.body, bold: base.bold || r.bold || void 0, italics: base.italic || r.italic || void 0 }));
2492
+ }
2493
+ var leftOf = (el) => ("indent" in el ? el.indent * STEP : 0) + ("snippet" in el && el.snippet !== void 0 ? SNIPPET_INSET : 0);
2494
+ function paragraph(el, prev) {
2495
+ const sid = "snippet" in el ? el.snippet : void 0;
2496
+ const startsSnippet = sid !== void 0 && (!prev || !("snippet" in prev) || prev.snippet !== sid);
2497
+ const before = sid !== void 0 ? startsSnippet ? SNIP_GAP : WITHIN : 100;
2498
+ switch (el.kind) {
2499
+ case "scene":
2500
+ return new Paragraph({
2501
+ heading: HeadingLevel.HEADING_1,
2502
+ keepNext: true,
2503
+ spacing: { before: 480, after: 200 },
2504
+ border: { bottom: { style: BorderStyle.SINGLE, size: 14, color: TOKENS.accent, space: 6 } },
2505
+ children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h1 })]
2727
2506
  });
2728
- }
2729
- }
2730
- return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name));
2731
- }
2732
- function runTagBrowse(loaded, tag, focus) {
2733
- if (!tag) return [];
2734
- const strings = sourceStrings(loaded);
2735
- const has = (tags) => !!tags && tags.includes(tag);
2736
- const out = [];
2737
- for (const scene of loaded.scenes) {
2738
- const file = loaded.sceneFiles[scene.id];
2739
- if (has(scene.tags)) out.push({ id: scene.id, kind: "scene", name: scene.name, gameId: effectiveGameId(scene), location: [scene.name], sceneId: scene.id, file });
2740
- for (const block of scene.blocks) {
2741
- const segments = [scene.name, block.name];
2742
- if (has(block.tags)) out.push({ id: block.id, kind: "block", name: block.name, gameId: effectiveGameId(block), location: segments, sceneId: scene.id, file });
2743
- walkNodes4(block.children, (node) => {
2744
- if (has(node.tags)) {
2745
- const preview = node.type === "group" ? node.prompt ? strings[node.prompt.id] : void 0 : strings[node.beats?.[0]?.id ?? ""];
2746
- out.push({ id: node.id, kind: node.type, text: preview, location: segments, sceneId: scene.id, file });
2747
- }
2748
- if (node.type === "group") {
2749
- if (node.prompt && has(node.prompt.tags)) out.push({ id: node.prompt.id, kind: "beat", text: strings[node.prompt.id], location: segments, sceneId: scene.id, file });
2750
- return;
2751
- }
2752
- for (const beat of node.beats ?? []) if (has(beat.tags)) out.push({ id: beat.id, kind: "beat", text: strings[beat.id], location: segments, sceneId: scene.id, file });
2507
+ case "block":
2508
+ return new Paragraph({
2509
+ heading: HeadingLevel.HEADING_2,
2510
+ keepNext: true,
2511
+ spacing: { before: 360, after: 160 },
2512
+ border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: TOKENS.line, space: 4 } },
2513
+ children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h2 })]
2753
2514
  });
2515
+ case "line": {
2516
+ const kids = [];
2517
+ if (el.character) kids.push(new TextRun({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
2518
+ if (el.direction) kids.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
2519
+ kids.push(...bodyRuns(el.runs, TOKENS.inkRead));
2520
+ return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids });
2521
+ }
2522
+ case "narration":
2523
+ return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: bodyRuns(el.runs, TOKENS.inkSoft) });
2524
+ case "condition":
2525
+ return new Paragraph({ keepNext: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: [new TextRun({ text: `\u2039 ${el.text} \u203A`, font: MONO, color: TOKENS.accent, size: S.mech - 1 })] });
2526
+ case "group":
2527
+ return new Paragraph({ keepNext: true, spacing: { before: 260, after: 60 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: el.label.toUpperCase(), font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: S.label, characterSpacing: 14 })] });
2528
+ case "else":
2529
+ return new Paragraph({ keepNext: true, spacing: { before: 140, after: 20 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: "else \xB7 catch-all", font: SANS, allCaps: true, color: TOKENS.muted, size: S.label, characterSpacing: 12 })] });
2530
+ case "option": {
2531
+ const kids = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
2532
+ if (el.tag) kids.push(new TextRun({ text: " ", size: S.body }), new TextRun({ text: el.tag, font: SANS, smallCaps: true, color: TOKENS.muted, size: S.tag }));
2533
+ return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + HANG, hanging: HANG }, tabStops: el.tag ? [{ type: TabStopType.RIGHT, position: RIGHT_TAB }] : void 0, children: kids });
2754
2534
  }
2535
+ case "jump":
2536
+ return new Paragraph({ keepLines: true, alignment: AlignmentType.RIGHT, spacing: { before: 40, after: 40 }, children: [new TextRun({ text: `\u21AA ${el.text}`, font: SANS, bold: true, color: TOKENS.accent, size: S.mech })] });
2537
+ case "gameEvent":
2538
+ return new Paragraph({ keepLines: true, alignment: AlignmentType.RIGHT, spacing: { before: 40, after: 40 }, children: [new TextRun({ text: `\u2699 ${el.text}`, font: MONO, color: TOKENS.accent, size: S.mech - 2 })] });
2755
2539
  }
2756
- if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
2757
- return out;
2758
2540
  }
2759
- function runPropertyUsage(loaded, query, focus) {
2760
- const parsed = parsePropertyQuery(query);
2761
- if (!parsed) return [];
2762
- const { regexes, value } = parsed;
2763
- const strings = sourceStrings(loaded);
2764
- const refs = (src) => !!src && regexes.some((re) => re.test(src));
2765
- const valueOk = (src) => !value || src.toLowerCase().includes(value);
2766
- const out = [];
2767
- for (const scene of loaded.scenes) {
2768
- const file = loaded.sceneFiles[scene.id];
2769
- const push = (id, kind, segments, usage) => {
2770
- out.push({ id, kind, text: usage, location: segments, sceneId: scene.id, file });
2771
- };
2772
- for (const e of scene.onEntry ?? []) {
2773
- const src = `${e.target} = ${e.value}`;
2774
- if ((refs(e.target) || refs(e.value)) && valueOk(src)) push(scene.id, "scene", [scene.name], `on entry: set ${src}`);
2541
+ async function scriptToDocx(doc) {
2542
+ const body = [
2543
+ new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: "Readable script", font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: 16, characterSpacing: 30 })] }),
2544
+ new Paragraph({ spacing: { after: 560 }, children: [new TextRun({ text: doc.project, font: SERIF, bold: true, color: TOKENS.ink, size: 52 })] }),
2545
+ ...doc.elements.map((el, i) => paragraph(el, doc.elements[i - 1]))
2546
+ ];
2547
+ const document = new Document({ creator: "Patter", title: doc.project, sections: [{ children: body }] });
2548
+ return Packer.toBuffer(document);
2549
+ }
2550
+
2551
+ // src/script-pdf.ts
2552
+ import PDFDocument from "pdfkit";
2553
+ var MARGIN = 64;
2554
+ var INDENT_STEP = 20;
2555
+ var EDGE_INSET = 12;
2556
+ var EDGE_OFF = 3;
2557
+ var DIALOGUE_INDENT2 = 14;
2558
+ var HANG2 = 18;
2559
+ var CUE_GAP = 7;
2560
+ var BODY = 11;
2561
+ var CUE = 8.5;
2562
+ var LEAD = 3.5;
2563
+ var WITHIN2 = 4;
2564
+ var BETWEEN = 10;
2565
+ var BEAT = 6;
2566
+ var TIGHT = 3;
2567
+ var hex = (h) => `#${h}`;
2568
+ var INK = hex(TOKENS.ink);
2569
+ var INK_READ = hex(TOKENS.inkRead);
2570
+ var INK_SOFT = hex(TOKENS.inkSoft);
2571
+ var MUTED = hex(TOKENS.muted);
2572
+ var ACCENT = hex(TOKENS.accent);
2573
+ var LINE = hex(TOKENS.line);
2574
+ var EDGE = "#d7d0c3";
2575
+ var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Serif-Bold" : italic ? "Serif-Italic" : "Serif";
2576
+ var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
2577
+ async function scriptToPdf(doc) {
2578
+ const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
2579
+ return new Promise((resolve5, reject) => {
2580
+ const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
2581
+ pdf.registerFont("Serif", scriptFont("serif"));
2582
+ pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
2583
+ pdf.registerFont("Serif-Italic", scriptFont("serifItalic"));
2584
+ pdf.registerFont("Serif-BoldItalic", scriptFont("serifBoldItalic"));
2585
+ pdf.registerFont("Sans", scriptFont("sans"));
2586
+ pdf.registerFont("Sans-Bold", scriptFont("sansBold"));
2587
+ pdf.registerFont("Mono", scriptFont("mono"));
2588
+ pdf.registerFont("Symbol", scriptFont("symbol"));
2589
+ pdf.registerFont("Emoji", scriptFont("emoji"));
2590
+ const coverage = {};
2591
+ const ascender = {};
2592
+ for (const name of ["Serif", "Serif-Bold", "Serif-Italic", "Serif-BoldItalic", "Sans", "Sans-Bold", "Mono"]) {
2593
+ pdf.font(name);
2594
+ const f = pdf._font;
2595
+ coverage[name] = (cp) => f.font.hasGlyphForCodePoint(cp);
2596
+ ascender[name] = f.ascender;
2775
2597
  }
2776
- for (const block of scene.blocks) {
2777
- const segments = [scene.name, block.name];
2778
- walkNodes4(block.children, (node) => {
2779
- if (refs(node.condition) && valueOk(node.condition)) push(node.id, node.type, segments, `if ${node.condition}`);
2780
- if (node.type === "group") {
2781
- if (node.prompt && refs(strings[node.prompt.id]) && valueOk(strings[node.prompt.id])) push(node.prompt.id, "beat", segments, strings[node.prompt.id]);
2782
- return;
2598
+ const ascentPt = (name, size) => ascender[name] / 1e3 * size;
2599
+ const split = (text, primary, color) => {
2600
+ const cov = coverage[primary];
2601
+ const out = [];
2602
+ let buf = "";
2603
+ let cur = primary;
2604
+ for (const ch of text) {
2605
+ const cp = ch.codePointAt(0);
2606
+ const f = isEmoji(cp) ? "Emoji" : cov(cp) ? primary : "Symbol";
2607
+ if (f !== cur) {
2608
+ if (buf) out.push({ text: buf, font: cur, color });
2609
+ buf = "";
2610
+ cur = f;
2783
2611
  }
2784
- for (const phase of ["onEnter", "onExit"]) {
2785
- for (const e of node[phase] ?? []) {
2786
- const src = `${e.target} = ${e.value}`;
2787
- if ((refs(e.target) || refs(e.value)) && valueOk(src)) push(node.id, "snippet", segments, `${phase === "onEnter" ? "on enter" : "on exit"}: set ${src}`);
2612
+ buf += ch;
2613
+ }
2614
+ if (buf) out.push({ text: buf, font: cur, color });
2615
+ return out;
2616
+ };
2617
+ 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));
2618
+ const chunks = [];
2619
+ pdf.on("data", (c) => chunks.push(c));
2620
+ pdf.on("end", () => resolve5(Buffer.concat(chunks)));
2621
+ pdf.on("error", reject);
2622
+ const pageRight = pdf.page.width - MARGIN;
2623
+ const pageBottom = pdf.page.height - MARGIN;
2624
+ const contentW = pdf.page.width - MARGIN * 2;
2625
+ const ensure = (space) => {
2626
+ if (pdf.y > pageBottom - space) pdf.addPage();
2627
+ };
2628
+ const draw = (pieces, x, opts) => {
2629
+ pieces.forEach((p, i) => {
2630
+ pdf.font(p.font).fillColor(p.color);
2631
+ const cont = i < pieces.length - 1;
2632
+ if (i === 0) pdf.text(p.text, x, pdf.y, { ...opts, continued: cont });
2633
+ else pdf.text(p.text, { continued: cont });
2634
+ });
2635
+ };
2636
+ const drawRight = (pieces, size) => {
2637
+ let total = 0;
2638
+ for (const p of pieces) {
2639
+ pdf.font(p.font).fontSize(size);
2640
+ total += pdf.widthOfString(p.text);
2641
+ }
2642
+ const x = Math.max(MARGIN, pageRight - total);
2643
+ pieces.forEach((p, i) => {
2644
+ pdf.font(p.font).fillColor(p.color).fontSize(size);
2645
+ const cont = i < pieces.length - 1;
2646
+ if (i === 0) pdf.text(p.text, x, pdf.y, { continued: cont });
2647
+ else pdf.text(p.text, { continued: cont });
2648
+ });
2649
+ };
2650
+ pdf.fontSize(9);
2651
+ draw(split("READABLE SCRIPT", "Sans-Bold", ACCENT), MARGIN, { characterSpacing: 2 });
2652
+ pdf.moveDown(0.4).fontSize(26);
2653
+ draw(split(doc.project, "Serif-Bold", INK), MARGIN, { width: contentW });
2654
+ pdf.strokeColor(ACCENT).lineWidth(2).moveTo(MARGIN, pdf.y + 10).lineTo(MARGIN + 40, pdf.y + 10).stroke();
2655
+ pdf.y += 28;
2656
+ const drawContent = (el, cx) => {
2657
+ const w = pageRight - cx;
2658
+ switch (el.kind) {
2659
+ case "scene":
2660
+ pdf.fontSize(17);
2661
+ draw(split(el.text, "Serif-Bold", INK), MARGIN, { width: contentW });
2662
+ pdf.strokeColor(ACCENT).lineWidth(1.5).moveTo(MARGIN, pdf.y + 4).lineTo(MARGIN + 34, pdf.y + 4).stroke();
2663
+ break;
2664
+ case "block":
2665
+ pdf.fontSize(13);
2666
+ draw(split(el.text, "Serif-Bold", INK), MARGIN, { width: contentW });
2667
+ pdf.strokeColor(LINE).lineWidth(0.75).moveTo(MARGIN, pdf.y + 3).lineTo(pageRight, pdf.y + 3).stroke();
2668
+ break;
2669
+ case "line": {
2670
+ const cueX = cx + DIALOGUE_INDENT2;
2671
+ let bodyX = cueX;
2672
+ if (el.character) {
2673
+ pdf.font("Sans-Bold").fontSize(CUE);
2674
+ bodyX = cueX + pdf.widthOfString(el.character.toUpperCase()) + CUE_GAP;
2675
+ }
2676
+ const yTop = pdf.y;
2677
+ if (el.character) {
2678
+ pdf.font("Sans-Bold").fontSize(CUE).fillColor(hex(characterColour(el.character))).text(el.character.toUpperCase(), cueX, yTop + ascentPt("Serif", BODY) - ascentPt("Sans-Bold", CUE), { lineBreak: false });
2679
+ pdf.y = yTop;
2788
2680
  }
2681
+ pdf.fontSize(BODY);
2682
+ draw([...el.direction ? split(`(${el.direction}) `, "Serif-Italic", MUTED) : [], ...bodyPieces(el.runs, INK_READ)], bodyX, { width: Math.max(120, pageRight - bodyX), lineGap: LEAD });
2683
+ break;
2789
2684
  }
2790
- for (const beat of node.beats ?? []) {
2791
- if (refs(strings[beat.id]) && valueOk(strings[beat.id])) push(beat.id, "beat", segments, strings[beat.id]);
2685
+ case "narration":
2686
+ pdf.fontSize(BODY);
2687
+ draw(bodyPieces(el.runs, INK_SOFT), cx, { width: w, lineGap: LEAD });
2688
+ break;
2689
+ case "condition":
2690
+ pdf.fontSize(9.5);
2691
+ draw(split(`\u2039 ${el.text} \u203A`, "Mono", ACCENT), cx, { width: w });
2692
+ break;
2693
+ case "group":
2694
+ pdf.fontSize(8);
2695
+ draw(split(el.label.toUpperCase(), "Sans-Bold", ACCENT), cx, { width: w, characterSpacing: 1.2 });
2696
+ break;
2697
+ case "else":
2698
+ pdf.fontSize(8);
2699
+ draw(split("ELSE \xB7 CATCH-ALL", "Sans", MUTED), cx, { width: w, characterSpacing: 1 });
2700
+ break;
2701
+ case "option": {
2702
+ const ox = cx + HANG2;
2703
+ const tag = el.tag ? el.tag.toUpperCase() : "";
2704
+ const tagW = tag ? pdf.font("Sans").fontSize(7.5).widthOfString(tag) + 8 : 0;
2705
+ const yStart = pdf.y;
2706
+ if (tag) {
2707
+ pdf.font("Sans").fontSize(7.5).fillColor(MUTED).text(tag, pageRight - tagW, yStart, { width: tagW, align: "right", lineBreak: false });
2708
+ pdf.y = yStart;
2709
+ }
2710
+ pdf.fontSize(BODY);
2711
+ draw([...split("\u25C7 ", "Serif", ACCENT), ...bodyPieces(el.runs, INK)], ox, { width: Math.max(80, pageRight - ox - tagW), indent: -HANG2, lineGap: LEAD });
2712
+ break;
2792
2713
  }
2793
- });
2794
- }
2795
- }
2796
- if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
2797
- return out;
2798
- }
2799
- function parsePropertyQuery(query) {
2800
- const m = /^@?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)(.*)$/.exec(query.trim());
2801
- if (!m) return null;
2802
- const prop = m[1];
2803
- const value = m[2].replace(/^[\s=<>!]+/, "").replace(/^["']|["']$/g, "").trim().toLowerCase() || void 0;
2804
- const esc2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2805
- const regexes = prop.includes(".") ? [new RegExp(`@${esc2(prop)}\\b`, "i")] : [new RegExp(`@${esc2(prop)}\\b`, "i"), new RegExp(`@patter\\.${esc2(prop)}\\b`, "i")];
2806
- return { regexes, value };
2807
- }
2808
- var indexCache = null;
2809
- function sameRefs(a, b) {
2810
- if (a.length !== b.length) return false;
2811
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
2812
- return true;
2813
- }
2814
- function indexProject(loaded) {
2815
- if (indexCache && indexCache.loaded === loaded && sameRefs(indexCache.scenes, loaded.scenes) && sameRefs(indexCache.locales, loaded.locales)) {
2816
- return indexCache.entries;
2817
- }
2818
- const entries = buildIndex(loaded);
2819
- indexCache = { loaded, scenes: [...loaded.scenes], locales: [...loaded.locales], entries };
2820
- return entries;
2821
- }
2822
- function buildIndex(loaded) {
2823
- const out = [];
2824
- const strings = sourceStrings(loaded);
2825
- for (const scene of loaded.scenes) {
2826
- const file = loaded.sceneFiles[scene.id];
2827
- out.push({
2828
- id: scene.id,
2829
- kind: "scene",
2830
- name: scene.name,
2831
- gameId: effectiveGameId(scene),
2832
- location: [scene.name],
2833
- sceneId: scene.id,
2834
- file
2835
- });
2836
- for (const block of scene.blocks) {
2837
- const segments = [scene.name, block.name];
2838
- out.push({
2839
- id: block.id,
2840
- kind: "block",
2841
- name: block.name,
2842
- gameId: effectiveGameId(block),
2843
- location: segments,
2844
- sceneId: scene.id,
2845
- file
2846
- });
2847
- walkNodes4(block.children, (node) => {
2848
- out.push({
2849
- id: node.id,
2850
- kind: node.type,
2851
- location: segments,
2852
- sceneId: scene.id,
2853
- file
2854
- });
2855
- if (node.type === "group" && node.prompt) {
2856
- out.push({
2857
- id: node.prompt.id,
2858
- kind: "beat",
2859
- text: strings[node.prompt.id],
2860
- location: segments,
2861
- sceneId: scene.id,
2862
- file
2863
- });
2864
- return;
2714
+ case "jump":
2715
+ drawRight(split(`\u21AA ${el.text}`, "Sans-Bold", ACCENT), 9.5);
2716
+ break;
2717
+ case "gameEvent": {
2718
+ const pieces = split(`\u2699 ${el.text}`, "Mono", ACCENT);
2719
+ pdf.fontSize(9);
2720
+ let total = 0;
2721
+ for (const p of pieces) {
2722
+ pdf.font(p.font);
2723
+ total += pdf.widthOfString(p.text);
2724
+ }
2725
+ if (total <= pageRight - cx) drawRight(pieces, 9);
2726
+ else draw(pieces, cx, { width: pageRight - cx });
2727
+ break;
2865
2728
  }
2866
- if (node.type !== "snippet") return;
2867
- for (const beat of node.beats ?? []) {
2868
- out.push({
2869
- id: beat.id,
2870
- kind: "beat",
2871
- text: strings[beat.id],
2872
- location: segments,
2873
- sceneId: scene.id,
2874
- file
2875
- });
2729
+ }
2730
+ };
2731
+ const leadOf = (k) => k === "scene" ? 24 : k === "block" ? 16 : k === "group" ? 13 : k === "else" ? 9 : 0;
2732
+ const els = doc.elements;
2733
+ let curSid;
2734
+ let curBase = 0;
2735
+ for (let i = 0; i < els.length; i++) {
2736
+ const el = els[i];
2737
+ const next = els[i + 1];
2738
+ const sid = "snippet" in el ? el.snippet : void 0;
2739
+ const sameNext = sid !== void 0 && next !== void 0 && "snippet" in next && next.snippet === sid;
2740
+ if (el.kind === "scene") ensure(90);
2741
+ else if (el.kind === "block") ensure(70);
2742
+ else ensure(leadOf(el.kind) + BODY + LEAD + 14);
2743
+ pdf.y += leadOf(el.kind);
2744
+ const base = "indent" in el ? MARGIN + el.indent * INDENT_STEP : MARGIN;
2745
+ const cx = base + (sid !== void 0 ? EDGE_INSET : 0);
2746
+ const yTop = pdf.y;
2747
+ const pageBefore = pdf.page;
2748
+ drawContent(el, cx);
2749
+ const bottom = pdf.y;
2750
+ const trail = el.kind === "condition" ? TIGHT : el.kind === "scene" ? 12 : el.kind === "block" ? 11 : el.kind === "group" ? 7 : el.kind === "else" ? 4 : sid !== void 0 ? sameNext ? WITHIN2 : BETWEEN : BEAT;
2751
+ if (sid !== void 0) {
2752
+ if (sid !== curSid) {
2753
+ curSid = sid;
2754
+ curBase = "indent" in el ? el.indent : 0;
2876
2755
  }
2877
- });
2756
+ const ex = MARGIN + curBase * INDENT_STEP + EDGE_OFF;
2757
+ const top = pdf.page === pageBefore ? yTop : MARGIN;
2758
+ pdf.strokeColor(EDGE).lineWidth(0.75).moveTo(ex, top).lineTo(ex, bottom + (sameNext ? trail : 0)).stroke();
2759
+ }
2760
+ pdf.y = bottom + trail;
2878
2761
  }
2879
- }
2880
- return out;
2762
+ pdf.end();
2763
+ });
2881
2764
  }
2882
2765
 
2883
- // src/replace.ts
2884
- import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
2885
- import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
2886
- function matcher(opts) {
2887
- if (!opts.query) return null;
2888
- const esc2 = opts.query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2889
- const body = opts.wholeWord ? `\\b${esc2}\\b` : esc2;
2890
- return new RegExp(body, opts.caseSensitive ? "g" : "gi");
2891
- }
2892
- function runReplace(loaded, opts) {
2893
- const plan = { hits: [], writes: [], shards: [], scenes: 0 };
2894
- const re = matcher(opts);
2895
- if (!re) return plan;
2896
- const def = loaded.project.locales.default;
2897
- const locOf = /* @__PURE__ */ new Map();
2898
- const sceneName = /* @__PURE__ */ new Map();
2899
- for (const scene of loaded.scenes) {
2900
- sceneName.set(scene.id, scene.name);
2901
- for (const block of scene.blocks) {
2902
- const segs = [scene.name, block.name];
2903
- walkNodes5(block.children, (node) => {
2904
- if (node.type === "group") {
2905
- if (node.prompt) locOf.set(node.prompt.id, segs);
2906
- return;
2907
- }
2908
- for (const b of node.beats ?? []) locOf.set(b.id, segs);
2909
- });
2910
- }
2766
+ // src/init.ts
2767
+ import { join as join4, basename as basename5, resolve as resolve3 } from "path";
2768
+ import { readdirSync as readdirSync3, existsSync as existsSync2 } from "fs";
2769
+ import { newId, slug, canonicalStringify as canonicalStringify5 } from "@patterkit/core";
2770
+ function runInit(opts) {
2771
+ const dir = resolve3(opts.dir);
2772
+ const name = opts.name?.trim() || basename5(dir).replace(/\.patter$/, "");
2773
+ const fileSlug = slug(name);
2774
+ let existing = [];
2775
+ try {
2776
+ existing = readdirSync3(dir).filter((f) => f.endsWith(".patterproj"));
2777
+ } catch {
2911
2778
  }
2912
- for (let i = 0; i < loaded.locales.length; i++) {
2913
- const shard = loaded.locales[i];
2914
- if (shard.locale !== def || shard.scene === PROJECT_LOCALE_SCENE2) continue;
2915
- const path = loaded.localeFiles[i];
2916
- if (!path) continue;
2917
- let changed = false;
2918
- const merged = { ...shard.strings };
2919
- for (const [id, before] of Object.entries(shard.strings)) {
2920
- if (opts.onlyId && id !== opts.onlyId) continue;
2921
- re.lastIndex = 0;
2922
- if (!re.test(before)) continue;
2923
- const after = before.replace(re, () => opts.replacement);
2924
- if (after === before) continue;
2925
- plan.hits.push({ id, sceneId: shard.scene, location: locOf.get(id) ?? [sceneName.get(shard.scene) ?? shard.scene], before, after });
2926
- merged[id] = after;
2927
- changed = true;
2928
- }
2929
- if (changed) {
2930
- const next = { ...shard, strings: merged };
2931
- plan.writes.push({ path, content: canonicalStringify5(next) });
2932
- plan.shards.push(next);
2933
- plan.scenes++;
2779
+ if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
2780
+ const sceneId = newId("scn");
2781
+ const beatId = newId("T");
2782
+ const project = {
2783
+ schema: "patter/project@0",
2784
+ project: { id: newId("proj"), name },
2785
+ locales: { default: "en", all: ["en"] },
2786
+ ...opts.vcs ? { vcs: opts.vcs } : {},
2787
+ // record the chosen VCS so it can be read / switched later
2788
+ voiced: true
2789
+ };
2790
+ const flow = {
2791
+ schema: "patter/flow@0",
2792
+ scene: {
2793
+ id: sceneId,
2794
+ type: "scene",
2795
+ name: "Start",
2796
+ blocks: [{
2797
+ id: newId("blk"),
2798
+ type: "block",
2799
+ name: "Main",
2800
+ children: [{
2801
+ id: newId("sn"),
2802
+ type: "snippet",
2803
+ beats: [{ id: beatId, kind: "text" }],
2804
+ jump: { to: "END" }
2805
+ }]
2806
+ }]
2934
2807
  }
2935
- }
2936
- return plan;
2937
- }
2938
-
2939
- // src/pack.ts
2940
- import JSZip from "jszip";
2941
- import { readFileSync as readFileSync4 } from "fs";
2942
- import { dirname as dirname4, relative, sep as sep2 } from "path";
2943
- import { parseSource as parseSource4 } from "@patterkit/core";
2944
- var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
2945
- var FIXED_DATE = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
2946
- var ENTRY_OPTS = { date: FIXED_DATE, createFolders: false };
2947
- async function runPack(startPath) {
2948
- const projectFile = findProjectFile(startPath);
2949
- const root = dirname4(projectFile);
2950
- const project = parseSource4(readFileSync4(projectFile, "utf8"));
2951
- const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
2952
- const manifest = {
2953
- schema: "patter/document@0",
2954
- project: { id: project.project.id, name: project.project.name },
2955
- files: files.map((f) => f.rel)
2956
2808
  };
2957
- const zip = new JSZip();
2958
- zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
2959
- for (const f of files) zip.file(f.rel, readFileSync4(f.abs, "utf8"), ENTRY_OPTS);
2960
- return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
2809
+ const locale = {
2810
+ schema: "patter/strings@0",
2811
+ scene: sceneId,
2812
+ locale: "en",
2813
+ default: true,
2814
+ // The starter line is STORY content the writer replaces, so it must read the same however the
2815
+ // project was made: no file paths, no commands. (Front-ends give their own next step - `patter init`
2816
+ // prints "try: patter play <dir>" on the terminal, and Patterpad just opens the scene for editing.)
2817
+ strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
2818
+ };
2819
+ const bundle = opts.bundle ?? "commit";
2820
+ const projectFile = join4(dir, `${fileSlug}.patterproj`);
2821
+ const writes = [
2822
+ { path: projectFile, content: canonicalStringify5(project) },
2823
+ { path: join4(dir, "scenes", "start.patterflow"), content: canonicalStringify5(flow) },
2824
+ { path: join4(dir, "loc", "en", "start.patterloc"), content: canonicalStringify5(locale) },
2825
+ { path: join4(dir, ".editorconfig"), content: EDITORCONFIG },
2826
+ ...vcsConfigWrites(dir, opts.vcs, bundle)
2827
+ ];
2828
+ const collisions = writes.map((w) => w.path).filter((p) => existsSync2(p));
2829
+ if (collisions.length > 0) {
2830
+ throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
2831
+ }
2832
+ return { writes, projectFile, name };
2833
+ }
2834
+ function vcsConfigWrites(dir, vcs, bundle = "commit") {
2835
+ const writes = [{ path: join4(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
2836
+ if (vcs === "git") writes.push({ path: join4(dir, ".gitattributes"), content: GITATTRIBUTES });
2837
+ const ignore = ignoreFileFor(vcs);
2838
+ if (ignore) writes.push({ path: join4(dir, ignore), content: ignoreContent(bundle) });
2839
+ return writes;
2961
2840
  }
2841
+ var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
2842
+ root = true
2962
2843
 
2963
- // src/unpack.ts
2964
- import JSZip2 from "jszip";
2965
- import { join as join5, normalize, isAbsolute as isAbsolute2, resolve as resolve4, sep as sep3 } from "path";
2966
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
2967
- import { parseSource as parseSource5, canonicalStringify as canonicalStringify7 } from "@patterkit/core";
2844
+ [*]
2845
+ charset = utf-8
2846
+ end_of_line = lf
2847
+ insert_final_newline = true
2968
2848
 
2969
- // src/merge.ts
2970
- import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
2971
- var UnsupportedMergeError = class extends Error {
2972
- };
2973
- var eq = (a, b) => canonicalStringify6(a) === canonicalStringify6(b);
2974
- var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2975
- function detectMergeType(file) {
2976
- const s = typeof file.schema === "string" ? file.schema : "";
2977
- if (s.startsWith("patter/flow")) return "flow";
2978
- if (s.startsWith("patter/strings")) return "loc";
2979
- if (s.startsWith("patter/authoring")) return "authoring";
2980
- if (s.startsWith("patter/project")) return "project";
2981
- throw new UnsupportedMergeError(`cannot detect a Patter merge type from schema '${s}'`);
2982
- }
2983
- function runMerge(base, ours, theirs, opts) {
2984
- const type = opts?.type ?? detectMergeType(ours);
2985
- switch (type) {
2986
- case "loc":
2987
- return mergeLoc(base, ours, theirs);
2988
- case "authoring":
2989
- return mergeAuthoring2(base, ours, theirs);
2990
- case "flow":
2991
- return mergeFlow(base, ours, theirs);
2992
- case "project":
2993
- return mergeProject(base, ours, theirs);
2849
+ [*.{patterflow,patterloc,patterx,patterproj}]
2850
+ indent_style = space
2851
+ indent_size = 2
2852
+ `;
2853
+ var GITATTRIBUTES = `# Patter source is UTF-8 + LF text (pinned; never let autocrlf touch it).
2854
+ *.patterflow text eol=lf
2855
+ *.patterloc text eol=lf
2856
+ *.patterx text eol=lf
2857
+ *.patterproj text eol=lf
2858
+
2859
+ # Id-keyed structured merge for Patter source (the 'patter merge' driver).
2860
+ # Register it once per clone (see vcs-setup.md); until then git falls back to a
2861
+ # normal text merge for these - no harm, just not structure-aware.
2862
+ *.patterflow merge=patter
2863
+ *.patterloc merge=patter
2864
+ *.patterx merge=patter
2865
+ *.patterproj merge=patter
2866
+
2867
+ # Generated artifacts (spec \xA711). The compiled bundle is committed but
2868
+ # REGENERATED, never hand-merged - keep ours on conflict and rebuild (the
2869
+ # validate staleness gate catches a stale one). Needs a one-time
2870
+ # 'git config merge.ours.driver true' (see vcs-setup.md).
2871
+ *.patterc text eol=lf merge=ours
2872
+ # The packed document (.patterpack) is a binary zip envelope - kept out of VCS (.gitignore).
2873
+ *.patterpack binary
2874
+ `;
2875
+ function ignoreFileFor(vcs) {
2876
+ switch (vcs) {
2877
+ case "git":
2878
+ return ".gitignore";
2879
+ case "perforce":
2880
+ return ".p4ignore";
2881
+ case "plastic":
2882
+ return "ignore.conf";
2883
+ default:
2884
+ return void 0;
2994
2885
  }
2995
2886
  }
2996
- function deletedKind(base, ours, theirs) {
2997
- const deleted = base !== void 0 && (ours === void 0 || theirs === void 0);
2998
- return deleted ? "delete-vs-edit" : "both-changed";
2999
- }
3000
- function merge3(base, ours, theirs, id, path, conflicts) {
3001
- if (eq(ours, theirs)) return ours;
3002
- if (eq(base, ours)) return theirs;
3003
- if (eq(base, theirs)) return ours;
3004
- conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
3005
- return ours;
3006
- }
3007
- function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
3008
- const out = {};
3009
- for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
3010
- const b = base[k], o = ours[k], t = theirs[k];
3011
- let v;
3012
- if (eq(o, t)) v = o;
3013
- else if (eq(b, o)) v = t;
3014
- else if (eq(b, t)) v = o;
3015
- else {
3016
- const r = resolve5?.(k, b, o, t);
3017
- if (r && r.resolved) v = r.value;
3018
- else {
3019
- conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
3020
- v = o;
3021
- }
3022
- }
3023
- if (v !== void 0) out[k] = v;
2887
+ function ignoreContent(bundle) {
2888
+ const lines = [
2889
+ "# Patter generated artifacts - not source (see vcs-setup.md).",
2890
+ "# Packed document (the send-and-return envelope):",
2891
+ "*.patterpack",
2892
+ "# Unresolved merge sidecar:",
2893
+ "*.patterconflict"
2894
+ ];
2895
+ if (bundle === "ignore") {
2896
+ lines.push("# Compiled bundle (built in CI, not committed under this posture):", "*.patterc");
3024
2897
  }
3025
- return out;
2898
+ return lines.join("\n") + "\n";
3026
2899
  }
3027
- var asMap = (v) => isObj(v) ? v : {};
3028
- var asArr = (v) => Array.isArray(v) ? v : [];
3029
- function setIf(obj, key, val) {
3030
- if (Array.isArray(val) ? val.length > 0 : Object.keys(val).length > 0) obj[key] = val;
2900
+ function vcsSetup(vcs, bundle) {
2901
+ const sections = {
2902
+ git: `## git
2903
+
2904
+ \`.gitattributes\` (already emitted) pins Patter source shards to UTF-8 + LF text
2905
+ and marks the generated artifacts (the \`.patterc\` bundle \`merge=ours\`, the
2906
+ \`.patterpack\` document \`binary\`). \`.gitignore\` (already emitted) keeps the document
2907
+ (and the bundle, under the "ignore" posture) out of source control.
2908
+
2909
+ Recommended pre-commit hook (\`.git/hooks/pre-commit\`, executable):
2910
+
2911
+ #!/bin/sh
2912
+ patter validate || exit 1
2913
+
2914
+ The \`merge=patter\` rules in \`.gitattributes\` are already active; register the
2915
+ drivers once per clone (git config is not repo-tracked) - until then git falls
2916
+ back to a normal text merge for those files:
2917
+
2918
+ git config merge.patter.name "Patter structured merge"
2919
+ git config merge.patter.driver "patter merge %O %A %B -o %A"
2920
+ git config merge.ours.driver true
2921
+
2922
+ git invokes the per-path driver directly (no \`mergetool\` wrapper needed). \`%O %A
2923
+ %B\` are base / ours / theirs; the merged result is written back to \`%A\`. On a
2924
+ conflict \`patter merge\` exits non-zero and writes a \`.patterconflict\` sidecar
2925
+ beside the file, so the merge stays unresolved.
2926
+ `,
2927
+ perforce: `## Perforce
2928
+
2929
+ Add Patter extensions to the typemap (\`p4 typemap\`): source shards and the
2930
+ compiled bundle are TEXT with LF; the packed document is BINARY:
2931
+
2932
+ text //....patterflow
2933
+ text //....patterloc
2934
+ text //....patterx
2935
+ text //....patterproj
2936
+ text //....patterc
2937
+ binary //....patterpack
2938
+
2939
+ On a unicode-mode server, set \`P4CHARSET=utf8\`. For the lock-based workflow
2940
+ (spec: one scene per file = one lock per scene), add \`+l\` to make checkouts
2941
+ exclusive. A \`.p4ignore\` (already emitted) keeps the packed document out of the
2942
+ depot.
2943
+
2944
+ Perforce allows ONE global merge tool, so set \`patter mergetool\` as it - it
2945
+ runs the structured merge for Patter source and hands everything else to your
2946
+ normal tool. Map Perforce's variables into the order BASE THEIRS OURS OUT:
2947
+
2948
+ patter mergetool --fallback "<your merge tool>" %b %t %y %r
2949
+
2950
+ (\`%b\` base, \`%t\` theirs, \`%y\` yours/ours, \`%r\` result - adjust to your P4
2951
+ client's variable names; the argument ORDER is what matters.)
2952
+ `,
2953
+ plastic: `## Plastic SCM (Unity VC)
2954
+
2955
+ Patter source shards are plain UTF-8 text; Plastic handles them as-is. The
2956
+ \`ignore.conf\` (already emitted) keeps the packed document out of the repo. For
2957
+ the lock-based workflow, configure exclusive checkout (lock.conf) for the shard
2958
+ extensions.
2959
+
2960
+ In Preferences > Merge tools, add an external tool (a global entry is fine -
2961
+ the wrapper sniffs the path) with the arguments in BASE THEIRS OURS OUT order:
2962
+
2963
+ patter mergetool --fallback "<your merge tool>" @basefile @sourcefile @destinationfile @output
2964
+ `,
2965
+ svn: `## SVN
2966
+
2967
+ Set auto-props so Patter source keeps LF (\`~/.subversion/config\` or repo config):
2968
+
2969
+ [auto-props]
2970
+ *.patterflow = svn:eol-style=LF
2971
+ *.patterloc = svn:eol-style=LF
2972
+ *.patterx = svn:eol-style=LF
2973
+ *.patterproj = svn:eol-style=LF
2974
+ *.patterc = svn:eol-style=LF
2975
+ *.patterpack = svn:mime-type=application/octet-stream
2976
+
2977
+ SVN's ignore is a directory PROPERTY, not a file - set it from the project root:
2978
+
2979
+ svn propset svn:ignore "*.patterpack${bundle === "ignore" ? "\\n*.patterc" : ""}\\n*.patterconflict" .
2980
+
2981
+ For the lock-based workflow add \`svn:needs-lock\` to the shard patterns.
2982
+
2983
+ SVN allows one global merge tool. Point \`[helpers] merge-tool-cmd\` at a small
2984
+ wrapper script that forwards SVN's four arguments (base theirs mine merged =
2985
+ BASE THEIRS OURS OUT) to \`patter mergetool\`:
2986
+
2987
+ #!/bin/sh
2988
+ exec patter mergetool --fallback "<your merge tool>" "$1" "$2" "$3" "$4"
2989
+ `
2990
+ };
2991
+ const head = vcs ? sections[vcs] : `No VCS selected (\`patter init --vcs git|perforce|plastic|svn\` emits tailored
2992
+ config). Whichever you adopt, the rules are the same: Patter source is UTF-8 +
2993
+ LF text, one scene per file, and \`patter validate\` belongs in your pre-commit /
2994
+ CI. Sections for every supported VCS:
2995
+
2996
+ ${Object.keys(sections).map((k) => sections[k]).join("\n")}`;
2997
+ return `# Version-control setup for this Patter project
2998
+
2999
+ ${head}
3000
+ ${bundleSection(bundle)}`;
3031
3001
  }
3032
- function mergeLoc(base, ours, theirs) {
3033
- const conflicts = [];
3034
- const merged = {};
3035
- for (const f of ["schema", "scene", "locale", "default"]) {
3036
- const v = merge3(base[f], ours[f], theirs[f], "", f, conflicts);
3037
- if (v !== void 0) merged[f] = v;
3038
- }
3039
- merged.strings = mergeMap(asMap(base.strings), asMap(ours.strings), asMap(theirs.strings), "strings", conflicts);
3040
- return { type: "loc", merged, conflicts, warnings: [] };
3002
+ function bundleSection(bundle) {
3003
+ const posture = bundle === "commit" ? `This project COMMITS the compiled \`.patterc\` bundle (\`patter init --bundle ignore\`
3004
+ to build it in CI instead). The bundle is regenerated, never hand-merged: on a
3005
+ conflict, keep ours and re-run \`patter export\`. \`patter validate\` recomputes the
3006
+ bundle's embedded hash from source and FAILS if it is stale, so a forgotten
3007
+ regenerate cannot ship silently.` : `This project IGNORES the compiled \`.patterc\` bundle (built in CI; \`patter init
3008
+ --bundle commit\` to commit it instead). Run \`patter export\` as a build step.`;
3009
+ return `## Compiled bundle & packed document
3010
+
3011
+ ${posture}
3012
+
3013
+ The packed \`.patterpack\` document (\`patter pack\`) is the send-and-return envelope
3014
+ for collaborators without VCS. It is a binary zip and a projection of the
3015
+ shards - NOT source - so it stays out of version control (ignored above); edits
3016
+ return via \`patter unpack\`.
3017
+ `;
3041
3018
  }
3042
- function mergeAuthoring2(base, ours, theirs) {
3043
- const conflicts = [];
3044
- const merged = {};
3045
- merged.schema = merge3(base.schema, ours.schema, theirs.schema, "", "schema", conflicts) ?? ours.schema;
3046
- setIf(merged, "comments", mergeComments(asArr(base.comments), asArr(ours.comments), asArr(theirs.comments)));
3047
- const edits = mergeEdits(asMap(base.edits), asMap(ours.edits), asMap(theirs.edits));
3048
- setIf(merged, "edits", edits);
3049
- const lww = lastWriterWins(asMap(ours.edits), asMap(theirs.edits));
3050
- for (const f of ["writing", "recording", "audio"]) {
3051
- setIf(merged, f, mergeMap(asMap(base[f]), asMap(ours[f]), asMap(theirs[f]), f, conflicts, lww));
3019
+
3020
+ // src/resolve.ts
3021
+ import { walkNodes as walkNodes4, effectiveGameId, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
3022
+ function runResolve(loaded, query) {
3023
+ const entries = indexProject(loaded);
3024
+ const q = query.trim();
3025
+ const ql = q.toLowerCase();
3026
+ const tiers = [
3027
+ (e) => e.id === q,
3028
+ (e) => e.gameId?.toLowerCase() === ql,
3029
+ (e) => e.name?.toLowerCase() === ql,
3030
+ (e) => e.id.toLowerCase().includes(ql) || (e.gameId?.toLowerCase().includes(ql) ?? false) || (e.name?.toLowerCase().includes(ql) ?? false)
3031
+ ];
3032
+ for (const match of tiers) {
3033
+ const hits = entries.filter(match);
3034
+ if (hits.length > 0) return hits;
3052
3035
  }
3053
- setIf(merged, "documentation", mergeMap(asMap(base.documentation), asMap(ours.documentation), asMap(theirs.documentation), "documentation", conflicts));
3054
- setIf(merged, "cut", mergeMap(asMap(base.cut), asMap(ours.cut), asMap(theirs.cut), "cut", conflicts, void 0));
3055
- return { type: "authoring", merged, conflicts, warnings: [] };
3036
+ return [];
3056
3037
  }
3057
- function mergeComments(base, ours, theirs) {
3058
- const byId = /* @__PURE__ */ new Map();
3059
- for (const c of [...base, ...ours, ...theirs]) {
3060
- if (isObj(c) && typeof c.id === "string" && !byId.has(c.id)) byId.set(c.id, c);
3038
+ function runSearch(loaded, query, focus) {
3039
+ const ql = query.trim().toLowerCase();
3040
+ if (!ql) return [];
3041
+ const entries = indexProject(loaded);
3042
+ const inMeta = (e) => (e.gameId?.toLowerCase().includes(ql) ?? false) || (e.name?.toLowerCase().includes(ql) ?? false);
3043
+ const inText = (e) => e.text?.toLowerCase().includes(ql) ?? false;
3044
+ const inTech = (e) => e.id.toLowerCase().includes(ql);
3045
+ const matched = entries.map((e, i) => ({ e, i })).filter(({ e }) => inMeta(e) || inText(e) || inTech(e));
3046
+ if (!focus) {
3047
+ const meta = matched.filter(({ e }) => inMeta(e)).map((m) => m.e);
3048
+ const text = matched.filter(({ e }) => !inMeta(e) && inText(e)).map((m) => m.e);
3049
+ const tech = matched.filter(({ e }) => !inMeta(e) && !inText(e) && inTech(e)).map((m) => m.e);
3050
+ return [...meta, ...text, ...tech];
3061
3051
  }
3062
- return [...byId.values()].sort((a, b) => String(a.ts ?? "").localeCompare(String(b.ts ?? "")));
3063
- }
3064
- function lastWriterWins(oursEdits, theirsEdits) {
3065
- return (id, _b, o, t) => {
3066
- const om = asMap(oursEdits[id]).modifiedAt, tm = asMap(theirsEdits[id]).modifiedAt;
3067
- if (typeof om === "string" && typeof tm === "string") return { resolved: true, value: om >= tm ? o : t };
3068
- return { resolved: false, kind: "no-timestamp" };
3069
- };
3052
+ const cut = focus.fromBeatId ? entries.findIndex((e) => e.id === focus.fromBeatId) : -1;
3053
+ const group = (e, i) => e.sceneId !== focus.sceneId ? 2 : cut < 0 || i >= cut ? 0 : 1;
3054
+ return matched.sort((a, b) => group(a.e, a.i) - group(b.e, b.i) || a.i - b.i).map((m) => m.e);
3070
3055
  }
3071
- function mergeEdits(base, ours, theirs) {
3072
- const out = {};
3073
- for (const id of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
3074
- const o = ours[id], t = theirs[id];
3075
- if (eq(o, t)) {
3076
- if (o !== void 0) out[id] = o;
3077
- continue;
3078
- }
3079
- if (o === void 0) {
3080
- if (t !== void 0) out[id] = t;
3081
- continue;
3082
- }
3083
- if (t === void 0) {
3084
- out[id] = o;
3085
- continue;
3056
+ function runStatusBrowse(loaded, status, dimension = "writing", focus, recordingOverride) {
3057
+ const strings = sourceStrings(loaded);
3058
+ const merged = mergeAuthoring2(loaded);
3059
+ const recording = dimension === "recording";
3060
+ const statusOf = recording ? recordingOverride ?? merged.recording : merged.writing;
3061
+ const ladder = recording ? loaded.project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES4 : loaded.project.writingStatuses ?? DEFAULT_WRITING_STATUSES3;
3062
+ const lowest = ladder[0]?.name;
3063
+ const statusAt = (id) => recording ? effectiveRecording(id, statusOf, merged.rerecord, lowest ?? "") : statusOf.get(id) ?? lowest;
3064
+ const out = [];
3065
+ for (const scene of loaded.scenes) {
3066
+ const file = loaded.sceneFiles[scene.id];
3067
+ for (const block of scene.blocks) {
3068
+ const segments = [scene.name, block.name];
3069
+ const consider = (beat) => {
3070
+ const tracked = recording ? beat.kind === "line" : beat.kind === "line" || beat.kind === "text";
3071
+ if (!tracked) return;
3072
+ if (statusAt(beat.id) !== status) return;
3073
+ out.push({ id: beat.id, kind: "beat", text: strings[beat.id], location: segments, sceneId: scene.id, file });
3074
+ };
3075
+ walkNodes4(block.children, (node) => {
3076
+ if (node.type === "group") {
3077
+ if (node.prompt) consider(node.prompt);
3078
+ return;
3079
+ }
3080
+ for (const beat of node.beats ?? []) consider(beat);
3081
+ });
3086
3082
  }
3087
- out[id] = mergeEditRecord(asMap(o), asMap(t));
3088
3083
  }
3084
+ if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
3089
3085
  return out;
3090
3086
  }
3091
- function mergeEditRecord(o, t) {
3092
- const om = typeof o.modifiedAt === "string" ? o.modifiedAt : "";
3093
- const tm = typeof t.modifiedAt === "string" ? t.modifiedAt : "";
3094
- const newer = om >= tm ? o : t;
3095
- const ol = asMap(o.localisedAt), tl = asMap(t.localisedAt);
3096
- const loc = {};
3097
- for (const k of /* @__PURE__ */ new Set([...Object.keys(ol), ...Object.keys(tl)])) {
3098
- const a = typeof ol[k] === "string" ? ol[k] : "";
3099
- const b = typeof tl[k] === "string" ? tl[k] : "";
3100
- loc[k] = a >= b ? ol[k] ?? tl[k] : tl[k] ?? ol[k];
3101
- }
3102
- const merged = { ...newer };
3103
- if (Object.keys(loc).length > 0) merged.localisedAt = loc;
3104
- return merged;
3105
- }
3106
- function mergeKeyedByName(base, ours, theirs, path, conflicts) {
3107
- const byName = (arr) => {
3108
- const m = {};
3109
- for (const p of arr) if (isObj(p) && typeof p.name === "string") m[p.name] = p;
3110
- return m;
3087
+ function listProjectTags(loaded) {
3088
+ const counts = /* @__PURE__ */ new Map();
3089
+ const add = (tags) => {
3090
+ for (const t of tags ?? []) counts.set(t, (counts.get(t) ?? 0) + 1);
3111
3091
  };
3112
- const merged = mergeMap(byName(base), byName(ours), byName(theirs), path, conflicts);
3113
- return Object.keys(merged).sort().map((name) => merged[name]);
3114
- }
3115
- function mergeProject(base, ours, theirs) {
3116
- const conflicts = [];
3117
- const merged = {};
3118
- for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
3119
- if (k === "properties" || k === "cast") {
3120
- const v = mergeKeyedByName(asArr(base[k]), asArr(ours[k]), asArr(theirs[k]), k, conflicts);
3121
- if (v.length > 0) merged[k] = v;
3122
- } else if (k === "gameDataFields") {
3123
- const v = mergeGameDataFields(asMap(base[k]), asMap(ours[k]), asMap(theirs[k]), conflicts);
3124
- if (Object.keys(v).length > 0) merged[k] = v;
3125
- } else if (k === "locales") {
3126
- merged.locales = mergeLocales(asMap(base.locales), asMap(ours.locales), asMap(theirs.locales), conflicts);
3127
- } else {
3128
- const v = merge3(base[k], ours[k], theirs[k], "", k, conflicts);
3129
- if (v !== void 0) merged[k] = v;
3092
+ for (const scene of loaded.scenes) {
3093
+ add(scene.tags);
3094
+ for (const block of scene.blocks) {
3095
+ add(block.tags);
3096
+ walkNodes4(block.children, (node) => {
3097
+ add(node.tags);
3098
+ if (node.type === "group") {
3099
+ add(node.prompt?.tags);
3100
+ return;
3101
+ }
3102
+ for (const beat of node.beats ?? []) add(beat.tags);
3103
+ });
3130
3104
  }
3131
3105
  }
3132
- return { type: "project", merged, conflicts, warnings: [] };
3133
- }
3134
- function mergeGameDataFields(b, o, t, conflicts) {
3135
- const out = {};
3136
- for (const kind of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
3137
- const v = mergeKeyedByName(asArr(b[kind]), asArr(o[kind]), asArr(t[kind]), `gameDataFields.${kind}`, conflicts);
3138
- if (v.length > 0) out[kind] = v;
3139
- }
3140
- return out;
3106
+ return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name));
3141
3107
  }
3142
- function mergeLocales(b, o, t, conflicts) {
3143
- const out = {};
3144
- const def = merge3(b.default, o.default, t.default, "", "locales.default", conflicts);
3145
- if (def !== void 0) out.default = def;
3146
- const all = [];
3147
- for (const arr of [asArr(b.all), asArr(o.all), asArr(t.all)]) {
3148
- for (const x of arr) if (typeof x === "string" && !all.includes(x)) all.push(x);
3108
+ function runTagBrowse(loaded, tag, focus) {
3109
+ if (!tag) return [];
3110
+ const strings = sourceStrings(loaded);
3111
+ const has = (tags) => !!tags && tags.includes(tag);
3112
+ const out = [];
3113
+ for (const scene of loaded.scenes) {
3114
+ const file = loaded.sceneFiles[scene.id];
3115
+ if (has(scene.tags)) out.push({ id: scene.id, kind: "scene", name: scene.name, gameId: effectiveGameId(scene), location: [scene.name], sceneId: scene.id, file });
3116
+ for (const block of scene.blocks) {
3117
+ const segments = [scene.name, block.name];
3118
+ if (has(block.tags)) out.push({ id: block.id, kind: "block", name: block.name, gameId: effectiveGameId(block), location: segments, sceneId: scene.id, file });
3119
+ walkNodes4(block.children, (node) => {
3120
+ if (has(node.tags)) {
3121
+ const preview = node.type === "group" ? node.prompt ? strings[node.prompt.id] : void 0 : strings[node.beats?.[0]?.id ?? ""];
3122
+ out.push({ id: node.id, kind: node.type, text: preview, location: segments, sceneId: scene.id, file });
3123
+ }
3124
+ if (node.type === "group") {
3125
+ if (node.prompt && has(node.prompt.tags)) out.push({ id: node.prompt.id, kind: "beat", text: strings[node.prompt.id], location: segments, sceneId: scene.id, file });
3126
+ return;
3127
+ }
3128
+ for (const beat of node.beats ?? []) if (has(beat.tags)) out.push({ id: beat.id, kind: "beat", text: strings[beat.id], location: segments, sceneId: scene.id, file });
3129
+ });
3130
+ }
3149
3131
  }
3150
- out.all = all;
3151
- return out;
3152
- }
3153
- var CHILD_KEYS = ["blocks", "children", "beats"];
3154
- function toTree(raw) {
3155
- const childKey = CHILD_KEYS.find((k) => Array.isArray(raw[k]));
3156
- const fields = {};
3157
- for (const [k, v] of Object.entries(raw)) if (k !== childKey) fields[k] = v;
3158
- const children = childKey ? raw[childKey].filter(isObj).map(toTree) : [];
3159
- return { id: typeof raw.id === "string" ? raw.id : "", fields, childKey, children };
3160
- }
3161
- function fromTree(n) {
3162
- const out = { ...n.fields };
3163
- if (n.childKey) out[n.childKey] = n.children.map(fromTree);
3132
+ if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
3164
3133
  return out;
3165
3134
  }
3166
- function mergeFlow(base, ours, theirs) {
3167
- const conflicts = [];
3168
- const warnings = [];
3169
- const merged = {};
3170
- merged.schema = merge3(base.schema, ours.schema, theirs.schema, "", "schema", conflicts) ?? ours.schema;
3171
- const bScene = isObj(base.scene) ? toTree(base.scene) : void 0;
3172
- const oScene = toTree(asMap(ours.scene));
3173
- const tScene = isObj(theirs.scene) ? toTree(theirs.scene) : void 0;
3174
- const mergedScene = mergeNode(bScene, oScene, tScene, "scene", conflicts, warnings);
3175
- merged.scene = fromTree(mergedScene);
3176
- checkDuplicateIds(mergedScene, conflicts);
3177
- return { type: "flow", merged, conflicts, warnings };
3135
+ function runPropertyUsage(loaded, query, focus) {
3136
+ const parsed = parsePropertyQuery(query);
3137
+ if (!parsed) return [];
3138
+ const { regexes, value } = parsed;
3139
+ const strings = sourceStrings(loaded);
3140
+ const refs = (src) => !!src && regexes.some((re) => re.test(src));
3141
+ const valueOk = (src) => !value || src.toLowerCase().includes(value);
3142
+ const out = [];
3143
+ for (const scene of loaded.scenes) {
3144
+ const file = loaded.sceneFiles[scene.id];
3145
+ const push = (id, kind, segments, usage) => {
3146
+ out.push({ id, kind, text: usage, location: segments, sceneId: scene.id, file });
3147
+ };
3148
+ for (const e of scene.onEntry ?? []) {
3149
+ const src = `${e.target} = ${e.value}`;
3150
+ if ((refs(e.target) || refs(e.value)) && valueOk(src)) push(scene.id, "scene", [scene.name], `on entry: set ${src}`);
3151
+ }
3152
+ for (const block of scene.blocks) {
3153
+ const segments = [scene.name, block.name];
3154
+ walkNodes4(block.children, (node) => {
3155
+ if (refs(node.condition) && valueOk(node.condition)) push(node.id, node.type, segments, `if ${node.condition}`);
3156
+ if (node.type === "group") {
3157
+ if (node.prompt && refs(strings[node.prompt.id]) && valueOk(strings[node.prompt.id])) push(node.prompt.id, "beat", segments, strings[node.prompt.id]);
3158
+ return;
3159
+ }
3160
+ for (const phase of ["onEnter", "onExit"]) {
3161
+ for (const e of node[phase] ?? []) {
3162
+ const src = `${e.target} = ${e.value}`;
3163
+ if ((refs(e.target) || refs(e.value)) && valueOk(src)) push(node.id, "snippet", segments, `${phase === "onEnter" ? "on enter" : "on exit"}: set ${src}`);
3164
+ }
3165
+ }
3166
+ for (const beat of node.beats ?? []) {
3167
+ if (refs(strings[beat.id]) && valueOk(strings[beat.id])) push(beat.id, "beat", segments, strings[beat.id]);
3168
+ }
3169
+ });
3170
+ }
3171
+ }
3172
+ if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
3173
+ return out;
3178
3174
  }
3179
- function mergeNode(b, o, t, path, conflicts, warnings) {
3180
- const fields = mergeFields(b?.fields ?? {}, o.fields, t?.fields ?? {}, o.id, path, conflicts);
3181
- const childKey = o.childKey ?? t?.childKey ?? b?.childKey;
3182
- const children = childKey ? mergeChildren(b?.children ?? [], o.children, t?.children ?? [], `${path}.${childKey}`, conflicts, warnings) : [];
3183
- return { id: o.id, fields, childKey, children };
3175
+ function parsePropertyQuery(query) {
3176
+ const m = /^@?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)(.*)$/.exec(query.trim());
3177
+ if (!m) return null;
3178
+ const prop = m[1];
3179
+ const value = m[2].replace(/^[\s=<>!]+/, "").replace(/^["']|["']$/g, "").trim().toLowerCase() || void 0;
3180
+ const esc2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3181
+ const regexes = prop.includes(".") ? [new RegExp(`@${esc2(prop)}\\b`, "i")] : [new RegExp(`@${esc2(prop)}\\b`, "i"), new RegExp(`@patter\\.${esc2(prop)}\\b`, "i")];
3182
+ return { regexes, value };
3184
3183
  }
3185
- function mergeFields(b, o, t, id, path, conflicts) {
3186
- const out = {};
3187
- for (const k of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
3188
- const v = merge3(b[k], o[k], t[k], id, `${path}.${k}`, conflicts);
3189
- if (v !== void 0) out[k] = v;
3184
+ var indexCache = null;
3185
+ function sameRefs(a, b) {
3186
+ if (a.length !== b.length) return false;
3187
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
3188
+ return true;
3189
+ }
3190
+ function indexProject(loaded) {
3191
+ if (indexCache && indexCache.loaded === loaded && sameRefs(indexCache.scenes, loaded.scenes) && sameRefs(indexCache.locales, loaded.locales)) {
3192
+ return indexCache.entries;
3190
3193
  }
3191
- return out;
3194
+ const entries = buildIndex(loaded);
3195
+ indexCache = { loaded, scenes: [...loaded.scenes], locales: [...loaded.locales], entries };
3196
+ return entries;
3192
3197
  }
3193
- function mergeChildren(B, O, T, path, conflicts, warnings) {
3194
- const Bm = idMap(B), Om = idMap(O), Tm = idMap(T);
3195
- const Bset = new Set(Bm.keys()), Oset = new Set(Om.keys()), Tset = new Set(Tm.keys());
3196
- const merged = /* @__PURE__ */ new Map();
3197
- const here = (id) => `${path}[${id}]`;
3198
- for (const id of /* @__PURE__ */ new Set([...Bm.keys(), ...Om.keys(), ...Tm.keys()])) {
3199
- const b = Bm.get(id), o = Om.get(id), t = Tm.get(id);
3200
- const inB = Bset.has(id), inO = Oset.has(id), inT = Tset.has(id);
3201
- if (inO && inT) {
3202
- if (!inB && !eq(fromTree(o), fromTree(t))) {
3203
- conflicts.push({ id, path: here(id), base: void 0, ours: fromTree(o), theirs: fromTree(t), kind: "added-both" });
3204
- merged.set(id, o);
3205
- } else {
3206
- merged.set(id, mergeNode(b, o, t, here(id), conflicts, warnings));
3207
- }
3208
- } else if (inO && !inT) {
3209
- if (inB && !eq(fromTree(o), fromTree(b))) {
3210
- conflicts.push({ id, path: here(id), base: fromTree(b), ours: fromTree(o), theirs: void 0, kind: "delete-vs-edit" });
3211
- merged.set(id, o);
3212
- } else if (!inB) {
3213
- merged.set(id, o);
3214
- }
3215
- } else if (!inO && inT) {
3216
- if (inB && !eq(fromTree(t), fromTree(b))) {
3217
- conflicts.push({ id, path: here(id), base: fromTree(b), ours: void 0, theirs: fromTree(t), kind: "delete-vs-edit" });
3218
- } else if (!inB) {
3219
- merged.set(id, t);
3220
- }
3198
+ function buildIndex(loaded) {
3199
+ const out = [];
3200
+ const strings = sourceStrings(loaded);
3201
+ for (const scene of loaded.scenes) {
3202
+ const file = loaded.sceneFiles[scene.id];
3203
+ out.push({
3204
+ id: scene.id,
3205
+ kind: "scene",
3206
+ name: scene.name,
3207
+ gameId: effectiveGameId(scene),
3208
+ location: [scene.name],
3209
+ sceneId: scene.id,
3210
+ file
3211
+ });
3212
+ for (const block of scene.blocks) {
3213
+ const segments = [scene.name, block.name];
3214
+ out.push({
3215
+ id: block.id,
3216
+ kind: "block",
3217
+ name: block.name,
3218
+ gameId: effectiveGameId(block),
3219
+ location: segments,
3220
+ sceneId: scene.id,
3221
+ file
3222
+ });
3223
+ walkNodes4(block.children, (node) => {
3224
+ out.push({
3225
+ id: node.id,
3226
+ kind: node.type,
3227
+ location: segments,
3228
+ sceneId: scene.id,
3229
+ file
3230
+ });
3231
+ if (node.type === "group" && node.prompt) {
3232
+ out.push({
3233
+ id: node.prompt.id,
3234
+ kind: "beat",
3235
+ text: strings[node.prompt.id],
3236
+ location: segments,
3237
+ sceneId: scene.id,
3238
+ file
3239
+ });
3240
+ return;
3241
+ }
3242
+ if (node.type !== "snippet") return;
3243
+ for (const beat of node.beats ?? []) {
3244
+ out.push({
3245
+ id: beat.id,
3246
+ kind: "beat",
3247
+ text: strings[beat.id],
3248
+ location: segments,
3249
+ sceneId: scene.id,
3250
+ file
3251
+ });
3252
+ }
3253
+ });
3221
3254
  }
3222
3255
  }
3223
- return orderChildren(merged, B, O, T, path, conflicts, warnings);
3256
+ return out;
3224
3257
  }
3225
- function orderChildren(merged, B, O, T, path, conflicts, warnings) {
3226
- const Bids = B.map((n) => n.id), Oids = O.map((n) => n.id), Tids = T.map((n) => n.id);
3227
- const Oidset = new Set(Oids);
3228
- const common = new Set(Bids.filter((id) => merged.has(id)));
3229
- const restrict = (ids) => ids.filter((id) => common.has(id));
3230
- const relB = restrict(Bids), relO = restrict(Oids), relT = restrict(Tids);
3231
- const fullO = relO.length === common.size, fullT = relT.length === common.size;
3232
- let commonOrder;
3233
- if (!fullO && !fullT) commonOrder = relB;
3234
- else if (!fullO) commonOrder = relT;
3235
- else if (!fullT) commonOrder = relO;
3236
- else if (arrEq(relO, relB)) commonOrder = relT;
3237
- else if (arrEq(relT, relB)) commonOrder = relO;
3238
- else if (arrEq(relO, relT)) commonOrder = relO;
3239
- else {
3240
- conflicts.push({ id: "", path, base: relB, ours: relO, theirs: relT, kind: "moved" });
3241
- commonOrder = relO;
3258
+
3259
+ // src/replace.ts
3260
+ import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3261
+ import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
3262
+ function matcher(opts) {
3263
+ if (!opts.query) return null;
3264
+ const esc2 = opts.query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3265
+ const body = opts.wholeWord ? `\\b${esc2}\\b` : esc2;
3266
+ return new RegExp(body, opts.caseSensitive ? "g" : "gi");
3267
+ }
3268
+ function runReplace(loaded, opts) {
3269
+ const plan = { hits: [], writes: [], shards: [], scenes: 0 };
3270
+ const re = matcher(opts);
3271
+ if (!re) return plan;
3272
+ const def = loaded.project.locales.default;
3273
+ const locOf = /* @__PURE__ */ new Map();
3274
+ const sceneName = /* @__PURE__ */ new Map();
3275
+ for (const scene of loaded.scenes) {
3276
+ sceneName.set(scene.id, scene.name);
3277
+ for (const block of scene.blocks) {
3278
+ const segs = [scene.name, block.name];
3279
+ walkNodes5(block.children, (node) => {
3280
+ if (node.type === "group") {
3281
+ if (node.prompt) locOf.set(node.prompt.id, segs);
3282
+ return;
3283
+ }
3284
+ for (const b of node.beats ?? []) locOf.set(b.id, segs);
3285
+ });
3286
+ }
3242
3287
  }
3243
- const anchorIn = (ids, id) => {
3244
- for (let i = ids.indexOf(id) - 1; i >= 0; i--) if (common.has(ids[i])) return ids[i];
3245
- return null;
3246
- };
3247
- const byAnchor = /* @__PURE__ */ new Map();
3248
- const group = (a) => {
3249
- let g = byAnchor.get(a);
3250
- if (!g) {
3251
- g = { ours: [], theirs: [] };
3252
- byAnchor.set(a, g);
3288
+ for (let i = 0; i < loaded.locales.length; i++) {
3289
+ const shard = loaded.locales[i];
3290
+ if (shard.locale !== def || shard.scene === PROJECT_LOCALE_SCENE2) continue;
3291
+ const path = loaded.localeFiles[i];
3292
+ if (!path) continue;
3293
+ let changed = false;
3294
+ const merged = { ...shard.strings };
3295
+ for (const [id, before] of Object.entries(shard.strings)) {
3296
+ if (opts.onlyId && id !== opts.onlyId) continue;
3297
+ re.lastIndex = 0;
3298
+ if (!re.test(before)) continue;
3299
+ const after = before.replace(re, () => opts.replacement);
3300
+ if (after === before) continue;
3301
+ plan.hits.push({ id, sceneId: shard.scene, location: locOf.get(id) ?? [sceneName.get(shard.scene) ?? shard.scene], before, after });
3302
+ merged[id] = after;
3303
+ changed = true;
3304
+ }
3305
+ if (changed) {
3306
+ const next = { ...shard, strings: merged };
3307
+ plan.writes.push({ path, content: canonicalStringify6(next) });
3308
+ plan.shards.push(next);
3309
+ plan.scenes++;
3253
3310
  }
3254
- return g;
3255
- };
3256
- for (const n of O) if (merged.has(n.id) && !common.has(n.id)) group(anchorIn(Oids, n.id)).ours.push(n.id);
3257
- for (const n of T) if (merged.has(n.id) && !common.has(n.id) && !Oidset.has(n.id)) group(anchorIn(Tids, n.id)).theirs.push(n.id);
3258
- const out = [];
3259
- const emit = (anchor) => {
3260
- const g = byAnchor.get(anchor);
3261
- if (!g) return;
3262
- if (g.ours.length > 0 && g.theirs.length > 0) warnings.push({ id: anchor ?? "", path, message: "both sides inserted here; kept ours-then-theirs - review the order" });
3263
- out.push(...g.ours, ...g.theirs);
3264
- };
3265
- emit(null);
3266
- for (const id of commonOrder) {
3267
- out.push(id);
3268
- emit(id);
3269
3311
  }
3270
- return out.map((id) => merged.get(id));
3312
+ return plan;
3271
3313
  }
3272
- function checkDuplicateIds(root, conflicts) {
3273
- const seen = /* @__PURE__ */ new Set(), dup = /* @__PURE__ */ new Set();
3274
- const walk = (n) => {
3275
- if (n.id) {
3276
- if (seen.has(n.id)) dup.add(n.id);
3277
- else seen.add(n.id);
3278
- }
3279
- n.children.forEach(walk);
3314
+
3315
+ // src/pack.ts
3316
+ import JSZip from "jszip";
3317
+ import { readFileSync as readFileSync4 } from "fs";
3318
+ import { dirname as dirname4, relative, sep as sep2 } from "path";
3319
+ import { parseSource as parseSource4 } from "@patterkit/core";
3320
+ var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
3321
+ var FIXED_DATE = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
3322
+ var ENTRY_OPTS = { date: FIXED_DATE, createFolders: false };
3323
+ async function runPack(startPath) {
3324
+ const projectFile = findProjectFile(startPath);
3325
+ const root = dirname4(projectFile);
3326
+ const project = parseSource4(readFileSync4(projectFile, "utf8"));
3327
+ const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
3328
+ if (unresolved.length) {
3329
+ throw new Error(
3330
+ `${unresolved.length} unresolved merge conflict(s) - resolve them before packing, or the recipient gets your side of a disagreement with no sign of it:
3331
+ ` + unresolved.map((i) => ` ${i.file}`).join("\n")
3332
+ );
3333
+ }
3334
+ const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
3335
+ const manifest = {
3336
+ schema: "patter/document@0",
3337
+ project: { id: project.project.id, name: project.project.name },
3338
+ files: files.map((f) => f.rel)
3280
3339
  };
3281
- walk(root);
3282
- for (const id of dup) conflicts.push({ id, path: "scene", base: void 0, ours: void 0, theirs: void 0, kind: "structural" });
3340
+ const zip = new JSZip();
3341
+ zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
3342
+ for (const f of files) zip.file(f.rel, readFileSync4(f.abs, "utf8"), ENTRY_OPTS);
3343
+ return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
3283
3344
  }
3284
- var idMap = (nodes) => new Map(nodes.filter((n) => n.id).map((n) => [n.id, n]));
3285
- var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
3286
3345
 
3287
3346
  // src/unpack.ts
3347
+ import JSZip2 from "jszip";
3348
+ import { join as join5, normalize, isAbsolute as isAbsolute2, resolve as resolve4, sep as sep3 } from "path";
3349
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
3350
+ import { parseSource as parseSource5, canonicalStringify as canonicalStringify7 } from "@patterkit/core";
3288
3351
  var MANIFEST = "patter.manifest.json";
3289
3352
  var UnsafeEntryError = class extends Error {
3290
3353
  };
@@ -3347,10 +3410,17 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3347
3410
  shards.push({ path: rel, added: true });
3348
3411
  continue;
3349
3412
  }
3350
- const oursObj = parseSource5(readFileSync5(outPath, "utf8"));
3351
- const theirsObj = parseSource5(theirText);
3413
+ const readSide = (side, text) => {
3414
+ try {
3415
+ return parseSource5(text);
3416
+ } catch (e) {
3417
+ throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
3418
+ }
3419
+ };
3420
+ const oursObj = readSide("ours", readFileSync5(outPath, "utf8"));
3421
+ const theirsObj = readSide("theirs", theirText);
3352
3422
  const baseText = base.get(rel);
3353
- const baseObj = baseText !== void 0 ? parseSource5(baseText) : {};
3423
+ const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
3354
3424
  const result = runMerge(baseObj, oursObj, theirsObj);
3355
3425
  writes.push({ path: outPath, content: canonicalStringify7(result.merged) });
3356
3426
  if (result.conflicts.length > 0) {
@@ -3376,6 +3446,8 @@ function containedWrite(dir, name) {
3376
3446
  return full;
3377
3447
  }
3378
3448
  export {
3449
+ AUTHORING_HANDLED,
3450
+ CONFLICT_SIDECAR,
3379
3451
  SHARD_EXTENSIONS,
3380
3452
  UnsafeEntryError,
3381
3453
  UnsupportedMergeError,
@@ -3430,6 +3502,7 @@ export {
3430
3502
  sceneIdForShard,
3431
3503
  scriptToDocx,
3432
3504
  scriptToPdf,
3505
+ sidecarIssues,
3433
3506
  vcsConfigWrites,
3434
3507
  voiceScriptToXlsx,
3435
3508
  xlsxToCatalog