@patterkit/runtime 0.15.0 → 0.17.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 +33 -0
- package/dist/index.cjs +199 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -3
- package/dist/index.d.ts +70 -3
- package/dist/index.js +198 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,39 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.17.0] - 2026-10-05
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **`openFlow` at an address that does not resolve changes nothing.** It threw, but only after closing
|
|
41
|
+
any flow already open under that name and putting a broken one in its place. It now throws before
|
|
42
|
+
anything changes. The block must be in the scene you named, as `goto` has always required: a block
|
|
43
|
+
from another scene used to open there, and now throws. Held across all four runtimes by the
|
|
44
|
+
conformance corpus, which now also pins a choice step whole (its `groupId` and each option's
|
|
45
|
+
`prompt`), as this runtime already gave it.
|
|
46
|
+
|
|
47
|
+
## [0.16.0] - 2026-10-04
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **Checkpoints: try something, then undo it.** `engine.checkpoint()` opens a checkpoint; `engine.rollback(checkpoint)` puts the game back exactly
|
|
52
|
+
as it was then, and `engine.commit(checkpoint)` keeps everything. A rollback undoes every change the story made in between:
|
|
53
|
+
property values in every scope (a game's own `@world` store is written back through the game), visit
|
|
54
|
+
counts, shuffle and sequence positions, `@scene` bags made since, and every flow's position and random
|
|
55
|
+
state; a flow opened since is closed, and its name is free again. `engine.inCheckpoint` says whether one is open.
|
|
56
|
+
The use is asking "would this say anything?" without consequences: move a flow to an address, step
|
|
57
|
+
it, and roll back if it had nothing to give, so the scene's on-entry effects, the visits, and the
|
|
58
|
+
shuffle draws never happened. Each change records how to undo itself as it happens, so a checkpoint
|
|
59
|
+
costs what the steps inside it do, not what the game has built up, and the record is let go when it
|
|
60
|
+
closes. One is open at a time; while it is, the calls a rollback couldn't undo are refused
|
|
61
|
+
(`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.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **`Flow.restore()` on a running flow keeps that flow's own property values.** It used to drop them back to
|
|
66
|
+
their defaults, since a snapshot holds a flow's position and memory, not its property values. Loading
|
|
67
|
+
a save is unchanged.
|
|
68
|
+
|
|
36
69
|
## [0.15.0] - 2026-10-03
|
|
37
70
|
|
|
38
71
|
### Added
|
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);
|
|
@@ -322,13 +332,28 @@ var Engine = class _Engine {
|
|
|
322
332
|
*
|
|
323
333
|
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
324
334
|
* call to reach for when you want a speaker's variation state to carry on.
|
|
335
|
+
*
|
|
336
|
+
* THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
|
|
337
|
+
* scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
|
|
338
|
+
* exactly as it was.
|
|
325
339
|
*/
|
|
326
340
|
openFlow(id, opts = {}) {
|
|
327
341
|
this.assertExternalScopes();
|
|
328
|
-
const sceneId = this.
|
|
329
|
-
const
|
|
330
|
-
this.
|
|
342
|
+
const { sceneId, blockId } = this.resolveOpenAddress(opts.scene, opts.block);
|
|
343
|
+
const prior = this.flowsById.get(id);
|
|
344
|
+
const journal = this.host.journal;
|
|
345
|
+
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`);
|
|
346
|
+
prior?.close();
|
|
331
347
|
const flow = new Flow(id, this.host, opts.seed ?? this.defaultSeed);
|
|
348
|
+
if (journal) {
|
|
349
|
+
journal.flows.add(flow);
|
|
350
|
+
journal.opened.add(flow);
|
|
351
|
+
journal.undo.push(() => {
|
|
352
|
+
flow.close();
|
|
353
|
+
if (prior) this.flowsById.set(id, prior);
|
|
354
|
+
else this.flowsById.delete(id);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
332
357
|
this.flowsById.set(id, flow);
|
|
333
358
|
flow.start(sceneId, blockId);
|
|
334
359
|
return flow;
|
|
@@ -375,6 +400,28 @@ var Engine = class _Engine {
|
|
|
375
400
|
}
|
|
376
401
|
}
|
|
377
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
|
|
405
|
+
* scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
|
|
406
|
+
* id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
|
|
407
|
+
* is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
|
|
408
|
+
*/
|
|
409
|
+
resolveOpenAddress(scene, block) {
|
|
410
|
+
if (scene !== void 0) {
|
|
411
|
+
const sceneId = this.resolveSceneRef(scene);
|
|
412
|
+
if (!this.host.bundle.scenes[sceneId]) throw new Error(`unknown scene: ${scene}`);
|
|
413
|
+
if (block === void 0) return { sceneId };
|
|
414
|
+
const blockId = this.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
|
|
415
|
+
if (blockId === void 0) throw new Error(`unknown block: ${block}`);
|
|
416
|
+
return { sceneId, blockId };
|
|
417
|
+
}
|
|
418
|
+
if (block !== void 0) {
|
|
419
|
+
if (!this.host.blockIndex.has(block)) throw new Error(`unknown block: ${block}`);
|
|
420
|
+
return { blockId: block };
|
|
421
|
+
}
|
|
422
|
+
if (Object.keys(this.host.bundle.scenes).length === 0) throw new Error("no scenes in bundle");
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
378
425
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
379
426
|
resolveSceneRef(ref) {
|
|
380
427
|
if (ref == null) return void 0;
|
|
@@ -584,9 +631,64 @@ var Engine = class _Engine {
|
|
|
584
631
|
for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
|
|
585
632
|
return mounts;
|
|
586
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* Open a checkpoint: from here until `rollback` or `commit`, the engine keeps what it needs to undo
|
|
636
|
+
* every change the story makes. `rollback` puts the game back exactly as it was at the checkpoint:
|
|
637
|
+
* property values (every scope, including a host's `@world`, which is written back through the
|
|
638
|
+
* game), visit counts, shuffle and sequence positions, `@scene` bags made since, every flow's cursor
|
|
639
|
+
* and PRNG, and any flow opened since (closed and forgotten). `commit` keeps everything and stops
|
|
640
|
+
* recording.
|
|
641
|
+
*
|
|
642
|
+
* The use is asking "would this say anything?" without consequences: move a flow to an address,
|
|
643
|
+
* advance it, and roll back if it had nothing to give, so its scene's `onEntry` effects, its visits,
|
|
644
|
+
* and its shuffle draws never happened.
|
|
645
|
+
*
|
|
646
|
+
* One checkpoint at a time. Inside one, the calls that replace or drop flows or their bags wholesale
|
|
647
|
+
* (`reset`, `loadGame`, `hotSwap`, `closeFlow`, `openFlow` on the name of an open flow, and a flow's own
|
|
648
|
+
* `start`, `reset`, and `restore`) throw. Not undone:
|
|
649
|
+
* trace and log events already emitted, and draws from a custom `rng` the game supplied.
|
|
650
|
+
*/
|
|
651
|
+
checkpoint() {
|
|
652
|
+
if (this.host.journal) throw new Error("a checkpoint is already open: roll it back or commit it first");
|
|
653
|
+
const journal = { undo: [], flows: /* @__PURE__ */ new Set(), opened: /* @__PURE__ */ new Set(), selectors: /* @__PURE__ */ new Set(), flowSelectors: /* @__PURE__ */ new Map() };
|
|
654
|
+
this.host.journal = journal;
|
|
655
|
+
return new Checkpoint(journal);
|
|
656
|
+
}
|
|
657
|
+
/** Undo everything since `checkpoint` (see there), and close it. */
|
|
658
|
+
rollback(checkpoint) {
|
|
659
|
+
this.endCheckpoint(checkpoint);
|
|
660
|
+
const undo = checkpoint.journal.undo;
|
|
661
|
+
for (let i = undo.length - 1; i >= 0; i--) undo[i]();
|
|
662
|
+
_Engine.forget(checkpoint.journal);
|
|
663
|
+
}
|
|
664
|
+
/** Keep everything since `checkpoint`, and close it. */
|
|
665
|
+
commit(checkpoint) {
|
|
666
|
+
this.endCheckpoint(checkpoint);
|
|
667
|
+
_Engine.forget(checkpoint.journal);
|
|
668
|
+
}
|
|
669
|
+
/** True while a checkpoint is open. */
|
|
670
|
+
get inCheckpoint() {
|
|
671
|
+
return this.host.journal !== null;
|
|
672
|
+
}
|
|
673
|
+
endCheckpoint(checkpoint) {
|
|
674
|
+
if (this.host.journal !== checkpoint.journal) throw new Error("that checkpoint is not the open one");
|
|
675
|
+
this.host.journal = null;
|
|
676
|
+
}
|
|
677
|
+
/** Let go of what a closed checkpoint recorded, so a handle the game keeps holds nothing. */
|
|
678
|
+
static forget(journal) {
|
|
679
|
+
journal.undo = [];
|
|
680
|
+
journal.flows.clear();
|
|
681
|
+
journal.opened.clear();
|
|
682
|
+
journal.selectors.clear();
|
|
683
|
+
journal.flowSelectors.clear();
|
|
684
|
+
}
|
|
685
|
+
refuseInCheckpoint(what) {
|
|
686
|
+
if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
|
|
687
|
+
}
|
|
587
688
|
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
588
689
|
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
589
690
|
closeFlow(id) {
|
|
691
|
+
this.refuseInCheckpoint("closeFlow");
|
|
590
692
|
this.flowsById.get(id)?.close();
|
|
591
693
|
this.flowsById.delete(id);
|
|
592
694
|
}
|
|
@@ -597,6 +699,7 @@ var Engine = class _Engine {
|
|
|
597
699
|
* After reset, open fresh flows with `openFlow`.
|
|
598
700
|
*/
|
|
599
701
|
reset() {
|
|
702
|
+
this.refuseInCheckpoint("reset");
|
|
600
703
|
for (const flow of this.flowsById.values()) flow.close();
|
|
601
704
|
this.flowsById.clear();
|
|
602
705
|
this.host.patterBag.reseed(this.host.patterSharedDecls);
|
|
@@ -634,7 +737,9 @@ var Engine = class _Engine {
|
|
|
634
737
|
* were each refused the one property they existed to move.) */
|
|
635
738
|
setProperty(ref, value) {
|
|
636
739
|
const { scope, name } = this.splitShared(ref);
|
|
740
|
+
const prev = this.host.journal ? this.host.registry.get(scope, name) : void 0;
|
|
637
741
|
this.host.registry.set(scope, name, value, { host: true });
|
|
742
|
+
if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
|
|
638
743
|
}
|
|
639
744
|
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
640
745
|
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
@@ -721,6 +826,7 @@ var Engine = class _Engine {
|
|
|
721
826
|
* to the registry (values kept) and the restored flows claim them as they register.
|
|
722
827
|
*/
|
|
723
828
|
loadGame(save) {
|
|
829
|
+
this.refuseInCheckpoint("loadGame");
|
|
724
830
|
const version = save.version;
|
|
725
831
|
if (version !== 2 && version !== import_model2.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
726
832
|
this.assertExternalScopes();
|
|
@@ -871,6 +977,7 @@ var Flow = class {
|
|
|
871
977
|
// -- Host API -------------------------------------------------------------
|
|
872
978
|
/** Begin this flow at a scene (and optionally a specific block within it). */
|
|
873
979
|
start(sceneId, blockId) {
|
|
980
|
+
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");
|
|
874
981
|
this.releaseBags(false);
|
|
875
982
|
this.host.registry.discardParked(keys.flow(this.id));
|
|
876
983
|
this.mountLocal();
|
|
@@ -930,6 +1037,7 @@ var Flow = class {
|
|
|
930
1037
|
*/
|
|
931
1038
|
goto(scene, block) {
|
|
932
1039
|
if (this.closed) return false;
|
|
1040
|
+
this.touch();
|
|
933
1041
|
if (scene === "END") {
|
|
934
1042
|
this.started = true;
|
|
935
1043
|
this.pendingChoice = null;
|
|
@@ -983,6 +1091,19 @@ var Flow = class {
|
|
|
983
1091
|
this.pendingPromptBeat = null;
|
|
984
1092
|
this.pendingPromptOwnerId = null;
|
|
985
1093
|
}
|
|
1094
|
+
/** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
|
|
1095
|
+
* put them back: a handful of fields and a copy of the call stack (a few frames), never its history.
|
|
1096
|
+
* Its visits, selector cursors, and property values are recorded change by change as they happen. */
|
|
1097
|
+
touch() {
|
|
1098
|
+
const journal = this.host.journal;
|
|
1099
|
+
if (!journal || journal.flows.has(this)) return;
|
|
1100
|
+
journal.flows.add(this);
|
|
1101
|
+
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId } = this;
|
|
1102
|
+
const stack = this.stack.map((f) => ({ ...f }));
|
|
1103
|
+
journal.undo.push(() => {
|
|
1104
|
+
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPromptBeat, pendingPromptOwnerId, stack });
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
986
1107
|
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
987
1108
|
get isClosed() {
|
|
988
1109
|
return this.closed;
|
|
@@ -997,6 +1118,7 @@ var Flow = class {
|
|
|
997
1118
|
advance() {
|
|
998
1119
|
if (this.closed) return { type: "end" };
|
|
999
1120
|
if (!this.started) throw new Error("flow has not been started");
|
|
1121
|
+
this.touch();
|
|
1000
1122
|
if (this.pendingPromptBeat) {
|
|
1001
1123
|
const b = this.pendingPromptBeat;
|
|
1002
1124
|
this.pendingPromptBeat = null;
|
|
@@ -1116,6 +1238,7 @@ var Flow = class {
|
|
|
1116
1238
|
const option = choice.options.find((o) => o.id === id);
|
|
1117
1239
|
if (!option) throw new Error(`unknown choice option: ${id}`);
|
|
1118
1240
|
if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
|
|
1241
|
+
this.touch();
|
|
1119
1242
|
const node = choice.byId.get(id);
|
|
1120
1243
|
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1121
1244
|
this.pendingChoice = null;
|
|
@@ -1143,13 +1266,27 @@ var Flow = class {
|
|
|
1143
1266
|
* about: the story is refused, the game is not. */
|
|
1144
1267
|
writeProperty(ref, value, host) {
|
|
1145
1268
|
const { scope, name } = this.splitRef(ref);
|
|
1269
|
+
const journal = this.host.journal;
|
|
1146
1270
|
if (scope === "patter") {
|
|
1271
|
+
if (journal) {
|
|
1272
|
+
const shared = this.host.patterSharedNames.has(name);
|
|
1273
|
+
const prev = shared ? this.host.patterBag.get(name) : this.local.get(name);
|
|
1274
|
+
const bag = this.local;
|
|
1275
|
+
if (prev !== void 0) journal.undo.push(shared ? () => this.host.registry.set("patter", name, prev) : () => bag.set(name, prev));
|
|
1276
|
+
}
|
|
1147
1277
|
this.patterResolver.set(name, value);
|
|
1148
1278
|
} else if (scope === "scene") {
|
|
1149
1279
|
if (this.currentSceneId === null) throw new Error(`'${ref}': the flow has not entered a scene yet`);
|
|
1280
|
+
if (journal) {
|
|
1281
|
+
const bag = this.sceneBagFor(this.currentSceneId, name);
|
|
1282
|
+
const prev = bag?.get(name);
|
|
1283
|
+
if (bag && prev !== void 0) journal.undo.push(() => bag.set(name, prev));
|
|
1284
|
+
}
|
|
1150
1285
|
this.sceneResolver.set(name, value);
|
|
1151
1286
|
} else {
|
|
1287
|
+
const prev = journal ? this.host.registry.get(scope, name) : void 0;
|
|
1152
1288
|
this.host.registry.set(scope, name, value, host ? { host: true } : void 0);
|
|
1289
|
+
if (journal && prev !== void 0) journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
|
|
1153
1290
|
}
|
|
1154
1291
|
}
|
|
1155
1292
|
// -- Save / restore (engine-driven) --------------------------------------
|
|
@@ -1176,8 +1313,19 @@ var Flow = class {
|
|
|
1176
1313
|
}
|
|
1177
1314
|
};
|
|
1178
1315
|
}
|
|
1179
|
-
/** @internal Restore this flow from a snapshot.
|
|
1316
|
+
/** @internal Restore this flow from a snapshot: its cursor, PRNG, visits, and selector cursors. Its
|
|
1317
|
+
* property values are the registry's and stay as they are (a load puts the saved ones there first). */
|
|
1180
1318
|
restore(snap) {
|
|
1319
|
+
if (this.host.journal) throw new Error("a flow can't be restored while a checkpoint is open");
|
|
1320
|
+
this.restoreCursor(snap);
|
|
1321
|
+
this.releaseBags(true);
|
|
1322
|
+
this.mountLocal();
|
|
1323
|
+
for (const s of /* @__PURE__ */ new Set([this.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
|
|
1324
|
+
if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
/** The cursor half of `restore`, without touching the bags (a rollback has already put those back). */
|
|
1328
|
+
restoreCursor(snap) {
|
|
1181
1329
|
this.rngState = (0, import_expr.toUint32)(snap.rngState);
|
|
1182
1330
|
this.visitCounts = new Map(Object.entries(snap.visits ?? {}));
|
|
1183
1331
|
const c = snap.cursor;
|
|
@@ -1193,11 +1341,6 @@ var Flow = class {
|
|
|
1193
1341
|
}
|
|
1194
1342
|
return { ...frame };
|
|
1195
1343
|
});
|
|
1196
|
-
this.releaseBags(false);
|
|
1197
|
-
this.mountLocal();
|
|
1198
|
-
for (const s of /* @__PURE__ */ new Set([c.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
|
|
1199
|
-
if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
|
|
1200
|
-
}
|
|
1201
1344
|
this.activeSnippet = null;
|
|
1202
1345
|
if (c.activeSnippetId !== null) {
|
|
1203
1346
|
const node = this.host.nodeIndex.get(c.activeSnippetId);
|
|
@@ -1474,6 +1617,24 @@ var Flow = class {
|
|
|
1474
1617
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
1475
1618
|
selectorState(group) {
|
|
1476
1619
|
const map = group.shared ? this.host.sharedSelectors : this.selectors;
|
|
1620
|
+
const journal = this.host.journal;
|
|
1621
|
+
let seen;
|
|
1622
|
+
if (journal) {
|
|
1623
|
+
seen = group.shared ? journal.selectors : journal.flowSelectors.get(this);
|
|
1624
|
+
if (!seen) {
|
|
1625
|
+
seen = /* @__PURE__ */ new Set();
|
|
1626
|
+
journal.flowSelectors.set(this, seen);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (journal && seen && !seen.has(group.id)) {
|
|
1630
|
+
seen.add(group.id);
|
|
1631
|
+
const was = map.get(group.id);
|
|
1632
|
+
const copy = was ? { ...was, ...was.bag ? { bag: [...was.bag] } : {} } : void 0;
|
|
1633
|
+
journal.undo.push(() => {
|
|
1634
|
+
if (copy) map.set(group.id, copy);
|
|
1635
|
+
else map.delete(group.id);
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1477
1638
|
let st = map.get(group.id);
|
|
1478
1639
|
if (!st) {
|
|
1479
1640
|
st = {};
|
|
@@ -1509,8 +1670,18 @@ var Flow = class {
|
|
|
1509
1670
|
}
|
|
1510
1671
|
/** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
|
|
1511
1672
|
enter(id) {
|
|
1512
|
-
|
|
1513
|
-
|
|
1673
|
+
const own = this.visitCounts;
|
|
1674
|
+
const ownBefore = own.get(id);
|
|
1675
|
+
own.set(id, (ownBefore ?? 0) + 1);
|
|
1676
|
+
const shared = this.host.sharedVisits;
|
|
1677
|
+
const before = shared.get(id);
|
|
1678
|
+
shared.set(id, (before ?? 0) + 1);
|
|
1679
|
+
if (this.host.journal) this.host.journal.undo.push(() => {
|
|
1680
|
+
if (ownBefore === void 0) own.delete(id);
|
|
1681
|
+
else own.set(id, ownBefore);
|
|
1682
|
+
if (before === void 0) shared.delete(id);
|
|
1683
|
+
else shared.set(id, before);
|
|
1684
|
+
});
|
|
1514
1685
|
}
|
|
1515
1686
|
/** Next float in [0, 1): the shared custom PRNG, or this flow's serialisable mulberry32.
|
|
1516
1687
|
*
|
|
@@ -1653,17 +1824,29 @@ var Flow = class {
|
|
|
1653
1824
|
ensureSceneBags(s) {
|
|
1654
1825
|
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
1655
1826
|
const props = this.host.bundle.scenes[s]?.sceneProps ?? [];
|
|
1827
|
+
const journal = this.host.journal;
|
|
1656
1828
|
if (!this.sceneBags.has(s)) {
|
|
1657
1829
|
const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1658
1830
|
const key = keys.flowScene(this.id, s);
|
|
1659
1831
|
this.host.registry.mountOwned(key, bag, { owner: OWNER });
|
|
1660
1832
|
this.registered.add(key);
|
|
1661
1833
|
this.sceneBags.set(s, bag);
|
|
1834
|
+
if (journal) journal.undo.push(() => {
|
|
1835
|
+
if (this.sceneBags.get(s) !== bag) return;
|
|
1836
|
+
this.host.registry.remove(key);
|
|
1837
|
+
this.registered.delete(key);
|
|
1838
|
+
this.sceneBags.delete(s);
|
|
1839
|
+
});
|
|
1662
1840
|
}
|
|
1663
1841
|
if (!this.host.stageBags.has(s)) {
|
|
1664
1842
|
const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1665
1843
|
this.host.registry.mountOwned(keys.stage(s), bag, { owner: OWNER });
|
|
1666
1844
|
this.host.stageBags.set(s, bag);
|
|
1845
|
+
if (journal) journal.undo.push(() => {
|
|
1846
|
+
if (this.host.stageBags.get(s) !== bag) return;
|
|
1847
|
+
this.host.registry.remove(keys.stage(s));
|
|
1848
|
+
this.host.stageBags.delete(s);
|
|
1849
|
+
});
|
|
1667
1850
|
}
|
|
1668
1851
|
}
|
|
1669
1852
|
/**
|
|
@@ -1680,7 +1863,10 @@ var Flow = class {
|
|
|
1680
1863
|
if (!decl.temporary) continue;
|
|
1681
1864
|
const name = decl.name.toLowerCase();
|
|
1682
1865
|
const bag = shared.has(name) ? this.host.stageBags.get(scene.id) : this.sceneBags.get(scene.id);
|
|
1683
|
-
if (bag)
|
|
1866
|
+
if (!bag) continue;
|
|
1867
|
+
const prev = this.host.journal ? bag.get(name) : void 0;
|
|
1868
|
+
if (prev !== void 0) this.host.journal.undo.push(() => bag.set(name, prev));
|
|
1869
|
+
bag.set(name, (0, import_scoperegistry2.defaultFor)(decl));
|
|
1684
1870
|
}
|
|
1685
1871
|
}
|
|
1686
1872
|
};
|
|
@@ -1880,6 +2066,7 @@ function effectiveGameData(fields, node) {
|
|
|
1880
2066
|
}
|
|
1881
2067
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1882
2068
|
0 && (module.exports = {
|
|
2069
|
+
Checkpoint,
|
|
1883
2070
|
Engine,
|
|
1884
2071
|
Flow,
|
|
1885
2072
|
buildTagIndex,
|