@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 +35 -0
- package/dist/index.cjs +195 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +194 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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)
|
|
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
|
-
|
|
1493
|
-
|
|
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)
|
|
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,
|