@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/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.resolveSceneRef(scene);
379
- if (!this.host.bundle.scenes[sceneId]) throw new Error(`unknown scene: ${scene}`);
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 = this.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
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 (a gameId address OR an internal id) to its internal id. */
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
- if (this.host.bundle.scenes[ref]) return ref;
396
- return this.sceneGameIdToId.get(ref) ?? ref;
400
+ return resolveScene(this.host, ref) ?? ref;
397
401
  }
398
- /** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
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 (this.host.blockById.has(ref)) return ref;
402
- if (sceneId != null) {
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.engineLog.length, ...scene ? { scene } : {} });
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 beat to deliver before its content. */
847
- pendingPromptBeat = null;
848
- /** The chosen option that owns `pendingPromptBeat`, so a save taken between choose() and the next
849
- * advance() can re-derive the prompt on load (the beat isn't otherwise reachable by id). */
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.pendingPromptBeat = null;
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 = this.host.sceneGameIdToId.get(scene) ?? (this.host.bundle.scenes[scene] ? scene : void 0);
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 = this.host.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
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.pendingPromptBeat = null;
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.pendingPromptBeat = null;
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, pendingPromptBeat, pendingPromptOwnerId } = this;
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, pendingPromptBeat, pendingPromptOwnerId, stack });
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.pendingPromptBeat) {
1090
- const b = this.pendingPromptBeat;
1091
- this.pendingPromptBeat = null;
1096
+ if (this.pendingPrompt) {
1097
+ const p = this.pendingPrompt;
1098
+ this.pendingPrompt = null;
1092
1099
  this.pendingPromptOwnerId = null;
1093
- return this.beatResult(b);
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
- this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
1213
- this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
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.pendingPromptBeat = null;
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
- this.pendingPromptBeat = owner ? this.promptBeatOf(owner) ?? null : null;
1334
- if (!this.pendingPromptBeat) this.pendingPromptOwnerId = null;
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.length > 0) {
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((c) => this.eligible(c));
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.length === 0) {
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
- pool.splice(i, 1);
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) => truthy(evaluate(n, this.context(), patterDialect));
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
- runEffects(effects) {
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
- const value = this.evalExpr(e.value);
1616
- const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
1617
- this.writeProperty(e.target, value, false);
1618
- this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
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
- return truthy(this.evalExpr(node.condition));
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.mountOwned(key, bag, { owner: OWNER });
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.mountOwned(keys.stage(s), bag, { owner: OWNER });
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();