@patterkit/runtime 0.19.0 → 0.21.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 +56 -0
- package/README.md +4 -3
- package/dist/index.cjs +107 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -20
- package/dist/index.d.ts +53 -20
- package/dist/index.js +107 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,62 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.21.0] - 2026-10-07
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **A new `Engine` on a bundle another engine already uses no longer rebuilds the tag index.** It is built once
|
|
41
|
+
per bundle and shared, so a tool that starts a fresh engine for every run (coverage does) starts each in about
|
|
42
|
+
a third of the time: 0.5 ms rather than 1.6 ms on a 100-scene project.
|
|
43
|
+
- **The conformance corpus covers more, and every runtime is held to it**: the outline and beat sequence, the
|
|
44
|
+
audio resolver's joins, `listProperties`, what a checkpoint refuses while it is open, a save taken inside one,
|
|
45
|
+
and a block named under another scene.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Tags handed out are the caller's own.** `tagsForBeat`, `tagsForScene`, `tagsForBlock`, and a delivered
|
|
50
|
+
line's `tags` are copies, so a game that edits one changes nothing the engine says afterwards.
|
|
51
|
+
|
|
52
|
+
## [0.20.0] - 2026-10-06
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **`hostScopes`, the engine option every other runtime takes**: the scopes the game binds, by token, each a `{
|
|
57
|
+
get, set? }` over values it owns, as in `hostScopes: { world: { get, set } }`. A resolver's type is
|
|
58
|
+
`HostScope`.
|
|
59
|
+
- **`engine.buildId`**, as the other runtimes have: the bundle's content hash, the build a debug link handshakes
|
|
60
|
+
with.
|
|
61
|
+
- **A bundle-description section in the conformance corpus.** What `describeBundle` says about one bundle, field
|
|
62
|
+
for field, held on every runtime.
|
|
63
|
+
- **`getVisitCounts()` on the engine and on a flow**, on every runtime: how many times each node has been
|
|
64
|
+
entered, across every flow or in this one, read without a save.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- **Less work per step.** A decision's log entry is built only while the log is on or something is tracing, and
|
|
69
|
+
a quality's stage ladder is looked up once per set of declarations rather than scanned for on every
|
|
70
|
+
comparison.
|
|
71
|
+
|
|
72
|
+
### Deprecated
|
|
73
|
+
|
|
74
|
+
- **`flow.start()` is deprecated: use `flow.reset()`.** They were the same call under two names. `flow.reset()`
|
|
75
|
+
is the one public name for beginning a flow again on every runtime (opening a flow begins a new one);
|
|
76
|
+
`flow.start()` goes in a later release.
|
|
77
|
+
- **The `world` option and the `WorldResolver` type: use `hostScopes: { world }` and `HostScope`.** `world` is
|
|
78
|
+
still the world scope, and goes in a later release.
|
|
79
|
+
- **The `TraceHandler` type.** Nothing takes one: `onTrace` takes an `EngineTraceHandler`. It goes in a later
|
|
80
|
+
release.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **`reset()` no longer replays a prompt from the run it abandoned.** With prompt replay on, `choose()` leaves
|
|
85
|
+
the chosen option's prompt waiting for the next `advance()`. `reset()` in between cleared only the choice, so
|
|
86
|
+
the old prompt played as the restarted run's first beat. Every move that leaves the flow's place (`reset()`,
|
|
87
|
+
`goto`, `close`) now drops both, through one helper.
|
|
88
|
+
- **`onTrace` hears every decision with the log off.** A `run` walk past a snippet whose condition failed, and a
|
|
89
|
+
write's previous value, were only worked out when the log was on, so a tool tracing a run without the log
|
|
90
|
+
missed the one and got a write without the other. They are now worked out whenever anything is tracing.
|
|
91
|
+
|
|
36
92
|
## [0.19.0] - 2026-10-06
|
|
37
93
|
|
|
38
94
|
### Added
|
package/README.md
CHANGED
|
@@ -44,9 +44,10 @@ for (;;) {
|
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`new Engine(bundle, options)` takes `{ rng?, seed?, locale?,
|
|
48
|
-
- `
|
|
49
|
-
|
|
47
|
+
`new Engine(bundle, options)` takes `{ rng?, seed?, locale?, hostScopes?, registry?, replayPromptOnChoose?, closedCaptions? }`:
|
|
48
|
+
- `hostScopes` binds the scopes your game owns, by token, each `{ get(name), set?(name, value) }`: for World
|
|
49
|
+
Properties, `hostScopes: { world: { get, set } }`. Leave `world` unbound and the runtime self-backs `@world`
|
|
50
|
+
from the declared defaults, as a property it saves.
|
|
50
51
|
- `registry` is the game's one `ScopeRegistry` (from `@wildwinter/scoperegistry`), holding every
|
|
51
52
|
engine's properties. Omit it and the engine makes its own. See *One registry per game* below.
|
|
52
53
|
- `locale` plays a non-default language (embedded localisation; an IDs-only bundle ignores it).
|
package/dist/index.cjs
CHANGED
|
@@ -76,6 +76,18 @@ function buildTagIndex(bundle) {
|
|
|
76
76
|
// src/engine.ts
|
|
77
77
|
var import_model2 = require("@patterkit/model");
|
|
78
78
|
var astCache = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
var tagIndexCache = /* @__PURE__ */ new WeakMap();
|
|
80
|
+
function tagIndexFor(bundle) {
|
|
81
|
+
let index = tagIndexCache.get(bundle);
|
|
82
|
+
if (!index) {
|
|
83
|
+
index = buildTagIndex(bundle);
|
|
84
|
+
tagIndexCache.set(bundle, index);
|
|
85
|
+
}
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
function tagsOf(host, id) {
|
|
89
|
+
return host.tagIndex.get(id)?.slice();
|
|
90
|
+
}
|
|
79
91
|
var OWNER = "Patter";
|
|
80
92
|
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
81
93
|
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
@@ -114,7 +126,7 @@ var Engine = class _Engine {
|
|
|
114
126
|
sceneGameIdToId = /* @__PURE__ */ new Map();
|
|
115
127
|
blockGameIdToId = /* @__PURE__ */ new Map();
|
|
116
128
|
/** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
|
|
117
|
-
* engine keeps the same
|
|
129
|
+
* engine keeps the same host scopes, custom RNG, and diagnostic hooks. */
|
|
118
130
|
creationOptions;
|
|
119
131
|
/** The run's ordered stream: every flow's events, each naming its flow. Empty and
|
|
120
132
|
* unwritten unless `options.log` asked for it. */
|
|
@@ -166,12 +178,12 @@ var Engine = class _Engine {
|
|
|
166
178
|
registry.mountOwned("patter", patterBag, { owner: OWNER });
|
|
167
179
|
registered.push("patter");
|
|
168
180
|
const hostBound = /* @__PURE__ */ new Set();
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const decls = (
|
|
172
|
-
registry.defineForeign(
|
|
173
|
-
registered.push(
|
|
174
|
-
hostBound.add(
|
|
181
|
+
for (const [token, resolver] of Object.entries(boundScopes(options))) {
|
|
182
|
+
const spec = bundle.scopeRegistry?.scopes.find((s) => s.token === token);
|
|
183
|
+
const decls = (spec?.declarations ?? []).map(toForeignDecl);
|
|
184
|
+
registry.defineForeign(token, resolver, decls, { writable: spec?.writable ?? true, owner: OWNER });
|
|
185
|
+
registered.push(token);
|
|
186
|
+
hostBound.add(token);
|
|
175
187
|
}
|
|
176
188
|
for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
|
|
177
189
|
if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
|
|
@@ -191,6 +203,7 @@ var Engine = class _Engine {
|
|
|
191
203
|
}
|
|
192
204
|
this.host = {
|
|
193
205
|
logEnabled: options.log ?? false,
|
|
206
|
+
tracing: options.log ?? false,
|
|
194
207
|
emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
|
|
195
208
|
bundle,
|
|
196
209
|
emitIds,
|
|
@@ -203,7 +216,7 @@ var Engine = class _Engine {
|
|
|
203
216
|
sceneGameIdToId: this.sceneGameIdToId,
|
|
204
217
|
blockGameIdToId: this.blockGameIdToId,
|
|
205
218
|
// same instances the engine resolves with
|
|
206
|
-
tagIndex:
|
|
219
|
+
tagIndex: tagIndexFor(bundle),
|
|
207
220
|
registry,
|
|
208
221
|
ownsRegistry,
|
|
209
222
|
patterBag,
|
|
@@ -213,6 +226,7 @@ var Engine = class _Engine {
|
|
|
213
226
|
patterSharedNames,
|
|
214
227
|
sceneSharedNames,
|
|
215
228
|
sharedVisits: /* @__PURE__ */ new Map(),
|
|
229
|
+
qualityLadders: /* @__PURE__ */ new Map(),
|
|
216
230
|
sharedSelectors: /* @__PURE__ */ new Map(),
|
|
217
231
|
stageBags: /* @__PURE__ */ new Map(),
|
|
218
232
|
journal: null,
|
|
@@ -239,6 +253,11 @@ var Engine = class _Engine {
|
|
|
239
253
|
get isSourceDebug() {
|
|
240
254
|
return this.sourceDebug;
|
|
241
255
|
}
|
|
256
|
+
/** The bundle's build identity (its content hash), the one a debug link handshakes with, or an empty
|
|
257
|
+
* string for a bundle compiled without one. */
|
|
258
|
+
get buildId() {
|
|
259
|
+
return this.host.bundle.content?.hash ?? "";
|
|
260
|
+
}
|
|
242
261
|
/**
|
|
243
262
|
* Switch the active locale LIVE - a real game's "language" setting can change mid-session. Subsequent
|
|
244
263
|
* string lookups (new beats, re-resolved character names, `{@ref}` interpolation) render in the new
|
|
@@ -359,7 +378,7 @@ var Engine = class _Engine {
|
|
|
359
378
|
});
|
|
360
379
|
}
|
|
361
380
|
this.flowsById.set(id, flow);
|
|
362
|
-
flow.
|
|
381
|
+
flow.begin(sceneId, blockId);
|
|
363
382
|
return flow;
|
|
364
383
|
}
|
|
365
384
|
/**
|
|
@@ -456,18 +475,18 @@ var Engine = class _Engine {
|
|
|
456
475
|
* delivered step carries. Empty array for an unknown id or a beat with no tags anywhere up the chain.
|
|
457
476
|
*/
|
|
458
477
|
tagsForBeat(beatId) {
|
|
459
|
-
return this.host
|
|
478
|
+
return tagsOf(this.host, beatId) ?? [];
|
|
460
479
|
}
|
|
461
480
|
/** A scene's own tags (by internal id or gameId address). Empty when none / unknown. */
|
|
462
481
|
tagsForScene(sceneRef) {
|
|
463
482
|
const id = this.resolveSceneRef(sceneRef);
|
|
464
|
-
return (id != null ? this.host
|
|
483
|
+
return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
|
|
465
484
|
}
|
|
466
485
|
/** A block's accumulated tags (scene + block), by scene + block ref (id or gameId). Empty when none / unknown. */
|
|
467
486
|
tagsForBlock(sceneRef, blockRef) {
|
|
468
487
|
const sceneId = this.resolveSceneRef(sceneRef);
|
|
469
488
|
const id = this.resolveBlockRef(sceneId, blockRef);
|
|
470
|
-
return (id != null ? this.host
|
|
489
|
+
return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
|
|
471
490
|
}
|
|
472
491
|
/**
|
|
473
492
|
* A scene's own author gameData (by internal id or gameId address): the RAW sparse overrides, exactly
|
|
@@ -586,7 +605,7 @@ var Engine = class _Engine {
|
|
|
586
605
|
}
|
|
587
606
|
/** One beat's static data (source locale), the same shape a delivered step carries. */
|
|
588
607
|
beatInfo(beat) {
|
|
589
|
-
const tags = this.host
|
|
608
|
+
const tags = tagsOf(this.host, beat.id);
|
|
590
609
|
const info = { id: beat.id, kind: beat.kind };
|
|
591
610
|
if (beat.kind === "line") {
|
|
592
611
|
if (beat.character !== void 0) {
|
|
@@ -606,7 +625,7 @@ var Engine = class _Engine {
|
|
|
606
625
|
}
|
|
607
626
|
/** A `{ tags }` fragment for an id, present only when the id has accumulated tags (keeps output tidy). */
|
|
608
627
|
tagsField(id) {
|
|
609
|
-
const tags = this.host
|
|
628
|
+
const tags = tagsOf(this.host, id);
|
|
610
629
|
return tags && tags.length ? { tags } : {};
|
|
611
630
|
}
|
|
612
631
|
/** Retrieve an open flow by id (undefined if none / closed). */
|
|
@@ -723,7 +742,7 @@ var Engine = class _Engine {
|
|
|
723
742
|
for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
|
|
724
743
|
this.host.stageBags.clear();
|
|
725
744
|
for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
|
|
726
|
-
|
|
745
|
+
for (const t of Object.keys(boundScopes(this.creationOptions))) if (reg.has(t)) reg.remove(t);
|
|
727
746
|
}
|
|
728
747
|
/** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
|
|
729
748
|
getProperty(ref) {
|
|
@@ -743,8 +762,6 @@ var Engine = class _Engine {
|
|
|
743
762
|
this.host.registry.set(scope, name, value, { host: true });
|
|
744
763
|
if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
|
|
745
764
|
}
|
|
746
|
-
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
747
|
-
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
748
765
|
/** The run's decisions, in order, each naming the flow it happened in. Empty unless the
|
|
749
766
|
* run was opened with `log: true`. A flow's own log stays flow-local; this is the only
|
|
750
767
|
* place a story spanning several flows reads as one sequence. */
|
|
@@ -760,13 +777,24 @@ var Engine = class _Engine {
|
|
|
760
777
|
* retained. Returns its own unsubscribe. */
|
|
761
778
|
onTrace(handler) {
|
|
762
779
|
this.engineTraceHandlers.add(handler);
|
|
763
|
-
|
|
780
|
+
this.host.tracing = true;
|
|
781
|
+
return () => {
|
|
782
|
+
this.engineTraceHandlers.delete(handler);
|
|
783
|
+
this.host.tracing = this.host.logEnabled || this.engineTraceHandlers.size > 0;
|
|
784
|
+
};
|
|
764
785
|
}
|
|
765
786
|
emitEngine(flow, event, scene) {
|
|
766
787
|
for (const h of this.engineTraceHandlers) h(flow, event);
|
|
767
788
|
if (!this.host.logEnabled) return;
|
|
768
789
|
this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...scene ? { scene } : {} });
|
|
769
790
|
}
|
|
791
|
+
/** How many times each node has been entered across every flow, by node id: the shared count. With a
|
|
792
|
+
* flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
|
|
793
|
+
getVisitCounts() {
|
|
794
|
+
return Object.fromEntries(this.host.sharedVisits);
|
|
795
|
+
}
|
|
796
|
+
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
797
|
+
* value, declared default (for reset), and enum options. Every runtime has the same call. */
|
|
770
798
|
listProperties() {
|
|
771
799
|
return this.host.patterSharedDecls.map((d) => ({
|
|
772
800
|
name: d.name,
|
|
@@ -971,19 +999,21 @@ var Flow = class {
|
|
|
971
999
|
* lowercase, as the compiler emits references (the selfBackedResolver lesson). */
|
|
972
1000
|
stagesFor(scope, name) {
|
|
973
1001
|
const key = name.toLowerCase();
|
|
974
|
-
const fromDecls = (decls) => decls?.find((d) => d.name.toLowerCase() === key && d.type === "quality")?.stages;
|
|
975
1002
|
if (scope === "patter") {
|
|
976
|
-
return
|
|
1003
|
+
return ladders(this.host, "patter", () => [...this.host.patterSharedDecls, ...this.host.patterLocalDecls]).get(key);
|
|
977
1004
|
}
|
|
978
1005
|
if (scope === "scene") {
|
|
979
|
-
const
|
|
980
|
-
|
|
1006
|
+
const sceneId = this.currentSceneId;
|
|
1007
|
+
if (sceneId == null) return void 0;
|
|
1008
|
+
return ladders(this.host, `scene/${sceneId}`, () => this.host.bundle.scenes[sceneId]?.sceneProps).get(key);
|
|
981
1009
|
}
|
|
982
|
-
return this.registryQualities?.(scope, name) ??
|
|
1010
|
+
return this.registryQualities?.(scope, name) ?? ladders(this.host, `host/${scope}`, () => this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations).get(key);
|
|
983
1011
|
}
|
|
984
1012
|
// -- Host API -------------------------------------------------------------
|
|
985
|
-
/** Begin this flow at a scene (and optionally a specific block within it).
|
|
986
|
-
|
|
1013
|
+
/** Begin this flow at a scene (and optionally a specific block within it). The engine's own entry point:
|
|
1014
|
+
* openFlow and a goto on an unstarted flow begin a flow here. A game calls {@link reset}.
|
|
1015
|
+
* @internal */
|
|
1016
|
+
begin(sceneId, blockId) {
|
|
987
1017
|
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");
|
|
988
1018
|
this.releaseBags(false);
|
|
989
1019
|
this.host.registry.discardParked(keys.flow(this.id));
|
|
@@ -995,7 +1025,7 @@ var Flow = class {
|
|
|
995
1025
|
this.flowEnded = false;
|
|
996
1026
|
this.activeSnippet = null;
|
|
997
1027
|
this.beatIndex = 0;
|
|
998
|
-
this.
|
|
1028
|
+
this.clearPending();
|
|
999
1029
|
this.started = true;
|
|
1000
1030
|
if (blockId) {
|
|
1001
1031
|
const loc = this.host.blockIndex.get(blockId);
|
|
@@ -1020,10 +1050,15 @@ var Flow = class {
|
|
|
1020
1050
|
* Forget everything in this flow and begin again - its per-flow state (not-shared
|
|
1021
1051
|
* `@patter` globals + `@scene` props), cursor, callstack, selector cursors, and
|
|
1022
1052
|
* visit counts. Shared state (shared `@patter` / `@scene`, world visit counts) is
|
|
1023
|
-
* untouched.
|
|
1053
|
+
* untouched. The one public way to begin a flow again (openFlow begins a new one).
|
|
1024
1054
|
*/
|
|
1025
1055
|
reset(sceneId, blockId) {
|
|
1026
|
-
this.
|
|
1056
|
+
this.begin(sceneId, blockId);
|
|
1057
|
+
}
|
|
1058
|
+
/** @deprecated Use {@link reset}, the one public name for beginning a flow again on every runtime. `start`
|
|
1059
|
+
* was the same call under a second name, and goes in a later release. */
|
|
1060
|
+
start(sceneId, blockId) {
|
|
1061
|
+
this.reset(sceneId, blockId);
|
|
1027
1062
|
}
|
|
1028
1063
|
/**
|
|
1029
1064
|
* Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
|
|
@@ -1047,9 +1082,7 @@ var Flow = class {
|
|
|
1047
1082
|
this.touch();
|
|
1048
1083
|
if (scene === "END") {
|
|
1049
1084
|
this.started = true;
|
|
1050
|
-
this.
|
|
1051
|
-
this.pendingPrompt = null;
|
|
1052
|
-
this.pendingPromptOwnerId = null;
|
|
1085
|
+
this.clearPending();
|
|
1053
1086
|
this.activeSnippet = null;
|
|
1054
1087
|
this.beatIndex = 0;
|
|
1055
1088
|
this.flowEnded = true;
|
|
@@ -1064,12 +1097,10 @@ var Flow = class {
|
|
|
1064
1097
|
if (blockId === void 0) return false;
|
|
1065
1098
|
}
|
|
1066
1099
|
if (!this.started) {
|
|
1067
|
-
this.
|
|
1100
|
+
this.begin(sceneId, blockId);
|
|
1068
1101
|
return true;
|
|
1069
1102
|
}
|
|
1070
|
-
this.
|
|
1071
|
-
this.pendingPrompt = null;
|
|
1072
|
-
this.pendingPromptOwnerId = null;
|
|
1103
|
+
this.clearPending();
|
|
1073
1104
|
this.activeSnippet = null;
|
|
1074
1105
|
this.beatIndex = 0;
|
|
1075
1106
|
this.flowEnded = false;
|
|
@@ -1094,6 +1125,15 @@ var Flow = class {
|
|
|
1094
1125
|
this.stack = [];
|
|
1095
1126
|
this.activeSnippet = null;
|
|
1096
1127
|
this.beatIndex = 0;
|
|
1128
|
+
this.clearPending();
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Drop everything waiting to be delivered: an open choice, and a chosen option's prompt still to be
|
|
1132
|
+
* replayed. Every move that abandons the flow's place (start and reset, goto, close) does this, so none
|
|
1133
|
+
* leaves a stale prompt behind. start() cleared only the choice, so a reset between choose() and the
|
|
1134
|
+
* next advance() replayed the abandoned run's prompt as the new run's first beat.
|
|
1135
|
+
*/
|
|
1136
|
+
clearPending() {
|
|
1097
1137
|
this.pendingChoice = null;
|
|
1098
1138
|
this.pendingPrompt = null;
|
|
1099
1139
|
this.pendingPromptOwnerId = null;
|
|
@@ -1190,7 +1230,7 @@ var Flow = class {
|
|
|
1190
1230
|
}
|
|
1191
1231
|
const from = frame.index;
|
|
1192
1232
|
while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
|
|
1193
|
-
if (this.host.
|
|
1233
|
+
if (this.host.tracing && frame.index !== from) {
|
|
1194
1234
|
this.emit({
|
|
1195
1235
|
type: "select",
|
|
1196
1236
|
group: frame.containerId,
|
|
@@ -1226,14 +1266,18 @@ var Flow = class {
|
|
|
1226
1266
|
clearLog() {
|
|
1227
1267
|
this.flowLog.length = 0;
|
|
1228
1268
|
}
|
|
1229
|
-
/** Record one decision, on this flow's log and the engine's
|
|
1230
|
-
*
|
|
1269
|
+
/** Record one decision, on this flow's log and the engine's, and hand it to any `onTrace`. Each call
|
|
1270
|
+
* site checks `host.tracing` first, so with nothing logging or tracing the entry is never built. */
|
|
1231
1271
|
emit(event) {
|
|
1232
1272
|
const scene = this.currentSceneId ?? void 0;
|
|
1233
1273
|
this.host.emitEngine(this.id, event, scene);
|
|
1234
1274
|
if (!this.host.logEnabled) return;
|
|
1235
1275
|
this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
|
|
1236
1276
|
}
|
|
1277
|
+
/** How many times this flow has entered each node, by node id. */
|
|
1278
|
+
getVisitCounts() {
|
|
1279
|
+
return Object.fromEntries(this.visitCounts);
|
|
1280
|
+
}
|
|
1237
1281
|
/** The options of a pending choice (empty when not at a choice point). */
|
|
1238
1282
|
getChoices() {
|
|
1239
1283
|
return this.pendingChoice?.options ?? [];
|
|
@@ -1247,7 +1291,7 @@ var Flow = class {
|
|
|
1247
1291
|
if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
|
|
1248
1292
|
this.touch();
|
|
1249
1293
|
const node = choice.byId.get(id);
|
|
1250
|
-
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1294
|
+
if (this.host.tracing) this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1251
1295
|
this.pendingChoice = null;
|
|
1252
1296
|
const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
|
|
1253
1297
|
this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
|
|
@@ -1443,7 +1487,7 @@ var Flow = class {
|
|
|
1443
1487
|
byId.set(child.id, child);
|
|
1444
1488
|
}
|
|
1445
1489
|
if (options.some((o) => o.eligible)) {
|
|
1446
|
-
this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1490
|
+
if (this.host.tracing) this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1447
1491
|
this.pendingChoice = { groupId: group.id, options, byId };
|
|
1448
1492
|
return;
|
|
1449
1493
|
}
|
|
@@ -1452,7 +1496,7 @@ var Flow = class {
|
|
|
1452
1496
|
this.enterChild(fallback);
|
|
1453
1497
|
return;
|
|
1454
1498
|
}
|
|
1455
|
-
this.emit({ type: "dry", group: group.id });
|
|
1499
|
+
if (this.host.tracing) this.emit({ type: "dry", group: group.id });
|
|
1456
1500
|
this.host.onDryChoice?.(group.id);
|
|
1457
1501
|
}
|
|
1458
1502
|
// -- Jumps (jump / call-return) ----------------------------------------
|
|
@@ -1467,7 +1511,7 @@ var Flow = class {
|
|
|
1467
1511
|
* hard-ends the flow regardless of the callstack.
|
|
1468
1512
|
*/
|
|
1469
1513
|
enterTarget(to, mode) {
|
|
1470
|
-
this.emit({ type: "jump", to, mode });
|
|
1514
|
+
if (this.host.tracing) this.emit({ type: "jump", to, mode });
|
|
1471
1515
|
if (to === "END") {
|
|
1472
1516
|
this.flowEnded = true;
|
|
1473
1517
|
this.stack = [];
|
|
@@ -1504,7 +1548,7 @@ var Flow = class {
|
|
|
1504
1548
|
const order = group.options?.order ?? "sequential";
|
|
1505
1549
|
const exhaust = group.options?.exhaust ?? "once";
|
|
1506
1550
|
const trace = (picked) => {
|
|
1507
|
-
this.emit({
|
|
1551
|
+
if (this.host.tracing) this.emit({
|
|
1508
1552
|
type: "select",
|
|
1509
1553
|
group: group.id,
|
|
1510
1554
|
selector: group.selector ?? "default",
|
|
@@ -1669,9 +1713,9 @@ var Flow = class {
|
|
|
1669
1713
|
let value;
|
|
1670
1714
|
try {
|
|
1671
1715
|
value = this.evalExpr(e.value);
|
|
1672
|
-
const prev = this.host.
|
|
1716
|
+
const prev = this.host.tracing ? this.getProperty(e.target) : void 0;
|
|
1673
1717
|
this.writeProperty(e.target, value, false);
|
|
1674
|
-
this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1718
|
+
if (this.host.tracing) this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1675
1719
|
} catch (err) {
|
|
1676
1720
|
this.reportError("effect", owner, e.value, err);
|
|
1677
1721
|
}
|
|
@@ -1692,7 +1736,7 @@ var Flow = class {
|
|
|
1692
1736
|
const message = err instanceof Error ? err.message : String(err);
|
|
1693
1737
|
const source = expr?.src;
|
|
1694
1738
|
this.host.onError({ flow: this.id, kind, node, ...source ? { source } : {}, message });
|
|
1695
|
-
this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
|
|
1739
|
+
if (this.host.tracing) this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
|
|
1696
1740
|
}
|
|
1697
1741
|
evalExpr(expr) {
|
|
1698
1742
|
return (0, import_expr.evaluate)(this.conditionAst(expr), this.context(), import_dialect.patterDialect);
|
|
@@ -1738,7 +1782,7 @@ var Flow = class {
|
|
|
1738
1782
|
};
|
|
1739
1783
|
// -- Strings / beats ------------------------------------------------------
|
|
1740
1784
|
beatResult(beat) {
|
|
1741
|
-
const tags = this.host
|
|
1785
|
+
const tags = tagsOf(this.host, beat.id);
|
|
1742
1786
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1743
1787
|
switch (beat.kind) {
|
|
1744
1788
|
case "gameEvent":
|
|
@@ -1805,7 +1849,7 @@ var Flow = class {
|
|
|
1805
1849
|
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
1806
1850
|
* choice showed. */
|
|
1807
1851
|
promptResult(beat, shown) {
|
|
1808
|
-
const tags = this.host
|
|
1852
|
+
const tags = tagsOf(this.host, beat.id);
|
|
1809
1853
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1810
1854
|
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1811
1855
|
return {
|
|
@@ -1979,6 +2023,21 @@ function mountClaims(registry, key, bag, ask) {
|
|
|
1979
2023
|
function toDecl(decl) {
|
|
1980
2024
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
|
|
1981
2025
|
}
|
|
2026
|
+
function boundScopes(options) {
|
|
2027
|
+
return { ...options.world ? { world: options.world } : {}, ...options.hostScopes };
|
|
2028
|
+
}
|
|
2029
|
+
function ladders(host, cacheKey, decls) {
|
|
2030
|
+
let found = host.qualityLadders.get(cacheKey);
|
|
2031
|
+
if (!found) {
|
|
2032
|
+
found = /* @__PURE__ */ new Map();
|
|
2033
|
+
for (const d of decls() ?? []) {
|
|
2034
|
+
const k = d.name.toLowerCase();
|
|
2035
|
+
if (d.type === "quality" && d.stages && !found.has(k)) found.set(k, d.stages);
|
|
2036
|
+
}
|
|
2037
|
+
host.qualityLadders.set(cacheKey, found);
|
|
2038
|
+
}
|
|
2039
|
+
return found;
|
|
2040
|
+
}
|
|
1982
2041
|
function toForeignDecl(decl) {
|
|
1983
2042
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
|
|
1984
2043
|
}
|