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