@storylet-studio/runtime 0.3.0 → 0.4.1

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.d.cts CHANGED
@@ -24,6 +24,16 @@ interface EngineOptions {
24
24
  * once, each engine saves its own envelope (design/flows.md).
25
25
  */
26
26
  world?: ScopeResolver;
27
+ /**
28
+ * Diagnostics hook (opt-in, dev tooling only): fired when `openFlow` REPLACES
29
+ * a flow that still had cards dealt, with the flow id and how many. The
30
+ * behaviour is unchanged - replacing is deliberate and the same in Patter -
31
+ * this only makes it observable, because the case it catches is a host
32
+ * calling `openFlow` straight after `loadGame` to "re-take" its handle and
33
+ * silently discarding the hand the save just restored (`getFlow` is the
34
+ * call). Zero cost when unset; leave it unset in shipped games.
35
+ */
36
+ onReplacedFlow?: (id: string, dealt: number) => void;
27
37
  }
28
38
  interface OpenFlowOptions {
29
39
  /** Seed for this flow's PRNG (defaults to the engine's `seed`). */
@@ -224,6 +234,9 @@ interface Internals {
224
234
  shared: Partition;
225
235
  /** @world: the host's resolver, or the self-backed bag's. */
226
236
  worldResolver: ScopeResolver;
237
+ /** @world names declared `writable: false`: the story's promise, kept at
238
+ * runtime as the compiler keeps it at publish (Reboot.md 10). */
239
+ worldReadOnly: Set<string>;
227
240
  /** `turn` is the box clock the event happened on, where the caller knows it
228
241
  * - the same stamp the flow's own log carries. Unity and Unreal passed it
229
242
  * from the start; JS and Godot dropped it, so their examiners printed "[-]"
@@ -235,6 +248,7 @@ interface Internals {
235
248
  declare class Engine {
236
249
  private readonly internals;
237
250
  private readonly seed;
251
+ private readonly onReplacedFlow;
238
252
  private readonly flowsById;
239
253
  private readonly engineTraceHandlers;
240
254
  /** The host's @world binding, if the engine was built with one: it
package/dist/index.d.ts CHANGED
@@ -24,6 +24,16 @@ interface EngineOptions {
24
24
  * once, each engine saves its own envelope (design/flows.md).
25
25
  */
26
26
  world?: ScopeResolver;
27
+ /**
28
+ * Diagnostics hook (opt-in, dev tooling only): fired when `openFlow` REPLACES
29
+ * a flow that still had cards dealt, with the flow id and how many. The
30
+ * behaviour is unchanged - replacing is deliberate and the same in Patter -
31
+ * this only makes it observable, because the case it catches is a host
32
+ * calling `openFlow` straight after `loadGame` to "re-take" its handle and
33
+ * silently discarding the hand the save just restored (`getFlow` is the
34
+ * call). Zero cost when unset; leave it unset in shipped games.
35
+ */
36
+ onReplacedFlow?: (id: string, dealt: number) => void;
27
37
  }
28
38
  interface OpenFlowOptions {
29
39
  /** Seed for this flow's PRNG (defaults to the engine's `seed`). */
@@ -224,6 +234,9 @@ interface Internals {
224
234
  shared: Partition;
225
235
  /** @world: the host's resolver, or the self-backed bag's. */
226
236
  worldResolver: ScopeResolver;
237
+ /** @world names declared `writable: false`: the story's promise, kept at
238
+ * runtime as the compiler keeps it at publish (Reboot.md 10). */
239
+ worldReadOnly: Set<string>;
227
240
  /** `turn` is the box clock the event happened on, where the caller knows it
228
241
  * - the same stamp the flow's own log carries. Unity and Unreal passed it
229
242
  * from the start; JS and Godot dropped it, so their examiners printed "[-]"
@@ -235,6 +248,7 @@ interface Internals {
235
248
  declare class Engine {
236
249
  private readonly internals;
237
250
  private readonly seed;
251
+ private readonly onReplacedFlow;
238
252
  private readonly flowsById;
239
253
  private readonly engineTraceHandlers;
240
254
  /** The host's @world binding, if the engine was built with one: it
package/dist/index.js CHANGED
@@ -603,6 +603,7 @@ var loadPartition = (p, values) => {
603
603
  var Engine = class {
604
604
  internals;
605
605
  seed;
606
+ onReplacedFlow;
606
607
  flowsById = /* @__PURE__ */ new Map();
607
608
  engineTraceHandlers = /* @__PURE__ */ new Set();
608
609
  /** The host's @world binding, if the engine was built with one: it
@@ -611,6 +612,7 @@ var Engine = class {
611
612
  hostWorld;
612
613
  constructor(bundle, opts = {}) {
613
614
  this.seed = opts.seed ?? 0;
615
+ this.onReplacedFlow = opts.onReplacedFlow;
614
616
  if (opts.world !== void 0) this.hostWorld = opts.world;
615
617
  const internals = {
616
618
  bundle,
@@ -631,6 +633,7 @@ var Engine = class {
631
633
  flowDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
632
634
  shared: void 0,
633
635
  worldResolver: void 0,
636
+ worldReadOnly: /* @__PURE__ */ new Set(),
634
637
  emitEngine: (flow, event, turn) => {
635
638
  if (this.internals.logCap !== void 0) {
636
639
  this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...turn !== void 0 ? { turn } : {} });
@@ -688,6 +691,7 @@ var Engine = class {
688
691
  initShared(hostWorld) {
689
692
  const internals = this.internals;
690
693
  internals.shared = buildPartition(internals, sharedHalf);
694
+ internals.worldReadOnly = new Set(internals.bundle.world.properties.filter((d) => d.writable === false).map((d) => d.name));
691
695
  if (hostWorld !== void 0) {
692
696
  internals.worldResolver = hostWorld;
693
697
  } else {
@@ -731,7 +735,12 @@ var Engine = class {
731
735
  * state; shared state is untouched. There is no default flow: "main" is
732
736
  * a caller convention, not an engine rule. */
733
737
  openFlow(id, opts = {}) {
734
- this.flowsById.get(id)?.markClosed();
738
+ const existing = this.flowsById.get(id);
739
+ if (existing) {
740
+ const dealt = existing.heldCardIds().length;
741
+ if (dealt > 0) this.onReplacedFlow?.(id, dealt);
742
+ existing.markClosed();
743
+ }
735
744
  const flow = new Flow(this, this.internals, id, opts.seed ?? this.seed);
736
745
  this.flowsById.set(id, flow);
737
746
  return flow;
@@ -1697,6 +1706,7 @@ var Flow = class {
1697
1706
  case "world": {
1698
1707
  const resolver = this.internals.worldResolver;
1699
1708
  if (!resolver.set) throw new Error(`@world.${name} cannot be written: the host bound @world read-only`);
1709
+ if (this.internals.worldReadOnly.has(name)) throw new Error(`'@world.${name}' is read-only (writable: false)`);
1700
1710
  const prev = resolver.get(name);
1701
1711
  resolver.set(name, value);
1702
1712
  return { path: `world.${name}`, ...prev !== void 0 ? { prev } : {} };