@patterkit/runtime 0.7.1 → 0.9.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 CHANGED
@@ -33,6 +33,80 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.9.0] - 2026-09-02
37
+
38
+ ### Changed
39
+
40
+ - **BREAKING: a property row's address is `path`, not `ref`, and the row carries `name` and
41
+ `writable`.** `listProperties()` returns the row type from `@wildwinter/scoperegistry` itself,
42
+ rather than a local interface redeclaring it. `path` holds exactly what `ref`
43
+ held: the reference `getProperty` / `setProperty` take. `name` is the bare declared name.
44
+ `writable` is always true here, because Patter has no read-only shared property; it is carried
45
+ because the row is shared with the Storylet Engine, which does declare them.
46
+
47
+ The fork it replaces was not free. A quality's `stages` was added to the local copy and so never
48
+ reached the shared type, and the Storylet Engine - reading the same property model through the
49
+ same registry - could not carry a ladder at all: it edited a quality as free text on every
50
+ platform it ships. One row, one place to add the next field.
51
+ - **BREAKING: a row's address is the qualified one, `@patter.gold`, not `@gold`.** Both forms
52
+ have always resolved on input and still do - an unqualified name defaults to the `patter`
53
+ scope - so `getProperty("@gold")` is unaffected. What changed is the address a row REPORTS,
54
+ which is what a state panel displays and what an inspector writes back through. It matches
55
+ what `@scene` and the other family's scopes have always looked like.
56
+
57
+ - **Scene and stage state is held in the shared property bag.** `@scene` properties lived in
58
+ hand-rolled maps that duplicated the bag's own seeding, so they missed its guards: two flows
59
+ entering one scene now never share a mutable flags list, and a `temporary` property's reset on
60
+ re-entry goes through the bag, which means a state logger sees it. **The save format is
61
+ unchanged** - a flat name/value map per scene, and a load that seeds from the bundle's
62
+ declarations before laying saved values over, so a property a save predates keeps its default.
63
+ - **BREAKING: `PropertyView` is gone; `listProperties()` returns `PropertyRow`.** It was the
64
+ shared row plus a `path`, and `path` is on the shared row now, so the name was a second name
65
+ for one type. `PropertyRow` is re-exported from `@patterkit/runtime`, so naming a row needs no
66
+ dependency on `@wildwinter/scoperegistry`.
67
+ - **Requires `@wildwinter/scoperegistry` ^0.4.0**, which is where the row's `path` lives.
68
+
69
+ ### Added
70
+
71
+ - **A decision log, and `onDryChoice`.** Opening a run with `log: true` records what the engine
72
+ decided and why - each choice with the options it offered, the ones it greyed out and the
73
+ reason, each jump, each property write with the value it replaced. `Engine.log()` is the whole
74
+ run in order, a `Flow`'s own log is flow-local, and `onTrace` streams entries live rather than
75
+ retaining them. `onDryChoice` fires when a choice runs dry - no takeable option, no eligible
76
+ fallback - so the silent fall-through is observable; it survives alongside the log because it is
77
+ live feedback, not a record.
78
+
79
+ ### Fixed
80
+
81
+ - **A quality row carries its ladder.** `stages` was on the row so an examiner could offer the
82
+ stages instead of a free-text box, and the code that builds rows never filled it in - on this
83
+ runtime and two others. Every quality row came out without one.
84
+
85
+ ## [0.8.0] - 2026-09-01
86
+
87
+ ### Changed
88
+
89
+ - **`rngState` is now written UNSIGNED in a save.** It was accumulated with
90
+ `| 0`, so more than half of all saves carried a negative number where the
91
+ schema says uint32. The draws were unaffected (signed and unsigned
92
+ accumulation produce bit-identical draws), but the native ports could not read
93
+ those saves back: Unity threw, and C++ read them through undefined behaviour.
94
+
95
+ Old saves still load here, because restore already coerced, and the native
96
+ ports now coerce too. No gameplay changes.
97
+
98
+ - **Flags compare as a SET.** `==` and `!=` on a flags value now ignore order.
99
+ They are compared as multisets, so a duplicated flag still counts. A flags
100
+ value IS a set, and its stored order was an artefact of the order somebody
101
+ happened to add things in: `set_flags(@f, +red)` then `+blue` compared UNEQUAL
102
+ to the same two flags added the other way round. An expression that relied on
103
+ that will change answer.
104
+
105
+ - The PRNG is now `@wildwinter/expr`'s `makePrng` rather than a copy inline here.
106
+ Same algorithm, same draws; mulberry32 existed thirteen times across the two
107
+ product families and is a fixed published algorithm neither owns.
108
+
109
+
36
110
  ## [0.7.1] - 2026-08-30
37
111
 
38
112
  ### Fixed
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,
@@ -520,14 +527,44 @@ var Engine = class _Engine {
520
527
  }
521
528
  /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
522
529
  * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
530
+ /** The run's decisions, in order, each naming the flow it happened in. Empty unless the
531
+ * run was opened with `log: true`. A flow's own log stays flow-local; this is the only
532
+ * place a story spanning several flows reads as one sequence. */
533
+ log() {
534
+ return this.engineLog;
535
+ }
536
+ /** Drop the retained entries. `seq` does NOT restart: two reads of a log either side of a
537
+ * clear still agree about what came first. */
538
+ clearLog() {
539
+ this.engineLog.length = 0;
540
+ }
541
+ /** Live tap on the run's decisions, for tooling that wants them as they happen rather than
542
+ * retained. Returns its own unsubscribe. */
543
+ onTrace(handler) {
544
+ this.engineTraceHandlers.add(handler);
545
+ return () => this.engineTraceHandlers.delete(handler);
546
+ }
547
+ emitEngine(flow, event, scene) {
548
+ for (const h of this.engineTraceHandlers) h(flow, event);
549
+ if (!this.host.logEnabled) return;
550
+ this.engineLog.push({ ...event, flow, seq: this.engineLog.length, ...scene ? { scene } : {} });
551
+ }
523
552
  listProperties() {
524
553
  return this.host.patterSharedDecls.map((d) => ({
525
- ref: `@${d.name}`,
554
+ name: d.name,
555
+ // The qualified address, matching what the bag composes for every other scope.
556
+ // `@gold` still resolves on input; it is the shorthand, not the address.
557
+ path: `@patter.${d.name}`,
526
558
  type: d.type,
527
559
  values: d.values,
528
560
  stages: d.stages,
529
561
  value: this.getProperty(`@${d.name}`),
530
- default: declDefault(d)
562
+ default: (0, import_scoperegistry2.defaultFor)(d),
563
+ // Part of the shared row. Always true here today: `toDecl` never sets it, because
564
+ // Patter has no read-only shared property. The Storylet Engine does declare them
565
+ // and its panels disable the editor accordingly, so the field is carried rather
566
+ // than dropped - and the day a read-only @patter property exists, the row says so.
567
+ writable: d.writable ?? true
531
568
  }));
532
569
  }
533
570
  // @scene is scene-namespaced and needs a flow's current scene - silently
@@ -560,7 +597,7 @@ var Engine = class _Engine {
560
597
  shared: this.host.shared.save(),
561
598
  sharedVisits: Object.fromEntries(this.host.sharedVisits),
562
599
  sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
563
- stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, { ...bag }])),
600
+ stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
564
601
  flows
565
602
  };
566
603
  }
@@ -573,7 +610,13 @@ var Engine = class _Engine {
573
610
  this.host.sharedSelectors.clear();
574
611
  for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
575
612
  this.host.stageBags.clear();
576
- for (const [s, bag] of Object.entries(save.stageBags ?? {})) this.host.stageBags.set(s, { ...bag });
613
+ for (const [s, values] of Object.entries(save.stageBags ?? {})) {
614
+ const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
615
+ const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => shared.has(d.name.toLowerCase()));
616
+ const bag = new import_scoperegistry2.PropertyBag(decls);
617
+ bag.load(values);
618
+ this.host.stageBags.set(s, bag);
619
+ }
577
620
  this.flowsById.clear();
578
621
  for (const [id, snap] of Object.entries(save.flows)) {
579
622
  const flow = new Flow(id, this.host, this.defaultSeed);
@@ -616,6 +659,7 @@ var Flow = class {
616
659
  // The SHARED halves live on the host (`host.shared` / `host.stageBags`). Each
617
660
  // resolver presents one merged scope, routing each property to its half by the
618
661
  // declared `shared` flag.
662
+ /** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
619
663
  sceneBags = /* @__PURE__ */ new Map();
620
664
  patterResolver = {
621
665
  get: (n) => this.host.patterSharedNames.has(n) ? this.host.shared.get("patter", n) : this.local.get("patter", n),
@@ -629,13 +673,13 @@ var Flow = class {
629
673
  const s = this.currentSceneId;
630
674
  if (s === null) return void 0;
631
675
  const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
632
- return bag?.[n];
676
+ return bag?.get(n);
633
677
  },
634
678
  set: (n, v) => {
635
679
  const s = this.currentSceneId;
636
680
  if (s === null) return;
637
681
  const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
638
- if (bag) bag[n] = v;
682
+ if (bag) bag.set(n, v);
639
683
  }
640
684
  };
641
685
  // The eval context is built ONCE: every constituent resolves live state at
@@ -644,10 +688,13 @@ var Flow = class {
644
688
  // `local`/`sceneBags`/`currentSceneId`; the host callbacks read current flow
645
689
  // fields). Rebuilding it per evaluation was the engine's hottest allocation.
646
690
  evalCtx;
691
+ flowLog = [];
692
+ /** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
693
+ flowSeq = 0;
647
694
  constructor(id, host, seed) {
648
695
  this.id = id;
649
696
  this.host = host;
650
- this.rngState = seed >>> 0;
697
+ this.rngState = (0, import_expr.toUint32)(seed);
651
698
  this.local = this.freshLocal();
652
699
  const scopes = { ...host.shared.toEvalContext().scopes };
653
700
  scopes["patter"] = this.patterResolver;
@@ -869,7 +916,17 @@ var Flow = class {
869
916
  this.stack.pop();
870
917
  continue;
871
918
  }
919
+ const from = frame.index;
872
920
  while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
921
+ if (this.host.logEnabled && frame.index !== from) {
922
+ this.emit({
923
+ type: "select",
924
+ group: frame.containerId,
925
+ selector: "run",
926
+ children: children.slice(from, frame.index + 1).map((c, i) => ({ id: c.id, eligible: from + i === frame.index })),
927
+ picked: children[frame.index]?.id ?? null
928
+ });
929
+ }
873
930
  if (frame.index >= children.length) {
874
931
  this.stack.pop();
875
932
  continue;
@@ -878,6 +935,24 @@ var Flow = class {
878
935
  }
879
936
  }
880
937
  /** The options of a pending choice (empty when not at a choice point). */
938
+ /** This flow's decisions, in order. Empty unless the run was opened with `log: true`.
939
+ * The engine's log carries the same events tagged with the flow; this one is what a
940
+ * single conversation reads as. */
941
+ log() {
942
+ return this.flowLog;
943
+ }
944
+ /** Drop the retained entries. `seq` keeps counting, so order survives a clear. */
945
+ clearLog() {
946
+ this.flowLog.length = 0;
947
+ }
948
+ /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
949
+ * off: the entry is never built. */
950
+ emit(event) {
951
+ const scene = this.currentSceneId ?? void 0;
952
+ this.host.emitEngine(this.id, event, scene);
953
+ if (!this.host.logEnabled) return;
954
+ this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
955
+ }
881
956
  getChoices() {
882
957
  return this.pendingChoice?.options ?? [];
883
958
  }
@@ -889,6 +964,7 @@ var Flow = class {
889
964
  if (!option) throw new Error(`unknown choice option: ${id}`);
890
965
  if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
891
966
  const node = choice.byId.get(id);
967
+ this.emit({ type: "chose", group: choice.groupId, option: id });
892
968
  this.pendingChoice = null;
893
969
  this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
894
970
  this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
@@ -922,7 +998,7 @@ var Flow = class {
922
998
  return {
923
999
  scopes: this.local.save(),
924
1000
  // owned scope "patter" = the NOT-shared globals (@scene saved separately)
925
- sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, { ...bag }])),
1001
+ sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
926
1002
  rngState: this.rngState,
927
1003
  visits: Object.fromEntries(this.visitCounts),
928
1004
  cursor: {
@@ -945,7 +1021,7 @@ var Flow = class {
945
1021
  }
946
1022
  /** @internal Restore this flow from a snapshot. */
947
1023
  restore(snap) {
948
- this.rngState = snap.rngState >>> 0;
1024
+ this.rngState = (0, import_expr.toUint32)(snap.rngState);
949
1025
  this.visitCounts = new Map(Object.entries(snap.visits ?? {}));
950
1026
  const c = snap.cursor;
951
1027
  this.started = true;
@@ -960,7 +1036,13 @@ var Flow = class {
960
1036
  }
961
1037
  return { ...frame };
962
1038
  });
963
- this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, bag]) => [s, { ...bag }]));
1039
+ this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, values]) => {
1040
+ const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
1041
+ const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => !shared.has(d.name.toLowerCase()));
1042
+ const bag = new import_scoperegistry2.PropertyBag(decls);
1043
+ bag.load(values);
1044
+ return [s, bag];
1045
+ }));
964
1046
  this.local = this.freshLocal();
965
1047
  this.local.load(snap.scopes);
966
1048
  this.activeSnippet = null;
@@ -1054,6 +1136,7 @@ var Flow = class {
1054
1136
  byId.set(child.id, child);
1055
1137
  }
1056
1138
  if (options.length > 0) {
1139
+ this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1057
1140
  this.pendingChoice = { groupId: group.id, options, byId };
1058
1141
  return;
1059
1142
  }
@@ -1062,6 +1145,7 @@ var Flow = class {
1062
1145
  this.enterChild(fallback);
1063
1146
  return;
1064
1147
  }
1148
+ this.emit({ type: "dry", group: group.id });
1065
1149
  this.host.onDryChoice?.(group.id);
1066
1150
  }
1067
1151
  // -- Jumps (jump / call-return) ----------------------------------------
@@ -1076,6 +1160,7 @@ var Flow = class {
1076
1160
  * hard-ends the flow regardless of the callstack.
1077
1161
  */
1078
1162
  enterTarget(to, mode) {
1163
+ this.emit({ type: "jump", to, mode });
1079
1164
  if (to === "END") {
1080
1165
  this.flowEnded = true;
1081
1166
  this.stack = [];
@@ -1107,17 +1192,28 @@ var Flow = class {
1107
1192
  }
1108
1193
  // -- Selectors ------------------------------------------------------------
1109
1194
  selectChild(group) {
1195
+ const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
1110
1196
  const eligible = group.children.filter((c) => this.eligible(c));
1111
- if (eligible.length === 0) return null;
1197
+ const order = group.options?.order ?? "sequential";
1198
+ const exhaust = group.options?.exhaust ?? "once";
1199
+ const trace = (picked) => {
1200
+ this.emit({
1201
+ type: "select",
1202
+ group: group.id,
1203
+ selector: group.selector ?? "default",
1204
+ ...group.selector === "sequence" ? { order, exhaust } : {},
1205
+ children: verdicts,
1206
+ picked: picked?.id ?? null
1207
+ });
1208
+ return picked;
1209
+ };
1210
+ if (eligible.length === 0) return trace(null);
1112
1211
  const st = this.selectorState(group);
1113
1212
  switch (group.selector) {
1114
1213
  case "branch":
1115
- return eligible[0];
1116
- case "sequence": {
1117
- const order = group.options?.order ?? "sequential";
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
- }
1214
+ return trace(eligible[0]);
1215
+ case "sequence":
1216
+ return trace(order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st));
1121
1217
  case "run":
1122
1218
  case "choice":
1123
1219
  default:
@@ -1235,7 +1331,10 @@ var Flow = class {
1235
1331
  // -- Effects + expressions ------------------------------------------------
1236
1332
  runEffects(effects) {
1237
1333
  for (const e of effects ?? []) {
1238
- this.setProperty(e.target, this.evalExpr(e.value));
1334
+ const value = this.evalExpr(e.value);
1335
+ const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
1336
+ this.setProperty(e.target, value);
1337
+ this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1239
1338
  }
1240
1339
  }
1241
1340
  eligible(node) {
@@ -1260,14 +1359,19 @@ var Flow = class {
1260
1359
  this.visitCounts.set(id, (this.visitCounts.get(id) ?? 0) + 1);
1261
1360
  this.host.sharedVisits.set(id, (this.host.sharedVisits.get(id) ?? 0) + 1);
1262
1361
  }
1263
- /** Next float in [0, 1): the shared custom PRNG, or this flow's serialisable mulberry32. */
1362
+ /** Next float in [0, 1): the shared custom PRNG, or this flow's serialisable mulberry32.
1363
+ *
1364
+ * The mixing is @wildwinter/expr's makePrng, not a copy inline here. It is a
1365
+ * fixed published algorithm that both product families need and neither
1366
+ * owns, and it existed thirteen times across the two of them. `rngState` is
1367
+ * still the serialisable position, so saves are unaffected: a generator is
1368
+ * made from it, drawn once, and the new state written back. */
1264
1369
  rng = () => {
1265
1370
  if (this.host.customRng) return this.host.customRng();
1266
- const a = this.rngState + 1831565813 | 0;
1267
- this.rngState = a;
1268
- let t = Math.imul(a ^ a >>> 15, 1 | a);
1269
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1270
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1371
+ const prng = (0, import_expr.makePrng)(this.rngState);
1372
+ const draw = prng.next();
1373
+ this.rngState = prng.state();
1374
+ return draw;
1271
1375
  };
1272
1376
  // -- Strings / beats ------------------------------------------------------
1273
1377
  beatResult(beat) {
@@ -1374,7 +1478,7 @@ var Flow = class {
1374
1478
  }
1375
1479
  /** The per-flow registry: the NOT-shared `@patter` globals (the shared ones live on the host). */
1376
1480
  freshLocal() {
1377
- return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls);
1481
+ return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls, "@patter.");
1378
1482
  }
1379
1483
  /**
1380
1484
  * Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
@@ -1385,27 +1489,18 @@ var Flow = class {
1385
1489
  */
1386
1490
  seedScene(scene) {
1387
1491
  const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
1492
+ const props = scene.sceneProps ?? [];
1388
1493
  if (!this.sceneBags.has(scene.id)) {
1389
- const bag = {};
1390
- for (const decl of scene.sceneProps ?? []) {
1391
- const name = decl.name.toLowerCase();
1392
- if (!shared.has(name)) bag[name] = sceneDefault(decl);
1393
- }
1394
- this.sceneBags.set(scene.id, bag);
1494
+ this.sceneBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
1395
1495
  }
1396
1496
  if (!this.host.stageBags.has(scene.id)) {
1397
- const bag = {};
1398
- for (const decl of scene.sceneProps ?? []) {
1399
- const name = decl.name.toLowerCase();
1400
- if (shared.has(name)) bag[name] = sceneDefault(decl);
1401
- }
1402
- this.host.stageBags.set(scene.id, bag);
1497
+ this.host.stageBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
1403
1498
  }
1404
1499
  for (const decl of scene.sceneProps ?? []) {
1405
1500
  if (!decl.temporary) continue;
1406
1501
  const name = decl.name.toLowerCase();
1407
1502
  const bag = shared.has(name) ? this.host.stageBags.get(scene.id) : this.sceneBags.get(scene.id);
1408
- if (bag) bag[name] = sceneDefault(decl);
1503
+ if (bag) bag.set(name, (0, import_scoperegistry2.defaultFor)(decl));
1409
1504
  }
1410
1505
  }
1411
1506
  };
@@ -1443,47 +1538,13 @@ function deserialiseSelectors(rec) {
1443
1538
  function toDecl(decl) {
1444
1539
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
1445
1540
  }
1446
- function declDefault(d) {
1447
- if (d.default !== void 0) return d.default;
1448
- switch (d.type) {
1449
- case "number":
1450
- return 0;
1451
- case "string":
1452
- return "";
1453
- case "flags":
1454
- return [];
1455
- case "enum":
1456
- return d.values?.[0] ?? "";
1457
- case "quality":
1458
- return d.stages?.[0] ?? "";
1459
- default:
1460
- return false;
1461
- }
1462
- }
1463
1541
  function toForeignDecl(decl) {
1464
1542
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
1465
1543
  }
1466
- function hostScopeDefault(decl) {
1467
- if (decl.default !== void 0) return decl.default;
1468
- switch (decl.type) {
1469
- case "boolean":
1470
- return false;
1471
- case "number":
1472
- return 0;
1473
- case "string":
1474
- return "";
1475
- case "flags":
1476
- return [];
1477
- case "enum":
1478
- return decl.values?.[0] ?? "";
1479
- case "quality":
1480
- return decl.stages?.[0] ?? "";
1481
- }
1482
- }
1483
1544
  function selfBackedResolver(decls) {
1484
1545
  const key = (name) => name.toLowerCase();
1485
1546
  const bag = /* @__PURE__ */ new Map();
1486
- for (const d of decls) bag.set(key(d.name), hostScopeDefault(d));
1547
+ for (const d of decls) bag.set(key(d.name), (0, import_scoperegistry2.defaultFor)(d));
1487
1548
  return {
1488
1549
  get: (name) => bag.get(key(name)),
1489
1550
  set: (name, value) => {
@@ -1491,23 +1552,6 @@ function selfBackedResolver(decls) {
1491
1552
  }
1492
1553
  };
1493
1554
  }
1494
- function sceneDefault(decl) {
1495
- if (decl.default !== void 0) return decl.default;
1496
- switch (decl.type) {
1497
- case "boolean":
1498
- return false;
1499
- case "number":
1500
- return 0;
1501
- case "string":
1502
- return "";
1503
- case "flags":
1504
- return [];
1505
- case "enum":
1506
- return decl.values?.[0] ?? "";
1507
- case "quality":
1508
- return decl.stages?.[0] ?? "";
1509
- }
1510
- }
1511
1555
  function truthy(v) {
1512
1556
  if (typeof v === "boolean") return v;
1513
1557
  if (typeof v === "number") return v !== 0;