@patterkit/runtime 0.8.0 → 0.10.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 +70 -0
- package/dist/index.cjs +148 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -14
- package/dist/index.d.ts +145 -14
- package/dist/index.js +148 -83
- package/dist/index.js.map +1 -1
- package/dist/patterplay.min.js +2 -2
- package/dist/patterplay.min.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,76 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.10.0] - 2026-09-02
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`Engine.listBags()` and `Flow.listBags()`.** The kernel bags with the path each answers
|
|
41
|
+
to in a log - the shared `@patter` globals and per-scene props on the engine, a flow's own
|
|
42
|
+
halves prefixed with its id. Parity with the Storylet Engine's method of the same name; it
|
|
43
|
+
is what a state logger mounts. A stage bag's LOG path is `@scene:<sceneId>.` where its
|
|
44
|
+
address is `@scene.`, because a log spans scenes and has to say which one.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **The state logger watches the property bags instead of diffing save snapshots.** A
|
|
49
|
+
property write is logged when it LANDS, on the bag's audit hook, rather than at the next
|
|
50
|
+
capture. The visit counts live in no bag, so those are still diffed - which is all this
|
|
51
|
+
logger used to do for everything. What it buys: a diff can only report the NET change
|
|
52
|
+
between two captures, so a value that changed and changed back was invisible, and every
|
|
53
|
+
write was reported late. The core is shared with the Storylet Engine, which has always
|
|
54
|
+
worked this way.
|
|
55
|
+
- **Requires `@wildwinter/scoperegistry` ^0.5.0**, which carries the shared state logger.
|
|
56
|
+
|
|
57
|
+
## [0.9.0] - 2026-09-02
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- **BREAKING: a property row's address is `path`, not `ref`, and the row carries `name` and
|
|
62
|
+
`writable`.** `listProperties()` returns the row type from `@wildwinter/scoperegistry` itself,
|
|
63
|
+
rather than a local interface redeclaring it. `path` holds exactly what `ref`
|
|
64
|
+
held: the reference `getProperty` / `setProperty` take. `name` is the bare declared name.
|
|
65
|
+
`writable` is always true here, because Patter has no read-only shared property; it is carried
|
|
66
|
+
because the row is shared with the Storylet Engine, which does declare them.
|
|
67
|
+
|
|
68
|
+
The fork it replaces was not free. A quality's `stages` was added to the local copy and so never
|
|
69
|
+
reached the shared type, and the Storylet Engine - reading the same property model through the
|
|
70
|
+
same registry - could not carry a ladder at all: it edited a quality as free text on every
|
|
71
|
+
platform it ships. One row, one place to add the next field.
|
|
72
|
+
- **BREAKING: a row's address is the qualified one, `@patter.gold`, not `@gold`.** Both forms
|
|
73
|
+
have always resolved on input and still do - an unqualified name defaults to the `patter`
|
|
74
|
+
scope - so `getProperty("@gold")` is unaffected. What changed is the address a row REPORTS,
|
|
75
|
+
which is what a state panel displays and what an inspector writes back through. It matches
|
|
76
|
+
what `@scene` and the other family's scopes have always looked like.
|
|
77
|
+
|
|
78
|
+
- **Scene and stage state is held in the shared property bag.** `@scene` properties lived in
|
|
79
|
+
hand-rolled maps that duplicated the bag's own seeding, so they missed its guards: two flows
|
|
80
|
+
entering one scene now never share a mutable flags list, and a `temporary` property's reset on
|
|
81
|
+
re-entry goes through the bag, which means a state logger sees it. **The save format is
|
|
82
|
+
unchanged** - a flat name/value map per scene, and a load that seeds from the bundle's
|
|
83
|
+
declarations before laying saved values over, so a property a save predates keeps its default.
|
|
84
|
+
- **BREAKING: `PropertyView` is gone; `listProperties()` returns `PropertyRow`.** It was the
|
|
85
|
+
shared row plus a `path`, and `path` is on the shared row now, so the name was a second name
|
|
86
|
+
for one type. `PropertyRow` is re-exported from `@patterkit/runtime`, so naming a row needs no
|
|
87
|
+
dependency on `@wildwinter/scoperegistry`.
|
|
88
|
+
- **Requires `@wildwinter/scoperegistry` ^0.4.0**, which is where the row's `path` lives.
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- **A decision log, and `onDryChoice`.** Opening a run with `log: true` records what the engine
|
|
93
|
+
decided and why - each choice with the options it offered, the ones it greyed out and the
|
|
94
|
+
reason, each jump, each property write with the value it replaced. `Engine.log()` is the whole
|
|
95
|
+
run in order, a `Flow`'s own log is flow-local, and `onTrace` streams entries live rather than
|
|
96
|
+
retaining them. `onDryChoice` fires when a choice runs dry - no takeable option, no eligible
|
|
97
|
+
fallback - so the silent fall-through is observable; it survives alongside the log because it is
|
|
98
|
+
live feedback, not a record.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- **A quality row carries its ladder.** `stages` was on the row so an examiner could offer the
|
|
103
|
+
stages instead of a free-text box, and the code that builds rows never filled it in - on this
|
|
104
|
+
runtime and two others. Every quality row came out without one.
|
|
105
|
+
|
|
36
106
|
## [0.8.0] - 2026-09-01
|
|
37
107
|
|
|
38
108
|
### Changed
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
34
34
|
var import_expr = require("@wildwinter/expr");
|
|
35
35
|
var import_expr_specificity = require("@wildwinter/expr-specificity");
|
|
36
36
|
var import_scoperegistry = require("@wildwinter/scoperegistry");
|
|
37
|
+
var import_scoperegistry2 = require("@wildwinter/scoperegistry");
|
|
37
38
|
var import_dialect = require("@patterkit/dialect");
|
|
38
39
|
var import_model = require("@patterkit/model");
|
|
39
40
|
|
|
@@ -92,6 +93,10 @@ var Engine = class _Engine {
|
|
|
92
93
|
/** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
|
|
93
94
|
* engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
|
|
94
95
|
creationOptions;
|
|
96
|
+
/** The run's ordered stream: every flow's events, each naming its flow. Empty and
|
|
97
|
+
* unwritten unless `options.log` asked for it. */
|
|
98
|
+
engineLog = [];
|
|
99
|
+
engineTraceHandlers = /* @__PURE__ */ new Set();
|
|
95
100
|
constructor(bundle, options = {}) {
|
|
96
101
|
this.creationOptions = options;
|
|
97
102
|
const locale = options.locale ?? bundle.locales.default;
|
|
@@ -127,7 +132,7 @@ var Engine = class _Engine {
|
|
|
127
132
|
const patterSharedDecls = props.filter((p) => p.shared ?? true).map(toDecl);
|
|
128
133
|
const patterLocalDecls = props.filter((p) => !(p.shared ?? true)).map(toDecl);
|
|
129
134
|
const patterSharedNames = new Set(patterSharedDecls.map((d) => d.name.toLowerCase()));
|
|
130
|
-
const shared = new import_scoperegistry.ScopeRegistry().defineOwned("patter", patterSharedDecls);
|
|
135
|
+
const shared = new import_scoperegistry.ScopeRegistry().defineOwned("patter", patterSharedDecls, "@patter.");
|
|
131
136
|
const hostBound = /* @__PURE__ */ new Set();
|
|
132
137
|
if (options.world) {
|
|
133
138
|
const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
|
|
@@ -146,6 +151,8 @@ var Engine = class _Engine {
|
|
|
146
151
|
sceneSharedNames.set(sceneId, names);
|
|
147
152
|
}
|
|
148
153
|
this.host = {
|
|
154
|
+
logEnabled: options.log ?? false,
|
|
155
|
+
emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
|
|
149
156
|
bundle,
|
|
150
157
|
emitIds,
|
|
151
158
|
strings,
|
|
@@ -488,6 +495,22 @@ var Engine = class _Engine {
|
|
|
488
495
|
flows() {
|
|
489
496
|
return [...this.flowsById.values()];
|
|
490
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* The SHARED kernel bags with the path each answers to in a log: the `@patter` globals,
|
|
500
|
+
* and one per scene for the shared `@scene` props. Parity with the Storylet Engine's
|
|
501
|
+
* listBags of the same name - it is what a state logger mounts.
|
|
502
|
+
*
|
|
503
|
+
* A stage bag's log path is `@scene:<sceneId>.`, not the bag's own `@scene.`: a property
|
|
504
|
+
* is ADDRESSED relative to a flow's current scene, but a log spans scenes and has to say
|
|
505
|
+
* which one. That is why a mount may override the bag's prefix.
|
|
506
|
+
*
|
|
507
|
+
* loadGame() replaces every bag, so re-enumerate after a load.
|
|
508
|
+
*/
|
|
509
|
+
listBags() {
|
|
510
|
+
const mounts = [{ bag: this.host.shared.ownedBag("patter") }];
|
|
511
|
+
for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
|
|
512
|
+
return mounts;
|
|
513
|
+
}
|
|
491
514
|
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
492
515
|
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
493
516
|
closeFlow(id) {
|
|
@@ -520,14 +543,44 @@ var Engine = class _Engine {
|
|
|
520
543
|
}
|
|
521
544
|
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
522
545
|
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
546
|
+
/** The run's decisions, in order, each naming the flow it happened in. Empty unless the
|
|
547
|
+
* run was opened with `log: true`. A flow's own log stays flow-local; this is the only
|
|
548
|
+
* place a story spanning several flows reads as one sequence. */
|
|
549
|
+
log() {
|
|
550
|
+
return this.engineLog;
|
|
551
|
+
}
|
|
552
|
+
/** Drop the retained entries. `seq` does NOT restart: two reads of a log either side of a
|
|
553
|
+
* clear still agree about what came first. */
|
|
554
|
+
clearLog() {
|
|
555
|
+
this.engineLog.length = 0;
|
|
556
|
+
}
|
|
557
|
+
/** Live tap on the run's decisions, for tooling that wants them as they happen rather than
|
|
558
|
+
* retained. Returns its own unsubscribe. */
|
|
559
|
+
onTrace(handler) {
|
|
560
|
+
this.engineTraceHandlers.add(handler);
|
|
561
|
+
return () => this.engineTraceHandlers.delete(handler);
|
|
562
|
+
}
|
|
563
|
+
emitEngine(flow, event, scene) {
|
|
564
|
+
for (const h of this.engineTraceHandlers) h(flow, event);
|
|
565
|
+
if (!this.host.logEnabled) return;
|
|
566
|
+
this.engineLog.push({ ...event, flow, seq: this.engineLog.length, ...scene ? { scene } : {} });
|
|
567
|
+
}
|
|
523
568
|
listProperties() {
|
|
524
569
|
return this.host.patterSharedDecls.map((d) => ({
|
|
525
|
-
|
|
570
|
+
name: d.name,
|
|
571
|
+
// The qualified address, matching what the bag composes for every other scope.
|
|
572
|
+
// `@gold` still resolves on input; it is the shorthand, not the address.
|
|
573
|
+
path: `@patter.${d.name}`,
|
|
526
574
|
type: d.type,
|
|
527
575
|
values: d.values,
|
|
528
576
|
stages: d.stages,
|
|
529
577
|
value: this.getProperty(`@${d.name}`),
|
|
530
|
-
default:
|
|
578
|
+
default: (0, import_scoperegistry2.defaultFor)(d),
|
|
579
|
+
// Part of the shared row. Always true here today: `toDecl` never sets it, because
|
|
580
|
+
// Patter has no read-only shared property. The Storylet Engine does declare them
|
|
581
|
+
// and its panels disable the editor accordingly, so the field is carried rather
|
|
582
|
+
// than dropped - and the day a read-only @patter property exists, the row says so.
|
|
583
|
+
writable: d.writable ?? true
|
|
531
584
|
}));
|
|
532
585
|
}
|
|
533
586
|
// @scene is scene-namespaced and needs a flow's current scene - silently
|
|
@@ -560,7 +613,7 @@ var Engine = class _Engine {
|
|
|
560
613
|
shared: this.host.shared.save(),
|
|
561
614
|
sharedVisits: Object.fromEntries(this.host.sharedVisits),
|
|
562
615
|
sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
|
|
563
|
-
stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s,
|
|
616
|
+
stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
|
|
564
617
|
flows
|
|
565
618
|
};
|
|
566
619
|
}
|
|
@@ -573,7 +626,13 @@ var Engine = class _Engine {
|
|
|
573
626
|
this.host.sharedSelectors.clear();
|
|
574
627
|
for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
|
|
575
628
|
this.host.stageBags.clear();
|
|
576
|
-
for (const [s,
|
|
629
|
+
for (const [s, values] of Object.entries(save.stageBags ?? {})) {
|
|
630
|
+
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
631
|
+
const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => shared.has(d.name.toLowerCase()));
|
|
632
|
+
const bag = new import_scoperegistry2.PropertyBag(decls);
|
|
633
|
+
bag.load(values);
|
|
634
|
+
this.host.stageBags.set(s, bag);
|
|
635
|
+
}
|
|
577
636
|
this.flowsById.clear();
|
|
578
637
|
for (const [id, snap] of Object.entries(save.flows)) {
|
|
579
638
|
const flow = new Flow(id, this.host, this.defaultSeed);
|
|
@@ -616,6 +675,7 @@ var Flow = class {
|
|
|
616
675
|
// The SHARED halves live on the host (`host.shared` / `host.stageBags`). Each
|
|
617
676
|
// resolver presents one merged scope, routing each property to its half by the
|
|
618
677
|
// declared `shared` flag.
|
|
678
|
+
/** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
|
|
619
679
|
sceneBags = /* @__PURE__ */ new Map();
|
|
620
680
|
patterResolver = {
|
|
621
681
|
get: (n) => this.host.patterSharedNames.has(n) ? this.host.shared.get("patter", n) : this.local.get("patter", n),
|
|
@@ -629,13 +689,13 @@ var Flow = class {
|
|
|
629
689
|
const s = this.currentSceneId;
|
|
630
690
|
if (s === null) return void 0;
|
|
631
691
|
const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
|
|
632
|
-
return bag?.
|
|
692
|
+
return bag?.get(n);
|
|
633
693
|
},
|
|
634
694
|
set: (n, v) => {
|
|
635
695
|
const s = this.currentSceneId;
|
|
636
696
|
if (s === null) return;
|
|
637
697
|
const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
|
|
638
|
-
if (bag) bag
|
|
698
|
+
if (bag) bag.set(n, v);
|
|
639
699
|
}
|
|
640
700
|
};
|
|
641
701
|
// The eval context is built ONCE: every constituent resolves live state at
|
|
@@ -644,6 +704,9 @@ var Flow = class {
|
|
|
644
704
|
// `local`/`sceneBags`/`currentSceneId`; the host callbacks read current flow
|
|
645
705
|
// fields). Rebuilding it per evaluation was the engine's hottest allocation.
|
|
646
706
|
evalCtx;
|
|
707
|
+
flowLog = [];
|
|
708
|
+
/** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
|
|
709
|
+
flowSeq = 0;
|
|
647
710
|
constructor(id, host, seed) {
|
|
648
711
|
this.id = id;
|
|
649
712
|
this.host = host;
|
|
@@ -869,7 +932,17 @@ var Flow = class {
|
|
|
869
932
|
this.stack.pop();
|
|
870
933
|
continue;
|
|
871
934
|
}
|
|
935
|
+
const from = frame.index;
|
|
872
936
|
while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
|
|
937
|
+
if (this.host.logEnabled && frame.index !== from) {
|
|
938
|
+
this.emit({
|
|
939
|
+
type: "select",
|
|
940
|
+
group: frame.containerId,
|
|
941
|
+
selector: "run",
|
|
942
|
+
children: children.slice(from, frame.index + 1).map((c, i) => ({ id: c.id, eligible: from + i === frame.index })),
|
|
943
|
+
picked: children[frame.index]?.id ?? null
|
|
944
|
+
});
|
|
945
|
+
}
|
|
873
946
|
if (frame.index >= children.length) {
|
|
874
947
|
this.stack.pop();
|
|
875
948
|
continue;
|
|
@@ -877,6 +950,34 @@ var Flow = class {
|
|
|
877
950
|
this.enterChild(children[frame.index++]);
|
|
878
951
|
}
|
|
879
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* THIS flow's own kernel bags: its not-shared `@patter` half and its per-scene `@scene`
|
|
955
|
+
* props, each prefixed with the flow id so one path space holds every flow. The shared
|
|
956
|
+
* halves are the Engine's listBags.
|
|
957
|
+
*/
|
|
958
|
+
listBags() {
|
|
959
|
+
const mounts = [{ bag: this.local.ownedBag("patter"), pathPrefix: `${this.id}/@patter.` }];
|
|
960
|
+
for (const [sceneId, bag] of this.sceneBags) mounts.push({ bag, pathPrefix: `${this.id}/@scene:${sceneId}.` });
|
|
961
|
+
return mounts;
|
|
962
|
+
}
|
|
963
|
+
/** This flow's decisions, in order. Empty unless the run was opened with `log: true`.
|
|
964
|
+
* The engine's log carries the same events tagged with the flow; this one is what a
|
|
965
|
+
* single conversation reads as. */
|
|
966
|
+
log() {
|
|
967
|
+
return this.flowLog;
|
|
968
|
+
}
|
|
969
|
+
/** Drop the retained entries. `seq` keeps counting, so order survives a clear. */
|
|
970
|
+
clearLog() {
|
|
971
|
+
this.flowLog.length = 0;
|
|
972
|
+
}
|
|
973
|
+
/** Record one decision, on this flow's log and the engine's. Cheap to call with logging
|
|
974
|
+
* off: the entry is never built. */
|
|
975
|
+
emit(event) {
|
|
976
|
+
const scene = this.currentSceneId ?? void 0;
|
|
977
|
+
this.host.emitEngine(this.id, event, scene);
|
|
978
|
+
if (!this.host.logEnabled) return;
|
|
979
|
+
this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
|
|
980
|
+
}
|
|
880
981
|
/** The options of a pending choice (empty when not at a choice point). */
|
|
881
982
|
getChoices() {
|
|
882
983
|
return this.pendingChoice?.options ?? [];
|
|
@@ -889,6 +990,7 @@ var Flow = class {
|
|
|
889
990
|
if (!option) throw new Error(`unknown choice option: ${id}`);
|
|
890
991
|
if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
|
|
891
992
|
const node = choice.byId.get(id);
|
|
993
|
+
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
892
994
|
this.pendingChoice = null;
|
|
893
995
|
this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
|
|
894
996
|
this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
|
|
@@ -922,7 +1024,7 @@ var Flow = class {
|
|
|
922
1024
|
return {
|
|
923
1025
|
scopes: this.local.save(),
|
|
924
1026
|
// owned scope "patter" = the NOT-shared globals (@scene saved separately)
|
|
925
|
-
sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s,
|
|
1027
|
+
sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
|
|
926
1028
|
rngState: this.rngState,
|
|
927
1029
|
visits: Object.fromEntries(this.visitCounts),
|
|
928
1030
|
cursor: {
|
|
@@ -960,7 +1062,13 @@ var Flow = class {
|
|
|
960
1062
|
}
|
|
961
1063
|
return { ...frame };
|
|
962
1064
|
});
|
|
963
|
-
this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s,
|
|
1065
|
+
this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, values]) => {
|
|
1066
|
+
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
1067
|
+
const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => !shared.has(d.name.toLowerCase()));
|
|
1068
|
+
const bag = new import_scoperegistry2.PropertyBag(decls);
|
|
1069
|
+
bag.load(values);
|
|
1070
|
+
return [s, bag];
|
|
1071
|
+
}));
|
|
964
1072
|
this.local = this.freshLocal();
|
|
965
1073
|
this.local.load(snap.scopes);
|
|
966
1074
|
this.activeSnippet = null;
|
|
@@ -1054,6 +1162,7 @@ var Flow = class {
|
|
|
1054
1162
|
byId.set(child.id, child);
|
|
1055
1163
|
}
|
|
1056
1164
|
if (options.length > 0) {
|
|
1165
|
+
this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
|
|
1057
1166
|
this.pendingChoice = { groupId: group.id, options, byId };
|
|
1058
1167
|
return;
|
|
1059
1168
|
}
|
|
@@ -1062,6 +1171,7 @@ var Flow = class {
|
|
|
1062
1171
|
this.enterChild(fallback);
|
|
1063
1172
|
return;
|
|
1064
1173
|
}
|
|
1174
|
+
this.emit({ type: "dry", group: group.id });
|
|
1065
1175
|
this.host.onDryChoice?.(group.id);
|
|
1066
1176
|
}
|
|
1067
1177
|
// -- Jumps (jump / call-return) ----------------------------------------
|
|
@@ -1076,6 +1186,7 @@ var Flow = class {
|
|
|
1076
1186
|
* hard-ends the flow regardless of the callstack.
|
|
1077
1187
|
*/
|
|
1078
1188
|
enterTarget(to, mode) {
|
|
1189
|
+
this.emit({ type: "jump", to, mode });
|
|
1079
1190
|
if (to === "END") {
|
|
1080
1191
|
this.flowEnded = true;
|
|
1081
1192
|
this.stack = [];
|
|
@@ -1107,17 +1218,28 @@ var Flow = class {
|
|
|
1107
1218
|
}
|
|
1108
1219
|
// -- Selectors ------------------------------------------------------------
|
|
1109
1220
|
selectChild(group) {
|
|
1221
|
+
const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
|
|
1110
1222
|
const eligible = group.children.filter((c) => this.eligible(c));
|
|
1111
|
-
|
|
1223
|
+
const order = group.options?.order ?? "sequential";
|
|
1224
|
+
const exhaust = group.options?.exhaust ?? "once";
|
|
1225
|
+
const trace = (picked) => {
|
|
1226
|
+
this.emit({
|
|
1227
|
+
type: "select",
|
|
1228
|
+
group: group.id,
|
|
1229
|
+
selector: group.selector ?? "default",
|
|
1230
|
+
...group.selector === "sequence" ? { order, exhaust } : {},
|
|
1231
|
+
children: verdicts,
|
|
1232
|
+
picked: picked?.id ?? null
|
|
1233
|
+
});
|
|
1234
|
+
return picked;
|
|
1235
|
+
};
|
|
1236
|
+
if (eligible.length === 0) return trace(null);
|
|
1112
1237
|
const st = this.selectorState(group);
|
|
1113
1238
|
switch (group.selector) {
|
|
1114
1239
|
case "branch":
|
|
1115
|
-
return eligible[0];
|
|
1116
|
-
case "sequence":
|
|
1117
|
-
|
|
1118
|
-
const exhaust = group.options?.exhaust ?? "once";
|
|
1119
|
-
return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
|
|
1120
|
-
}
|
|
1240
|
+
return trace(eligible[0]);
|
|
1241
|
+
case "sequence":
|
|
1242
|
+
return trace(order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st));
|
|
1121
1243
|
case "run":
|
|
1122
1244
|
case "choice":
|
|
1123
1245
|
default:
|
|
@@ -1235,7 +1357,10 @@ var Flow = class {
|
|
|
1235
1357
|
// -- Effects + expressions ------------------------------------------------
|
|
1236
1358
|
runEffects(effects) {
|
|
1237
1359
|
for (const e of effects ?? []) {
|
|
1238
|
-
|
|
1360
|
+
const value = this.evalExpr(e.value);
|
|
1361
|
+
const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
|
|
1362
|
+
this.setProperty(e.target, value);
|
|
1363
|
+
this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1239
1364
|
}
|
|
1240
1365
|
}
|
|
1241
1366
|
eligible(node) {
|
|
@@ -1379,7 +1504,7 @@ var Flow = class {
|
|
|
1379
1504
|
}
|
|
1380
1505
|
/** The per-flow registry: the NOT-shared `@patter` globals (the shared ones live on the host). */
|
|
1381
1506
|
freshLocal() {
|
|
1382
|
-
return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls);
|
|
1507
|
+
return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls, "@patter.");
|
|
1383
1508
|
}
|
|
1384
1509
|
/**
|
|
1385
1510
|
* Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
|
|
@@ -1390,27 +1515,18 @@ var Flow = class {
|
|
|
1390
1515
|
*/
|
|
1391
1516
|
seedScene(scene) {
|
|
1392
1517
|
const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
|
|
1518
|
+
const props = scene.sceneProps ?? [];
|
|
1393
1519
|
if (!this.sceneBags.has(scene.id)) {
|
|
1394
|
-
|
|
1395
|
-
for (const decl of scene.sceneProps ?? []) {
|
|
1396
|
-
const name = decl.name.toLowerCase();
|
|
1397
|
-
if (!shared.has(name)) bag[name] = sceneDefault(decl);
|
|
1398
|
-
}
|
|
1399
|
-
this.sceneBags.set(scene.id, bag);
|
|
1520
|
+
this.sceneBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
|
|
1400
1521
|
}
|
|
1401
1522
|
if (!this.host.stageBags.has(scene.id)) {
|
|
1402
|
-
|
|
1403
|
-
for (const decl of scene.sceneProps ?? []) {
|
|
1404
|
-
const name = decl.name.toLowerCase();
|
|
1405
|
-
if (shared.has(name)) bag[name] = sceneDefault(decl);
|
|
1406
|
-
}
|
|
1407
|
-
this.host.stageBags.set(scene.id, bag);
|
|
1523
|
+
this.host.stageBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
|
|
1408
1524
|
}
|
|
1409
1525
|
for (const decl of scene.sceneProps ?? []) {
|
|
1410
1526
|
if (!decl.temporary) continue;
|
|
1411
1527
|
const name = decl.name.toLowerCase();
|
|
1412
1528
|
const bag = shared.has(name) ? this.host.stageBags.get(scene.id) : this.sceneBags.get(scene.id);
|
|
1413
|
-
if (bag) bag
|
|
1529
|
+
if (bag) bag.set(name, (0, import_scoperegistry2.defaultFor)(decl));
|
|
1414
1530
|
}
|
|
1415
1531
|
}
|
|
1416
1532
|
};
|
|
@@ -1448,47 +1564,13 @@ function deserialiseSelectors(rec) {
|
|
|
1448
1564
|
function toDecl(decl) {
|
|
1449
1565
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
|
|
1450
1566
|
}
|
|
1451
|
-
function declDefault(d) {
|
|
1452
|
-
if (d.default !== void 0) return d.default;
|
|
1453
|
-
switch (d.type) {
|
|
1454
|
-
case "number":
|
|
1455
|
-
return 0;
|
|
1456
|
-
case "string":
|
|
1457
|
-
return "";
|
|
1458
|
-
case "flags":
|
|
1459
|
-
return [];
|
|
1460
|
-
case "enum":
|
|
1461
|
-
return d.values?.[0] ?? "";
|
|
1462
|
-
case "quality":
|
|
1463
|
-
return d.stages?.[0] ?? "";
|
|
1464
|
-
default:
|
|
1465
|
-
return false;
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
1567
|
function toForeignDecl(decl) {
|
|
1469
1568
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
|
|
1470
1569
|
}
|
|
1471
|
-
function hostScopeDefault(decl) {
|
|
1472
|
-
if (decl.default !== void 0) return decl.default;
|
|
1473
|
-
switch (decl.type) {
|
|
1474
|
-
case "boolean":
|
|
1475
|
-
return false;
|
|
1476
|
-
case "number":
|
|
1477
|
-
return 0;
|
|
1478
|
-
case "string":
|
|
1479
|
-
return "";
|
|
1480
|
-
case "flags":
|
|
1481
|
-
return [];
|
|
1482
|
-
case "enum":
|
|
1483
|
-
return decl.values?.[0] ?? "";
|
|
1484
|
-
case "quality":
|
|
1485
|
-
return decl.stages?.[0] ?? "";
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
1570
|
function selfBackedResolver(decls) {
|
|
1489
1571
|
const key = (name) => name.toLowerCase();
|
|
1490
1572
|
const bag = /* @__PURE__ */ new Map();
|
|
1491
|
-
for (const d of decls) bag.set(key(d.name),
|
|
1573
|
+
for (const d of decls) bag.set(key(d.name), (0, import_scoperegistry2.defaultFor)(d));
|
|
1492
1574
|
return {
|
|
1493
1575
|
get: (name) => bag.get(key(name)),
|
|
1494
1576
|
set: (name, value) => {
|
|
@@ -1496,23 +1578,6 @@ function selfBackedResolver(decls) {
|
|
|
1496
1578
|
}
|
|
1497
1579
|
};
|
|
1498
1580
|
}
|
|
1499
|
-
function sceneDefault(decl) {
|
|
1500
|
-
if (decl.default !== void 0) return decl.default;
|
|
1501
|
-
switch (decl.type) {
|
|
1502
|
-
case "boolean":
|
|
1503
|
-
return false;
|
|
1504
|
-
case "number":
|
|
1505
|
-
return 0;
|
|
1506
|
-
case "string":
|
|
1507
|
-
return "";
|
|
1508
|
-
case "flags":
|
|
1509
|
-
return [];
|
|
1510
|
-
case "enum":
|
|
1511
|
-
return decl.values?.[0] ?? "";
|
|
1512
|
-
case "quality":
|
|
1513
|
-
return decl.stages?.[0] ?? "";
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
1581
|
function truthy(v) {
|
|
1517
1582
|
if (typeof v === "boolean") return v;
|
|
1518
1583
|
if (typeof v === "number") return v !== 0;
|