@patterkit/runtime 0.18.0 → 0.20.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 +86 -0
- package/README.md +4 -3
- package/dist/index.cjs +162 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -23
- package/dist/index.d.ts +93 -23
- package/dist/index.js +162 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ function buildTagIndex(bundle) {
|
|
|
21
21
|
const visit = (node, inherited) => {
|
|
22
22
|
const acc = dedupe([...inherited, ...node.tags ?? []]);
|
|
23
23
|
index.set(node.id, acc);
|
|
24
|
+
if (node.type === "group" && node.prompt) index.set(node.prompt.id, dedupe([...acc, ...node.prompt.tags ?? []]));
|
|
24
25
|
if (node.type === "group") {
|
|
25
26
|
for (const child of node.children) visit(child, acc);
|
|
26
27
|
} else {
|
|
@@ -80,11 +81,13 @@ var Engine = class _Engine {
|
|
|
80
81
|
sceneGameIdToId = /* @__PURE__ */ new Map();
|
|
81
82
|
blockGameIdToId = /* @__PURE__ */ new Map();
|
|
82
83
|
/** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
|
|
83
|
-
* engine keeps the same
|
|
84
|
+
* engine keeps the same host scopes, custom RNG, and diagnostic hooks. */
|
|
84
85
|
creationOptions;
|
|
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;
|
|
@@ -130,12 +133,12 @@ var Engine = class _Engine {
|
|
|
130
133
|
registry.mountOwned("patter", patterBag, { owner: OWNER });
|
|
131
134
|
registered.push("patter");
|
|
132
135
|
const hostBound = /* @__PURE__ */ new Set();
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const decls = (
|
|
136
|
-
registry.defineForeign(
|
|
137
|
-
registered.push(
|
|
138
|
-
hostBound.add(
|
|
136
|
+
for (const [token, resolver] of Object.entries(boundScopes(options))) {
|
|
137
|
+
const spec = bundle.scopeRegistry?.scopes.find((s) => s.token === token);
|
|
138
|
+
const decls = (spec?.declarations ?? []).map(toForeignDecl);
|
|
139
|
+
registry.defineForeign(token, resolver, decls, { writable: spec?.writable ?? true, owner: OWNER });
|
|
140
|
+
registered.push(token);
|
|
141
|
+
hostBound.add(token);
|
|
139
142
|
}
|
|
140
143
|
for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
|
|
141
144
|
if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
|
|
@@ -155,6 +158,7 @@ var Engine = class _Engine {
|
|
|
155
158
|
}
|
|
156
159
|
this.host = {
|
|
157
160
|
logEnabled: options.log ?? false,
|
|
161
|
+
tracing: options.log ?? false,
|
|
158
162
|
emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
|
|
159
163
|
bundle,
|
|
160
164
|
emitIds,
|
|
@@ -177,11 +181,13 @@ var Engine = class _Engine {
|
|
|
177
181
|
patterSharedNames,
|
|
178
182
|
sceneSharedNames,
|
|
179
183
|
sharedVisits: /* @__PURE__ */ new Map(),
|
|
184
|
+
qualityLadders: /* @__PURE__ */ new Map(),
|
|
180
185
|
sharedSelectors: /* @__PURE__ */ new Map(),
|
|
181
186
|
stageBags: /* @__PURE__ */ new Map(),
|
|
182
187
|
journal: null,
|
|
183
188
|
customRng: options.rng,
|
|
184
189
|
onDryChoice: options.onDryChoice,
|
|
190
|
+
onError: options.onError ?? ((e) => console.warn(`Patterplay: ${e.kind} on ${e.node} in flow '${e.flow}' failed, played through: ${e.message}`)),
|
|
185
191
|
replayPromptOnChoose: options.replayPromptOnChoose ?? false,
|
|
186
192
|
captionsOn: options.closedCaptions ?? true,
|
|
187
193
|
// captions shown by default (full text)
|
|
@@ -202,6 +208,11 @@ var Engine = class _Engine {
|
|
|
202
208
|
get isSourceDebug() {
|
|
203
209
|
return this.sourceDebug;
|
|
204
210
|
}
|
|
211
|
+
/** The bundle's build identity (its content hash), the one a debug link handshakes with, or an empty
|
|
212
|
+
* string for a bundle compiled without one. */
|
|
213
|
+
get buildId() {
|
|
214
|
+
return this.host.bundle.content?.hash ?? "";
|
|
215
|
+
}
|
|
205
216
|
/**
|
|
206
217
|
* Switch the active locale LIVE - a real game's "language" setting can change mid-session. Subsequent
|
|
207
218
|
* string lookups (new beats, re-resolved character names, `{@ref}` interpolation) render in the new
|
|
@@ -322,7 +333,7 @@ var Engine = class _Engine {
|
|
|
322
333
|
});
|
|
323
334
|
}
|
|
324
335
|
this.flowsById.set(id, flow);
|
|
325
|
-
flow.
|
|
336
|
+
flow.begin(sceneId, blockId);
|
|
326
337
|
return flow;
|
|
327
338
|
}
|
|
328
339
|
/**
|
|
@@ -375,10 +386,10 @@ var Engine = class _Engine {
|
|
|
375
386
|
*/
|
|
376
387
|
resolveOpenAddress(scene, block) {
|
|
377
388
|
if (scene !== void 0) {
|
|
378
|
-
const sceneId = this.
|
|
379
|
-
if (
|
|
389
|
+
const sceneId = resolveScene(this.host, scene);
|
|
390
|
+
if (sceneId === void 0) throw new Error(`unknown scene: ${scene}`);
|
|
380
391
|
if (block === void 0) return { sceneId };
|
|
381
|
-
const blockId =
|
|
392
|
+
const blockId = resolveBlock(this.host, sceneId, block);
|
|
382
393
|
if (blockId === void 0) throw new Error(`unknown block: ${block}`);
|
|
383
394
|
return { sceneId, blockId };
|
|
384
395
|
}
|
|
@@ -389,21 +400,19 @@ var Engine = class _Engine {
|
|
|
389
400
|
if (Object.keys(this.host.bundle.scenes).length === 0) throw new Error("no scenes in bundle");
|
|
390
401
|
return {};
|
|
391
402
|
}
|
|
392
|
-
/** Resolve a scene reference
|
|
403
|
+
/** Resolve a scene reference to its internal id by the one address rule (see resolveScene); an
|
|
404
|
+
* unknown one passes through, for the caller to find nothing. */
|
|
393
405
|
resolveSceneRef(ref) {
|
|
394
406
|
if (ref == null) return void 0;
|
|
395
|
-
|
|
396
|
-
return this.sceneGameIdToId.get(ref) ?? ref;
|
|
407
|
+
return resolveScene(this.host, ref) ?? ref;
|
|
397
408
|
}
|
|
398
|
-
/** Resolve a block reference
|
|
409
|
+
/** Resolve a block reference to its internal id by the one address rule (see resolveBlock): within its
|
|
410
|
+
* scene, so a block of another scene resolves to nothing. With no scene, only an internal id resolves,
|
|
411
|
+
* since a block's gameId is only unique within its scene. */
|
|
399
412
|
resolveBlockRef(sceneId, ref) {
|
|
400
413
|
if (ref == null) return void 0;
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
const id = this.blockGameIdToId.get(sceneId)?.get(ref);
|
|
404
|
-
if (id) return id;
|
|
405
|
-
}
|
|
406
|
-
return ref;
|
|
414
|
+
if (sceneId == null) return ref;
|
|
415
|
+
return resolveBlock(this.host, sceneId, ref);
|
|
407
416
|
}
|
|
408
417
|
/** The host-facing address (gameId) of a scene / block by internal id, or undefined if unknown.
|
|
409
418
|
* The inverse of the resolve helpers - for a host that wants to display / log the address. */
|
|
@@ -688,7 +697,7 @@ var Engine = class _Engine {
|
|
|
688
697
|
for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
|
|
689
698
|
this.host.stageBags.clear();
|
|
690
699
|
for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
|
|
691
|
-
|
|
700
|
+
for (const t of Object.keys(boundScopes(this.creationOptions))) if (reg.has(t)) reg.remove(t);
|
|
692
701
|
}
|
|
693
702
|
/** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
|
|
694
703
|
getProperty(ref) {
|
|
@@ -725,12 +734,21 @@ var Engine = class _Engine {
|
|
|
725
734
|
* retained. Returns its own unsubscribe. */
|
|
726
735
|
onTrace(handler) {
|
|
727
736
|
this.engineTraceHandlers.add(handler);
|
|
728
|
-
|
|
737
|
+
this.host.tracing = true;
|
|
738
|
+
return () => {
|
|
739
|
+
this.engineTraceHandlers.delete(handler);
|
|
740
|
+
this.host.tracing = this.host.logEnabled || this.engineTraceHandlers.size > 0;
|
|
741
|
+
};
|
|
729
742
|
}
|
|
730
743
|
emitEngine(flow, event, scene) {
|
|
731
744
|
for (const h of this.engineTraceHandlers) h(flow, event);
|
|
732
745
|
if (!this.host.logEnabled) return;
|
|
733
|
-
this.engineLog.push({ ...event, flow, seq: this.
|
|
746
|
+
this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...scene ? { scene } : {} });
|
|
747
|
+
}
|
|
748
|
+
/** How many times each node has been entered across every flow, by node id: the shared count. With a
|
|
749
|
+
* flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
|
|
750
|
+
getVisitCounts() {
|
|
751
|
+
return Object.fromEntries(this.host.sharedVisits);
|
|
734
752
|
}
|
|
735
753
|
listProperties() {
|
|
736
754
|
return this.host.patterSharedDecls.map((d) => ({
|
|
@@ -796,6 +814,8 @@ var Engine = class _Engine {
|
|
|
796
814
|
this.refuseInCheckpoint("loadGame");
|
|
797
815
|
const version = save.version;
|
|
798
816
|
if (version !== 2 && version !== SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
817
|
+
const flows = save.flows;
|
|
818
|
+
if (typeof flows !== "object" || flows === null || Array.isArray(flows)) throw new Error("malformed save: no flows");
|
|
799
819
|
this.assertExternalScopes();
|
|
800
820
|
const reg = this.host.registry;
|
|
801
821
|
for (const [id, flow] of this.flowsById) {
|
|
@@ -934,19 +954,21 @@ var Flow = class {
|
|
|
934
954
|
* lowercase, as the compiler emits references (the selfBackedResolver lesson). */
|
|
935
955
|
stagesFor(scope, name) {
|
|
936
956
|
const key = name.toLowerCase();
|
|
937
|
-
const fromDecls = (decls) => decls?.find((d) => d.name.toLowerCase() === key && d.type === "quality")?.stages;
|
|
938
957
|
if (scope === "patter") {
|
|
939
|
-
return
|
|
958
|
+
return ladders(this.host, "patter", () => [...this.host.patterSharedDecls, ...this.host.patterLocalDecls]).get(key);
|
|
940
959
|
}
|
|
941
960
|
if (scope === "scene") {
|
|
942
|
-
const
|
|
943
|
-
|
|
961
|
+
const sceneId = this.currentSceneId;
|
|
962
|
+
if (sceneId == null) return void 0;
|
|
963
|
+
return ladders(this.host, `scene/${sceneId}`, () => this.host.bundle.scenes[sceneId]?.sceneProps).get(key);
|
|
944
964
|
}
|
|
945
|
-
return this.registryQualities?.(scope, name) ??
|
|
965
|
+
return this.registryQualities?.(scope, name) ?? ladders(this.host, `host/${scope}`, () => this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations).get(key);
|
|
946
966
|
}
|
|
947
967
|
// -- Host API -------------------------------------------------------------
|
|
948
|
-
/** Begin this flow at a scene (and optionally a specific block within it).
|
|
949
|
-
|
|
968
|
+
/** Begin this flow at a scene (and optionally a specific block within it). The engine's own entry point:
|
|
969
|
+
* openFlow and a goto on an unstarted flow begin a flow here. A game calls {@link reset}.
|
|
970
|
+
* @internal */
|
|
971
|
+
begin(sceneId, blockId) {
|
|
950
972
|
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");
|
|
951
973
|
this.releaseBags(false);
|
|
952
974
|
this.host.registry.discardParked(keys.flow(this.id));
|
|
@@ -958,7 +980,7 @@ var Flow = class {
|
|
|
958
980
|
this.flowEnded = false;
|
|
959
981
|
this.activeSnippet = null;
|
|
960
982
|
this.beatIndex = 0;
|
|
961
|
-
this.
|
|
983
|
+
this.clearPending();
|
|
962
984
|
this.started = true;
|
|
963
985
|
if (blockId) {
|
|
964
986
|
const loc = this.host.blockIndex.get(blockId);
|
|
@@ -983,10 +1005,15 @@ var Flow = class {
|
|
|
983
1005
|
* Forget everything in this flow and begin again - its per-flow state (not-shared
|
|
984
1006
|
* `@patter` globals + `@scene` props), cursor, callstack, selector cursors, and
|
|
985
1007
|
* visit counts. Shared state (shared `@patter` / `@scene`, world visit counts) is
|
|
986
|
-
* untouched.
|
|
1008
|
+
* untouched. The one public way to begin a flow again (openFlow begins a new one).
|
|
987
1009
|
*/
|
|
988
1010
|
reset(sceneId, blockId) {
|
|
989
|
-
this.
|
|
1011
|
+
this.begin(sceneId, blockId);
|
|
1012
|
+
}
|
|
1013
|
+
/** @deprecated Use {@link reset}, the one public name for beginning a flow again on every runtime. `start`
|
|
1014
|
+
* was the same call under a second name, and goes in a later release. */
|
|
1015
|
+
start(sceneId, blockId) {
|
|
1016
|
+
this.reset(sceneId, blockId);
|
|
990
1017
|
}
|
|
991
1018
|
/**
|
|
992
1019
|
* Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
|
|
@@ -1010,29 +1037,25 @@ var Flow = class {
|
|
|
1010
1037
|
this.touch();
|
|
1011
1038
|
if (scene === "END") {
|
|
1012
1039
|
this.started = true;
|
|
1013
|
-
this.
|
|
1014
|
-
this.pendingPrompt = null;
|
|
1015
|
-
this.pendingPromptOwnerId = null;
|
|
1040
|
+
this.clearPending();
|
|
1016
1041
|
this.activeSnippet = null;
|
|
1017
1042
|
this.beatIndex = 0;
|
|
1018
1043
|
this.flowEnded = true;
|
|
1019
1044
|
this.stack = [];
|
|
1020
1045
|
return true;
|
|
1021
1046
|
}
|
|
1022
|
-
const sceneId =
|
|
1047
|
+
const sceneId = resolveScene(this.host, scene);
|
|
1023
1048
|
if (sceneId === void 0) return false;
|
|
1024
1049
|
let blockId;
|
|
1025
1050
|
if (block !== void 0) {
|
|
1026
|
-
blockId =
|
|
1051
|
+
blockId = resolveBlock(this.host, sceneId, block);
|
|
1027
1052
|
if (blockId === void 0) return false;
|
|
1028
1053
|
}
|
|
1029
1054
|
if (!this.started) {
|
|
1030
|
-
this.
|
|
1055
|
+
this.begin(sceneId, blockId);
|
|
1031
1056
|
return true;
|
|
1032
1057
|
}
|
|
1033
|
-
this.
|
|
1034
|
-
this.pendingPrompt = null;
|
|
1035
|
-
this.pendingPromptOwnerId = null;
|
|
1058
|
+
this.clearPending();
|
|
1036
1059
|
this.activeSnippet = null;
|
|
1037
1060
|
this.beatIndex = 0;
|
|
1038
1061
|
this.flowEnded = false;
|
|
@@ -1057,6 +1080,15 @@ var Flow = class {
|
|
|
1057
1080
|
this.stack = [];
|
|
1058
1081
|
this.activeSnippet = null;
|
|
1059
1082
|
this.beatIndex = 0;
|
|
1083
|
+
this.clearPending();
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Drop everything waiting to be delivered: an open choice, and a chosen option's prompt still to be
|
|
1087
|
+
* replayed. Every move that abandons the flow's place (start and reset, goto, close) does this, so none
|
|
1088
|
+
* leaves a stale prompt behind. start() cleared only the choice, so a reset between choose() and the
|
|
1089
|
+
* next advance() replayed the abandoned run's prompt as the new run's first beat.
|
|
1090
|
+
*/
|
|
1091
|
+
clearPending() {
|
|
1060
1092
|
this.pendingChoice = null;
|
|
1061
1093
|
this.pendingPrompt = null;
|
|
1062
1094
|
this.pendingPromptOwnerId = null;
|
|
@@ -1133,7 +1165,7 @@ var Flow = class {
|
|
|
1133
1165
|
if (this.flowEnded || this.pendingChoice) return;
|
|
1134
1166
|
if (this.activeSnippet) {
|
|
1135
1167
|
if (this.beatIndex < (this.activeSnippet.beats?.length ?? 0)) return;
|
|
1136
|
-
this.runEffects(this.activeSnippet.onExit);
|
|
1168
|
+
this.runEffects(this.activeSnippet.onExit, this.activeSnippet.id);
|
|
1137
1169
|
const jump = this.activeSnippet.jump;
|
|
1138
1170
|
this.activeSnippet = null;
|
|
1139
1171
|
this.beatIndex = 0;
|
|
@@ -1153,7 +1185,7 @@ var Flow = class {
|
|
|
1153
1185
|
}
|
|
1154
1186
|
const from = frame.index;
|
|
1155
1187
|
while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
|
|
1156
|
-
if (this.host.
|
|
1188
|
+
if (this.host.tracing && frame.index !== from) {
|
|
1157
1189
|
this.emit({
|
|
1158
1190
|
type: "select",
|
|
1159
1191
|
group: frame.containerId,
|
|
@@ -1189,14 +1221,18 @@ var Flow = class {
|
|
|
1189
1221
|
clearLog() {
|
|
1190
1222
|
this.flowLog.length = 0;
|
|
1191
1223
|
}
|
|
1192
|
-
/** Record one decision, on this flow's log and the engine's
|
|
1193
|
-
*
|
|
1224
|
+
/** Record one decision, on this flow's log and the engine's, and hand it to any `onTrace`. Each call
|
|
1225
|
+
* site checks `host.tracing` first, so with nothing logging or tracing the entry is never built. */
|
|
1194
1226
|
emit(event) {
|
|
1195
1227
|
const scene = this.currentSceneId ?? void 0;
|
|
1196
1228
|
this.host.emitEngine(this.id, event, scene);
|
|
1197
1229
|
if (!this.host.logEnabled) return;
|
|
1198
1230
|
this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
|
|
1199
1231
|
}
|
|
1232
|
+
/** How many times this flow has entered each node, by node id. */
|
|
1233
|
+
getVisitCounts() {
|
|
1234
|
+
return Object.fromEntries(this.visitCounts);
|
|
1235
|
+
}
|
|
1200
1236
|
/** The options of a pending choice (empty when not at a choice point). */
|
|
1201
1237
|
getChoices() {
|
|
1202
1238
|
return this.pendingChoice?.options ?? [];
|
|
@@ -1210,7 +1246,7 @@ var Flow = class {
|
|
|
1210
1246
|
if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
|
|
1211
1247
|
this.touch();
|
|
1212
1248
|
const node = choice.byId.get(id);
|
|
1213
|
-
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1249
|
+
if (this.host.tracing) this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1214
1250
|
this.pendingChoice = null;
|
|
1215
1251
|
const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
|
|
1216
1252
|
this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
|
|
@@ -1349,7 +1385,7 @@ var Flow = class {
|
|
|
1349
1385
|
this.currentSceneId = sceneId;
|
|
1350
1386
|
this.enter(sceneId);
|
|
1351
1387
|
this.seedScene(scene);
|
|
1352
|
-
this.runEffects(scene.onEntry);
|
|
1388
|
+
this.runEffects(scene.onEntry, scene.id);
|
|
1353
1389
|
}
|
|
1354
1390
|
/**
|
|
1355
1391
|
* Play one child of the active run. A snippet begins delivering. A group is
|
|
@@ -1385,7 +1421,7 @@ var Flow = class {
|
|
|
1385
1421
|
return void 0;
|
|
1386
1422
|
}
|
|
1387
1423
|
beginSnippet(snippet) {
|
|
1388
|
-
this.runEffects(snippet.onEnter);
|
|
1424
|
+
this.runEffects(snippet.onEnter, snippet.id);
|
|
1389
1425
|
this.activeSnippet = snippet;
|
|
1390
1426
|
this.beatIndex = 0;
|
|
1391
1427
|
}
|
|
@@ -1405,8 +1441,8 @@ var Flow = class {
|
|
|
1405
1441
|
options.push({ id: child.id, prompt: this.promptFor(child), eligible, gameData: child.gameData });
|
|
1406
1442
|
byId.set(child.id, child);
|
|
1407
1443
|
}
|
|
1408
|
-
if (options.
|
|
1409
|
-
this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1444
|
+
if (options.some((o) => o.eligible)) {
|
|
1445
|
+
if (this.host.tracing) this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1410
1446
|
this.pendingChoice = { groupId: group.id, options, byId };
|
|
1411
1447
|
return;
|
|
1412
1448
|
}
|
|
@@ -1415,7 +1451,7 @@ var Flow = class {
|
|
|
1415
1451
|
this.enterChild(fallback);
|
|
1416
1452
|
return;
|
|
1417
1453
|
}
|
|
1418
|
-
this.emit({ type: "dry", group: group.id });
|
|
1454
|
+
if (this.host.tracing) this.emit({ type: "dry", group: group.id });
|
|
1419
1455
|
this.host.onDryChoice?.(group.id);
|
|
1420
1456
|
}
|
|
1421
1457
|
// -- Jumps (jump / call-return) ----------------------------------------
|
|
@@ -1430,7 +1466,7 @@ var Flow = class {
|
|
|
1430
1466
|
* hard-ends the flow regardless of the callstack.
|
|
1431
1467
|
*/
|
|
1432
1468
|
enterTarget(to, mode) {
|
|
1433
|
-
this.emit({ type: "jump", to, mode });
|
|
1469
|
+
if (this.host.tracing) this.emit({ type: "jump", to, mode });
|
|
1434
1470
|
if (to === "END") {
|
|
1435
1471
|
this.flowEnded = true;
|
|
1436
1472
|
this.stack = [];
|
|
@@ -1463,11 +1499,11 @@ var Flow = class {
|
|
|
1463
1499
|
// -- Selectors ------------------------------------------------------------
|
|
1464
1500
|
selectChild(group) {
|
|
1465
1501
|
const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
|
|
1466
|
-
const eligible = group.children.filter((
|
|
1502
|
+
const eligible = group.children.filter((_, i) => verdicts[i].eligible);
|
|
1467
1503
|
const order = group.options?.order ?? "sequential";
|
|
1468
1504
|
const exhaust = group.options?.exhaust ?? "once";
|
|
1469
1505
|
const trace = (picked) => {
|
|
1470
|
-
this.emit({
|
|
1506
|
+
if (this.host.tracing) this.emit({
|
|
1471
1507
|
type: "select",
|
|
1472
1508
|
group: group.id,
|
|
1473
1509
|
selector: group.selector ?? "default",
|
|
@@ -1510,8 +1546,9 @@ var Flow = class {
|
|
|
1510
1546
|
const len = eligible.length;
|
|
1511
1547
|
const stick = exhaust === "stick";
|
|
1512
1548
|
const fill = () => (stick ? eligible.slice(0, len - 1) : eligible).map((c) => c.id);
|
|
1549
|
+
const eligibleIds = new Set(eligible.map((c) => c.id));
|
|
1513
1550
|
if (st.bag === void 0) st.bag = fill();
|
|
1514
|
-
if (st.bag.
|
|
1551
|
+
if (!st.bag.some((id2) => eligibleIds.has(id2))) {
|
|
1515
1552
|
if (exhaust === "once") return null;
|
|
1516
1553
|
if (stick) {
|
|
1517
1554
|
const last = eligible[len - 1];
|
|
@@ -1519,13 +1556,14 @@ var Flow = class {
|
|
|
1519
1556
|
return last;
|
|
1520
1557
|
}
|
|
1521
1558
|
st.bag = fill();
|
|
1559
|
+
if (st.bag.length === 0) return null;
|
|
1522
1560
|
}
|
|
1523
|
-
const pool = st.bag;
|
|
1561
|
+
const pool = st.bag.filter((id2) => eligibleIds.has(id2));
|
|
1524
1562
|
const p = st.last !== void 0 && pool.length > 1 ? pool.indexOf(st.last) : -1;
|
|
1525
1563
|
let i = Math.floor(this.rng() * (p >= 0 ? pool.length - 1 : pool.length));
|
|
1526
1564
|
if (p >= 0 && i >= p) i++;
|
|
1527
1565
|
const id = pool[i];
|
|
1528
|
-
|
|
1566
|
+
st.bag.splice(st.bag.indexOf(id), 1);
|
|
1529
1567
|
st.last = id;
|
|
1530
1568
|
return eligible.find((c) => c.id === id);
|
|
1531
1569
|
}
|
|
@@ -1574,7 +1612,7 @@ var Flow = class {
|
|
|
1574
1612
|
/** A child's Best-match score against the current state: 0 when it has no condition (the filler
|
|
1575
1613
|
* tier), else the specificity of its (already-passing) condition. */
|
|
1576
1614
|
specScore(node) {
|
|
1577
|
-
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true) : 0;
|
|
1615
|
+
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true, node) : 0;
|
|
1578
1616
|
}
|
|
1579
1617
|
/**
|
|
1580
1618
|
* The **matched-specificity** metric (parity contract): how many atomic constraints are actively
|
|
@@ -1584,8 +1622,15 @@ var Flow = class {
|
|
|
1584
1622
|
* to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
|
|
1585
1623
|
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1586
1624
|
*/
|
|
1587
|
-
matchedSpec(node, want) {
|
|
1588
|
-
const evalTruthy = (n) =>
|
|
1625
|
+
matchedSpec(node, want, owner) {
|
|
1626
|
+
const evalTruthy = (n) => {
|
|
1627
|
+
try {
|
|
1628
|
+
return truthy(evaluate(n, this.context(), patterDialect));
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
this.reportError("best-match", owner.id, owner.condition, err);
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1589
1634
|
return scoreSpecificity(node, evalTruthy, { want });
|
|
1590
1635
|
}
|
|
1591
1636
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
@@ -1617,17 +1662,36 @@ var Flow = class {
|
|
|
1617
1662
|
return st;
|
|
1618
1663
|
}
|
|
1619
1664
|
// -- Effects + expressions ------------------------------------------------
|
|
1620
|
-
|
|
1665
|
+
/** Run an effect list. `owner` is the snippet or scene it belongs to, for an error report. */
|
|
1666
|
+
runEffects(effects, owner) {
|
|
1621
1667
|
for (const e of effects ?? []) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1668
|
+
let value;
|
|
1669
|
+
try {
|
|
1670
|
+
value = this.evalExpr(e.value);
|
|
1671
|
+
const prev = this.host.tracing ? this.getProperty(e.target) : void 0;
|
|
1672
|
+
this.writeProperty(e.target, value, false);
|
|
1673
|
+
if (this.host.tracing) this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
this.reportError("effect", owner, e.value, err);
|
|
1676
|
+
}
|
|
1626
1677
|
}
|
|
1627
1678
|
}
|
|
1679
|
+
/** Whether a node's condition holds. A condition that fails to evaluate counts as false (see PlayError). */
|
|
1628
1680
|
eligible(node) {
|
|
1629
1681
|
if (!node.condition) return true;
|
|
1630
|
-
|
|
1682
|
+
try {
|
|
1683
|
+
return truthy(this.evalExpr(node.condition));
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
this.reportError("condition", node.id, node.condition, err);
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/** Report a content error the engine is playing through: to the host's onError, and to the log. */
|
|
1690
|
+
reportError(kind, node, expr, err) {
|
|
1691
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1692
|
+
const source = expr?.src;
|
|
1693
|
+
this.host.onError({ flow: this.id, kind, node, ...source ? { source } : {}, message });
|
|
1694
|
+
if (this.host.tracing) this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
|
|
1631
1695
|
}
|
|
1632
1696
|
evalExpr(expr) {
|
|
1633
1697
|
return evaluate(this.conditionAst(expr), this.context(), patterDialect);
|
|
@@ -1823,23 +1887,23 @@ var Flow = class {
|
|
|
1823
1887
|
if (!this.sceneBags.has(s)) {
|
|
1824
1888
|
const bag = new PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1825
1889
|
const key = keys.flowScene(this.id, s);
|
|
1826
|
-
this.host.registry
|
|
1890
|
+
const claimed = mountClaims(this.host.registry, key, bag, journal !== null);
|
|
1827
1891
|
this.registered.add(key);
|
|
1828
1892
|
this.sceneBags.set(s, bag);
|
|
1829
1893
|
if (journal) journal.undo.push(() => {
|
|
1830
1894
|
if (this.sceneBags.get(s) !== bag) return;
|
|
1831
|
-
this.host.registry.remove(key);
|
|
1895
|
+
this.host.registry.remove(key, claimed ? { keep: true } : void 0);
|
|
1832
1896
|
this.registered.delete(key);
|
|
1833
1897
|
this.sceneBags.delete(s);
|
|
1834
1898
|
});
|
|
1835
1899
|
}
|
|
1836
1900
|
if (!this.host.stageBags.has(s)) {
|
|
1837
1901
|
const bag = new PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1838
|
-
this.host.registry
|
|
1902
|
+
const claimed = mountClaims(this.host.registry, keys.stage(s), bag, journal !== null);
|
|
1839
1903
|
this.host.stageBags.set(s, bag);
|
|
1840
1904
|
if (journal) journal.undo.push(() => {
|
|
1841
1905
|
if (this.host.stageBags.get(s) !== bag) return;
|
|
1842
|
-
this.host.registry.remove(keys.stage(s));
|
|
1906
|
+
this.host.registry.remove(keys.stage(s), claimed ? { keep: true } : void 0);
|
|
1843
1907
|
this.host.stageBags.delete(s);
|
|
1844
1908
|
});
|
|
1845
1909
|
}
|
|
@@ -1906,9 +1970,29 @@ function deserialiseSelectors(rec) {
|
|
|
1906
1970
|
}
|
|
1907
1971
|
return map;
|
|
1908
1972
|
}
|
|
1973
|
+
function mountClaims(registry, key, bag, ask) {
|
|
1974
|
+
const before = ask ? JSON.stringify(bag.save()) : "";
|
|
1975
|
+
registry.mountOwned(key, bag, { owner: OWNER });
|
|
1976
|
+
return ask && JSON.stringify(bag.save()) !== before;
|
|
1977
|
+
}
|
|
1909
1978
|
function toDecl(decl) {
|
|
1910
1979
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
|
|
1911
1980
|
}
|
|
1981
|
+
function boundScopes(options) {
|
|
1982
|
+
return { ...options.world ? { world: options.world } : {}, ...options.hostScopes };
|
|
1983
|
+
}
|
|
1984
|
+
function ladders(host, cacheKey, decls) {
|
|
1985
|
+
let found = host.qualityLadders.get(cacheKey);
|
|
1986
|
+
if (!found) {
|
|
1987
|
+
found = /* @__PURE__ */ new Map();
|
|
1988
|
+
for (const d of decls() ?? []) {
|
|
1989
|
+
const k = d.name.toLowerCase();
|
|
1990
|
+
if (d.type === "quality" && d.stages && !found.has(k)) found.set(k, d.stages);
|
|
1991
|
+
}
|
|
1992
|
+
host.qualityLadders.set(cacheKey, found);
|
|
1993
|
+
}
|
|
1994
|
+
return found;
|
|
1995
|
+
}
|
|
1912
1996
|
function toForeignDecl(decl) {
|
|
1913
1997
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
|
|
1914
1998
|
}
|
|
@@ -1917,6 +2001,12 @@ function selfBackedDecl(decl, scopeWritable) {
|
|
|
1917
2001
|
const writable = decl.writable ?? scopeWritable;
|
|
1918
2002
|
return writable === void 0 ? d : { ...d, writable };
|
|
1919
2003
|
}
|
|
2004
|
+
function resolveScene(host, ref) {
|
|
2005
|
+
return host.sceneGameIdToId.get(ref) ?? (host.bundle.scenes[ref] ? ref : void 0);
|
|
2006
|
+
}
|
|
2007
|
+
function resolveBlock(host, sceneId, ref) {
|
|
2008
|
+
return host.blockGameIdToId.get(sceneId)?.get(ref) ?? (host.blockIndex.get(ref)?.sceneId === sceneId ? ref : void 0);
|
|
2009
|
+
}
|
|
1920
2010
|
function splitHostRef(host, ref) {
|
|
1921
2011
|
if (host.refSplitRevision !== host.registry.revision) {
|
|
1922
2012
|
host.refSplitCache.clear();
|