@storylet-studio/runtime 0.2.0 → 0.4.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/dist/index.cjs CHANGED
@@ -453,8 +453,12 @@ var PropertyBag = class _PropertyBag {
453
453
  * long-standing contract); a product whose names are case-significant
454
454
  * passes identity. */
455
455
  norm;
456
+ /** The address prefix this bag's rows carry, separator included (`@`,
457
+ * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */
458
+ pathPrefix;
456
459
  constructor(declarations = [], opts) {
457
460
  this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
461
+ this.pathPrefix = opts?.pathPrefix ?? "";
458
462
  this.seed(declarations);
459
463
  }
460
464
  seed(declarations) {
@@ -498,7 +502,7 @@ var PropertyBag = class _PropertyBag {
498
502
  /** Examiner rows: the declared surface only (stray values are storage,
499
503
  * not surface). */
500
504
  rows() {
501
- return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name));
505
+ return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name, this.pathPrefix));
502
506
  }
503
507
  declarations() {
504
508
  return [...this.decls.values()];
@@ -507,7 +511,7 @@ var PropertyBag = class _PropertyBag {
507
511
  * duplicated, the normalisation policy carried, subscriptions NOT
508
512
  * carried. */
509
513
  clone() {
510
- const c = new _PropertyBag([], { normalise: this.norm });
514
+ const c = new _PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });
511
515
  c.decls = new Map(this.decls);
512
516
  Object.assign(c.values, structuredClone(this.values));
513
517
  return c;
@@ -530,13 +534,19 @@ var PropertyBag = class _PropertyBag {
530
534
  for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
531
535
  }
532
536
  };
533
- function rowFor(d, value, writable, name) {
537
+ function rowFor(d, value, writable, name, pathPrefix = "") {
538
+ const rowName = name ?? d.name.toLowerCase();
534
539
  return {
535
- name: name ?? d.name.toLowerCase(),
540
+ name: rowName,
541
+ path: pathPrefix + rowName,
536
542
  type: d.type,
537
543
  value,
538
544
  default: d.default ?? defaultFor(d),
539
545
  ...d.values !== void 0 ? { values: d.values } : {},
546
+ // `stages` was added to the row so an examiner could offer a quality's ladder
547
+ // instead of a free-text box, and then never populated here: every quality row
548
+ // this function built came out without one. Fixed 2026-09-02.
549
+ ...d.stages !== void 0 ? { stages: d.stages } : {},
540
550
  writable: writable ?? d.writable ?? true
541
551
  };
542
552
  }
@@ -556,6 +566,13 @@ function defaultFor(d) {
556
566
  // A quality starts at the first rung of its ladder.
557
567
  case "quality":
558
568
  return d.stages?.[0] ?? "";
569
+ // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle
570
+ // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the
571
+ // union does not have. Falling off the switch would seed `undefined`, which is not a
572
+ // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the
573
+ // guard and this one did not, which is the drift you only find by removing a duplicate.
574
+ default:
575
+ return false;
559
576
  }
560
577
  }
561
578
 
@@ -563,7 +580,7 @@ function defaultFor(d) {
563
580
  var tagKey = (groupId, tagId) => `${groupId}${tagId}`;
564
581
  var cardIsShared = (card, deckShared) => card.shared ?? deckShared;
565
582
  var sharedCap = (card) => card.sharedCopies ?? card.copies ?? 1;
566
- var bagFromDecls = (decls) => new PropertyBag(decls, { normalise: (n) => n });
583
+ var bagFromDecls = (decls, pathPrefix) => new PropertyBag(decls, { normalise: (n) => n, pathPrefix });
567
584
  function conditionPasses(v) {
568
585
  if (typeof v === "boolean") return v;
569
586
  if (typeof v === "number") return v !== 0;
@@ -583,18 +600,18 @@ var handDeclsOf = (internals, hand) => {
583
600
  var buildPartition = (internals, half) => {
584
601
  const b = internals.bundle;
585
602
  return {
586
- story: bagFromDecls(half("story", b.story.properties)),
587
- box: new Map(b.boxes.map((box) => [box.id, bagFromDecls(half("box", box.properties))])),
603
+ story: bagFromDecls(half("story", b.story.properties), "story."),
604
+ box: new Map(b.boxes.map((box) => [box.id, bagFromDecls(half("box", box.properties), `box.${box.id}.`)])),
588
605
  deck: new Map(b.boxes.flatMap((box) => box.decks.map(
589
- (deck) => [deck.id, bagFromDecls(half("deck", deck.properties))]
606
+ (deck) => [deck.id, bagFromDecls(half("deck", deck.properties), `deck.${deck.id}.`)]
590
607
  ))),
591
608
  // A template instance inherits the template's property declarations;
592
609
  // a standalone hand declares its own (schema 2.6).
593
610
  hand: new Map(b.boxes.flatMap((box) => box.hands.map(
594
- (hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)))]
611
+ (hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)), `hand.${hand.id}.`)]
595
612
  ))),
596
613
  value: new Map(b.boxes.flatMap((box) => box.tagGroups.flatMap((group) => group.tags.map(
597
- (tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []))]
614
+ (tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []), `value.${tag.id}.`)]
598
615
  ))))
599
616
  };
600
617
  };
@@ -616,6 +633,7 @@ var loadPartition = (p, values) => {
616
633
  var Engine = class {
617
634
  internals;
618
635
  seed;
636
+ onReplacedFlow;
619
637
  flowsById = /* @__PURE__ */ new Map();
620
638
  engineTraceHandlers = /* @__PURE__ */ new Set();
621
639
  /** The host's @world binding, if the engine was built with one: it
@@ -624,6 +642,7 @@ var Engine = class {
624
642
  hostWorld;
625
643
  constructor(bundle, opts = {}) {
626
644
  this.seed = opts.seed ?? 0;
645
+ this.onReplacedFlow = opts.onReplacedFlow;
627
646
  if (opts.world !== void 0) this.hostWorld = opts.world;
628
647
  const internals = {
629
648
  bundle,
@@ -644,6 +663,7 @@ var Engine = class {
644
663
  flowDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
645
664
  shared: void 0,
646
665
  worldResolver: void 0,
666
+ worldReadOnly: /* @__PURE__ */ new Set(),
647
667
  emitEngine: (flow, event, turn) => {
648
668
  if (this.internals.logCap !== void 0) {
649
669
  this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...turn !== void 0 ? { turn } : {} });
@@ -701,10 +721,11 @@ var Engine = class {
701
721
  initShared(hostWorld) {
702
722
  const internals = this.internals;
703
723
  internals.shared = buildPartition(internals, sharedHalf);
724
+ internals.worldReadOnly = new Set(internals.bundle.world.properties.filter((d) => d.writable === false).map((d) => d.name));
704
725
  if (hostWorld !== void 0) {
705
726
  internals.worldResolver = hostWorld;
706
727
  } else {
707
- const bag = bagFromDecls(internals.bundle.world.properties);
728
+ const bag = bagFromDecls(internals.bundle.world.properties, "world.");
708
729
  internals.worldResolver = {
709
730
  get: (n) => bag.get(n),
710
731
  set: (n, v) => {
@@ -744,7 +765,12 @@ var Engine = class {
744
765
  * state; shared state is untouched. There is no default flow: "main" is
745
766
  * a caller convention, not an engine rule. */
746
767
  openFlow(id, opts = {}) {
747
- this.flowsById.get(id)?.markClosed();
768
+ const existing = this.flowsById.get(id);
769
+ if (existing) {
770
+ const dealt = existing.heldCardIds().length;
771
+ if (dealt > 0) this.onReplacedFlow?.(id, dealt);
772
+ existing.markClosed();
773
+ }
748
774
  const flow = new Flow(this, this.internals, id, opts.seed ?? this.seed);
749
775
  this.flowsById.set(id, flow);
750
776
  return flow;
@@ -880,11 +906,16 @@ var Engine = class {
880
906
  value: value ?? d.default,
881
907
  default: d.default,
882
908
  ...d.values !== void 0 ? { values: d.values } : {},
883
- ...d.stages !== void 0 ? { stages: d.stages } : {}
909
+ ...d.stages !== void 0 ? { stages: d.stages } : {},
910
+ // @world is FOREIGN - a host resolver backs it - so writability is whether that
911
+ // resolver can be written at all, which is the shared registry's own rule for a
912
+ // foreign scope. The `as PropertyView` cast this replaced was hiding the field's
913
+ // absence: the row type has always required it, and these rows shipped without one.
914
+ writable: this.internals.worldResolver.set !== void 0
884
915
  });
885
916
  }
886
- const add = (prefix, bag) => {
887
- for (const row of bag.rows()) out.push({ path: `${prefix}.${row.name}`, ...row });
917
+ const add = (_prefix, bag) => {
918
+ for (const row of bag.rows()) out.push(row);
888
919
  };
889
920
  add("story", this.internals.shared.story);
890
921
  for (const [id, bag] of this.internals.shared.box) add(`box.${id}`, bag);
@@ -1705,6 +1736,7 @@ var Flow = class {
1705
1736
  case "world": {
1706
1737
  const resolver = this.internals.worldResolver;
1707
1738
  if (!resolver.set) throw new Error(`@world.${name} cannot be written: the host bound @world read-only`);
1739
+ if (this.internals.worldReadOnly.has(name)) throw new Error(`'@world.${name}' is read-only (writable: false)`);
1708
1740
  const prev = resolver.get(name);
1709
1741
  resolver.set(name, value);
1710
1742
  return { path: `world.${name}`, ...prev !== void 0 ? { prev } : {} };
@@ -1776,13 +1808,18 @@ var Flow = class {
1776
1808
  value: value ?? d.default,
1777
1809
  default: d.default,
1778
1810
  ...d.values !== void 0 ? { values: d.values } : {},
1779
- ...d.stages !== void 0 ? { stages: d.stages } : {}
1811
+ ...d.stages !== void 0 ? { stages: d.stages } : {},
1812
+ // @world is FOREIGN - a host resolver backs it - so writability is whether that
1813
+ // resolver can be written at all, which is the shared registry's own rule for a
1814
+ // foreign scope. The `as PropertyView` cast this replaced was hiding the field's
1815
+ // absence: the row type has always required it, and these rows shipped without one.
1816
+ writable: this.internals.worldResolver.set !== void 0
1780
1817
  });
1781
1818
  }
1782
- const add = (prefix, shared, own) => {
1819
+ const add = (_prefix, shared, own) => {
1783
1820
  for (const bag of [shared, own]) {
1784
1821
  if (bag === void 0) continue;
1785
- for (const row of bag.rows()) out.push({ path: `${prefix}.${row.name}`, ...row });
1822
+ for (const row of bag.rows()) out.push(row);
1786
1823
  }
1787
1824
  };
1788
1825
  add("story", this.internals.shared.story, this.stores.story);