@patterkit/runtime 0.17.0 → 0.19.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/CHANGELOG.md +66 -0
- package/dist/index.cjs +133 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -10
- package/dist/index.d.ts +55 -10
- package/dist/index.js +133 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ function buildTagIndex(bundle) {
|
|
|
21
21
|
const visit = (node, inherited) => {
|
|
22
22
|
const acc = dedupe([...inherited, ...node.tags ?? []]);
|
|
23
23
|
index.set(node.id, acc);
|
|
24
|
+
if (node.type === "group" && node.prompt) index.set(node.prompt.id, dedupe([...acc, ...node.prompt.tags ?? []]));
|
|
24
25
|
if (node.type === "group") {
|
|
25
26
|
for (const child of node.children) visit(child, acc);
|
|
26
27
|
} else {
|
|
@@ -85,6 +86,8 @@ var Engine = class _Engine {
|
|
|
85
86
|
/** The run's ordered stream: every flow's events, each naming its flow. Empty and
|
|
86
87
|
* unwritten unless `options.log` asked for it. */
|
|
87
88
|
engineLog = [];
|
|
89
|
+
/** The next engine-log `seq`: its own counter, since the log's length restarts after a clear. */
|
|
90
|
+
engineSeq = 0;
|
|
88
91
|
engineTraceHandlers = /* @__PURE__ */ new Set();
|
|
89
92
|
constructor(bundle, options = {}) {
|
|
90
93
|
this.creationOptions = options;
|
|
@@ -182,6 +185,7 @@ var Engine = class _Engine {
|
|
|
182
185
|
journal: null,
|
|
183
186
|
customRng: options.rng,
|
|
184
187
|
onDryChoice: options.onDryChoice,
|
|
188
|
+
onError: options.onError ?? ((e) => console.warn(`Patterplay: ${e.kind} on ${e.node} in flow '${e.flow}' failed, played through: ${e.message}`)),
|
|
185
189
|
replayPromptOnChoose: options.replayPromptOnChoose ?? false,
|
|
186
190
|
captionsOn: options.closedCaptions ?? true,
|
|
187
191
|
// captions shown by default (full text)
|
|
@@ -375,10 +379,10 @@ var Engine = class _Engine {
|
|
|
375
379
|
*/
|
|
376
380
|
resolveOpenAddress(scene, block) {
|
|
377
381
|
if (scene !== void 0) {
|
|
378
|
-
const sceneId = this.
|
|
379
|
-
if (
|
|
382
|
+
const sceneId = resolveScene(this.host, scene);
|
|
383
|
+
if (sceneId === void 0) throw new Error(`unknown scene: ${scene}`);
|
|
380
384
|
if (block === void 0) return { sceneId };
|
|
381
|
-
const blockId =
|
|
385
|
+
const blockId = resolveBlock(this.host, sceneId, block);
|
|
382
386
|
if (blockId === void 0) throw new Error(`unknown block: ${block}`);
|
|
383
387
|
return { sceneId, blockId };
|
|
384
388
|
}
|
|
@@ -389,21 +393,19 @@ var Engine = class _Engine {
|
|
|
389
393
|
if (Object.keys(this.host.bundle.scenes).length === 0) throw new Error("no scenes in bundle");
|
|
390
394
|
return {};
|
|
391
395
|
}
|
|
392
|
-
/** Resolve a scene reference
|
|
396
|
+
/** Resolve a scene reference to its internal id by the one address rule (see resolveScene); an
|
|
397
|
+
* unknown one passes through, for the caller to find nothing. */
|
|
393
398
|
resolveSceneRef(ref) {
|
|
394
399
|
if (ref == null) return void 0;
|
|
395
|
-
|
|
396
|
-
return this.sceneGameIdToId.get(ref) ?? ref;
|
|
400
|
+
return resolveScene(this.host, ref) ?? ref;
|
|
397
401
|
}
|
|
398
|
-
/** Resolve a block reference
|
|
402
|
+
/** Resolve a block reference to its internal id by the one address rule (see resolveBlock): within its
|
|
403
|
+
* scene, so a block of another scene resolves to nothing. With no scene, only an internal id resolves,
|
|
404
|
+
* since a block's gameId is only unique within its scene. */
|
|
399
405
|
resolveBlockRef(sceneId, ref) {
|
|
400
406
|
if (ref == null) return void 0;
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
const id = this.blockGameIdToId.get(sceneId)?.get(ref);
|
|
404
|
-
if (id) return id;
|
|
405
|
-
}
|
|
406
|
-
return ref;
|
|
407
|
+
if (sceneId == null) return ref;
|
|
408
|
+
return resolveBlock(this.host, sceneId, ref);
|
|
407
409
|
}
|
|
408
410
|
/** The host-facing address (gameId) of a scene / block by internal id, or undefined if unknown.
|
|
409
411
|
* The inverse of the resolve helpers - for a host that wants to display / log the address. */
|
|
@@ -730,7 +732,7 @@ var Engine = class _Engine {
|
|
|
730
732
|
emitEngine(flow, event, scene) {
|
|
731
733
|
for (const h of this.engineTraceHandlers) h(flow, event);
|
|
732
734
|
if (!this.host.logEnabled) return;
|
|
733
|
-
this.engineLog.push({ ...event, flow, seq: this.
|
|
735
|
+
this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...scene ? { scene } : {} });
|
|
734
736
|
}
|
|
735
737
|
listProperties() {
|
|
736
738
|
return this.host.patterSharedDecls.map((d) => ({
|
|
@@ -796,6 +798,8 @@ var Engine = class _Engine {
|
|
|
796
798
|
this.refuseInCheckpoint("loadGame");
|
|
797
799
|
const version = save.version;
|
|
798
800
|
if (version !== 2 && version !== SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
801
|
+
const flows = save.flows;
|
|
802
|
+
if (typeof flows !== "object" || flows === null || Array.isArray(flows)) throw new Error("malformed save: no flows");
|
|
799
803
|
this.assertExternalScopes();
|
|
800
804
|
const reg = this.host.registry;
|
|
801
805
|
for (const [id, flow] of this.flowsById) {
|
|
@@ -843,10 +847,13 @@ var Flow = class {
|
|
|
843
847
|
activeSnippet = null;
|
|
844
848
|
beatIndex = 0;
|
|
845
849
|
pendingChoice = null;
|
|
846
|
-
/** When `replayPromptOnChoose`, the chosen option's prompt
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
*
|
|
850
|
+
/** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
|
|
851
|
+
* beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
|
|
852
|
+
* `shown` is missing only after loading a save written before saves carried it: the beat is then
|
|
853
|
+
* resolved when it is delivered, as it always was. */
|
|
854
|
+
pendingPrompt = null;
|
|
855
|
+
/** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
|
|
856
|
+
* advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
|
|
850
857
|
pendingPromptOwnerId = null;
|
|
851
858
|
selectors = /* @__PURE__ */ new Map();
|
|
852
859
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
@@ -1008,7 +1015,7 @@ var Flow = class {
|
|
|
1008
1015
|
if (scene === "END") {
|
|
1009
1016
|
this.started = true;
|
|
1010
1017
|
this.pendingChoice = null;
|
|
1011
|
-
this.
|
|
1018
|
+
this.pendingPrompt = null;
|
|
1012
1019
|
this.pendingPromptOwnerId = null;
|
|
1013
1020
|
this.activeSnippet = null;
|
|
1014
1021
|
this.beatIndex = 0;
|
|
@@ -1016,11 +1023,11 @@ var Flow = class {
|
|
|
1016
1023
|
this.stack = [];
|
|
1017
1024
|
return true;
|
|
1018
1025
|
}
|
|
1019
|
-
const sceneId =
|
|
1026
|
+
const sceneId = resolveScene(this.host, scene);
|
|
1020
1027
|
if (sceneId === void 0) return false;
|
|
1021
1028
|
let blockId;
|
|
1022
1029
|
if (block !== void 0) {
|
|
1023
|
-
blockId =
|
|
1030
|
+
blockId = resolveBlock(this.host, sceneId, block);
|
|
1024
1031
|
if (blockId === void 0) return false;
|
|
1025
1032
|
}
|
|
1026
1033
|
if (!this.started) {
|
|
@@ -1028,7 +1035,7 @@ var Flow = class {
|
|
|
1028
1035
|
return true;
|
|
1029
1036
|
}
|
|
1030
1037
|
this.pendingChoice = null;
|
|
1031
|
-
this.
|
|
1038
|
+
this.pendingPrompt = null;
|
|
1032
1039
|
this.pendingPromptOwnerId = null;
|
|
1033
1040
|
this.activeSnippet = null;
|
|
1034
1041
|
this.beatIndex = 0;
|
|
@@ -1055,7 +1062,7 @@ var Flow = class {
|
|
|
1055
1062
|
this.activeSnippet = null;
|
|
1056
1063
|
this.beatIndex = 0;
|
|
1057
1064
|
this.pendingChoice = null;
|
|
1058
|
-
this.
|
|
1065
|
+
this.pendingPrompt = null;
|
|
1059
1066
|
this.pendingPromptOwnerId = null;
|
|
1060
1067
|
}
|
|
1061
1068
|
/** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
|
|
@@ -1065,10 +1072,10 @@ var Flow = class {
|
|
|
1065
1072
|
const journal = this.host.journal;
|
|
1066
1073
|
if (!journal || journal.flows.has(this)) return;
|
|
1067
1074
|
journal.flows.add(this);
|
|
1068
|
-
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice,
|
|
1075
|
+
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId } = this;
|
|
1069
1076
|
const stack = this.stack.map((f) => ({ ...f }));
|
|
1070
1077
|
journal.undo.push(() => {
|
|
1071
|
-
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice,
|
|
1078
|
+
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId, stack });
|
|
1072
1079
|
});
|
|
1073
1080
|
}
|
|
1074
1081
|
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
@@ -1086,11 +1093,11 @@ var Flow = class {
|
|
|
1086
1093
|
if (this.closed) return { type: "end" };
|
|
1087
1094
|
if (!this.started) throw new Error("flow has not been started");
|
|
1088
1095
|
this.touch();
|
|
1089
|
-
if (this.
|
|
1090
|
-
const
|
|
1091
|
-
this.
|
|
1096
|
+
if (this.pendingPrompt) {
|
|
1097
|
+
const p = this.pendingPrompt;
|
|
1098
|
+
this.pendingPrompt = null;
|
|
1092
1099
|
this.pendingPromptOwnerId = null;
|
|
1093
|
-
return this.beatResult(
|
|
1100
|
+
return p.shown ? this.promptResult(p.beat, p.shown) : this.beatResult(p.beat);
|
|
1094
1101
|
}
|
|
1095
1102
|
this.settle();
|
|
1096
1103
|
if (this.flowEnded) return { type: "end" };
|
|
@@ -1130,7 +1137,7 @@ var Flow = class {
|
|
|
1130
1137
|
if (this.flowEnded || this.pendingChoice) return;
|
|
1131
1138
|
if (this.activeSnippet) {
|
|
1132
1139
|
if (this.beatIndex < (this.activeSnippet.beats?.length ?? 0)) return;
|
|
1133
|
-
this.runEffects(this.activeSnippet.onExit);
|
|
1140
|
+
this.runEffects(this.activeSnippet.onExit, this.activeSnippet.id);
|
|
1134
1141
|
const jump = this.activeSnippet.jump;
|
|
1135
1142
|
this.activeSnippet = null;
|
|
1136
1143
|
this.beatIndex = 0;
|
|
@@ -1209,8 +1216,9 @@ var Flow = class {
|
|
|
1209
1216
|
const node = choice.byId.get(id);
|
|
1210
1217
|
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1211
1218
|
this.pendingChoice = null;
|
|
1212
|
-
|
|
1213
|
-
this.
|
|
1219
|
+
const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
|
|
1220
|
+
this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
|
|
1221
|
+
this.pendingPromptOwnerId = this.pendingPrompt ? node.id : null;
|
|
1214
1222
|
this.enterChild(node);
|
|
1215
1223
|
}
|
|
1216
1224
|
isEnded() {
|
|
@@ -1276,6 +1284,8 @@ var Flow = class {
|
|
|
1276
1284
|
beatIndex: this.beatIndex,
|
|
1277
1285
|
pendingChoice: this.pendingChoice ? { groupId: this.pendingChoice.groupId, options: this.pendingChoice.options.map((o) => ({ ...o })) } : null,
|
|
1278
1286
|
pendingPromptOwnerId: this.pendingPromptOwnerId,
|
|
1287
|
+
// The prompt still to be replayed, as the choice showed it: present only in the choose -> advance window.
|
|
1288
|
+
...this.pendingPrompt?.shown ? { pendingPrompt: savedPrompt(this.pendingPrompt.shown) } : {},
|
|
1279
1289
|
selectors: serialiseSelectors(this.selectors)
|
|
1280
1290
|
}
|
|
1281
1291
|
};
|
|
@@ -1326,12 +1336,13 @@ var Flow = class {
|
|
|
1326
1336
|
}
|
|
1327
1337
|
if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
|
|
1328
1338
|
}
|
|
1329
|
-
this.
|
|
1339
|
+
this.pendingPrompt = null;
|
|
1330
1340
|
this.pendingPromptOwnerId = c.pendingPromptOwnerId ?? null;
|
|
1331
1341
|
if (this.pendingPromptOwnerId) {
|
|
1332
1342
|
const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
|
|
1333
|
-
|
|
1334
|
-
if (
|
|
1343
|
+
const beat = owner ? this.authoredPromptOf(owner) : void 0;
|
|
1344
|
+
if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
|
|
1345
|
+
else this.pendingPromptOwnerId = null;
|
|
1335
1346
|
}
|
|
1336
1347
|
}
|
|
1337
1348
|
// -- Scene / block / node entry ------------------------------------------
|
|
@@ -1342,7 +1353,7 @@ var Flow = class {
|
|
|
1342
1353
|
this.currentSceneId = sceneId;
|
|
1343
1354
|
this.enter(sceneId);
|
|
1344
1355
|
this.seedScene(scene);
|
|
1345
|
-
this.runEffects(scene.onEntry);
|
|
1356
|
+
this.runEffects(scene.onEntry, scene.id);
|
|
1346
1357
|
}
|
|
1347
1358
|
/**
|
|
1348
1359
|
* Play one child of the active run. A snippet begins delivering. A group is
|
|
@@ -1378,7 +1389,7 @@ var Flow = class {
|
|
|
1378
1389
|
return void 0;
|
|
1379
1390
|
}
|
|
1380
1391
|
beginSnippet(snippet) {
|
|
1381
|
-
this.runEffects(snippet.onEnter);
|
|
1392
|
+
this.runEffects(snippet.onEnter, snippet.id);
|
|
1382
1393
|
this.activeSnippet = snippet;
|
|
1383
1394
|
this.beatIndex = 0;
|
|
1384
1395
|
}
|
|
@@ -1398,7 +1409,7 @@ var Flow = class {
|
|
|
1398
1409
|
options.push({ id: child.id, prompt: this.promptFor(child), eligible, gameData: child.gameData });
|
|
1399
1410
|
byId.set(child.id, child);
|
|
1400
1411
|
}
|
|
1401
|
-
if (options.
|
|
1412
|
+
if (options.some((o) => o.eligible)) {
|
|
1402
1413
|
this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1403
1414
|
this.pendingChoice = { groupId: group.id, options, byId };
|
|
1404
1415
|
return;
|
|
@@ -1456,7 +1467,7 @@ var Flow = class {
|
|
|
1456
1467
|
// -- Selectors ------------------------------------------------------------
|
|
1457
1468
|
selectChild(group) {
|
|
1458
1469
|
const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
|
|
1459
|
-
const eligible = group.children.filter((
|
|
1470
|
+
const eligible = group.children.filter((_, i) => verdicts[i].eligible);
|
|
1460
1471
|
const order = group.options?.order ?? "sequential";
|
|
1461
1472
|
const exhaust = group.options?.exhaust ?? "once";
|
|
1462
1473
|
const trace = (picked) => {
|
|
@@ -1503,8 +1514,9 @@ var Flow = class {
|
|
|
1503
1514
|
const len = eligible.length;
|
|
1504
1515
|
const stick = exhaust === "stick";
|
|
1505
1516
|
const fill = () => (stick ? eligible.slice(0, len - 1) : eligible).map((c) => c.id);
|
|
1517
|
+
const eligibleIds = new Set(eligible.map((c) => c.id));
|
|
1506
1518
|
if (st.bag === void 0) st.bag = fill();
|
|
1507
|
-
if (st.bag.
|
|
1519
|
+
if (!st.bag.some((id2) => eligibleIds.has(id2))) {
|
|
1508
1520
|
if (exhaust === "once") return null;
|
|
1509
1521
|
if (stick) {
|
|
1510
1522
|
const last = eligible[len - 1];
|
|
@@ -1512,13 +1524,14 @@ var Flow = class {
|
|
|
1512
1524
|
return last;
|
|
1513
1525
|
}
|
|
1514
1526
|
st.bag = fill();
|
|
1527
|
+
if (st.bag.length === 0) return null;
|
|
1515
1528
|
}
|
|
1516
|
-
const pool = st.bag;
|
|
1529
|
+
const pool = st.bag.filter((id2) => eligibleIds.has(id2));
|
|
1517
1530
|
const p = st.last !== void 0 && pool.length > 1 ? pool.indexOf(st.last) : -1;
|
|
1518
1531
|
let i = Math.floor(this.rng() * (p >= 0 ? pool.length - 1 : pool.length));
|
|
1519
1532
|
if (p >= 0 && i >= p) i++;
|
|
1520
1533
|
const id = pool[i];
|
|
1521
|
-
|
|
1534
|
+
st.bag.splice(st.bag.indexOf(id), 1);
|
|
1522
1535
|
st.last = id;
|
|
1523
1536
|
return eligible.find((c) => c.id === id);
|
|
1524
1537
|
}
|
|
@@ -1567,7 +1580,7 @@ var Flow = class {
|
|
|
1567
1580
|
/** A child's Best-match score against the current state: 0 when it has no condition (the filler
|
|
1568
1581
|
* tier), else the specificity of its (already-passing) condition. */
|
|
1569
1582
|
specScore(node) {
|
|
1570
|
-
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true) : 0;
|
|
1583
|
+
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true, node) : 0;
|
|
1571
1584
|
}
|
|
1572
1585
|
/**
|
|
1573
1586
|
* The **matched-specificity** metric (parity contract): how many atomic constraints are actively
|
|
@@ -1577,8 +1590,15 @@ var Flow = class {
|
|
|
1577
1590
|
* to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
|
|
1578
1591
|
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1579
1592
|
*/
|
|
1580
|
-
matchedSpec(node, want) {
|
|
1581
|
-
const evalTruthy = (n) =>
|
|
1593
|
+
matchedSpec(node, want, owner) {
|
|
1594
|
+
const evalTruthy = (n) => {
|
|
1595
|
+
try {
|
|
1596
|
+
return truthy(evaluate(n, this.context(), patterDialect));
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
this.reportError("best-match", owner.id, owner.condition, err);
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1582
1602
|
return scoreSpecificity(node, evalTruthy, { want });
|
|
1583
1603
|
}
|
|
1584
1604
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
@@ -1610,17 +1630,36 @@ var Flow = class {
|
|
|
1610
1630
|
return st;
|
|
1611
1631
|
}
|
|
1612
1632
|
// -- Effects + expressions ------------------------------------------------
|
|
1613
|
-
|
|
1633
|
+
/** Run an effect list. `owner` is the snippet or scene it belongs to, for an error report. */
|
|
1634
|
+
runEffects(effects, owner) {
|
|
1614
1635
|
for (const e of effects ?? []) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1636
|
+
let value;
|
|
1637
|
+
try {
|
|
1638
|
+
value = this.evalExpr(e.value);
|
|
1639
|
+
const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
|
|
1640
|
+
this.writeProperty(e.target, value, false);
|
|
1641
|
+
this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
this.reportError("effect", owner, e.value, err);
|
|
1644
|
+
}
|
|
1619
1645
|
}
|
|
1620
1646
|
}
|
|
1647
|
+
/** Whether a node's condition holds. A condition that fails to evaluate counts as false (see PlayError). */
|
|
1621
1648
|
eligible(node) {
|
|
1622
1649
|
if (!node.condition) return true;
|
|
1623
|
-
|
|
1650
|
+
try {
|
|
1651
|
+
return truthy(this.evalExpr(node.condition));
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
this.reportError("condition", node.id, node.condition, err);
|
|
1654
|
+
return false;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
/** Report a content error the engine is playing through: to the host's onError, and to the log. */
|
|
1658
|
+
reportError(kind, node, expr, err) {
|
|
1659
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1660
|
+
const source = expr?.src;
|
|
1661
|
+
this.host.onError({ flow: this.id, kind, node, ...source ? { source } : {}, message });
|
|
1662
|
+
this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
|
|
1624
1663
|
}
|
|
1625
1664
|
evalExpr(expr) {
|
|
1626
1665
|
return evaluate(this.conditionAst(expr), this.context(), patterDialect);
|
|
@@ -1726,6 +1765,27 @@ var Flow = class {
|
|
|
1726
1765
|
const text = this.interpolate(this.resolveString(beat.id));
|
|
1727
1766
|
return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
|
|
1728
1767
|
}
|
|
1768
|
+
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
1769
|
+
authoredPromptOf(node) {
|
|
1770
|
+
return node.type === "group" ? node.prompt : void 0;
|
|
1771
|
+
}
|
|
1772
|
+
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
1773
|
+
* choice showed. */
|
|
1774
|
+
promptResult(beat, shown) {
|
|
1775
|
+
const tags = this.host.tagIndex.get(beat.id);
|
|
1776
|
+
const withTags = tags && tags.length ? { tags } : {};
|
|
1777
|
+
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1778
|
+
return {
|
|
1779
|
+
type: "line",
|
|
1780
|
+
id: beat.id,
|
|
1781
|
+
text: shown.text,
|
|
1782
|
+
character: shown.character,
|
|
1783
|
+
characterName: shown.characterName,
|
|
1784
|
+
direction: shown.direction,
|
|
1785
|
+
gameData: beat.gameData,
|
|
1786
|
+
...withTags
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1729
1789
|
/** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
|
|
1730
1790
|
promptBeatOf(node) {
|
|
1731
1791
|
if (node.type === "group" && node.prompt) return node.prompt;
|
|
@@ -1795,23 +1855,23 @@ var Flow = class {
|
|
|
1795
1855
|
if (!this.sceneBags.has(s)) {
|
|
1796
1856
|
const bag = new PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1797
1857
|
const key = keys.flowScene(this.id, s);
|
|
1798
|
-
this.host.registry
|
|
1858
|
+
const claimed = mountClaims(this.host.registry, key, bag, journal !== null);
|
|
1799
1859
|
this.registered.add(key);
|
|
1800
1860
|
this.sceneBags.set(s, bag);
|
|
1801
1861
|
if (journal) journal.undo.push(() => {
|
|
1802
1862
|
if (this.sceneBags.get(s) !== bag) return;
|
|
1803
|
-
this.host.registry.remove(key);
|
|
1863
|
+
this.host.registry.remove(key, claimed ? { keep: true } : void 0);
|
|
1804
1864
|
this.registered.delete(key);
|
|
1805
1865
|
this.sceneBags.delete(s);
|
|
1806
1866
|
});
|
|
1807
1867
|
}
|
|
1808
1868
|
if (!this.host.stageBags.has(s)) {
|
|
1809
1869
|
const bag = new PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1810
|
-
this.host.registry
|
|
1870
|
+
const claimed = mountClaims(this.host.registry, keys.stage(s), bag, journal !== null);
|
|
1811
1871
|
this.host.stageBags.set(s, bag);
|
|
1812
1872
|
if (journal) journal.undo.push(() => {
|
|
1813
1873
|
if (this.host.stageBags.get(s) !== bag) return;
|
|
1814
|
-
this.host.registry.remove(keys.stage(s));
|
|
1874
|
+
this.host.registry.remove(keys.stage(s), claimed ? { keep: true } : void 0);
|
|
1815
1875
|
this.host.stageBags.delete(s);
|
|
1816
1876
|
});
|
|
1817
1877
|
}
|
|
@@ -1849,6 +1909,13 @@ function collectCast(nodes, out) {
|
|
|
1849
1909
|
function gameDataField(gameData) {
|
|
1850
1910
|
return gameData && Object.keys(gameData).length ? { gameData } : {};
|
|
1851
1911
|
}
|
|
1912
|
+
function savedPrompt(p) {
|
|
1913
|
+
const out = { kind: p.kind, text: p.text };
|
|
1914
|
+
if (p.character !== void 0) out.character = p.character;
|
|
1915
|
+
if (p.characterName !== void 0) out.characterName = p.characterName;
|
|
1916
|
+
if (p.direction !== void 0) out.direction = p.direction;
|
|
1917
|
+
return out;
|
|
1918
|
+
}
|
|
1852
1919
|
function serialiseSelectors(map) {
|
|
1853
1920
|
const out = {};
|
|
1854
1921
|
for (const [id, st] of map) {
|
|
@@ -1871,6 +1938,11 @@ function deserialiseSelectors(rec) {
|
|
|
1871
1938
|
}
|
|
1872
1939
|
return map;
|
|
1873
1940
|
}
|
|
1941
|
+
function mountClaims(registry, key, bag, ask) {
|
|
1942
|
+
const before = ask ? JSON.stringify(bag.save()) : "";
|
|
1943
|
+
registry.mountOwned(key, bag, { owner: OWNER });
|
|
1944
|
+
return ask && JSON.stringify(bag.save()) !== before;
|
|
1945
|
+
}
|
|
1874
1946
|
function toDecl(decl) {
|
|
1875
1947
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
|
|
1876
1948
|
}
|
|
@@ -1882,6 +1954,12 @@ function selfBackedDecl(decl, scopeWritable) {
|
|
|
1882
1954
|
const writable = decl.writable ?? scopeWritable;
|
|
1883
1955
|
return writable === void 0 ? d : { ...d, writable };
|
|
1884
1956
|
}
|
|
1957
|
+
function resolveScene(host, ref) {
|
|
1958
|
+
return host.sceneGameIdToId.get(ref) ?? (host.bundle.scenes[ref] ? ref : void 0);
|
|
1959
|
+
}
|
|
1960
|
+
function resolveBlock(host, sceneId, ref) {
|
|
1961
|
+
return host.blockGameIdToId.get(sceneId)?.get(ref) ?? (host.blockIndex.get(ref)?.sceneId === sceneId ? ref : void 0);
|
|
1962
|
+
}
|
|
1885
1963
|
function splitHostRef(host, ref) {
|
|
1886
1964
|
if (host.refSplitRevision !== host.registry.revision) {
|
|
1887
1965
|
host.refSplitCache.clear();
|