@patterkit/runtime 0.18.0 → 0.19.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 +46 -0
- package/dist/index.cjs +80 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -6
- package/dist/index.d.ts +43 -6
- package/dist/index.js +80 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -194,12 +194,34 @@ type TraceEvent = {
|
|
|
194
194
|
value: ScalarValue;
|
|
195
195
|
prev?: ScalarValue;
|
|
196
196
|
}
|
|
197
|
-
/**
|
|
197
|
+
/** Content the engine could not evaluate and played through (see {@link PlayError}): a condition
|
|
198
|
+
* counted as false, an effect skipped, or a Best-match part scored as false. */
|
|
198
199
|
| {
|
|
199
200
|
type: "diagnostic";
|
|
200
|
-
|
|
201
|
+
kind: PlayError["kind"];
|
|
202
|
+
node: string;
|
|
203
|
+
source?: string;
|
|
201
204
|
message: string;
|
|
202
205
|
};
|
|
206
|
+
/**
|
|
207
|
+
* A content error the engine played through. Content can fail at run time in ways the compiler cannot
|
|
208
|
+
* see: a division by zero, a host value of the wrong type, a story write to a read-only `@world` value.
|
|
209
|
+
* The story never stops for one. A condition that fails counts as false; an effect that fails is skipped
|
|
210
|
+
* and the rest of its list still runs; a part of a Best-match condition that fails scores as false. Each
|
|
211
|
+
* is reported, through `EngineOptions.onError` and as a `diagnostic` entry in the decision log. The same
|
|
212
|
+
* rule holds on all four runtimes, including the two whose languages cannot throw.
|
|
213
|
+
*/
|
|
214
|
+
interface PlayError {
|
|
215
|
+
/** The flow it happened in. */
|
|
216
|
+
flow: string;
|
|
217
|
+
/** What failed: a condition, an effect, or a part of a condition being scored for Best match. */
|
|
218
|
+
kind: "condition" | "effect" | "best-match";
|
|
219
|
+
/** The snippet, group, or option whose condition failed, or that owns the effect (a scene, for its onEntry). */
|
|
220
|
+
node: string;
|
|
221
|
+
/** The expression's source text, when the bundle carries it. */
|
|
222
|
+
source?: string;
|
|
223
|
+
message: string;
|
|
224
|
+
}
|
|
203
225
|
type TraceHandler = (event: TraceEvent) => void;
|
|
204
226
|
/** The engine-level tap: every flow's events, tagged with the flow id - one stream for tools. */
|
|
205
227
|
type EngineTraceHandler = (flow: string, event: TraceEvent) => void;
|
|
@@ -256,6 +278,10 @@ interface EngineOptions {
|
|
|
256
278
|
* unchanged; this only makes the fall-through observable. The coverage harness uses it to flag choices
|
|
257
279
|
* that ran dry. Leave it unset in shipped games (zero cost). */
|
|
258
280
|
onDryChoice?: (groupId: string) => void;
|
|
281
|
+
/** Called with each content error the engine played through (see {@link PlayError}): a condition
|
|
282
|
+
* that failed and counted as false, an effect that failed and was skipped. Unset, each is written
|
|
283
|
+
* to `console.warn`, so a content bug is never silent. */
|
|
284
|
+
onError?: (error: PlayError) => void;
|
|
259
285
|
/** Retain a trace of the engine's DECISIONS (what it chose and why), readable through
|
|
260
286
|
* `engine.log()` and `flow.log()`. Off by default: a shipped game pays nothing for a
|
|
261
287
|
* debugging surface it never reads. `onDryChoice` above is unaffected and stays useful
|
|
@@ -365,6 +391,8 @@ interface FlowHost {
|
|
|
365
391
|
* fallback - so it falls through and the flow continues past it. Zero cost when unset; the coverage
|
|
366
392
|
* harness passes it to surface silent fall-throughs. Not a gameplay signal (the behaviour is unchanged). */
|
|
367
393
|
onDryChoice?: (groupId: string) => void;
|
|
394
|
+
/** Where a content error the engine played through is reported (see PlayError). */
|
|
395
|
+
onError: (error: PlayError) => void;
|
|
368
396
|
/** Memoised `splitRef` results (ref string -> {scope,name}). The split depends only on the registry's
|
|
369
397
|
* scope set, so the cache is dropped whenever that moves (`refSplitRevision`). */
|
|
370
398
|
refSplitCache: Map<string, {
|
|
@@ -396,6 +424,8 @@ declare class Engine {
|
|
|
396
424
|
/** The run's ordered stream: every flow's events, each naming its flow. Empty and
|
|
397
425
|
* unwritten unless `options.log` asked for it. */
|
|
398
426
|
private readonly engineLog;
|
|
427
|
+
/** The next engine-log `seq`: its own counter, since the log's length restarts after a clear. */
|
|
428
|
+
private engineSeq;
|
|
399
429
|
private readonly engineTraceHandlers;
|
|
400
430
|
constructor(bundle: Bundle, options?: EngineOptions);
|
|
401
431
|
/** The active locale (string + character-name lookups resolve in it). */
|
|
@@ -501,9 +531,12 @@ declare class Engine {
|
|
|
501
531
|
* is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
|
|
502
532
|
*/
|
|
503
533
|
private resolveOpenAddress;
|
|
504
|
-
/** Resolve a scene reference
|
|
534
|
+
/** Resolve a scene reference to its internal id by the one address rule (see resolveScene); an
|
|
535
|
+
* unknown one passes through, for the caller to find nothing. */
|
|
505
536
|
private resolveSceneRef;
|
|
506
|
-
/** Resolve a block reference
|
|
537
|
+
/** Resolve a block reference to its internal id by the one address rule (see resolveBlock): within its
|
|
538
|
+
* scene, so a block of another scene resolves to nothing. With no scene, only an internal id resolves,
|
|
539
|
+
* since a block's gameId is only unique within its scene. */
|
|
507
540
|
private resolveBlockRef;
|
|
508
541
|
/** The host-facing address (gameId) of a scene / block by internal id, or undefined if unknown.
|
|
509
542
|
* The inverse of the resolve helpers - for a host that wants to display / log the address. */
|
|
@@ -870,8 +903,12 @@ declare class Flow {
|
|
|
870
903
|
private matchedSpec;
|
|
871
904
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
872
905
|
private selectorState;
|
|
906
|
+
/** Run an effect list. `owner` is the snippet or scene it belongs to, for an error report. */
|
|
873
907
|
private runEffects;
|
|
908
|
+
/** Whether a node's condition holds. A condition that fails to evaluate counts as false (see PlayError). */
|
|
874
909
|
private eligible;
|
|
910
|
+
/** Report a content error the engine is playing through: to the host's onError, and to the log. */
|
|
911
|
+
private reportError;
|
|
875
912
|
private evalExpr;
|
|
876
913
|
/** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
|
|
877
914
|
* evaluator and the Best-match specificity walker so both work off one parse. */
|
|
@@ -1095,11 +1132,11 @@ declare function gameDataValue(fields: GameDataField[], node: GameData | undefin
|
|
|
1095
1132
|
declare function effectiveGameData(fields: GameDataField[], node: GameData | undefined): GameData;
|
|
1096
1133
|
|
|
1097
1134
|
/**
|
|
1098
|
-
* Map every node id (scene / block / group / snippet / beat) to its accumulated
|
|
1135
|
+
* Map every node id (scene / block / group / snippet / beat, prompt beats included) to its accumulated
|
|
1099
1136
|
* tags. Node ids are globally unique within a project (the validator enforces
|
|
1100
1137
|
* it), so one flat map suffices. Nodes with no tags anywhere up the chain map to
|
|
1101
1138
|
* an empty array.
|
|
1102
1139
|
*/
|
|
1103
1140
|
declare function buildTagIndex(bundle: Bundle): Map<string, string[]>;
|
|
1104
1141
|
|
|
1105
|
-
export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
|
|
1142
|
+
export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PlayError, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -194,12 +194,34 @@ type TraceEvent = {
|
|
|
194
194
|
value: ScalarValue;
|
|
195
195
|
prev?: ScalarValue;
|
|
196
196
|
}
|
|
197
|
-
/**
|
|
197
|
+
/** Content the engine could not evaluate and played through (see {@link PlayError}): a condition
|
|
198
|
+
* counted as false, an effect skipped, or a Best-match part scored as false. */
|
|
198
199
|
| {
|
|
199
200
|
type: "diagnostic";
|
|
200
|
-
|
|
201
|
+
kind: PlayError["kind"];
|
|
202
|
+
node: string;
|
|
203
|
+
source?: string;
|
|
201
204
|
message: string;
|
|
202
205
|
};
|
|
206
|
+
/**
|
|
207
|
+
* A content error the engine played through. Content can fail at run time in ways the compiler cannot
|
|
208
|
+
* see: a division by zero, a host value of the wrong type, a story write to a read-only `@world` value.
|
|
209
|
+
* The story never stops for one. A condition that fails counts as false; an effect that fails is skipped
|
|
210
|
+
* and the rest of its list still runs; a part of a Best-match condition that fails scores as false. Each
|
|
211
|
+
* is reported, through `EngineOptions.onError` and as a `diagnostic` entry in the decision log. The same
|
|
212
|
+
* rule holds on all four runtimes, including the two whose languages cannot throw.
|
|
213
|
+
*/
|
|
214
|
+
interface PlayError {
|
|
215
|
+
/** The flow it happened in. */
|
|
216
|
+
flow: string;
|
|
217
|
+
/** What failed: a condition, an effect, or a part of a condition being scored for Best match. */
|
|
218
|
+
kind: "condition" | "effect" | "best-match";
|
|
219
|
+
/** The snippet, group, or option whose condition failed, or that owns the effect (a scene, for its onEntry). */
|
|
220
|
+
node: string;
|
|
221
|
+
/** The expression's source text, when the bundle carries it. */
|
|
222
|
+
source?: string;
|
|
223
|
+
message: string;
|
|
224
|
+
}
|
|
203
225
|
type TraceHandler = (event: TraceEvent) => void;
|
|
204
226
|
/** The engine-level tap: every flow's events, tagged with the flow id - one stream for tools. */
|
|
205
227
|
type EngineTraceHandler = (flow: string, event: TraceEvent) => void;
|
|
@@ -256,6 +278,10 @@ interface EngineOptions {
|
|
|
256
278
|
* unchanged; this only makes the fall-through observable. The coverage harness uses it to flag choices
|
|
257
279
|
* that ran dry. Leave it unset in shipped games (zero cost). */
|
|
258
280
|
onDryChoice?: (groupId: string) => void;
|
|
281
|
+
/** Called with each content error the engine played through (see {@link PlayError}): a condition
|
|
282
|
+
* that failed and counted as false, an effect that failed and was skipped. Unset, each is written
|
|
283
|
+
* to `console.warn`, so a content bug is never silent. */
|
|
284
|
+
onError?: (error: PlayError) => void;
|
|
259
285
|
/** Retain a trace of the engine's DECISIONS (what it chose and why), readable through
|
|
260
286
|
* `engine.log()` and `flow.log()`. Off by default: a shipped game pays nothing for a
|
|
261
287
|
* debugging surface it never reads. `onDryChoice` above is unaffected and stays useful
|
|
@@ -365,6 +391,8 @@ interface FlowHost {
|
|
|
365
391
|
* fallback - so it falls through and the flow continues past it. Zero cost when unset; the coverage
|
|
366
392
|
* harness passes it to surface silent fall-throughs. Not a gameplay signal (the behaviour is unchanged). */
|
|
367
393
|
onDryChoice?: (groupId: string) => void;
|
|
394
|
+
/** Where a content error the engine played through is reported (see PlayError). */
|
|
395
|
+
onError: (error: PlayError) => void;
|
|
368
396
|
/** Memoised `splitRef` results (ref string -> {scope,name}). The split depends only on the registry's
|
|
369
397
|
* scope set, so the cache is dropped whenever that moves (`refSplitRevision`). */
|
|
370
398
|
refSplitCache: Map<string, {
|
|
@@ -396,6 +424,8 @@ declare class Engine {
|
|
|
396
424
|
/** The run's ordered stream: every flow's events, each naming its flow. Empty and
|
|
397
425
|
* unwritten unless `options.log` asked for it. */
|
|
398
426
|
private readonly engineLog;
|
|
427
|
+
/** The next engine-log `seq`: its own counter, since the log's length restarts after a clear. */
|
|
428
|
+
private engineSeq;
|
|
399
429
|
private readonly engineTraceHandlers;
|
|
400
430
|
constructor(bundle: Bundle, options?: EngineOptions);
|
|
401
431
|
/** The active locale (string + character-name lookups resolve in it). */
|
|
@@ -501,9 +531,12 @@ declare class Engine {
|
|
|
501
531
|
* is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
|
|
502
532
|
*/
|
|
503
533
|
private resolveOpenAddress;
|
|
504
|
-
/** Resolve a scene reference
|
|
534
|
+
/** Resolve a scene reference to its internal id by the one address rule (see resolveScene); an
|
|
535
|
+
* unknown one passes through, for the caller to find nothing. */
|
|
505
536
|
private resolveSceneRef;
|
|
506
|
-
/** Resolve a block reference
|
|
537
|
+
/** Resolve a block reference to its internal id by the one address rule (see resolveBlock): within its
|
|
538
|
+
* scene, so a block of another scene resolves to nothing. With no scene, only an internal id resolves,
|
|
539
|
+
* since a block's gameId is only unique within its scene. */
|
|
507
540
|
private resolveBlockRef;
|
|
508
541
|
/** The host-facing address (gameId) of a scene / block by internal id, or undefined if unknown.
|
|
509
542
|
* The inverse of the resolve helpers - for a host that wants to display / log the address. */
|
|
@@ -870,8 +903,12 @@ declare class Flow {
|
|
|
870
903
|
private matchedSpec;
|
|
871
904
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
872
905
|
private selectorState;
|
|
906
|
+
/** Run an effect list. `owner` is the snippet or scene it belongs to, for an error report. */
|
|
873
907
|
private runEffects;
|
|
908
|
+
/** Whether a node's condition holds. A condition that fails to evaluate counts as false (see PlayError). */
|
|
874
909
|
private eligible;
|
|
910
|
+
/** Report a content error the engine is playing through: to the host's onError, and to the log. */
|
|
911
|
+
private reportError;
|
|
875
912
|
private evalExpr;
|
|
876
913
|
/** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
|
|
877
914
|
* evaluator and the Best-match specificity walker so both work off one parse. */
|
|
@@ -1095,11 +1132,11 @@ declare function gameDataValue(fields: GameDataField[], node: GameData | undefin
|
|
|
1095
1132
|
declare function effectiveGameData(fields: GameDataField[], node: GameData | undefined): GameData;
|
|
1096
1133
|
|
|
1097
1134
|
/**
|
|
1098
|
-
* Map every node id (scene / block / group / snippet / beat) to its accumulated
|
|
1135
|
+
* Map every node id (scene / block / group / snippet / beat, prompt beats included) to its accumulated
|
|
1099
1136
|
* tags. Node ids are globally unique within a project (the validator enforces
|
|
1100
1137
|
* it), so one flat map suffices. Nodes with no tags anywhere up the chain map to
|
|
1101
1138
|
* an empty array.
|
|
1102
1139
|
*/
|
|
1103
1140
|
declare function buildTagIndex(bundle: Bundle): Map<string, string[]>;
|
|
1104
1141
|
|
|
1105
|
-
export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
|
|
1142
|
+
export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PlayError, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
|
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 {
|
|
@@ -85,6 +86,8 @@ var Engine = class _Engine {
|
|
|
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;
|
|
@@ -182,6 +185,7 @@ var Engine = class _Engine {
|
|
|
182
185
|
journal: null,
|
|
183
186
|
customRng: options.rng,
|
|
184
187
|
onDryChoice: options.onDryChoice,
|
|
188
|
+
onError: options.onError ?? ((e) => console.warn(`Patterplay: ${e.kind} on ${e.node} in flow '${e.flow}' failed, played through: ${e.message}`)),
|
|
185
189
|
replayPromptOnChoose: options.replayPromptOnChoose ?? false,
|
|
186
190
|
captionsOn: options.closedCaptions ?? true,
|
|
187
191
|
// captions shown by default (full text)
|
|
@@ -375,10 +379,10 @@ var Engine = class _Engine {
|
|
|
375
379
|
*/
|
|
376
380
|
resolveOpenAddress(scene, block) {
|
|
377
381
|
if (scene !== void 0) {
|
|
378
|
-
const sceneId = this.
|
|
379
|
-
if (
|
|
382
|
+
const sceneId = resolveScene(this.host, scene);
|
|
383
|
+
if (sceneId === void 0) throw new Error(`unknown scene: ${scene}`);
|
|
380
384
|
if (block === void 0) return { sceneId };
|
|
381
|
-
const blockId =
|
|
385
|
+
const blockId = resolveBlock(this.host, sceneId, block);
|
|
382
386
|
if (blockId === void 0) throw new Error(`unknown block: ${block}`);
|
|
383
387
|
return { sceneId, blockId };
|
|
384
388
|
}
|
|
@@ -389,21 +393,19 @@ var Engine = class _Engine {
|
|
|
389
393
|
if (Object.keys(this.host.bundle.scenes).length === 0) throw new Error("no scenes in bundle");
|
|
390
394
|
return {};
|
|
391
395
|
}
|
|
392
|
-
/** Resolve a scene reference
|
|
396
|
+
/** Resolve a scene reference to its internal id by the one address rule (see resolveScene); an
|
|
397
|
+
* unknown one passes through, for the caller to find nothing. */
|
|
393
398
|
resolveSceneRef(ref) {
|
|
394
399
|
if (ref == null) return void 0;
|
|
395
|
-
|
|
396
|
-
return this.sceneGameIdToId.get(ref) ?? ref;
|
|
400
|
+
return resolveScene(this.host, ref) ?? ref;
|
|
397
401
|
}
|
|
398
|
-
/** Resolve a block reference
|
|
402
|
+
/** Resolve a block reference to its internal id by the one address rule (see resolveBlock): within its
|
|
403
|
+
* scene, so a block of another scene resolves to nothing. With no scene, only an internal id resolves,
|
|
404
|
+
* since a block's gameId is only unique within its scene. */
|
|
399
405
|
resolveBlockRef(sceneId, ref) {
|
|
400
406
|
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;
|
|
407
|
+
if (sceneId == null) return ref;
|
|
408
|
+
return resolveBlock(this.host, sceneId, ref);
|
|
407
409
|
}
|
|
408
410
|
/** The host-facing address (gameId) of a scene / block by internal id, or undefined if unknown.
|
|
409
411
|
* The inverse of the resolve helpers - for a host that wants to display / log the address. */
|
|
@@ -730,7 +732,7 @@ var Engine = class _Engine {
|
|
|
730
732
|
emitEngine(flow, event, scene) {
|
|
731
733
|
for (const h of this.engineTraceHandlers) h(flow, event);
|
|
732
734
|
if (!this.host.logEnabled) return;
|
|
733
|
-
this.engineLog.push({ ...event, flow, seq: this.
|
|
735
|
+
this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...scene ? { scene } : {} });
|
|
734
736
|
}
|
|
735
737
|
listProperties() {
|
|
736
738
|
return this.host.patterSharedDecls.map((d) => ({
|
|
@@ -796,6 +798,8 @@ var Engine = class _Engine {
|
|
|
796
798
|
this.refuseInCheckpoint("loadGame");
|
|
797
799
|
const version = save.version;
|
|
798
800
|
if (version !== 2 && version !== SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
801
|
+
const flows = save.flows;
|
|
802
|
+
if (typeof flows !== "object" || flows === null || Array.isArray(flows)) throw new Error("malformed save: no flows");
|
|
799
803
|
this.assertExternalScopes();
|
|
800
804
|
const reg = this.host.registry;
|
|
801
805
|
for (const [id, flow] of this.flowsById) {
|
|
@@ -1019,11 +1023,11 @@ var Flow = class {
|
|
|
1019
1023
|
this.stack = [];
|
|
1020
1024
|
return true;
|
|
1021
1025
|
}
|
|
1022
|
-
const sceneId =
|
|
1026
|
+
const sceneId = resolveScene(this.host, scene);
|
|
1023
1027
|
if (sceneId === void 0) return false;
|
|
1024
1028
|
let blockId;
|
|
1025
1029
|
if (block !== void 0) {
|
|
1026
|
-
blockId =
|
|
1030
|
+
blockId = resolveBlock(this.host, sceneId, block);
|
|
1027
1031
|
if (blockId === void 0) return false;
|
|
1028
1032
|
}
|
|
1029
1033
|
if (!this.started) {
|
|
@@ -1133,7 +1137,7 @@ var Flow = class {
|
|
|
1133
1137
|
if (this.flowEnded || this.pendingChoice) return;
|
|
1134
1138
|
if (this.activeSnippet) {
|
|
1135
1139
|
if (this.beatIndex < (this.activeSnippet.beats?.length ?? 0)) return;
|
|
1136
|
-
this.runEffects(this.activeSnippet.onExit);
|
|
1140
|
+
this.runEffects(this.activeSnippet.onExit, this.activeSnippet.id);
|
|
1137
1141
|
const jump = this.activeSnippet.jump;
|
|
1138
1142
|
this.activeSnippet = null;
|
|
1139
1143
|
this.beatIndex = 0;
|
|
@@ -1349,7 +1353,7 @@ var Flow = class {
|
|
|
1349
1353
|
this.currentSceneId = sceneId;
|
|
1350
1354
|
this.enter(sceneId);
|
|
1351
1355
|
this.seedScene(scene);
|
|
1352
|
-
this.runEffects(scene.onEntry);
|
|
1356
|
+
this.runEffects(scene.onEntry, scene.id);
|
|
1353
1357
|
}
|
|
1354
1358
|
/**
|
|
1355
1359
|
* Play one child of the active run. A snippet begins delivering. A group is
|
|
@@ -1385,7 +1389,7 @@ var Flow = class {
|
|
|
1385
1389
|
return void 0;
|
|
1386
1390
|
}
|
|
1387
1391
|
beginSnippet(snippet) {
|
|
1388
|
-
this.runEffects(snippet.onEnter);
|
|
1392
|
+
this.runEffects(snippet.onEnter, snippet.id);
|
|
1389
1393
|
this.activeSnippet = snippet;
|
|
1390
1394
|
this.beatIndex = 0;
|
|
1391
1395
|
}
|
|
@@ -1405,7 +1409,7 @@ var Flow = class {
|
|
|
1405
1409
|
options.push({ id: child.id, prompt: this.promptFor(child), eligible, gameData: child.gameData });
|
|
1406
1410
|
byId.set(child.id, child);
|
|
1407
1411
|
}
|
|
1408
|
-
if (options.
|
|
1412
|
+
if (options.some((o) => o.eligible)) {
|
|
1409
1413
|
this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1410
1414
|
this.pendingChoice = { groupId: group.id, options, byId };
|
|
1411
1415
|
return;
|
|
@@ -1463,7 +1467,7 @@ var Flow = class {
|
|
|
1463
1467
|
// -- Selectors ------------------------------------------------------------
|
|
1464
1468
|
selectChild(group) {
|
|
1465
1469
|
const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
|
|
1466
|
-
const eligible = group.children.filter((
|
|
1470
|
+
const eligible = group.children.filter((_, i) => verdicts[i].eligible);
|
|
1467
1471
|
const order = group.options?.order ?? "sequential";
|
|
1468
1472
|
const exhaust = group.options?.exhaust ?? "once";
|
|
1469
1473
|
const trace = (picked) => {
|
|
@@ -1510,8 +1514,9 @@ var Flow = class {
|
|
|
1510
1514
|
const len = eligible.length;
|
|
1511
1515
|
const stick = exhaust === "stick";
|
|
1512
1516
|
const fill = () => (stick ? eligible.slice(0, len - 1) : eligible).map((c) => c.id);
|
|
1517
|
+
const eligibleIds = new Set(eligible.map((c) => c.id));
|
|
1513
1518
|
if (st.bag === void 0) st.bag = fill();
|
|
1514
|
-
if (st.bag.
|
|
1519
|
+
if (!st.bag.some((id2) => eligibleIds.has(id2))) {
|
|
1515
1520
|
if (exhaust === "once") return null;
|
|
1516
1521
|
if (stick) {
|
|
1517
1522
|
const last = eligible[len - 1];
|
|
@@ -1519,13 +1524,14 @@ var Flow = class {
|
|
|
1519
1524
|
return last;
|
|
1520
1525
|
}
|
|
1521
1526
|
st.bag = fill();
|
|
1527
|
+
if (st.bag.length === 0) return null;
|
|
1522
1528
|
}
|
|
1523
|
-
const pool = st.bag;
|
|
1529
|
+
const pool = st.bag.filter((id2) => eligibleIds.has(id2));
|
|
1524
1530
|
const p = st.last !== void 0 && pool.length > 1 ? pool.indexOf(st.last) : -1;
|
|
1525
1531
|
let i = Math.floor(this.rng() * (p >= 0 ? pool.length - 1 : pool.length));
|
|
1526
1532
|
if (p >= 0 && i >= p) i++;
|
|
1527
1533
|
const id = pool[i];
|
|
1528
|
-
|
|
1534
|
+
st.bag.splice(st.bag.indexOf(id), 1);
|
|
1529
1535
|
st.last = id;
|
|
1530
1536
|
return eligible.find((c) => c.id === id);
|
|
1531
1537
|
}
|
|
@@ -1574,7 +1580,7 @@ var Flow = class {
|
|
|
1574
1580
|
/** A child's Best-match score against the current state: 0 when it has no condition (the filler
|
|
1575
1581
|
* tier), else the specificity of its (already-passing) condition. */
|
|
1576
1582
|
specScore(node) {
|
|
1577
|
-
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true) : 0;
|
|
1583
|
+
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true, node) : 0;
|
|
1578
1584
|
}
|
|
1579
1585
|
/**
|
|
1580
1586
|
* The **matched-specificity** metric (parity contract): how many atomic constraints are actively
|
|
@@ -1584,8 +1590,15 @@ var Flow = class {
|
|
|
1584
1590
|
* to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
|
|
1585
1591
|
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1586
1592
|
*/
|
|
1587
|
-
matchedSpec(node, want) {
|
|
1588
|
-
const evalTruthy = (n) =>
|
|
1593
|
+
matchedSpec(node, want, owner) {
|
|
1594
|
+
const evalTruthy = (n) => {
|
|
1595
|
+
try {
|
|
1596
|
+
return truthy(evaluate(n, this.context(), patterDialect));
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
this.reportError("best-match", owner.id, owner.condition, err);
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1589
1602
|
return scoreSpecificity(node, evalTruthy, { want });
|
|
1590
1603
|
}
|
|
1591
1604
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
@@ -1617,17 +1630,36 @@ var Flow = class {
|
|
|
1617
1630
|
return st;
|
|
1618
1631
|
}
|
|
1619
1632
|
// -- Effects + expressions ------------------------------------------------
|
|
1620
|
-
|
|
1633
|
+
/** Run an effect list. `owner` is the snippet or scene it belongs to, for an error report. */
|
|
1634
|
+
runEffects(effects, owner) {
|
|
1621
1635
|
for (const e of effects ?? []) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1636
|
+
let value;
|
|
1637
|
+
try {
|
|
1638
|
+
value = this.evalExpr(e.value);
|
|
1639
|
+
const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
|
|
1640
|
+
this.writeProperty(e.target, value, false);
|
|
1641
|
+
this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
this.reportError("effect", owner, e.value, err);
|
|
1644
|
+
}
|
|
1626
1645
|
}
|
|
1627
1646
|
}
|
|
1647
|
+
/** Whether a node's condition holds. A condition that fails to evaluate counts as false (see PlayError). */
|
|
1628
1648
|
eligible(node) {
|
|
1629
1649
|
if (!node.condition) return true;
|
|
1630
|
-
|
|
1650
|
+
try {
|
|
1651
|
+
return truthy(this.evalExpr(node.condition));
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
this.reportError("condition", node.id, node.condition, err);
|
|
1654
|
+
return false;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
/** Report a content error the engine is playing through: to the host's onError, and to the log. */
|
|
1658
|
+
reportError(kind, node, expr, err) {
|
|
1659
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1660
|
+
const source = expr?.src;
|
|
1661
|
+
this.host.onError({ flow: this.id, kind, node, ...source ? { source } : {}, message });
|
|
1662
|
+
this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
|
|
1631
1663
|
}
|
|
1632
1664
|
evalExpr(expr) {
|
|
1633
1665
|
return evaluate(this.conditionAst(expr), this.context(), patterDialect);
|
|
@@ -1823,23 +1855,23 @@ var Flow = class {
|
|
|
1823
1855
|
if (!this.sceneBags.has(s)) {
|
|
1824
1856
|
const bag = new PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1825
1857
|
const key = keys.flowScene(this.id, s);
|
|
1826
|
-
this.host.registry
|
|
1858
|
+
const claimed = mountClaims(this.host.registry, key, bag, journal !== null);
|
|
1827
1859
|
this.registered.add(key);
|
|
1828
1860
|
this.sceneBags.set(s, bag);
|
|
1829
1861
|
if (journal) journal.undo.push(() => {
|
|
1830
1862
|
if (this.sceneBags.get(s) !== bag) return;
|
|
1831
|
-
this.host.registry.remove(key);
|
|
1863
|
+
this.host.registry.remove(key, claimed ? { keep: true } : void 0);
|
|
1832
1864
|
this.registered.delete(key);
|
|
1833
1865
|
this.sceneBags.delete(s);
|
|
1834
1866
|
});
|
|
1835
1867
|
}
|
|
1836
1868
|
if (!this.host.stageBags.has(s)) {
|
|
1837
1869
|
const bag = new PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1838
|
-
this.host.registry
|
|
1870
|
+
const claimed = mountClaims(this.host.registry, keys.stage(s), bag, journal !== null);
|
|
1839
1871
|
this.host.stageBags.set(s, bag);
|
|
1840
1872
|
if (journal) journal.undo.push(() => {
|
|
1841
1873
|
if (this.host.stageBags.get(s) !== bag) return;
|
|
1842
|
-
this.host.registry.remove(keys.stage(s));
|
|
1874
|
+
this.host.registry.remove(keys.stage(s), claimed ? { keep: true } : void 0);
|
|
1843
1875
|
this.host.stageBags.delete(s);
|
|
1844
1876
|
});
|
|
1845
1877
|
}
|
|
@@ -1906,6 +1938,11 @@ function deserialiseSelectors(rec) {
|
|
|
1906
1938
|
}
|
|
1907
1939
|
return map;
|
|
1908
1940
|
}
|
|
1941
|
+
function mountClaims(registry, key, bag, ask) {
|
|
1942
|
+
const before = ask ? JSON.stringify(bag.save()) : "";
|
|
1943
|
+
registry.mountOwned(key, bag, { owner: OWNER });
|
|
1944
|
+
return ask && JSON.stringify(bag.save()) !== before;
|
|
1945
|
+
}
|
|
1909
1946
|
function toDecl(decl) {
|
|
1910
1947
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
|
|
1911
1948
|
}
|
|
@@ -1917,6 +1954,12 @@ function selfBackedDecl(decl, scopeWritable) {
|
|
|
1917
1954
|
const writable = decl.writable ?? scopeWritable;
|
|
1918
1955
|
return writable === void 0 ? d : { ...d, writable };
|
|
1919
1956
|
}
|
|
1957
|
+
function resolveScene(host, ref) {
|
|
1958
|
+
return host.sceneGameIdToId.get(ref) ?? (host.bundle.scenes[ref] ? ref : void 0);
|
|
1959
|
+
}
|
|
1960
|
+
function resolveBlock(host, sceneId, ref) {
|
|
1961
|
+
return host.blockGameIdToId.get(sceneId)?.get(ref) ?? (host.blockIndex.get(ref)?.sceneId === sceneId ? ref : void 0);
|
|
1962
|
+
}
|
|
1920
1963
|
function splitHostRef(host, ref) {
|
|
1921
1964
|
if (host.refSplitRevision !== host.registry.revision) {
|
|
1922
1965
|
host.refSplitCache.clear();
|