@patterkit/ops 0.3.5 → 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.
- package/dist/index.d.ts +25 -5
- package/dist/index.js +1873 -1799
- package/package.json +5 -5
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,251 +226,626 @@ function applyWrites(writes) {
|
|
|
225
226
|
|
|
226
227
|
// src/validate.ts
|
|
227
228
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
293
|
+
return out;
|
|
276
294
|
}
|
|
277
|
-
var
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
issues.push(...hit.issues);
|
|
364
|
+
if (o === void 0) {
|
|
365
|
+
if (t !== void 0) out[id] = t;
|
|
290
366
|
continue;
|
|
291
367
|
}
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
305
|
-
issues.push(...fileIssues);
|
|
372
|
+
out[id] = mergeEditRecord(asMap(o), asMap(t));
|
|
306
373
|
}
|
|
307
|
-
return
|
|
374
|
+
return out;
|
|
308
375
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
342
|
-
const
|
|
343
|
-
|
|
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
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
var PLAYABLE_RUNTIME_JS = '"use strict";var Patterplay=(()=>{var A=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var me=Object.getOwnPropertyNames;var Se=Object.prototype.hasOwnProperty;var ye=(n,e)=>{for(var t in e)A(n,t,{get:e[t],enumerable:!0})},ve=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of me(e))!Se.call(n,s)&&s!==t&&A(n,s,{get:()=>e[s],enumerable:!(r=he(e,s))||r.enumerable});return n};var be=n=>ve(A({},"__esModule",{value:!0}),n);var Ge={};ye(Ge,{Engine:()=>O,Flow:()=>w,buildTagIndex:()=>B,describeBundle:()=>pe,effectiveGameData:()=>ue,gameDataFields:()=>de,gameDataValue:()=>_});function S(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:S(n[2])};case"bin":return{kind:"binary",op:n[1],left:S(n[2]),right:S(n[3])};case"call":{let e=n.slice(2).map(S);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var p=class extends Error{constructor(e){super(e),this.name="EvalError"}};function C(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 p(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{let o=t.functions[i.name];if(!o)throw new p(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new p("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 p(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new p(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let c=s(i.left);if(typeof c!="boolean")throw new p(`\'and\' requires boolean operands, left is ${typeof c}`);if(!c)return!1;let l=s(i.right);if(typeof l!="boolean")throw new p(`\'and\' requires boolean operands, right is ${typeof l}`);return l}if(i.op==="or"){let c=s(i.left);if(typeof c!="boolean")throw new p(`\'or\' requires boolean operands, left is ${typeof c}`);if(c)return!0;let l=s(i.right);if(typeof l!="boolean")throw new p(`\'or\' requires boolean operands, right is ${typeof l}`);return l}let o=s(i.left),a=s(i.right);switch(i.op){case"==":return J(o,a);case"!=":return!J(o,a);case">":return y(o,a,">"),o>a;case">=":return y(o,a,">="),o>=a;case"<":return y(o,a,"<"),o<a;case"<=":return y(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new p(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return y(o,a,"-"),o-a;case"*":return y(o,a,"*"),o*a;case"/":if(y(o,a,"/"),a===0)throw new p("division by zero");return o/a}}}};return s(n)}function J(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 y(n,e,t){if(typeof n!="number"||typeof e!="number")throw new p(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}var we={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},xe=[we];function z(n,e,t){let r=t?.countingCalls??xe;return E(n,t?.want??!0,e,r)}function E(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=E(n.left,e,t,r),i=E(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 E(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 G=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??Y(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])=>X(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 X(n,e,t,r){return{name:r??n.name.toLowerCase(),type:n.type,value:e,default:n.default??Y(n),...n.values!==void 0?{values:n.values}:{},writable:t??n.writable??!0}}var F=1,x=class{scopes=new Map;defineOwned(e,t){return this.mountOwned(e,new G(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,...X(s,r.resolver.get(s.name.toLowerCase()),this.foreignWritable(r,s.name.toLowerCase()))});return e}toEvalContext(e){let t={};for(let[r,s]of this.scopes)t[r]=s.kind==="owned"?s.bag.values:s.resolver;return{scopes:t,host: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});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:F,scopes:this.save()}}loadFragment(e){if(e.version!==F)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${F})`);this.load(e.scopes)}assertFree(e){if(this.scopes.has(e))throw new Error(`scope \'@${e}\' is already registered`)}};function Y(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[]}}function k(n){return n.ctx.host??{}}var T={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new p("random(a, b) requires exactly 2 arguments");let t=k(e).nextRandom;if(!t)throw new p("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 p("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new p("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:Q("check_flags"),eval(n,e){let t=Z(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("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:Q("set_flags"),eval(n,e){let t=[...Z(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("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:I("visits"),eval:(n,e)=>k(e).visits?.(D(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:I("seen"),eval:(n,e)=>(k(e).visits?.(D(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:I("patter_visits"),eval:(n,e)=>k(e).patterVisits?.(D(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:I("patter_seen"),eval:(n,e)=>(k(e).patterVisits?.(D(n,e,"patter_seen"))??0)>0}}};function V(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 D(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new p(`${t}(id) requires a string node id`);return r}function I(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 Z(n,e,t){if(!n)throw new p(`${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 p(`${t}() first argument must be a flags property`)}function Q(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 ke=/^@[A-Za-z0-9_.]+$/;function*Ce(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:ke.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 Ee(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function De(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function Ie(n){let e="",t=!1;for(let r of n){if(De(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ee(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?Ie(r):n}function te(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of Ce(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?"":Ee(s)}return t}function b(n,e){for(let t of n){e(t);let r=t.children;r&&b(r,e)}}function Re(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function g(n){let e=n.gameId?.trim();return e||Re(n.name)}function M(n){return`cast:${n}`}var j={open:"[",close:"]"},re="SFX";function R(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function B(n){let e=new Map,t=(r,s)=>{let i=R([...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,R([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=R(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=R([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var ne=new WeakMap,O=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 l=new Map;for(let d of e.cast??[])d.displayName&&l.set(d.name,d.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let u=new Map,f=new Map,H=new Map;for(let[d,m]of Object.entries(e.scenes)){this.sceneGameIdToId.set(g(m),d);let v=new Map;for(let h of m.blocks)f.set(h.id,{sceneId:d}),H.set(h.id,h),v.set(g(h),h.id),b(h.children,K=>u.set(K.id,K));this.blockGameIdToId.set(d,v)}let q=e.properties??[],P=q.filter(d=>d.shared??!0).map(ie),ge=q.filter(d=>!(d.shared??!0)).map(ie),fe=new Set(P.map(d=>d.name.toLowerCase())),N=new x().defineOwned("patter",P),U=new Set;if(t.world){let d=e.scopeRegistry?.scopes.find(v=>v.token==="world"),m=(d?.declarations??[]).map(oe);N.defineForeign("world",t.world,m,d?.writable??!0),U.add("world")}for(let d of e.scopeRegistry?.scopes??[]){if(U.has(d.token))continue;let m=(d.declarations??[]).map(oe);N.defineForeign(d.token,Pe(d.declarations??[]),m,d.writable??!0)}let W=new Map;for(let[d,m]of Object.entries(e.scenes)){let v=new Set((m.sceneProps??[]).filter(h=>h.shared??!1).map(h=>h.name.toLowerCase()));W.set(d,v)}this.host={bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:u,blockIndex:f,blockById:H,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:B(e),shared:N,patterSharedDecls:P,patterLocalDecls:ge,patterSharedNames:fe,sceneSharedNames:W,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??j).open,captionClose:(e.closedCaptions??j).close,captionCharacter:e.closedCaptions?.character||re,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 w(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?g(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?g(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)se(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 se(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...g(e)?{gameId:g(e)}:{},name:e.name,...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...g(t)?{gameId:g(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)b(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[M(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,value:this.getProperty(`@${e.name}`),default:Be(e)}))}splitShared(e){let t=this.host.refSplitCache.get(e);if(t||(t=V(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:ce(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 le(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 w(t,this.host,this.defaultSeed);s.restore(r),this.flowsById.set(t,s)}}},w=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}}}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:ce(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=le(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(f=>f.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let f=e[s-1];return r.last=f.id,f}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,l=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&l>=c&&l++;let u=a[l];return a.splice(l,1),r.last=u,e.find(f=>f.id===u)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(u=>u.id));let l=new Set(r.bag);if(s=e.filter(u=>l.has(u.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(u=>u.id===r.last)??null:null}let i=-1,a=s.map(l=>{let u=this.specScore(l);return u>i&&(i=u),{c:l,s:u}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(f=>f.id===r.last):-1,u=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&u>=l&&u++,c=a[u]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0):0}matchedSpec(e,t){return z(e,s=>ae(C(s,this.evalCtx,T)),{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?ae(this.evalExpr(e.condition)):!0}evalExpr(e){return C(this.conditionAst(e),this.evalCtx,T)}conditionAst(e){let t=ne.get(e);return t||(t=S(e.ast),ne.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 te(e,t=>this.getProperty(t))}stripCaptions(e){return ee(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 b(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=M(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=V(e,r=>r==="scene"||this.host.shared.has(r)),this.host.refSplitCache.set(e,t)),t}freshLocal(){return new x().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]=L(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]=L(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]=L(r))}}};function se(n,e){b(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function ce(n){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 le(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function ie(n){return{name:n.name,type:n.type,values:n.values,default:n.default}}function Be(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]??"";default:return!1}}function oe(n){return{name:n.name,type:n.type,values:n.values,default:n.default,writable:n.writable}}function Oe(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]??""}}function Pe(n){let e=r=>r.toLowerCase(),t=new Map;for(let r of n)t.set(e(r.name),Oe(r));return{get:r=>t.get(e(r)),set:(r,s)=>{t.set(e(r),s)}}}function L(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]??""}}function ae(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Ne=(n,e)=>n.shared??e;function $(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Ne(n,e)}}function Ae(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Fe(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 pe(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=g(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:g(c),name:c.name}))});for(let c of o.blocks)Fe(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>$(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=>$(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(Ae)}));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=>$(o,!0)),scene:r},gameData:i,counts:e}}function de(n,e){return n.gameDataFields?.[e]??[]}function _(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function ue(n,e){let t={};for(let r of n){let s=_(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 be(Ge);})();\n//# sourceMappingURL=patterplay.min.js.map';
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">" })[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 { "&": "&", "<": "<", ">": ">" }[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; }
|
|
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; }
|
|
475
851
|
`;
|
|
@@ -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 =
|
|
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
|
|
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();
|
|
@@ -808,7 +1184,7 @@ function proposeCoverageDrivers(loaded) {
|
|
|
808
1184
|
const { written, proposals } = analyzeHostScopes(bundle, hostTokens);
|
|
809
1185
|
const declByRef = /* @__PURE__ */ new Map();
|
|
810
1186
|
for (const s of loaded.project.scopeRegistry?.scopes ?? []) {
|
|
811
|
-
for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values });
|
|
1187
|
+
for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages });
|
|
812
1188
|
}
|
|
813
1189
|
const drivers = [];
|
|
814
1190
|
for (const [ref, pool] of proposals) {
|
|
@@ -818,6 +1194,7 @@ function proposeCoverageDrivers(loaded) {
|
|
|
818
1194
|
if (values.length === 0 && decl) {
|
|
819
1195
|
if (decl.type === "boolean") values = [true, false];
|
|
820
1196
|
else if (decl.type === "enum" && decl.values) values = [...decl.values];
|
|
1197
|
+
else if (decl.type === "quality" && decl.stages) values = [...decl.stages];
|
|
821
1198
|
}
|
|
822
1199
|
if (values.length === 0) continue;
|
|
823
1200
|
drivers.push({ ref, kind: "recurring", cadence: "sometimes", values: sortValues(values) });
|
|
@@ -1020,13 +1397,13 @@ function renderCoverageText(report, sceneName = (id) => id) {
|
|
|
1020
1397
|
|
|
1021
1398
|
// src/format.ts
|
|
1022
1399
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1023
|
-
import { canonicalStringify as
|
|
1400
|
+
import { canonicalStringify as canonicalStringify3, parseSource as parseSource3 } from "@patterkit/core";
|
|
1024
1401
|
function runFormat(files) {
|
|
1025
1402
|
return files.map((file) => {
|
|
1026
1403
|
const original = readFileSync3(file, "utf8");
|
|
1027
1404
|
let canonical;
|
|
1028
1405
|
try {
|
|
1029
|
-
canonical =
|
|
1406
|
+
canonical = canonicalStringify3(parseSource3(original));
|
|
1030
1407
|
} catch (e) {
|
|
1031
1408
|
throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1032
1409
|
}
|
|
@@ -1066,7 +1443,7 @@ function runReport(loaded, recordingOverride) {
|
|
|
1066
1443
|
const thresholdIdx = estimatingOn ? writingIndex.get(est?.thresholdStatus ?? "") ?? 0 : -1;
|
|
1067
1444
|
const defaultLines = est?.defaultLines ?? 0;
|
|
1068
1445
|
const tagMap = new Map((est?.tagEstimates ?? []).map((t) => [t.tag, t.lines]));
|
|
1069
|
-
const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, edits: editsOf } =
|
|
1446
|
+
const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, edits: editsOf } = mergeAuthoring2(loaded);
|
|
1070
1447
|
const recordingBase = recordingOverride ?? manualRecordingOf;
|
|
1071
1448
|
const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
|
|
1072
1449
|
const byLocale = stringsByLocale(loaded);
|
|
@@ -1411,7 +1788,7 @@ async function runReportXlsx(data) {
|
|
|
1411
1788
|
|
|
1412
1789
|
// src/localisation.ts
|
|
1413
1790
|
import { basename as basename4, join as join3, sep } from "path";
|
|
1414
|
-
import { canonicalStringify as
|
|
1791
|
+
import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
|
|
1415
1792
|
import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
|
|
1416
1793
|
|
|
1417
1794
|
// src/documentation.ts
|
|
@@ -1426,7 +1803,7 @@ function classesForChannel(classes, channel) {
|
|
|
1426
1803
|
function resolveDocumentation(loaded, channel) {
|
|
1427
1804
|
const classes = loaded.project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES;
|
|
1428
1805
|
const allowed = classesForChannel(classes, channel);
|
|
1429
|
-
const docsOf =
|
|
1806
|
+
const docsOf = mergeAuthoring2(loaded).documentation;
|
|
1430
1807
|
const own = (id) => (docsOf.get(id) ?? []).filter((l) => l.type !== void 0 && allowed.has(l.type));
|
|
1431
1808
|
const out = /* @__PURE__ */ new Map();
|
|
1432
1809
|
const visit = (id, inherited) => {
|
|
@@ -1462,7 +1839,7 @@ function extractLoc(loaded, opts = {}) {
|
|
|
1462
1839
|
const targetLocale = isTemplate ? void 0 : opts.locale;
|
|
1463
1840
|
const source = tableFor(loaded, defaultLocale);
|
|
1464
1841
|
const target = targetLocale ? tableFor(loaded, targetLocale) : {};
|
|
1465
|
-
const edits =
|
|
1842
|
+
const edits = mergeAuthoring2(loaded).edits;
|
|
1466
1843
|
const docs = resolveDocumentation(loaded, "loc");
|
|
1467
1844
|
const commentsOf = (id) => (docs.get(id) ?? []).map((d) => d.text);
|
|
1468
1845
|
const staleFor = (id, translation) => {
|
|
@@ -1569,7 +1946,7 @@ function applyLoc(loaded, catalog, opts = {}) {
|
|
|
1569
1946
|
merged[id] = text;
|
|
1570
1947
|
}
|
|
1571
1948
|
const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
|
|
1572
|
-
writes.push({ path, content:
|
|
1949
|
+
writes.push({ path, content: canonicalStringify4(file) });
|
|
1573
1950
|
const fresh = bySceneFresh.get(scene);
|
|
1574
1951
|
if (!fresh?.size) continue;
|
|
1575
1952
|
const aPath = authoringPath(loaded, scene);
|
|
@@ -1578,7 +1955,7 @@ function applyLoc(loaded, catalog, opts = {}) {
|
|
|
1578
1955
|
const editsMap = { ...authoring.edits ?? {} };
|
|
1579
1956
|
for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
|
|
1580
1957
|
authoring.edits = editsMap;
|
|
1581
|
-
writes.push({ path: aPath, content:
|
|
1958
|
+
writes.push({ path: aPath, content: canonicalStringify4(authoring) });
|
|
1582
1959
|
}
|
|
1583
1960
|
return { writes, stats: { updated, files: bySceneText.size } };
|
|
1584
1961
|
}
|
|
@@ -1630,1660 +2007,1347 @@ function catalogToPo(catalog) {
|
|
|
1630
2007
|
out.push(`#: ${e.scene}`);
|
|
1631
2008
|
if (e.stale) out.push("#, fuzzy");
|
|
1632
2009
|
out.push(poField("msgctxt", e.id), poField("msgid", e.source), poField("msgstr", e.translation), "");
|
|
1633
|
-
}
|
|
1634
|
-
return out.join("\n");
|
|
1635
|
-
}
|
|
1636
|
-
function readQuoted(lines, start) {
|
|
1637
|
-
const first = lines[start];
|
|
1638
|
-
let value = poUnescape(first.slice(first.indexOf('"') + 1, first.lastIndexOf('"')));
|
|
1639
|
-
let i = start + 1;
|
|
1640
|
-
while (i < lines.length && lines[i].trimStart().startsWith('"')) {
|
|
1641
|
-
const l = lines[i].trim();
|
|
1642
|
-
value += poUnescape(l.slice(1, l.lastIndexOf('"')));
|
|
1643
|
-
i++;
|
|
1644
|
-
}
|
|
1645
|
-
return { value, next: i };
|
|
1646
|
-
}
|
|
1647
|
-
function poToCatalog(text) {
|
|
1648
|
-
const lines = text.split(/\r?\n/);
|
|
1649
|
-
const entries = [];
|
|
1650
|
-
let locale;
|
|
1651
|
-
let project = "";
|
|
1652
|
-
let comments = [];
|
|
1653
|
-
let scene = "";
|
|
1654
|
-
let stale = false;
|
|
1655
|
-
let ctxt;
|
|
1656
|
-
let msgid = "";
|
|
1657
|
-
let i = 0;
|
|
1658
|
-
const flush = (msgstr) => {
|
|
1659
|
-
if (ctxt !== void 0) {
|
|
1660
|
-
entries.push({ id: ctxt, scene, source: msgid, translation: msgstr, comments, stale });
|
|
1661
|
-
} else if (msgid === "") {
|
|
1662
|
-
const lang = /Language:\s*([^\\\n]*)/.exec(msgstr);
|
|
1663
|
-
if (lang?.[1]?.trim()) locale = lang[1].trim();
|
|
1664
|
-
const pid = /Project-Id-Version:\s*([^\\\n]*)/.exec(msgstr);
|
|
1665
|
-
if (pid?.[1]?.trim()) project = pid[1].trim();
|
|
1666
|
-
}
|
|
1667
|
-
comments = [];
|
|
1668
|
-
scene = "";
|
|
1669
|
-
stale = false;
|
|
1670
|
-
ctxt = void 0;
|
|
1671
|
-
msgid = "";
|
|
1672
|
-
};
|
|
1673
|
-
while (i < lines.length) {
|
|
1674
|
-
const line = lines[i];
|
|
1675
|
-
const t = line.trim();
|
|
1676
|
-
if (t === "") {
|
|
1677
|
-
i++;
|
|
1678
|
-
continue;
|
|
1679
|
-
}
|
|
1680
|
-
if (t.startsWith("#.")) {
|
|
1681
|
-
comments.push(t.slice(2).trim());
|
|
1682
|
-
i++;
|
|
1683
|
-
continue;
|
|
1684
|
-
}
|
|
1685
|
-
if (t.startsWith("#:")) {
|
|
1686
|
-
scene = t.slice(2).trim().split(/\s+/)[0] ?? "";
|
|
1687
|
-
i++;
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
if (t.startsWith("#,")) {
|
|
1691
|
-
if (t.includes("fuzzy")) stale = true;
|
|
1692
|
-
i++;
|
|
1693
|
-
continue;
|
|
1694
|
-
}
|
|
1695
|
-
if (t.startsWith("#")) {
|
|
1696
|
-
i++;
|
|
1697
|
-
continue;
|
|
1698
|
-
}
|
|
1699
|
-
if (t.startsWith("msgctxt")) {
|
|
1700
|
-
const r = readQuoted(lines, i);
|
|
1701
|
-
ctxt = r.value;
|
|
1702
|
-
i = r.next;
|
|
1703
|
-
continue;
|
|
1704
|
-
}
|
|
1705
|
-
if (t.startsWith("msgid")) {
|
|
1706
|
-
const r = readQuoted(lines, i);
|
|
1707
|
-
msgid = r.value;
|
|
1708
|
-
i = r.next;
|
|
1709
|
-
continue;
|
|
1710
|
-
}
|
|
1711
|
-
if (t.startsWith("msgstr")) {
|
|
1712
|
-
const r = readQuoted(lines, i);
|
|
1713
|
-
flush(r.value);
|
|
1714
|
-
i = r.next;
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
i++;
|
|
1718
|
-
}
|
|
1719
|
-
return { project, defaultLocale: locale ?? "", locale, entries };
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// src/loc-xlsx.ts
|
|
1723
|
-
var HEADERS = ["ID", "Source", "Translation", "Comments", "Status", "Gender"];
|
|
1724
|
-
var sheetName = (scene) => scene.replace(/[:\\/?*[\]]/g, "-").slice(0, 31);
|
|
1725
|
-
async function catalogToXlsx(catalog) {
|
|
1726
|
-
const { default: ExcelJS } = await import("exceljs");
|
|
1727
|
-
const wb = new ExcelJS.Workbook();
|
|
1728
|
-
wb.creator = "patter";
|
|
1729
|
-
const byScene = /* @__PURE__ */ new Map();
|
|
1730
|
-
for (const e of catalog.entries) (byScene.get(e.scene) ?? byScene.set(e.scene, []).get(e.scene)).push(e);
|
|
1731
|
-
for (const [scene, entries] of byScene) {
|
|
1732
|
-
const ws = wb.addWorksheet(sheetName(scene));
|
|
1733
|
-
ws.columns = [
|
|
1734
|
-
{ header: "ID", key: "id", width: 22 },
|
|
1735
|
-
{ header: "Source", key: "source", width: 40 },
|
|
1736
|
-
{ header: "Translation", key: "translation", width: 40 },
|
|
1737
|
-
{ header: "Comments", key: "comments", width: 30 },
|
|
1738
|
-
{ header: "Status", key: "status", width: 10 },
|
|
1739
|
-
{ header: "Gender", key: "gender", width: 12 }
|
|
1740
|
-
];
|
|
1741
|
-
ws.getRow(1).font = { bold: true };
|
|
1742
|
-
ws.views = [{ state: "frozen", ySplit: 1 }];
|
|
1743
|
-
for (const e of entries) {
|
|
1744
|
-
ws.addRow({
|
|
1745
|
-
id: e.id,
|
|
1746
|
-
source: e.source,
|
|
1747
|
-
translation: e.translation,
|
|
1748
|
-
comments: e.comments.join("\n"),
|
|
1749
|
-
status: e.stale ? "stale" : e.translation ? "translated" : "",
|
|
1750
|
-
gender: e.context?.gender ?? ""
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
return Buffer.from(await wb.xlsx.writeBuffer());
|
|
1755
|
-
}
|
|
1756
|
-
async function xlsxToCatalog(buffer) {
|
|
1757
|
-
const { default: ExcelJS } = await import("exceljs");
|
|
1758
|
-
const wb = new ExcelJS.Workbook();
|
|
1759
|
-
await wb.xlsx.load(buffer);
|
|
1760
|
-
const entries = [];
|
|
1761
|
-
for (const ws of wb.worksheets) {
|
|
1762
|
-
const scene = ws.name;
|
|
1763
|
-
const header = ws.getRow(1).values.slice(1).map((v) => String(v ?? ""));
|
|
1764
|
-
if (header[0] !== HEADERS[0]) continue;
|
|
1765
|
-
ws.eachRow((row, n) => {
|
|
1766
|
-
if (n === 1) return;
|
|
1767
|
-
const cell = (c) => {
|
|
1768
|
-
const v = row.getCell(c).value;
|
|
1769
|
-
return v == null ? "" : String(typeof v === "object" && "text" in v ? v.text : v);
|
|
1770
|
-
};
|
|
1771
|
-
const id = cell(1).trim();
|
|
1772
|
-
if (!id) return;
|
|
1773
|
-
const status = cell(5).trim().toLowerCase();
|
|
1774
|
-
entries.push({
|
|
1775
|
-
id,
|
|
1776
|
-
scene,
|
|
1777
|
-
source: cell(2),
|
|
1778
|
-
translation: cell(3),
|
|
1779
|
-
comments: cell(4) ? cell(4).split("\n").filter(Boolean) : [],
|
|
1780
|
-
stale: status === "stale"
|
|
1781
|
-
});
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
return { project: "", defaultLocale: "", locale: void 0, entries };
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// src/voice-script.ts
|
|
1788
|
-
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";
|
|
1789
|
-
function plainVoice(text) {
|
|
1790
|
-
return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
1791
|
-
}
|
|
1792
|
-
function groupLabel(node, source) {
|
|
1793
|
-
if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
|
|
1794
|
-
if (node.selector === "choice") return "";
|
|
1795
|
-
return node.selector ?? "group";
|
|
1796
|
-
}
|
|
1797
|
-
function runVoiceScript(loaded, opts = {}) {
|
|
1798
|
-
const { project } = loaded;
|
|
1799
|
-
const everything = !!opts.everything;
|
|
1800
|
-
const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
|
|
1801
|
-
const writingLadder = ladderDecls.map((s) => s.name);
|
|
1802
|
-
const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3).map((s) => s.name);
|
|
1803
|
-
const stub = writingLadder[0];
|
|
1804
|
-
const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
|
|
1805
|
-
const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
|
|
1806
|
-
const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring(loaded);
|
|
1807
|
-
const recordingBase = opts.recordingOverride ?? manualRecordingOf;
|
|
1808
|
-
const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
|
|
1809
|
-
const source = sourceStrings(loaded);
|
|
1810
|
-
const actorOf = /* @__PURE__ */ new Map();
|
|
1811
|
-
for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
|
|
1812
|
-
const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES2, "vo");
|
|
1813
|
-
const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
|
|
1814
|
-
const lines = [];
|
|
1815
|
-
const walkNode = (node, scope, ancestorVo) => {
|
|
1816
|
-
if (node.type === "group") {
|
|
1817
|
-
if (cutSet.has(node.id)) return;
|
|
1818
|
-
const label = groupLabel(node, source);
|
|
1819
|
-
const childScope = label ? [...scope, label] : scope;
|
|
1820
|
-
const childVo = [...ancestorVo, ...ownVo(node.id)];
|
|
1821
|
-
for (const child of node.children ?? []) walkNode(child, childScope, childVo);
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
if (cutSet.has(node.id)) return;
|
|
1825
|
-
let leading = true;
|
|
1826
|
-
for (const beat of node.beats ?? []) {
|
|
1827
|
-
if (beat.kind !== "line" || cutSet.has(beat.id)) continue;
|
|
1828
|
-
const ws = writingOf.get(beat.id) ?? stub;
|
|
1829
|
-
if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) continue;
|
|
1830
|
-
const own = ownVo(beat.id);
|
|
1831
|
-
lines.push({
|
|
1832
|
-
scope: scope.join(" \u203A "),
|
|
1833
|
-
id: beat.id,
|
|
1834
|
-
character: beat.character ?? "",
|
|
1835
|
-
actor: beat.character ? actorOf.get(beat.character) : void 0,
|
|
1836
|
-
text: plainVoice(source[beat.id] ?? ""),
|
|
1837
|
-
comments: leading ? [...ancestorVo, ...own] : own,
|
|
1838
|
-
// first line of the run gets the enclosing context
|
|
1839
|
-
recordingStatus: recordingOf(beat.id)
|
|
1840
|
-
});
|
|
1841
|
-
leading = false;
|
|
1842
|
-
}
|
|
1843
|
-
};
|
|
1844
|
-
for (const scene of loaded.scenes) {
|
|
1845
|
-
if (cutSet.has(scene.id)) continue;
|
|
1846
|
-
const sceneVo = ownVo(scene.id);
|
|
1847
|
-
for (const block of scene.blocks) {
|
|
1848
|
-
if (cutSet.has(block.id)) continue;
|
|
1849
|
-
const scope = [scene.name, block.name].filter(Boolean);
|
|
1850
|
-
const blockVo = [...sceneVo, ...ownVo(block.id)];
|
|
1851
|
-
for (const child of block.children ?? []) walkNode(child, scope, blockVo);
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
// src/voice-script-xlsx.ts
|
|
1858
|
-
async function voiceScriptToXlsx(data) {
|
|
1859
|
-
const { default: ExcelJS } = await import("exceljs");
|
|
1860
|
-
const wb = new ExcelJS.Workbook();
|
|
1861
|
-
wb.creator = "patter";
|
|
1862
|
-
const ws = wb.addWorksheet("Voice Script");
|
|
1863
|
-
ws.columns = [
|
|
1864
|
-
{ header: "Scope", key: "scope", width: 28 },
|
|
1865
|
-
{ header: "Line ID", key: "id", width: 18 },
|
|
1866
|
-
{ header: "Character", key: "character", width: 16 },
|
|
1867
|
-
{ header: "Actor", key: "actor", width: 16 },
|
|
1868
|
-
{ header: "Text", key: "text", width: 60 },
|
|
1869
|
-
{ header: "Comments", key: "comments", width: 36 },
|
|
1870
|
-
{ header: "Status", key: "status", width: 12 }
|
|
1871
|
-
];
|
|
1872
|
-
ws.getRow(1).font = { bold: true };
|
|
1873
|
-
ws.views = [{ state: "frozen", ySplit: 1 }];
|
|
1874
|
-
for (const l of data.lines) {
|
|
1875
|
-
const row = ws.addRow({
|
|
1876
|
-
scope: l.scope,
|
|
1877
|
-
id: l.id,
|
|
1878
|
-
character: l.character,
|
|
1879
|
-
actor: l.actor ?? "",
|
|
1880
|
-
text: l.text,
|
|
1881
|
-
comments: l.comments.join("\n"),
|
|
1882
|
-
status: l.recordingStatus
|
|
1883
|
-
});
|
|
1884
|
-
row.getCell("text").alignment = { wrapText: true, vertical: "top" };
|
|
1885
|
-
row.getCell("comments").alignment = { wrapText: true, vertical: "top" };
|
|
1886
|
-
}
|
|
1887
|
-
return Buffer.from(await wb.xlsx.writeBuffer());
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// src/script-doc.ts
|
|
1891
|
-
var TOKENS = {
|
|
1892
|
-
ink: "26221c",
|
|
1893
|
-
// near-black warm ink (headings)
|
|
1894
|
-
inkRead: "3a352d",
|
|
1895
|
-
// dialogue body (ink ~84% + muted)
|
|
1896
|
-
inkSoft: "5c554b",
|
|
1897
|
-
// narration (ink ~68% + muted) - a step softer than dialogue
|
|
1898
|
-
muted: "8c8479",
|
|
1899
|
-
// labels, quiet tags
|
|
1900
|
-
accent: "8a5a34",
|
|
1901
|
-
// structural marks: conditions, options, jumps, game events
|
|
1902
|
-
line: "e4dfd5"
|
|
1903
|
-
// hairlines / rules / the group rail
|
|
1904
|
-
};
|
|
1905
|
-
var CHAR_PALETTE = [
|
|
1906
|
-
"b23b3b",
|
|
1907
|
-
"b5642b",
|
|
1908
|
-
"927524",
|
|
1909
|
-
"6b7a31",
|
|
1910
|
-
"3f7d49",
|
|
1911
|
-
"2c7d6e",
|
|
1912
|
-
"2d7791",
|
|
1913
|
-
"3a6aa8",
|
|
1914
|
-
"5a5ba6",
|
|
1915
|
-
"7e4e9c",
|
|
1916
|
-
"9c4486",
|
|
1917
|
-
"a83e5c"
|
|
1918
|
-
];
|
|
1919
|
-
function colourIndex(name) {
|
|
1920
|
-
let h = 2166136261;
|
|
1921
|
-
for (let i = 0; i < name.length; i++) {
|
|
1922
|
-
h ^= name.charCodeAt(i);
|
|
1923
|
-
h = Math.imul(h, 16777619);
|
|
1924
|
-
}
|
|
1925
|
-
h ^= h >>> 16;
|
|
1926
|
-
h = Math.imul(h, 2246822507);
|
|
1927
|
-
h ^= h >>> 13;
|
|
1928
|
-
h = Math.imul(h, 3266489909);
|
|
1929
|
-
h ^= h >>> 16;
|
|
1930
|
-
return (h >>> 0) % 12;
|
|
1931
|
-
}
|
|
1932
|
-
function characterColour(name) {
|
|
1933
|
-
return CHAR_PALETTE[colourIndex(name)];
|
|
1934
|
-
}
|
|
1935
|
-
var VISIT_FN_RE = /\b(patter_visits|patter_seen|visits|seen)\s*\(\s*(['"])(.*?)\2\s*\)/g;
|
|
1936
|
-
function humanizeCondition(cond, label) {
|
|
1937
|
-
return cond.replace(VISIT_FN_RE, (_m, fn, _q, id) => `${fn}(${label(id)})`);
|
|
1938
|
-
}
|
|
1939
|
-
var MARKUP_RE = /<(b|i|bi)>([\s\S]*?)<\/\1>/g;
|
|
1940
|
-
var INTERP_RE = /\{@[^}]*\}/g;
|
|
1941
|
-
function textRuns(text) {
|
|
1942
|
-
const marked = [];
|
|
1943
|
-
let last = 0;
|
|
1944
|
-
let m;
|
|
1945
|
-
MARKUP_RE.lastIndex = 0;
|
|
1946
|
-
while ((m = MARKUP_RE.exec(text)) !== null) {
|
|
1947
|
-
if (m.index > last) marked.push({ text: text.slice(last, m.index), bold: false, italic: false });
|
|
1948
|
-
const tag = m[1];
|
|
1949
|
-
marked.push({ text: m[2], bold: tag === "b" || tag === "bi", italic: tag === "i" || tag === "bi" });
|
|
1950
|
-
last = MARKUP_RE.lastIndex;
|
|
1951
|
-
}
|
|
1952
|
-
if (last < text.length) marked.push({ text: text.slice(last), bold: false, italic: false });
|
|
1953
|
-
if (!marked.length) marked.push({ text, bold: false, italic: false });
|
|
1954
|
-
const out = [];
|
|
1955
|
-
for (const r of marked) {
|
|
1956
|
-
let li = 0;
|
|
1957
|
-
let im;
|
|
1958
|
-
INTERP_RE.lastIndex = 0;
|
|
1959
|
-
while ((im = INTERP_RE.exec(r.text)) !== null) {
|
|
1960
|
-
if (im.index > li) out.push({ text: r.text.slice(li, im.index), bold: r.bold, italic: r.italic, code: false });
|
|
1961
|
-
out.push({ text: im[0], bold: r.bold, italic: r.italic, code: true });
|
|
1962
|
-
li = INTERP_RE.lastIndex;
|
|
1963
|
-
}
|
|
1964
|
-
if (li < r.text.length) out.push({ text: r.text.slice(li), bold: r.bold, italic: r.italic, code: false });
|
|
1965
|
-
}
|
|
1966
|
-
return out.length ? out : [{ text, bold: false, italic: false, code: false }];
|
|
1967
|
-
}
|
|
1968
|
-
function targetLabel(to, sceneOf, blockTrail) {
|
|
1969
|
-
if (to === "END") return "END";
|
|
1970
|
-
return blockTrail.get(to) ?? sceneOf.get(to) ?? to;
|
|
1971
|
-
}
|
|
1972
|
-
function gameEventLabel(beat) {
|
|
1973
|
-
const gd = beat.gameData;
|
|
1974
|
-
if (gd && typeof gd === "object") {
|
|
1975
|
-
const parts = Object.entries(gd).map(([k, v]) => {
|
|
1976
|
-
const val = v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
1977
|
-
return val === "" ? k : `${k}: ${val}`;
|
|
1978
|
-
});
|
|
1979
|
-
if (parts.length) return `game event \xB7 ${parts.join(", ")}`;
|
|
1980
|
-
}
|
|
1981
|
-
return "game event";
|
|
1982
|
-
}
|
|
1983
|
-
function optionTag(node) {
|
|
1984
|
-
const parts = [];
|
|
1985
|
-
if (node.fallback) parts.push("fallback");
|
|
1986
|
-
parts.push(node.sticky ? "repeatable" : "once only");
|
|
1987
|
-
if (node.secretUntilEligible) parts.push("hidden");
|
|
1988
|
-
return parts.join(" \xB7 ");
|
|
1989
|
-
}
|
|
1990
|
-
function sequenceLabel(node) {
|
|
1991
|
-
const o = node.options ?? {};
|
|
1992
|
-
const bits = [];
|
|
1993
|
-
if ((o.order ?? "sequential") === "shuffle") bits.push("shuffled");
|
|
1994
|
-
const ex = o.exhaust ?? "once";
|
|
1995
|
-
if (ex === "repeat") bits.push("repeating");
|
|
1996
|
-
else if (ex === "stick") bits.push("holds on the last");
|
|
1997
|
-
return bits.length ? `In sequence \xB7 ${bits.join(", ")}` : "In sequence";
|
|
1998
|
-
}
|
|
1999
|
-
function runScriptDoc(loaded) {
|
|
2000
|
-
const { project } = loaded;
|
|
2001
|
-
const source = sourceStrings(loaded);
|
|
2002
|
-
const { cut } = mergeAuthoring(loaded);
|
|
2003
|
-
const sceneOf = /* @__PURE__ */ new Map();
|
|
2004
|
-
const blockTrail = /* @__PURE__ */ new Map();
|
|
2005
|
-
const blockName = /* @__PURE__ */ new Map();
|
|
2006
|
-
for (const scene of loaded.scenes) {
|
|
2007
|
-
sceneOf.set(scene.id, scene.name);
|
|
2008
|
-
for (const block of scene.blocks) {
|
|
2009
|
-
blockTrail.set(block.id, `${scene.name} \u203A ${block.name}`);
|
|
2010
|
-
blockName.set(block.id, block.name);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
const humanize = (cond) => humanizeCondition(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
|
|
2014
|
-
const els = [];
|
|
2015
|
-
const textOf = (id) => source[id] ?? "";
|
|
2016
|
-
const jumpText = (j) => {
|
|
2017
|
-
const dest = targetLabel(j.to, sceneOf, blockTrail);
|
|
2018
|
-
return j.mode === "call" ? `${dest} (and return)` : dest;
|
|
2019
|
-
};
|
|
2020
|
-
let snippetSeq = 0;
|
|
2021
|
-
const nextSid = () => ++snippetSeq;
|
|
2022
|
-
const emitBeats = (node, indent, sid) => {
|
|
2023
|
-
for (const beat of node.beats ?? []) {
|
|
2024
|
-
if (cut.has(beat.id)) continue;
|
|
2025
|
-
if (beat.kind === "line") els.push({ kind: "line", indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
|
|
2026
|
-
else if (beat.kind === "text") els.push({ kind: "narration", indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
|
|
2027
|
-
else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent, snippet: sid, text: gameEventLabel(beat) });
|
|
2028
|
-
}
|
|
2029
|
-
if (node.jump) els.push({ kind: "jump", indent, snippet: sid, text: jumpText(node.jump) });
|
|
2030
|
-
};
|
|
2031
|
-
const walk = (node, indent, sid) => {
|
|
2032
|
-
if (cut.has(node.id)) return;
|
|
2033
|
-
if (node.type === "snippet") {
|
|
2034
|
-
if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
|
|
2035
|
-
emitBeats(node, indent, sid);
|
|
2036
|
-
return;
|
|
2037
|
-
}
|
|
2038
|
-
if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
|
|
2039
|
-
if (node.selector === "choice") {
|
|
2040
|
-
els.push({ kind: "group", indent, label: "Choose" });
|
|
2041
|
-
for (const child of node.children ?? []) {
|
|
2042
|
-
if (cut.has(child.id)) continue;
|
|
2043
|
-
const cid = nextSid();
|
|
2044
|
-
if (child.type === "group" && child.prompt) {
|
|
2045
|
-
if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
|
|
2046
|
-
els.push({ kind: "option", indent, snippet: cid, runs: textRuns(textOf(child.prompt.id) || "(option)"), tag: optionTag(child) });
|
|
2047
|
-
for (const c of child.children ?? []) walk(c, indent + 1, cid);
|
|
2048
|
-
} else if (child.type === "snippet") {
|
|
2049
|
-
const beats = child.beats ?? [];
|
|
2050
|
-
const first = beats.find((b) => b.kind === "line" || b.kind === "text");
|
|
2051
|
-
if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
|
|
2052
|
-
els.push({ kind: "option", indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child) });
|
|
2053
|
-
let seenPrompt = false;
|
|
2054
|
-
for (const beat of beats) {
|
|
2055
|
-
if (!seenPrompt && beat === first) {
|
|
2056
|
-
seenPrompt = true;
|
|
2057
|
-
continue;
|
|
2058
|
-
}
|
|
2059
|
-
if (cut.has(beat.id)) continue;
|
|
2060
|
-
if (beat.kind === "line") els.push({ kind: "line", indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
|
|
2061
|
-
else if (beat.kind === "text") els.push({ kind: "narration", indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
|
|
2062
|
-
else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
|
|
2063
|
-
}
|
|
2064
|
-
if (child.jump) els.push({ kind: "jump", indent: indent + 1, snippet: cid, text: jumpText(child.jump) });
|
|
2065
|
-
} else {
|
|
2066
|
-
walk(child, indent + 1, cid);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
if (node.selector === "branch") {
|
|
2072
|
-
els.push({ kind: "group", indent, label: "One of \xB7 first match wins" });
|
|
2073
|
-
const kids = (node.children ?? []).filter((c) => !cut.has(c.id));
|
|
2074
|
-
kids.forEach((child, i) => {
|
|
2075
|
-
const isCatchAll = i === kids.length - 1 && kids.length > 1 && !("condition" in child && child.condition);
|
|
2076
|
-
if (isCatchAll) els.push({ kind: "else", indent });
|
|
2077
|
-
walk(child, indent, nextSid());
|
|
2078
|
-
});
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
if (node.selector === "sequence") {
|
|
2082
|
-
els.push({ kind: "group", indent, label: sequenceLabel(node) });
|
|
2083
|
-
for (const child of node.children ?? []) walk(child, indent, nextSid());
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
for (const child of node.children ?? []) walk(child, indent, sid);
|
|
2087
|
-
};
|
|
2088
|
-
for (const scene of loaded.scenes) {
|
|
2089
|
-
if (cut.has(scene.id)) continue;
|
|
2090
|
-
els.push({ kind: "scene", text: scene.name });
|
|
2091
|
-
for (const block of scene.blocks) {
|
|
2092
|
-
if (cut.has(block.id)) continue;
|
|
2093
|
-
els.push({ kind: "block", text: block.name });
|
|
2094
|
-
for (const child of block.children ?? []) walk(child, 0, void 0);
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
return { project: project.project.name, elements: els };
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
// src/script-docx.ts
|
|
2101
|
-
import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, TabStopType, TextRun } from "docx";
|
|
2102
|
-
var SERIF = "Georgia";
|
|
2103
|
-
var SANS = "Calibri";
|
|
2104
|
-
var MONO = "Consolas";
|
|
2105
|
-
var STEP = 360;
|
|
2106
|
-
var SNIPPET_INSET = 220;
|
|
2107
|
-
var DIALOGUE_INDENT = 300;
|
|
2108
|
-
var HANG = 340;
|
|
2109
|
-
var RIGHT_TAB = 9020;
|
|
2110
|
-
var SNIP_GAP = 170;
|
|
2111
|
-
var WITHIN = 30;
|
|
2112
|
-
var S = { h1: 32, h2: 26, body: 22, cue: 18, label: 16, tag: 15, mech: 18 };
|
|
2113
|
-
function bodyRuns(runs, color, base = {}) {
|
|
2114
|
-
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 }));
|
|
2115
|
-
}
|
|
2116
|
-
var leftOf = (el) => ("indent" in el ? el.indent * STEP : 0) + ("snippet" in el && el.snippet !== void 0 ? SNIPPET_INSET : 0);
|
|
2117
|
-
function paragraph(el, prev) {
|
|
2118
|
-
const sid = "snippet" in el ? el.snippet : void 0;
|
|
2119
|
-
const startsSnippet = sid !== void 0 && (!prev || !("snippet" in prev) || prev.snippet !== sid);
|
|
2120
|
-
const before = sid !== void 0 ? startsSnippet ? SNIP_GAP : WITHIN : 100;
|
|
2121
|
-
switch (el.kind) {
|
|
2122
|
-
case "scene":
|
|
2123
|
-
return new Paragraph({
|
|
2124
|
-
heading: HeadingLevel.HEADING_1,
|
|
2125
|
-
keepNext: true,
|
|
2126
|
-
spacing: { before: 480, after: 200 },
|
|
2127
|
-
border: { bottom: { style: BorderStyle.SINGLE, size: 14, color: TOKENS.accent, space: 6 } },
|
|
2128
|
-
children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h1 })]
|
|
2129
|
-
});
|
|
2130
|
-
case "block":
|
|
2131
|
-
return new Paragraph({
|
|
2132
|
-
heading: HeadingLevel.HEADING_2,
|
|
2133
|
-
keepNext: true,
|
|
2134
|
-
spacing: { before: 360, after: 160 },
|
|
2135
|
-
border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: TOKENS.line, space: 4 } },
|
|
2136
|
-
children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h2 })]
|
|
2137
|
-
});
|
|
2138
|
-
case "line": {
|
|
2139
|
-
const kids = [];
|
|
2140
|
-
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 }));
|
|
2141
|
-
if (el.direction) kids.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
|
|
2142
|
-
kids.push(...bodyRuns(el.runs, TOKENS.inkRead));
|
|
2143
|
-
return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids });
|
|
2144
|
-
}
|
|
2145
|
-
case "narration":
|
|
2146
|
-
return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: bodyRuns(el.runs, TOKENS.inkSoft) });
|
|
2147
|
-
case "condition":
|
|
2148
|
-
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 })] });
|
|
2149
|
-
case "group":
|
|
2150
|
-
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 })] });
|
|
2151
|
-
case "else":
|
|
2152
|
-
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 })] });
|
|
2153
|
-
case "option": {
|
|
2154
|
-
const kids = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
|
|
2155
|
-
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 }));
|
|
2156
|
-
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 });
|
|
2157
|
-
}
|
|
2158
|
-
case "jump":
|
|
2159
|
-
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 })] });
|
|
2160
|
-
case "gameEvent":
|
|
2161
|
-
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 })] });
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
async function scriptToDocx(doc) {
|
|
2165
|
-
const body = [
|
|
2166
|
-
new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: "Readable script", font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: 16, characterSpacing: 30 })] }),
|
|
2167
|
-
new Paragraph({ spacing: { after: 560 }, children: [new TextRun({ text: doc.project, font: SERIF, bold: true, color: TOKENS.ink, size: 52 })] }),
|
|
2168
|
-
...doc.elements.map((el, i) => paragraph(el, doc.elements[i - 1]))
|
|
2169
|
-
];
|
|
2170
|
-
const document = new Document({ creator: "Patter", title: doc.project, sections: [{ children: body }] });
|
|
2171
|
-
return Packer.toBuffer(document);
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
// src/script-pdf.ts
|
|
2175
|
-
import PDFDocument from "pdfkit";
|
|
2176
|
-
var MARGIN = 64;
|
|
2177
|
-
var INDENT_STEP = 20;
|
|
2178
|
-
var EDGE_INSET = 12;
|
|
2179
|
-
var EDGE_OFF = 3;
|
|
2180
|
-
var DIALOGUE_INDENT2 = 14;
|
|
2181
|
-
var HANG2 = 18;
|
|
2182
|
-
var CUE_GAP = 7;
|
|
2183
|
-
var BODY = 11;
|
|
2184
|
-
var CUE = 8.5;
|
|
2185
|
-
var LEAD = 3.5;
|
|
2186
|
-
var WITHIN2 = 4;
|
|
2187
|
-
var BETWEEN = 10;
|
|
2188
|
-
var BEAT = 6;
|
|
2189
|
-
var TIGHT = 3;
|
|
2190
|
-
var hex = (h) => `#${h}`;
|
|
2191
|
-
var INK = hex(TOKENS.ink);
|
|
2192
|
-
var INK_READ = hex(TOKENS.inkRead);
|
|
2193
|
-
var INK_SOFT = hex(TOKENS.inkSoft);
|
|
2194
|
-
var MUTED = hex(TOKENS.muted);
|
|
2195
|
-
var ACCENT = hex(TOKENS.accent);
|
|
2196
|
-
var LINE = hex(TOKENS.line);
|
|
2197
|
-
var EDGE = "#d7d0c3";
|
|
2198
|
-
var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Serif-Bold" : italic ? "Serif-Italic" : "Serif";
|
|
2199
|
-
var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
|
|
2200
|
-
async function scriptToPdf(doc) {
|
|
2201
|
-
const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
|
|
2202
|
-
return new Promise((resolve5, reject) => {
|
|
2203
|
-
const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
|
|
2204
|
-
pdf.registerFont("Serif", scriptFont("serif"));
|
|
2205
|
-
pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
|
|
2206
|
-
pdf.registerFont("Serif-Italic", scriptFont("serifItalic"));
|
|
2207
|
-
pdf.registerFont("Serif-BoldItalic", scriptFont("serifBoldItalic"));
|
|
2208
|
-
pdf.registerFont("Sans", scriptFont("sans"));
|
|
2209
|
-
pdf.registerFont("Sans-Bold", scriptFont("sansBold"));
|
|
2210
|
-
pdf.registerFont("Mono", scriptFont("mono"));
|
|
2211
|
-
pdf.registerFont("Symbol", scriptFont("symbol"));
|
|
2212
|
-
pdf.registerFont("Emoji", scriptFont("emoji"));
|
|
2213
|
-
const coverage = {};
|
|
2214
|
-
const ascender = {};
|
|
2215
|
-
for (const name of ["Serif", "Serif-Bold", "Serif-Italic", "Serif-BoldItalic", "Sans", "Sans-Bold", "Mono"]) {
|
|
2216
|
-
pdf.font(name);
|
|
2217
|
-
const f = pdf._font;
|
|
2218
|
-
coverage[name] = (cp) => f.font.hasGlyphForCodePoint(cp);
|
|
2219
|
-
ascender[name] = f.ascender;
|
|
2220
|
-
}
|
|
2221
|
-
const ascentPt = (name, size) => ascender[name] / 1e3 * size;
|
|
2222
|
-
const split = (text, primary, color) => {
|
|
2223
|
-
const cov = coverage[primary];
|
|
2224
|
-
const out = [];
|
|
2225
|
-
let buf = "";
|
|
2226
|
-
let cur = primary;
|
|
2227
|
-
for (const ch of text) {
|
|
2228
|
-
const cp = ch.codePointAt(0);
|
|
2229
|
-
const f = isEmoji(cp) ? "Emoji" : cov(cp) ? primary : "Symbol";
|
|
2230
|
-
if (f !== cur) {
|
|
2231
|
-
if (buf) out.push({ text: buf, font: cur, color });
|
|
2232
|
-
buf = "";
|
|
2233
|
-
cur = f;
|
|
2234
|
-
}
|
|
2235
|
-
buf += ch;
|
|
2236
|
-
}
|
|
2237
|
-
if (buf) out.push({ text: buf, font: cur, color });
|
|
2238
|
-
return out;
|
|
2239
|
-
};
|
|
2240
|
-
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));
|
|
2241
|
-
const chunks = [];
|
|
2242
|
-
pdf.on("data", (c) => chunks.push(c));
|
|
2243
|
-
pdf.on("end", () => resolve5(Buffer.concat(chunks)));
|
|
2244
|
-
pdf.on("error", reject);
|
|
2245
|
-
const pageRight = pdf.page.width - MARGIN;
|
|
2246
|
-
const pageBottom = pdf.page.height - MARGIN;
|
|
2247
|
-
const contentW = pdf.page.width - MARGIN * 2;
|
|
2248
|
-
const ensure = (space) => {
|
|
2249
|
-
if (pdf.y > pageBottom - space) pdf.addPage();
|
|
2250
|
-
};
|
|
2251
|
-
const draw = (pieces, x, opts) => {
|
|
2252
|
-
pieces.forEach((p, i) => {
|
|
2253
|
-
pdf.font(p.font).fillColor(p.color);
|
|
2254
|
-
const cont = i < pieces.length - 1;
|
|
2255
|
-
if (i === 0) pdf.text(p.text, x, pdf.y, { ...opts, continued: cont });
|
|
2256
|
-
else pdf.text(p.text, { continued: cont });
|
|
2257
|
-
});
|
|
2258
|
-
};
|
|
2259
|
-
const drawRight = (pieces, size) => {
|
|
2260
|
-
let total = 0;
|
|
2261
|
-
for (const p of pieces) {
|
|
2262
|
-
pdf.font(p.font).fontSize(size);
|
|
2263
|
-
total += pdf.widthOfString(p.text);
|
|
2264
|
-
}
|
|
2265
|
-
const x = Math.max(MARGIN, pageRight - total);
|
|
2266
|
-
pieces.forEach((p, i) => {
|
|
2267
|
-
pdf.font(p.font).fillColor(p.color).fontSize(size);
|
|
2268
|
-
const cont = i < pieces.length - 1;
|
|
2269
|
-
if (i === 0) pdf.text(p.text, x, pdf.y, { continued: cont });
|
|
2270
|
-
else pdf.text(p.text, { continued: cont });
|
|
2271
|
-
});
|
|
2272
|
-
};
|
|
2273
|
-
pdf.fontSize(9);
|
|
2274
|
-
draw(split("READABLE SCRIPT", "Sans-Bold", ACCENT), MARGIN, { characterSpacing: 2 });
|
|
2275
|
-
pdf.moveDown(0.4).fontSize(26);
|
|
2276
|
-
draw(split(doc.project, "Serif-Bold", INK), MARGIN, { width: contentW });
|
|
2277
|
-
pdf.strokeColor(ACCENT).lineWidth(2).moveTo(MARGIN, pdf.y + 10).lineTo(MARGIN + 40, pdf.y + 10).stroke();
|
|
2278
|
-
pdf.y += 28;
|
|
2279
|
-
const drawContent = (el, cx) => {
|
|
2280
|
-
const w = pageRight - cx;
|
|
2281
|
-
switch (el.kind) {
|
|
2282
|
-
case "scene":
|
|
2283
|
-
pdf.fontSize(17);
|
|
2284
|
-
draw(split(el.text, "Serif-Bold", INK), MARGIN, { width: contentW });
|
|
2285
|
-
pdf.strokeColor(ACCENT).lineWidth(1.5).moveTo(MARGIN, pdf.y + 4).lineTo(MARGIN + 34, pdf.y + 4).stroke();
|
|
2286
|
-
break;
|
|
2287
|
-
case "block":
|
|
2288
|
-
pdf.fontSize(13);
|
|
2289
|
-
draw(split(el.text, "Serif-Bold", INK), MARGIN, { width: contentW });
|
|
2290
|
-
pdf.strokeColor(LINE).lineWidth(0.75).moveTo(MARGIN, pdf.y + 3).lineTo(pageRight, pdf.y + 3).stroke();
|
|
2291
|
-
break;
|
|
2292
|
-
case "line": {
|
|
2293
|
-
const cueX = cx + DIALOGUE_INDENT2;
|
|
2294
|
-
let bodyX = cueX;
|
|
2295
|
-
if (el.character) {
|
|
2296
|
-
pdf.font("Sans-Bold").fontSize(CUE);
|
|
2297
|
-
bodyX = cueX + pdf.widthOfString(el.character.toUpperCase()) + CUE_GAP;
|
|
2298
|
-
}
|
|
2299
|
-
const yTop = pdf.y;
|
|
2300
|
-
if (el.character) {
|
|
2301
|
-
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 });
|
|
2302
|
-
pdf.y = yTop;
|
|
2303
|
-
}
|
|
2304
|
-
pdf.fontSize(BODY);
|
|
2305
|
-
draw([...el.direction ? split(`(${el.direction}) `, "Serif-Italic", MUTED) : [], ...bodyPieces(el.runs, INK_READ)], bodyX, { width: Math.max(120, pageRight - bodyX), lineGap: LEAD });
|
|
2306
|
-
break;
|
|
2307
|
-
}
|
|
2308
|
-
case "narration":
|
|
2309
|
-
pdf.fontSize(BODY);
|
|
2310
|
-
draw(bodyPieces(el.runs, INK_SOFT), cx, { width: w, lineGap: LEAD });
|
|
2311
|
-
break;
|
|
2312
|
-
case "condition":
|
|
2313
|
-
pdf.fontSize(9.5);
|
|
2314
|
-
draw(split(`\u2039 ${el.text} \u203A`, "Mono", ACCENT), cx, { width: w });
|
|
2315
|
-
break;
|
|
2316
|
-
case "group":
|
|
2317
|
-
pdf.fontSize(8);
|
|
2318
|
-
draw(split(el.label.toUpperCase(), "Sans-Bold", ACCENT), cx, { width: w, characterSpacing: 1.2 });
|
|
2319
|
-
break;
|
|
2320
|
-
case "else":
|
|
2321
|
-
pdf.fontSize(8);
|
|
2322
|
-
draw(split("ELSE \xB7 CATCH-ALL", "Sans", MUTED), cx, { width: w, characterSpacing: 1 });
|
|
2323
|
-
break;
|
|
2324
|
-
case "option": {
|
|
2325
|
-
const ox = cx + HANG2;
|
|
2326
|
-
const tag = el.tag ? el.tag.toUpperCase() : "";
|
|
2327
|
-
const tagW = tag ? pdf.font("Sans").fontSize(7.5).widthOfString(tag) + 8 : 0;
|
|
2328
|
-
const yStart = pdf.y;
|
|
2329
|
-
if (tag) {
|
|
2330
|
-
pdf.font("Sans").fontSize(7.5).fillColor(MUTED).text(tag, pageRight - tagW, yStart, { width: tagW, align: "right", lineBreak: false });
|
|
2331
|
-
pdf.y = yStart;
|
|
2332
|
-
}
|
|
2333
|
-
pdf.fontSize(BODY);
|
|
2334
|
-
draw([...split("\u25C7 ", "Serif", ACCENT), ...bodyPieces(el.runs, INK)], ox, { width: Math.max(80, pageRight - ox - tagW), indent: -HANG2, lineGap: LEAD });
|
|
2335
|
-
break;
|
|
2336
|
-
}
|
|
2337
|
-
case "jump":
|
|
2338
|
-
drawRight(split(`\u21AA ${el.text}`, "Sans-Bold", ACCENT), 9.5);
|
|
2339
|
-
break;
|
|
2340
|
-
case "gameEvent": {
|
|
2341
|
-
const pieces = split(`\u2699 ${el.text}`, "Mono", ACCENT);
|
|
2342
|
-
pdf.fontSize(9);
|
|
2343
|
-
let total = 0;
|
|
2344
|
-
for (const p of pieces) {
|
|
2345
|
-
pdf.font(p.font);
|
|
2346
|
-
total += pdf.widthOfString(p.text);
|
|
2347
|
-
}
|
|
2348
|
-
if (total <= pageRight - cx) drawRight(pieces, 9);
|
|
2349
|
-
else draw(pieces, cx, { width: pageRight - cx });
|
|
2350
|
-
break;
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
const leadOf = (k) => k === "scene" ? 24 : k === "block" ? 16 : k === "group" ? 13 : k === "else" ? 9 : 0;
|
|
2355
|
-
const els = doc.elements;
|
|
2356
|
-
let curSid;
|
|
2357
|
-
let curBase = 0;
|
|
2358
|
-
for (let i = 0; i < els.length; i++) {
|
|
2359
|
-
const el = els[i];
|
|
2360
|
-
const next = els[i + 1];
|
|
2361
|
-
const sid = "snippet" in el ? el.snippet : void 0;
|
|
2362
|
-
const sameNext = sid !== void 0 && next !== void 0 && "snippet" in next && next.snippet === sid;
|
|
2363
|
-
if (el.kind === "scene") ensure(90);
|
|
2364
|
-
else if (el.kind === "block") ensure(70);
|
|
2365
|
-
else ensure(leadOf(el.kind) + BODY + LEAD + 14);
|
|
2366
|
-
pdf.y += leadOf(el.kind);
|
|
2367
|
-
const base = "indent" in el ? MARGIN + el.indent * INDENT_STEP : MARGIN;
|
|
2368
|
-
const cx = base + (sid !== void 0 ? EDGE_INSET : 0);
|
|
2369
|
-
const yTop = pdf.y;
|
|
2370
|
-
const pageBefore = pdf.page;
|
|
2371
|
-
drawContent(el, cx);
|
|
2372
|
-
const bottom = pdf.y;
|
|
2373
|
-
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;
|
|
2374
|
-
if (sid !== void 0) {
|
|
2375
|
-
if (sid !== curSid) {
|
|
2376
|
-
curSid = sid;
|
|
2377
|
-
curBase = "indent" in el ? el.indent : 0;
|
|
2378
|
-
}
|
|
2379
|
-
const ex = MARGIN + curBase * INDENT_STEP + EDGE_OFF;
|
|
2380
|
-
const top = pdf.page === pageBefore ? yTop : MARGIN;
|
|
2381
|
-
pdf.strokeColor(EDGE).lineWidth(0.75).moveTo(ex, top).lineTo(ex, bottom + (sameNext ? trail : 0)).stroke();
|
|
2382
|
-
}
|
|
2383
|
-
pdf.y = bottom + trail;
|
|
2384
|
-
}
|
|
2385
|
-
pdf.end();
|
|
2386
|
-
});
|
|
2010
|
+
}
|
|
2011
|
+
return out.join("\n");
|
|
2387
2012
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
const fileSlug = slug(name);
|
|
2397
|
-
let existing = [];
|
|
2398
|
-
try {
|
|
2399
|
-
existing = readdirSync3(dir).filter((f) => f.endsWith(".patterproj"));
|
|
2400
|
-
} 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++;
|
|
2401
2021
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
const
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
children: [{
|
|
2424
|
-
id: newId("sn"),
|
|
2425
|
-
type: "snippet",
|
|
2426
|
-
beats: [{ id: beatId, kind: "text" }],
|
|
2427
|
-
jump: { to: "END" }
|
|
2428
|
-
}]
|
|
2429
|
-
}]
|
|
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();
|
|
2430
2043
|
}
|
|
2044
|
+
comments = [];
|
|
2045
|
+
scene = "";
|
|
2046
|
+
stale = false;
|
|
2047
|
+
ctxt = void 0;
|
|
2048
|
+
msgid = "";
|
|
2431
2049
|
};
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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++;
|
|
2454
2095
|
}
|
|
2455
|
-
return {
|
|
2456
|
-
}
|
|
2457
|
-
function vcsConfigWrites(dir, vcs, bundle = "commit") {
|
|
2458
|
-
const writes = [{ path: join4(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
|
|
2459
|
-
if (vcs === "git") writes.push({ path: join4(dir, ".gitattributes"), content: GITATTRIBUTES });
|
|
2460
|
-
const ignore = ignoreFileFor(vcs);
|
|
2461
|
-
if (ignore) writes.push({ path: join4(dir, ignore), content: ignoreContent(bundle) });
|
|
2462
|
-
return writes;
|
|
2096
|
+
return { project, defaultLocale: locale ?? "", locale, entries };
|
|
2463
2097
|
}
|
|
2464
|
-
var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
|
|
2465
|
-
root = true
|
|
2466
|
-
|
|
2467
|
-
[*]
|
|
2468
|
-
charset = utf-8
|
|
2469
|
-
end_of_line = lf
|
|
2470
|
-
insert_final_newline = true
|
|
2471
|
-
|
|
2472
|
-
[*.{patterflow,patterloc,patterx,patterproj}]
|
|
2473
|
-
indent_style = space
|
|
2474
|
-
indent_size = 2
|
|
2475
|
-
`;
|
|
2476
|
-
var GITATTRIBUTES = `# Patter source is UTF-8 + LF text (pinned; never let autocrlf touch it).
|
|
2477
|
-
*.patterflow text eol=lf
|
|
2478
|
-
*.patterloc text eol=lf
|
|
2479
|
-
*.patterx text eol=lf
|
|
2480
|
-
*.patterproj text eol=lf
|
|
2481
|
-
|
|
2482
|
-
# Id-keyed structured merge for Patter source (the 'patter merge' driver).
|
|
2483
|
-
# Register it once per clone (see vcs-setup.md); until then git falls back to a
|
|
2484
|
-
# normal text merge for these - no harm, just not structure-aware.
|
|
2485
|
-
*.patterflow merge=patter
|
|
2486
|
-
*.patterloc merge=patter
|
|
2487
|
-
*.patterx merge=patter
|
|
2488
|
-
*.patterproj merge=patter
|
|
2489
2098
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
+
}
|
|
2508
2130
|
}
|
|
2131
|
+
return Buffer.from(await wb.xlsx.writeBuffer());
|
|
2509
2132
|
}
|
|
2510
|
-
function
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
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
|
+
});
|
|
2520
2160
|
}
|
|
2521
|
-
return
|
|
2161
|
+
return { project: "", defaultLocale: "", locale: void 0, entries };
|
|
2522
2162
|
}
|
|
2523
|
-
function vcsSetup(vcs, bundle) {
|
|
2524
|
-
const sections = {
|
|
2525
|
-
git: `## git
|
|
2526
|
-
|
|
2527
|
-
\`.gitattributes\` (already emitted) pins Patter source shards to UTF-8 + LF text
|
|
2528
|
-
and marks the generated artifacts (the \`.patterc\` bundle \`merge=ours\`, the
|
|
2529
|
-
\`.patterpack\` document \`binary\`). \`.gitignore\` (already emitted) keeps the document
|
|
2530
|
-
(and the bundle, under the "ignore" posture) out of source control.
|
|
2531
|
-
|
|
2532
|
-
Recommended pre-commit hook (\`.git/hooks/pre-commit\`, executable):
|
|
2533
|
-
|
|
2534
|
-
#!/bin/sh
|
|
2535
|
-
patter validate || exit 1
|
|
2536
|
-
|
|
2537
|
-
The \`merge=patter\` rules in \`.gitattributes\` are already active; register the
|
|
2538
|
-
drivers once per clone (git config is not repo-tracked) - until then git falls
|
|
2539
|
-
back to a normal text merge for those files:
|
|
2540
|
-
|
|
2541
|
-
git config merge.patter.name "Patter structured merge"
|
|
2542
|
-
git config merge.patter.driver "patter merge %O %A %B -o %A"
|
|
2543
|
-
git config merge.ours.driver true
|
|
2544
|
-
|
|
2545
|
-
git invokes the per-path driver directly (no \`mergetool\` wrapper needed). \`%O %A
|
|
2546
|
-
%B\` are base / ours / theirs; the merged result is written back to \`%A\`. On a
|
|
2547
|
-
conflict \`patter merge\` exits non-zero and writes a \`.patterconflict\` sidecar
|
|
2548
|
-
beside the file, so the merge stays unresolved.
|
|
2549
|
-
`,
|
|
2550
|
-
perforce: `## Perforce
|
|
2551
|
-
|
|
2552
|
-
Add Patter extensions to the typemap (\`p4 typemap\`): source shards and the
|
|
2553
|
-
compiled bundle are TEXT with LF; the packed document is BINARY:
|
|
2554
|
-
|
|
2555
|
-
text //....patterflow
|
|
2556
|
-
text //....patterloc
|
|
2557
|
-
text //....patterx
|
|
2558
|
-
text //....patterproj
|
|
2559
|
-
text //....patterc
|
|
2560
|
-
binary //....patterpack
|
|
2561
|
-
|
|
2562
|
-
On a unicode-mode server, set \`P4CHARSET=utf8\`. For the lock-based workflow
|
|
2563
|
-
(spec: one scene per file = one lock per scene), add \`+l\` to make checkouts
|
|
2564
|
-
exclusive. A \`.p4ignore\` (already emitted) keeps the packed document out of the
|
|
2565
|
-
depot.
|
|
2566
|
-
|
|
2567
|
-
Perforce allows ONE global merge tool, so set \`patter mergetool\` as it - it
|
|
2568
|
-
runs the structured merge for Patter source and hands everything else to your
|
|
2569
|
-
normal tool. Map Perforce's variables into the order BASE THEIRS OURS OUT:
|
|
2570
|
-
|
|
2571
|
-
patter mergetool --fallback "<your merge tool>" %b %t %y %r
|
|
2572
|
-
|
|
2573
|
-
(\`%b\` base, \`%t\` theirs, \`%y\` yours/ours, \`%r\` result - adjust to your P4
|
|
2574
|
-
client's variable names; the argument ORDER is what matters.)
|
|
2575
|
-
`,
|
|
2576
|
-
plastic: `## Plastic SCM (Unity VC)
|
|
2577
|
-
|
|
2578
|
-
Patter source shards are plain UTF-8 text; Plastic handles them as-is. The
|
|
2579
|
-
\`ignore.conf\` (already emitted) keeps the packed document out of the repo. For
|
|
2580
|
-
the lock-based workflow, configure exclusive checkout (lock.conf) for the shard
|
|
2581
|
-
extensions.
|
|
2582
|
-
|
|
2583
|
-
In Preferences > Merge tools, add an external tool (a global entry is fine -
|
|
2584
|
-
the wrapper sniffs the path) with the arguments in BASE THEIRS OURS OUT order:
|
|
2585
|
-
|
|
2586
|
-
patter mergetool --fallback "<your merge tool>" @basefile @sourcefile @destinationfile @output
|
|
2587
|
-
`,
|
|
2588
|
-
svn: `## SVN
|
|
2589
|
-
|
|
2590
|
-
Set auto-props so Patter source keeps LF (\`~/.subversion/config\` or repo config):
|
|
2591
|
-
|
|
2592
|
-
[auto-props]
|
|
2593
|
-
*.patterflow = svn:eol-style=LF
|
|
2594
|
-
*.patterloc = svn:eol-style=LF
|
|
2595
|
-
*.patterx = svn:eol-style=LF
|
|
2596
|
-
*.patterproj = svn:eol-style=LF
|
|
2597
|
-
*.patterc = svn:eol-style=LF
|
|
2598
|
-
*.patterpack = svn:mime-type=application/octet-stream
|
|
2599
|
-
|
|
2600
|
-
SVN's ignore is a directory PROPERTY, not a file - set it from the project root:
|
|
2601
|
-
|
|
2602
|
-
svn propset svn:ignore "*.patterpack${bundle === "ignore" ? "\\n*.patterc" : ""}\\n*.patterconflict" .
|
|
2603
|
-
|
|
2604
|
-
For the lock-based workflow add \`svn:needs-lock\` to the shard patterns.
|
|
2605
|
-
|
|
2606
|
-
SVN allows one global merge tool. Point \`[helpers] merge-tool-cmd\` at a small
|
|
2607
|
-
wrapper script that forwards SVN's four arguments (base theirs mine merged =
|
|
2608
|
-
BASE THEIRS OURS OUT) to \`patter mergetool\`:
|
|
2609
2163
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
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(/</g, "<").replace(/>/g, ">").replace(/&/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
|
+
}
|
|
2613
2220
|
};
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
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 };
|
|
2624
2232
|
}
|
|
2625
|
-
function bundleSection(bundle) {
|
|
2626
|
-
const posture = bundle === "commit" ? `This project COMMITS the compiled \`.patterc\` bundle (\`patter init --bundle ignore\`
|
|
2627
|
-
to build it in CI instead). The bundle is regenerated, never hand-merged: on a
|
|
2628
|
-
conflict, keep ours and re-run \`patter export\`. \`patter validate\` recomputes the
|
|
2629
|
-
bundle's embedded hash from source and FAILS if it is stale, so a forgotten
|
|
2630
|
-
regenerate cannot ship silently.` : `This project IGNORES the compiled \`.patterc\` bundle (built in CI; \`patter init
|
|
2631
|
-
--bundle commit\` to commit it instead). Run \`patter export\` as a build step.`;
|
|
2632
|
-
return `## Compiled bundle & packed document
|
|
2633
2233
|
|
|
2634
|
-
|
|
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
|
+
}
|
|
2635
2266
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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;
|
|
2641
2308
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
if (
|
|
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;
|
|
2658
2328
|
}
|
|
2659
|
-
|
|
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 }];
|
|
2660
2344
|
}
|
|
2661
|
-
function
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
const
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
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(", ")}`;
|
|
2674
2357
|
}
|
|
2675
|
-
|
|
2676
|
-
const group = (e, i) => e.sceneId !== focus.sceneId ? 2 : cut < 0 || i >= cut ? 0 : 1;
|
|
2677
|
-
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";
|
|
2678
2359
|
}
|
|
2679
|
-
function
|
|
2680
|
-
const
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
const
|
|
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();
|
|
2688
2383
|
for (const scene of loaded.scenes) {
|
|
2689
|
-
|
|
2384
|
+
sceneOf.set(scene.id, scene.name);
|
|
2690
2385
|
for (const block of scene.blocks) {
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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);
|
|
2702
2444
|
}
|
|
2703
|
-
|
|
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());
|
|
2704
2455
|
});
|
|
2456
|
+
return;
|
|
2705
2457
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
const add = (tags) => {
|
|
2713
|
-
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);
|
|
2714
2464
|
};
|
|
2715
2465
|
for (const scene of loaded.scenes) {
|
|
2716
|
-
|
|
2466
|
+
if (cut.has(scene.id)) continue;
|
|
2467
|
+
els.push({ kind: "scene", text: scene.name });
|
|
2717
2468
|
for (const block of scene.blocks) {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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 })]
|
|
2726
2506
|
});
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
const has = (tags) => !!tags && tags.includes(tag);
|
|
2735
|
-
const out = [];
|
|
2736
|
-
for (const scene of loaded.scenes) {
|
|
2737
|
-
const file = loaded.sceneFiles[scene.id];
|
|
2738
|
-
if (has(scene.tags)) out.push({ id: scene.id, kind: "scene", name: scene.name, gameId: effectiveGameId(scene), location: [scene.name], sceneId: scene.id, file });
|
|
2739
|
-
for (const block of scene.blocks) {
|
|
2740
|
-
const segments = [scene.name, block.name];
|
|
2741
|
-
if (has(block.tags)) out.push({ id: block.id, kind: "block", name: block.name, gameId: effectiveGameId(block), location: segments, sceneId: scene.id, file });
|
|
2742
|
-
walkNodes4(block.children, (node) => {
|
|
2743
|
-
if (has(node.tags)) {
|
|
2744
|
-
const preview = node.type === "group" ? node.prompt ? strings[node.prompt.id] : void 0 : strings[node.beats?.[0]?.id ?? ""];
|
|
2745
|
-
out.push({ id: node.id, kind: node.type, text: preview, location: segments, sceneId: scene.id, file });
|
|
2746
|
-
}
|
|
2747
|
-
if (node.type === "group") {
|
|
2748
|
-
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 });
|
|
2749
|
-
return;
|
|
2750
|
-
}
|
|
2751
|
-
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 })]
|
|
2752
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 });
|
|
2753
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 })] });
|
|
2754
2539
|
}
|
|
2755
|
-
if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
|
|
2756
|
-
return out;
|
|
2757
2540
|
}
|
|
2758
|
-
function
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
const
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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;
|
|
2774
2597
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
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;
|
|
2782
2611
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
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;
|
|
2787
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;
|
|
2788
2684
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
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;
|
|
2791
2713
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
}
|
|
2807
|
-
var indexCache = null;
|
|
2808
|
-
function sameRefs(a, b) {
|
|
2809
|
-
if (a.length !== b.length) return false;
|
|
2810
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2811
|
-
return true;
|
|
2812
|
-
}
|
|
2813
|
-
function indexProject(loaded) {
|
|
2814
|
-
if (indexCache && indexCache.loaded === loaded && sameRefs(indexCache.scenes, loaded.scenes) && sameRefs(indexCache.locales, loaded.locales)) {
|
|
2815
|
-
return indexCache.entries;
|
|
2816
|
-
}
|
|
2817
|
-
const entries = buildIndex(loaded);
|
|
2818
|
-
indexCache = { loaded, scenes: [...loaded.scenes], locales: [...loaded.locales], entries };
|
|
2819
|
-
return entries;
|
|
2820
|
-
}
|
|
2821
|
-
function buildIndex(loaded) {
|
|
2822
|
-
const out = [];
|
|
2823
|
-
const strings = sourceStrings(loaded);
|
|
2824
|
-
for (const scene of loaded.scenes) {
|
|
2825
|
-
const file = loaded.sceneFiles[scene.id];
|
|
2826
|
-
out.push({
|
|
2827
|
-
id: scene.id,
|
|
2828
|
-
kind: "scene",
|
|
2829
|
-
name: scene.name,
|
|
2830
|
-
gameId: effectiveGameId(scene),
|
|
2831
|
-
location: [scene.name],
|
|
2832
|
-
sceneId: scene.id,
|
|
2833
|
-
file
|
|
2834
|
-
});
|
|
2835
|
-
for (const block of scene.blocks) {
|
|
2836
|
-
const segments = [scene.name, block.name];
|
|
2837
|
-
out.push({
|
|
2838
|
-
id: block.id,
|
|
2839
|
-
kind: "block",
|
|
2840
|
-
name: block.name,
|
|
2841
|
-
gameId: effectiveGameId(block),
|
|
2842
|
-
location: segments,
|
|
2843
|
-
sceneId: scene.id,
|
|
2844
|
-
file
|
|
2845
|
-
});
|
|
2846
|
-
walkNodes4(block.children, (node) => {
|
|
2847
|
-
out.push({
|
|
2848
|
-
id: node.id,
|
|
2849
|
-
kind: node.type,
|
|
2850
|
-
location: segments,
|
|
2851
|
-
sceneId: scene.id,
|
|
2852
|
-
file
|
|
2853
|
-
});
|
|
2854
|
-
if (node.type === "group" && node.prompt) {
|
|
2855
|
-
out.push({
|
|
2856
|
-
id: node.prompt.id,
|
|
2857
|
-
kind: "beat",
|
|
2858
|
-
text: strings[node.prompt.id],
|
|
2859
|
-
location: segments,
|
|
2860
|
-
sceneId: scene.id,
|
|
2861
|
-
file
|
|
2862
|
-
});
|
|
2863
|
-
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;
|
|
2864
2728
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
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;
|
|
2875
2755
|
}
|
|
2876
|
-
|
|
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;
|
|
2877
2761
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2762
|
+
pdf.end();
|
|
2763
|
+
});
|
|
2880
2764
|
}
|
|
2881
2765
|
|
|
2882
|
-
// src/
|
|
2883
|
-
import {
|
|
2884
|
-
import {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const
|
|
2888
|
-
const
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
if (!re) return plan;
|
|
2895
|
-
const def = loaded.project.locales.default;
|
|
2896
|
-
const locOf = /* @__PURE__ */ new Map();
|
|
2897
|
-
const sceneName = /* @__PURE__ */ new Map();
|
|
2898
|
-
for (const scene of loaded.scenes) {
|
|
2899
|
-
sceneName.set(scene.id, scene.name);
|
|
2900
|
-
for (const block of scene.blocks) {
|
|
2901
|
-
const segs = [scene.name, block.name];
|
|
2902
|
-
walkNodes5(block.children, (node) => {
|
|
2903
|
-
if (node.type === "group") {
|
|
2904
|
-
if (node.prompt) locOf.set(node.prompt.id, segs);
|
|
2905
|
-
return;
|
|
2906
|
-
}
|
|
2907
|
-
for (const b of node.beats ?? []) locOf.set(b.id, segs);
|
|
2908
|
-
});
|
|
2909
|
-
}
|
|
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 {
|
|
2910
2778
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
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
|
+
}]
|
|
2933
2807
|
}
|
|
2934
|
-
}
|
|
2935
|
-
return plan;
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
// src/pack.ts
|
|
2939
|
-
import JSZip from "jszip";
|
|
2940
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
2941
|
-
import { dirname as dirname4, relative, sep as sep2 } from "path";
|
|
2942
|
-
import { parseSource as parseSource4 } from "@patterkit/core";
|
|
2943
|
-
var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
|
|
2944
|
-
var FIXED_DATE = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
|
|
2945
|
-
var ENTRY_OPTS = { date: FIXED_DATE, createFolders: false };
|
|
2946
|
-
async function runPack(startPath) {
|
|
2947
|
-
const projectFile = findProjectFile(startPath);
|
|
2948
|
-
const root = dirname4(projectFile);
|
|
2949
|
-
const project = parseSource4(readFileSync4(projectFile, "utf8"));
|
|
2950
|
-
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));
|
|
2951
|
-
const manifest = {
|
|
2952
|
-
schema: "patter/document@0",
|
|
2953
|
-
project: { id: project.project.id, name: project.project.name },
|
|
2954
|
-
files: files.map((f) => f.rel)
|
|
2955
2808
|
};
|
|
2956
|
-
const
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
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;
|
|
2960
2840
|
}
|
|
2841
|
+
var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
|
|
2842
|
+
root = true
|
|
2961
2843
|
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
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
|
|
2967
2848
|
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
var
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
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;
|
|
2993
2885
|
}
|
|
2994
2886
|
}
|
|
2995
|
-
function
|
|
2996
|
-
const
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
}
|
|
3006
|
-
function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
|
|
3007
|
-
const out = {};
|
|
3008
|
-
for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
|
|
3009
|
-
const b = base[k], o = ours[k], t = theirs[k];
|
|
3010
|
-
let v;
|
|
3011
|
-
if (eq(o, t)) v = o;
|
|
3012
|
-
else if (eq(b, o)) v = t;
|
|
3013
|
-
else if (eq(b, t)) v = o;
|
|
3014
|
-
else {
|
|
3015
|
-
const r = resolve5?.(k, b, o, t);
|
|
3016
|
-
if (r && r.resolved) v = r.value;
|
|
3017
|
-
else {
|
|
3018
|
-
conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
|
|
3019
|
-
v = o;
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
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");
|
|
3023
2897
|
}
|
|
3024
|
-
return
|
|
2898
|
+
return lines.join("\n") + "\n";
|
|
3025
2899
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
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)}`;
|
|
3030
3001
|
}
|
|
3031
|
-
function
|
|
3032
|
-
const
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
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
|
+
`;
|
|
3040
3018
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3048
|
-
const
|
|
3049
|
-
|
|
3050
|
-
|
|
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;
|
|
3051
3035
|
}
|
|
3052
|
-
|
|
3053
|
-
setIf(merged, "cut", mergeMap(asMap(base.cut), asMap(ours.cut), asMap(theirs.cut), "cut", conflicts, void 0));
|
|
3054
|
-
return { type: "authoring", merged, conflicts, warnings: [] };
|
|
3036
|
+
return [];
|
|
3055
3037
|
}
|
|
3056
|
-
function
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3059
|
-
|
|
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];
|
|
3060
3051
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
return (id, _b, o, t) => {
|
|
3065
|
-
const om = asMap(oursEdits[id]).modifiedAt, tm = asMap(theirsEdits[id]).modifiedAt;
|
|
3066
|
-
if (typeof om === "string" && typeof tm === "string") return { resolved: true, value: om >= tm ? o : t };
|
|
3067
|
-
return { resolved: false, kind: "no-timestamp" };
|
|
3068
|
-
};
|
|
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);
|
|
3069
3055
|
}
|
|
3070
|
-
function
|
|
3071
|
-
const
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
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
|
+
});
|
|
3085
3082
|
}
|
|
3086
|
-
out[id] = mergeEditRecord(asMap(o), asMap(t));
|
|
3087
3083
|
}
|
|
3084
|
+
if (focus) out.sort((a, b) => (a.sceneId === focus.sceneId ? 0 : 1) - (b.sceneId === focus.sceneId ? 0 : 1));
|
|
3088
3085
|
return out;
|
|
3089
3086
|
}
|
|
3090
|
-
function
|
|
3091
|
-
const
|
|
3092
|
-
const
|
|
3093
|
-
|
|
3094
|
-
const ol = asMap(o.localisedAt), tl = asMap(t.localisedAt);
|
|
3095
|
-
const loc = {};
|
|
3096
|
-
for (const k of /* @__PURE__ */ new Set([...Object.keys(ol), ...Object.keys(tl)])) {
|
|
3097
|
-
const a = typeof ol[k] === "string" ? ol[k] : "";
|
|
3098
|
-
const b = typeof tl[k] === "string" ? tl[k] : "";
|
|
3099
|
-
loc[k] = a >= b ? ol[k] ?? tl[k] : tl[k] ?? ol[k];
|
|
3100
|
-
}
|
|
3101
|
-
const merged = { ...newer };
|
|
3102
|
-
if (Object.keys(loc).length > 0) merged.localisedAt = loc;
|
|
3103
|
-
return merged;
|
|
3104
|
-
}
|
|
3105
|
-
function mergeKeyedByName(base, ours, theirs, path, conflicts) {
|
|
3106
|
-
const byName = (arr) => {
|
|
3107
|
-
const m = {};
|
|
3108
|
-
for (const p of arr) if (isObj(p) && typeof p.name === "string") m[p.name] = p;
|
|
3109
|
-
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);
|
|
3110
3091
|
};
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
if (Object.keys(v).length > 0) merged[k] = v;
|
|
3124
|
-
} else if (k === "locales") {
|
|
3125
|
-
merged.locales = mergeLocales(asMap(base.locales), asMap(ours.locales), asMap(theirs.locales), conflicts);
|
|
3126
|
-
} else {
|
|
3127
|
-
const v = merge3(base[k], ours[k], theirs[k], "", k, conflicts);
|
|
3128
|
-
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
|
+
});
|
|
3129
3104
|
}
|
|
3130
3105
|
}
|
|
3131
|
-
return
|
|
3132
|
-
}
|
|
3133
|
-
function mergeGameDataFields(b, o, t, conflicts) {
|
|
3134
|
-
const out = {};
|
|
3135
|
-
for (const kind of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
|
|
3136
|
-
const v = mergeKeyedByName(asArr(b[kind]), asArr(o[kind]), asArr(t[kind]), `gameDataFields.${kind}`, conflicts);
|
|
3137
|
-
if (v.length > 0) out[kind] = v;
|
|
3138
|
-
}
|
|
3139
|
-
return out;
|
|
3106
|
+
return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3140
3107
|
}
|
|
3141
|
-
function
|
|
3142
|
-
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
const
|
|
3146
|
-
for (const
|
|
3147
|
-
|
|
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
|
+
}
|
|
3148
3131
|
}
|
|
3149
|
-
out.
|
|
3150
|
-
return out;
|
|
3151
|
-
}
|
|
3152
|
-
var CHILD_KEYS = ["blocks", "children", "beats"];
|
|
3153
|
-
function toTree(raw) {
|
|
3154
|
-
const childKey = CHILD_KEYS.find((k) => Array.isArray(raw[k]));
|
|
3155
|
-
const fields = {};
|
|
3156
|
-
for (const [k, v] of Object.entries(raw)) if (k !== childKey) fields[k] = v;
|
|
3157
|
-
const children = childKey ? raw[childKey].filter(isObj).map(toTree) : [];
|
|
3158
|
-
return { id: typeof raw.id === "string" ? raw.id : "", fields, childKey, children };
|
|
3159
|
-
}
|
|
3160
|
-
function fromTree(n) {
|
|
3161
|
-
const out = { ...n.fields };
|
|
3162
|
-
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));
|
|
3163
3133
|
return out;
|
|
3164
3134
|
}
|
|
3165
|
-
function
|
|
3166
|
-
const
|
|
3167
|
-
|
|
3168
|
-
const
|
|
3169
|
-
|
|
3170
|
-
const
|
|
3171
|
-
const
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
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;
|
|
3177
3174
|
}
|
|
3178
|
-
function
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
const
|
|
3182
|
-
|
|
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 };
|
|
3183
3183
|
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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;
|
|
3189
3193
|
}
|
|
3190
|
-
|
|
3194
|
+
const entries = buildIndex(loaded);
|
|
3195
|
+
indexCache = { loaded, scenes: [...loaded.scenes], locales: [...loaded.locales], entries };
|
|
3196
|
+
return entries;
|
|
3191
3197
|
}
|
|
3192
|
-
function
|
|
3193
|
-
const
|
|
3194
|
-
const
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
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
|
+
});
|
|
3220
3254
|
}
|
|
3221
3255
|
}
|
|
3222
|
-
return
|
|
3256
|
+
return out;
|
|
3223
3257
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
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
|
+
}
|
|
3241
3287
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
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++;
|
|
3252
3310
|
}
|
|
3253
|
-
return g;
|
|
3254
|
-
};
|
|
3255
|
-
for (const n of O) if (merged.has(n.id) && !common.has(n.id)) group(anchorIn(Oids, n.id)).ours.push(n.id);
|
|
3256
|
-
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);
|
|
3257
|
-
const out = [];
|
|
3258
|
-
const emit = (anchor) => {
|
|
3259
|
-
const g = byAnchor.get(anchor);
|
|
3260
|
-
if (!g) return;
|
|
3261
|
-
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" });
|
|
3262
|
-
out.push(...g.ours, ...g.theirs);
|
|
3263
|
-
};
|
|
3264
|
-
emit(null);
|
|
3265
|
-
for (const id of commonOrder) {
|
|
3266
|
-
out.push(id);
|
|
3267
|
-
emit(id);
|
|
3268
3311
|
}
|
|
3269
|
-
return
|
|
3312
|
+
return plan;
|
|
3270
3313
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
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)
|
|
3279
3339
|
};
|
|
3280
|
-
|
|
3281
|
-
|
|
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 });
|
|
3282
3344
|
}
|
|
3283
|
-
var idMap = (nodes) => new Map(nodes.filter((n) => n.id).map((n) => [n.id, n]));
|
|
3284
|
-
var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
3285
3345
|
|
|
3286
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";
|
|
3287
3351
|
var MANIFEST = "patter.manifest.json";
|
|
3288
3352
|
var UnsafeEntryError = class extends Error {
|
|
3289
3353
|
};
|
|
@@ -3346,10 +3410,17 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3346
3410
|
shards.push({ path: rel, added: true });
|
|
3347
3411
|
continue;
|
|
3348
3412
|
}
|
|
3349
|
-
const
|
|
3350
|
-
|
|
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);
|
|
3351
3422
|
const baseText = base.get(rel);
|
|
3352
|
-
const baseObj = baseText !== void 0 ?
|
|
3423
|
+
const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
|
|
3353
3424
|
const result = runMerge(baseObj, oursObj, theirsObj);
|
|
3354
3425
|
writes.push({ path: outPath, content: canonicalStringify7(result.merged) });
|
|
3355
3426
|
if (result.conflicts.length > 0) {
|
|
@@ -3375,6 +3446,8 @@ function containedWrite(dir, name) {
|
|
|
3375
3446
|
return full;
|
|
3376
3447
|
}
|
|
3377
3448
|
export {
|
|
3449
|
+
AUTHORING_HANDLED,
|
|
3450
|
+
CONFLICT_SIDECAR,
|
|
3378
3451
|
SHARD_EXTENSIONS,
|
|
3379
3452
|
UnsafeEntryError,
|
|
3380
3453
|
UnsupportedMergeError,
|
|
@@ -3429,6 +3502,7 @@ export {
|
|
|
3429
3502
|
sceneIdForShard,
|
|
3430
3503
|
scriptToDocx,
|
|
3431
3504
|
scriptToPdf,
|
|
3505
|
+
sidecarIssues,
|
|
3432
3506
|
vcsConfigWrites,
|
|
3433
3507
|
voiceScriptToXlsx,
|
|
3434
3508
|
xlsxToCatalog
|