@patterkit/runtime 0.14.3 → 0.16.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 CHANGED
@@ -33,6 +33,41 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.16.0] - 2026-10-04
37
+
38
+ ### Added
39
+
40
+ - **Checkpoints: try something, then undo it.** `engine.checkpoint()` opens a checkpoint; `engine.rollback(checkpoint)` puts the game back exactly
41
+ as it was then, and `engine.commit(checkpoint)` keeps everything. A rollback undoes every change the story made in between:
42
+ property values in every scope (a game's own `@world` store is written back through the game), visit
43
+ counts, shuffle and sequence positions, `@scene` bags made since, and every flow's position and random
44
+ state; a flow opened since is closed, and its name is free again. `engine.inCheckpoint` says whether one is open.
45
+ The use is asking "would this say anything?" without consequences: move a flow to an address, step
46
+ it, and roll back if it had nothing to give, so the scene's on-entry effects, the visits, and the
47
+ shuffle draws never happened. Each change records how to undo itself as it happens, so a checkpoint
48
+ costs what the steps inside it do, not what the game has built up, and the record is let go when it
49
+ closes. One is open at a time; while it is, the calls a rollback couldn't undo are refused
50
+ (`reset`, `loadGame`, `hotSwap`, `closeFlow`, `openFlow` over an open flow, and a flow's own `start`, `reset`, and `restore`). Held across all four runtimes by the conformance corpus.
51
+
52
+ ### Fixed
53
+
54
+ - **`Flow.restore()` on a running flow keeps that flow's own property values.** It used to drop them back to
55
+ their defaults, since a snapshot holds a flow's position and memory, not its property values. Loading
56
+ a save is unchanged.
57
+
58
+ ## [0.15.0] - 2026-10-03
59
+
60
+ ### Added
61
+
62
+ - **Scene and block gameData you can read at runtime.** The bundle always carried a scene's and a
63
+ block's gameData, but nothing handed it to the host. Two static reads now do, by internal id or
64
+ gameId address. They return the node's own overrides, raw, the same rule as a beat's
65
+ `step.gameData`: the project's field defaults are not merged in (resolve them with
66
+ `effectiveGameData`), and a block does not inherit its scene's. A node that sets none, or a ref
67
+ that does not resolve, gives an empty object. `getOutline()` carries the same overrides as
68
+ `gameData` on each scene and block, left out when empty. Held across all four runtimes by the
69
+ conformance corpus. `engine.gameDataForScene(sceneRef)`, `engine.gameDataForBlock(sceneRef, blockRef)`.
70
+
36
71
  ## [0.14.3] - 2026-09-27
37
72
 
38
73
  ### Fixed
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ Checkpoint: () => Checkpoint,
23
24
  Engine: () => Engine,
24
25
  Flow: () => Flow,
25
26
  buildTagIndex: () => buildTagIndex,
@@ -87,6 +88,13 @@ var keys = {
87
88
  /** A flow's NOT-shared `@scene` props for one scene. */
88
89
  flowScene: (flowId, sceneId) => `patter/flow/${esc(flowId)}/scene/${esc(sceneId)}`
89
90
  };
91
+ var Checkpoint = class {
92
+ /** @internal */
93
+ constructor(journal) {
94
+ this.journal = journal;
95
+ }
96
+ journal;
97
+ };
90
98
  var Engine = class _Engine {
91
99
  host;
92
100
  defaultSeed;
@@ -204,6 +212,7 @@ var Engine = class _Engine {
204
212
  sharedVisits: /* @__PURE__ */ new Map(),
205
213
  sharedSelectors: /* @__PURE__ */ new Map(),
206
214
  stageBags: /* @__PURE__ */ new Map(),
215
+ journal: null,
207
216
  customRng: options.rng,
208
217
  onDryChoice: options.onDryChoice,
209
218
  replayPromptOnChoose: options.replayPromptOnChoose ?? false,
@@ -267,6 +276,7 @@ var Engine = class _Engine {
267
276
  * top of the scene it was in; the shared properties carry over.
268
277
  */
269
278
  hotSwap(bundle) {
279
+ this.refuseInCheckpoint("hotSwap");
270
280
  const snapshot = this.saveGame();
271
281
  const carryOver = (next2) => {
272
282
  next2.setLocale(this.currentLocale);
@@ -327,8 +337,20 @@ var Engine = class _Engine {
327
337
  this.assertExternalScopes();
328
338
  const sceneId = this.resolveSceneRef(opts.scene);
329
339
  const blockId = this.resolveBlockRef(sceneId, opts.block);
330
- this.flowsById.get(id)?.close();
340
+ const prior = this.flowsById.get(id);
341
+ const journal = this.host.journal;
342
+ if (journal && prior && !prior.isClosed) throw new Error(`openFlow would replace the open flow '${id}', which can't be undone while a checkpoint is open`);
343
+ prior?.close();
331
344
  const flow = new Flow(id, this.host, opts.seed ?? this.defaultSeed);
345
+ if (journal) {
346
+ journal.flows.add(flow);
347
+ journal.opened.add(flow);
348
+ journal.undo.push(() => {
349
+ flow.close();
350
+ if (prior) this.flowsById.set(id, prior);
351
+ else this.flowsById.delete(id);
352
+ });
353
+ }
332
354
  this.flowsById.set(id, flow);
333
355
  flow.start(sceneId, blockId);
334
356
  return flow;
@@ -420,6 +442,24 @@ var Engine = class _Engine {
420
442
  const id = this.resolveBlockRef(sceneId, blockRef);
421
443
  return (id != null ? this.host.tagIndex.get(id) : void 0) ?? [];
422
444
  }
445
+ /**
446
+ * A scene's own author gameData (by internal id or gameId address): the RAW sparse overrides, exactly
447
+ * as a beat's step carries its own, not merged with the project's declared field defaults (resolve
448
+ * those with `effectiveGameData(gameDataFields(bundle, "scene"), ...)`). Not inherited by its blocks.
449
+ * A fresh object each call; empty when the scene sets none or the ref is unknown.
450
+ */
451
+ gameDataForScene(sceneRef) {
452
+ const id = this.resolveSceneRef(sceneRef);
453
+ const scene = id != null ? this.host.bundle.scenes[id] : void 0;
454
+ return { ...scene?.gameData };
455
+ }
456
+ /** A block's own author gameData, by scene + block ref (id or gameId). Raw overrides, like
457
+ * {@link gameDataForScene}; the scene's gameData is not folded in. Empty when none / unknown. */
458
+ gameDataForBlock(sceneRef, blockRef) {
459
+ const id = this.resolveBlockRef(this.resolveSceneRef(sceneRef), blockRef);
460
+ const block = id != null ? this.host.blockById.get(id) : void 0;
461
+ return { ...block?.gameData };
462
+ }
423
463
  /**
424
464
  * Every cast member the PROJECT declares, in authored order - the same list `describeBundle` counts.
425
465
  * A superset of any scene's cast: the validator holds a beat's `character` to a declared member, so
@@ -466,11 +506,13 @@ var Engine = class _Engine {
466
506
  id: scene.id,
467
507
  ...(0, import_model.effectiveGameId)(scene) ? { gameId: (0, import_model.effectiveGameId)(scene) } : {},
468
508
  name: scene.name,
509
+ ...gameDataField(scene.gameData),
469
510
  ...this.tagsField(scene.id),
470
511
  blocks: scene.blocks.map((block) => ({
471
512
  id: block.id,
472
513
  ...(0, import_model.effectiveGameId)(block) ? { gameId: (0, import_model.effectiveGameId)(block) } : {},
473
514
  name: block.name,
515
+ ...gameDataField(block.gameData),
474
516
  ...this.tagsField(block.id),
475
517
  children: block.children.map((n) => this.outlineNode(n))
476
518
  }))
@@ -564,9 +606,64 @@ var Engine = class _Engine {
564
606
  for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
565
607
  return mounts;
566
608
  }
609
+ /**
610
+ * Open a checkpoint: from here until `rollback` or `commit`, the engine keeps what it needs to undo
611
+ * every change the story makes. `rollback` puts the game back exactly as it was at the checkpoint:
612
+ * property values (every scope, including a host's `@world`, which is written back through the
613
+ * game), visit counts, shuffle and sequence positions, `@scene` bags made since, every flow's cursor
614
+ * and PRNG, and any flow opened since (closed and forgotten). `commit` keeps everything and stops
615
+ * recording.
616
+ *
617
+ * The use is asking "would this say anything?" without consequences: move a flow to an address,
618
+ * advance it, and roll back if it had nothing to give, so its scene's `onEntry` effects, its visits,
619
+ * and its shuffle draws never happened.
620
+ *
621
+ * One checkpoint at a time. Inside one, the calls that replace or drop flows or their bags wholesale
622
+ * (`reset`, `loadGame`, `hotSwap`, `closeFlow`, `openFlow` on the name of an open flow, and a flow's own
623
+ * `start`, `reset`, and `restore`) throw. Not undone:
624
+ * trace and log events already emitted, and draws from a custom `rng` the game supplied.
625
+ */
626
+ checkpoint() {
627
+ if (this.host.journal) throw new Error("a checkpoint is already open: roll it back or commit it first");
628
+ const journal = { undo: [], flows: /* @__PURE__ */ new Set(), opened: /* @__PURE__ */ new Set(), selectors: /* @__PURE__ */ new Set(), flowSelectors: /* @__PURE__ */ new Map() };
629
+ this.host.journal = journal;
630
+ return new Checkpoint(journal);
631
+ }
632
+ /** Undo everything since `checkpoint` (see there), and close it. */
633
+ rollback(checkpoint) {
634
+ this.endCheckpoint(checkpoint);
635
+ const undo = checkpoint.journal.undo;
636
+ for (let i = undo.length - 1; i >= 0; i--) undo[i]();
637
+ _Engine.forget(checkpoint.journal);
638
+ }
639
+ /** Keep everything since `checkpoint`, and close it. */
640
+ commit(checkpoint) {
641
+ this.endCheckpoint(checkpoint);
642
+ _Engine.forget(checkpoint.journal);
643
+ }
644
+ /** True while a checkpoint is open. */
645
+ get inCheckpoint() {
646
+ return this.host.journal !== null;
647
+ }
648
+ endCheckpoint(checkpoint) {
649
+ if (this.host.journal !== checkpoint.journal) throw new Error("that checkpoint is not the open one");
650
+ this.host.journal = null;
651
+ }
652
+ /** Let go of what a closed checkpoint recorded, so a handle the game keeps holds nothing. */
653
+ static forget(journal) {
654
+ journal.undo = [];
655
+ journal.flows.clear();
656
+ journal.opened.clear();
657
+ journal.selectors.clear();
658
+ journal.flowSelectors.clear();
659
+ }
660
+ refuseInCheckpoint(what) {
661
+ if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
662
+ }
567
663
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
568
664
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
569
665
  closeFlow(id) {
666
+ this.refuseInCheckpoint("closeFlow");
570
667
  this.flowsById.get(id)?.close();
571
668
  this.flowsById.delete(id);
572
669
  }
@@ -577,6 +674,7 @@ var Engine = class _Engine {
577
674
  * After reset, open fresh flows with `openFlow`.
578
675
  */
579
676
  reset() {
677
+ this.refuseInCheckpoint("reset");
580
678
  for (const flow of this.flowsById.values()) flow.close();
581
679
  this.flowsById.clear();
582
680
  this.host.patterBag.reseed(this.host.patterSharedDecls);
@@ -614,7 +712,9 @@ var Engine = class _Engine {
614
712
  * were each refused the one property they existed to move.) */
615
713
  setProperty(ref, value) {
616
714
  const { scope, name } = this.splitShared(ref);
715
+ const prev = this.host.journal ? this.host.registry.get(scope, name) : void 0;
617
716
  this.host.registry.set(scope, name, value, { host: true });
717
+ if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
618
718
  }
619
719
  /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
620
720
  * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
@@ -701,6 +801,7 @@ var Engine = class _Engine {
701
801
  * to the registry (values kept) and the restored flows claim them as they register.
702
802
  */
703
803
  loadGame(save) {
804
+ this.refuseInCheckpoint("loadGame");
704
805
  const version = save.version;
705
806
  if (version !== 2 && version !== import_model2.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
706
807
  this.assertExternalScopes();
@@ -851,6 +952,7 @@ var Flow = class {
851
952
  // -- Host API -------------------------------------------------------------
852
953
  /** Begin this flow at a scene (and optionally a specific block within it). */
853
954
  start(sceneId, blockId) {
955
+ if (this.host.journal && !this.host.journal.opened.has(this)) throw new Error("a flow can't be started or reset while a checkpoint is open");
854
956
  this.releaseBags(false);
855
957
  this.host.registry.discardParked(keys.flow(this.id));
856
958
  this.mountLocal();
@@ -910,6 +1012,7 @@ var Flow = class {
910
1012
  */
911
1013
  goto(scene, block) {
912
1014
  if (this.closed) return false;
1015
+ this.touch();
913
1016
  if (scene === "END") {
914
1017
  this.started = true;
915
1018
  this.pendingChoice = null;
@@ -963,6 +1066,19 @@ var Flow = class {
963
1066
  this.pendingPromptBeat = null;
964
1067
  this.pendingPromptOwnerId = null;
965
1068
  }
1069
+ /** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
1070
+ * put them back: a handful of fields and a copy of the call stack (a few frames), never its history.
1071
+ * Its visits, selector cursors, and property values are recorded change by change as they happen. */
1072
+ touch() {
1073
+ const journal = this.host.journal;
1074
+ if (!journal || journal.flows.has(this)) return;
1075
+ journal.flows.add(this);
1076
+ const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId } = this;
1077
+ const stack = this.stack.map((f) => ({ ...f }));
1078
+ journal.undo.push(() => {
1079
+ Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId, stack });
1080
+ });
1081
+ }
966
1082
  /** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
967
1083
  get isClosed() {
968
1084
  return this.closed;
@@ -977,6 +1093,7 @@ var Flow = class {
977
1093
  advance() {
978
1094
  if (this.closed) return { type: "end" };
979
1095
  if (!this.started) throw new Error("flow has not been started");
1096
+ this.touch();
980
1097
  if (this.pendingPromptBeat) {
981
1098
  const b = this.pendingPromptBeat;
982
1099
  this.pendingPromptBeat = null;
@@ -1096,6 +1213,7 @@ var Flow = class {
1096
1213
  const option = choice.options.find((o) => o.id === id);
1097
1214
  if (!option) throw new Error(`unknown choice option: ${id}`);
1098
1215
  if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
1216
+ this.touch();
1099
1217
  const node = choice.byId.get(id);
1100
1218
  this.emit({ type: "chose", group: choice.groupId, option: id });
1101
1219
  this.pendingChoice = null;
@@ -1123,13 +1241,27 @@ var Flow = class {
1123
1241
  * about: the story is refused, the game is not. */
1124
1242
  writeProperty(ref, value, host) {
1125
1243
  const { scope, name } = this.splitRef(ref);
1244
+ const journal = this.host.journal;
1126
1245
  if (scope === "patter") {
1246
+ if (journal) {
1247
+ const shared = this.host.patterSharedNames.has(name);
1248
+ const prev = shared ? this.host.patterBag.get(name) : this.local.get(name);
1249
+ const bag = this.local;
1250
+ if (prev !== void 0) journal.undo.push(shared ? () => this.host.registry.set("patter", name, prev) : () => bag.set(name, prev));
1251
+ }
1127
1252
  this.patterResolver.set(name, value);
1128
1253
  } else if (scope === "scene") {
1129
1254
  if (this.currentSceneId === null) throw new Error(`'${ref}': the flow has not entered a scene yet`);
1255
+ if (journal) {
1256
+ const bag = this.sceneBagFor(this.currentSceneId, name);
1257
+ const prev = bag?.get(name);
1258
+ if (bag && prev !== void 0) journal.undo.push(() => bag.set(name, prev));
1259
+ }
1130
1260
  this.sceneResolver.set(name, value);
1131
1261
  } else {
1262
+ const prev = journal ? this.host.registry.get(scope, name) : void 0;
1132
1263
  this.host.registry.set(scope, name, value, host ? { host: true } : void 0);
1264
+ if (journal && prev !== void 0) journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
1133
1265
  }
1134
1266
  }
1135
1267
  // -- Save / restore (engine-driven) --------------------------------------
@@ -1156,8 +1288,19 @@ var Flow = class {
1156
1288
  }
1157
1289
  };
1158
1290
  }
1159
- /** @internal Restore this flow from a snapshot. */
1291
+ /** @internal Restore this flow from a snapshot: its cursor, PRNG, visits, and selector cursors. Its
1292
+ * property values are the registry's and stay as they are (a load puts the saved ones there first). */
1160
1293
  restore(snap) {
1294
+ if (this.host.journal) throw new Error("a flow can't be restored while a checkpoint is open");
1295
+ this.restoreCursor(snap);
1296
+ this.releaseBags(true);
1297
+ this.mountLocal();
1298
+ for (const s of /* @__PURE__ */ new Set([this.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
1299
+ if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
1300
+ }
1301
+ }
1302
+ /** The cursor half of `restore`, without touching the bags (a rollback has already put those back). */
1303
+ restoreCursor(snap) {
1161
1304
  this.rngState = (0, import_expr.toUint32)(snap.rngState);
1162
1305
  this.visitCounts = new Map(Object.entries(snap.visits ?? {}));
1163
1306
  const c = snap.cursor;
@@ -1173,11 +1316,6 @@ var Flow = class {
1173
1316
  }
1174
1317
  return { ...frame };
1175
1318
  });
1176
- this.releaseBags(false);
1177
- this.mountLocal();
1178
- for (const s of /* @__PURE__ */ new Set([c.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
1179
- if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
1180
- }
1181
1319
  this.activeSnippet = null;
1182
1320
  if (c.activeSnippetId !== null) {
1183
1321
  const node = this.host.nodeIndex.get(c.activeSnippetId);
@@ -1454,6 +1592,24 @@ var Flow = class {
1454
1592
  /** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
1455
1593
  selectorState(group) {
1456
1594
  const map = group.shared ? this.host.sharedSelectors : this.selectors;
1595
+ const journal = this.host.journal;
1596
+ let seen;
1597
+ if (journal) {
1598
+ seen = group.shared ? journal.selectors : journal.flowSelectors.get(this);
1599
+ if (!seen) {
1600
+ seen = /* @__PURE__ */ new Set();
1601
+ journal.flowSelectors.set(this, seen);
1602
+ }
1603
+ }
1604
+ if (journal && seen && !seen.has(group.id)) {
1605
+ seen.add(group.id);
1606
+ const was = map.get(group.id);
1607
+ const copy = was ? { ...was, ...was.bag ? { bag: [...was.bag] } : {} } : void 0;
1608
+ journal.undo.push(() => {
1609
+ if (copy) map.set(group.id, copy);
1610
+ else map.delete(group.id);
1611
+ });
1612
+ }
1457
1613
  let st = map.get(group.id);
1458
1614
  if (!st) {
1459
1615
  st = {};
@@ -1489,8 +1645,18 @@ var Flow = class {
1489
1645
  }
1490
1646
  /** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
1491
1647
  enter(id) {
1492
- this.visitCounts.set(id, (this.visitCounts.get(id) ?? 0) + 1);
1493
- this.host.sharedVisits.set(id, (this.host.sharedVisits.get(id) ?? 0) + 1);
1648
+ const own = this.visitCounts;
1649
+ const ownBefore = own.get(id);
1650
+ own.set(id, (ownBefore ?? 0) + 1);
1651
+ const shared = this.host.sharedVisits;
1652
+ const before = shared.get(id);
1653
+ shared.set(id, (before ?? 0) + 1);
1654
+ if (this.host.journal) this.host.journal.undo.push(() => {
1655
+ if (ownBefore === void 0) own.delete(id);
1656
+ else own.set(id, ownBefore);
1657
+ if (before === void 0) shared.delete(id);
1658
+ else shared.set(id, before);
1659
+ });
1494
1660
  }
1495
1661
  /** Next float in [0, 1): the shared custom PRNG, or this flow's serialisable mulberry32.
1496
1662
  *
@@ -1633,17 +1799,29 @@ var Flow = class {
1633
1799
  ensureSceneBags(s) {
1634
1800
  const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
1635
1801
  const props = this.host.bundle.scenes[s]?.sceneProps ?? [];
1802
+ const journal = this.host.journal;
1636
1803
  if (!this.sceneBags.has(s)) {
1637
1804
  const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
1638
1805
  const key = keys.flowScene(this.id, s);
1639
1806
  this.host.registry.mountOwned(key, bag, { owner: OWNER });
1640
1807
  this.registered.add(key);
1641
1808
  this.sceneBags.set(s, bag);
1809
+ if (journal) journal.undo.push(() => {
1810
+ if (this.sceneBags.get(s) !== bag) return;
1811
+ this.host.registry.remove(key);
1812
+ this.registered.delete(key);
1813
+ this.sceneBags.delete(s);
1814
+ });
1642
1815
  }
1643
1816
  if (!this.host.stageBags.has(s)) {
1644
1817
  const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
1645
1818
  this.host.registry.mountOwned(keys.stage(s), bag, { owner: OWNER });
1646
1819
  this.host.stageBags.set(s, bag);
1820
+ if (journal) journal.undo.push(() => {
1821
+ if (this.host.stageBags.get(s) !== bag) return;
1822
+ this.host.registry.remove(keys.stage(s));
1823
+ this.host.stageBags.delete(s);
1824
+ });
1647
1825
  }
1648
1826
  }
1649
1827
  /**
@@ -1660,7 +1838,10 @@ var Flow = class {
1660
1838
  if (!decl.temporary) continue;
1661
1839
  const name = decl.name.toLowerCase();
1662
1840
  const bag = shared.has(name) ? this.host.stageBags.get(scene.id) : this.sceneBags.get(scene.id);
1663
- if (bag) bag.set(name, (0, import_scoperegistry2.defaultFor)(decl));
1841
+ if (!bag) continue;
1842
+ const prev = this.host.journal ? bag.get(name) : void 0;
1843
+ if (prev !== void 0) this.host.journal.undo.push(() => bag.set(name, prev));
1844
+ bag.set(name, (0, import_scoperegistry2.defaultFor)(decl));
1664
1845
  }
1665
1846
  }
1666
1847
  };
@@ -1673,6 +1854,9 @@ function collectCast(nodes, out) {
1673
1854
  for (const beat of n.beats ?? []) if (beat.kind === "line" && beat.character) out.add(beat.character);
1674
1855
  });
1675
1856
  }
1857
+ function gameDataField(gameData) {
1858
+ return gameData && Object.keys(gameData).length ? { gameData } : {};
1859
+ }
1676
1860
  function serialiseSelectors(map) {
1677
1861
  const out = {};
1678
1862
  for (const [id, st] of map) {
@@ -1857,6 +2041,7 @@ function effectiveGameData(fields, node) {
1857
2041
  }
1858
2042
  // Annotate the CommonJS export names for ESM import in node:
1859
2043
  0 && (module.exports = {
2044
+ Checkpoint,
1860
2045
  Engine,
1861
2046
  Flow,
1862
2047
  buildTagIndex,