@storylet-studio/play-helpers 0.4.1 → 0.5.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.d.cts CHANGED
@@ -4,10 +4,17 @@ export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, State
4
4
  import { PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
5
5
 
6
6
  /** The full flattened snapshot of ONE FLOW's view - the shared partitions
7
- * plus that flow's own - straight off the save envelope, so "what the
8
- * snapshot sees" is by construction "what a save persists". @world is not
9
- * here for the same reason it is not in the envelope: the host owns that
10
- * container and mounts/saves it itself (createWorldContainer). */
7
+ * plus that flow's own - plus its turns / cooldowns / board. @world is not
8
+ * here for the same reason it is not in a save envelope: the host owns that
9
+ * container and mounts/saves it itself (createWorldContainer).
10
+ *
11
+ * Taken off the BAGS, which is what a save envelope is made of, rather than
12
+ * off the envelope itself. The two used to be interchangeable; from 4.4 they
13
+ * are not, because a property ADDRESS names its owner by gameId while the
14
+ * envelope stays keyed by internal id (a save has to survive a rename). The
15
+ * bags carry the address, so reading them is what keeps this snapshot and
16
+ * the live logger's lines in ONE path space - which is the invariant the
17
+ * whole diff rests on. */
11
18
  declare function snapshotState(engine: Engine, flow: Flow): StateSnapshot;
12
19
  /** The storylets state logger: the kernel core mounted on the SHARED bags
13
20
  * (engine.listBags()) and one flow's own (flow.listBags()) - the same
@@ -81,7 +88,10 @@ interface BundleInspector {
81
88
  description: BundleDescription;
82
89
  destroy(): void;
83
90
  }
84
- /** "name: type = default", plus enum/flags options where declared. */
91
+ /** "name: type = default", plus enum/flags options where declared, plus
92
+ * "(durable)" where the declaration says the value outlives a run
93
+ * (design/engine-server.md 4.2). Nothing is added for the ordinary
94
+ * run-scoped property: that is what a property is. */
85
95
  declare function formatPropertySummary(p: PropertySummary): string;
86
96
  /** The scope label a declaration block files under ("world", "box box",
87
97
  * "tag docks (zone)"). */
@@ -237,7 +247,11 @@ interface WorldContainer {
237
247
  /** Pass as `new Engine(bundle, { world: container.resolver })`. */
238
248
  resolver: ScopeResolver;
239
249
  /** The kernel bag itself (subscribe, audit, rows live there) - mount it
240
- * into a state logger or examiner beside the engine's own bags. */
250
+ * into a state logger or examiner beside the engine's own bags. Writing it
251
+ * DIRECTLY is writing the kernel, so a `writable: false` declaration asks
252
+ * the kernel's question: pass `{ host: true }` to say the game is speaking
253
+ * (`bag.set(name, value, { host: true })`). Through `resolver` or an
254
+ * engine's setProperty that is already answered. */
241
255
  bag: PropertyBag$1;
242
256
  /** The current values, for saving beside the engine's envelope. */
243
257
  values(): PropertyBag;
@@ -245,7 +259,26 @@ interface WorldContainer {
245
259
  * declarations keep their defaults - the same drift rule as loadGame. */
246
260
  load(values: PropertyBag): void;
247
261
  }
248
- /** A world container seeded from the bundle's @world declarations. */
262
+ /** A world container seeded from the bundle's @world declarations.
263
+ *
264
+ * The container is the GAME's state, so it WRITES - even a declaration
265
+ * carrying `writable: false`. That flag is the STORY's promise not to write
266
+ * the value (Reboot.md 10), and the engine keeps it where the story writes: an
267
+ * outcome is refused against the engine's read-only table before it ever
268
+ * reaches this resolver. Enforcing it here as well refused the HOST too - the
269
+ * clock the game must move, the harness driving the value it is testing
270
+ * against - which is the opposite of what the flag says.
271
+ *
272
+ * So the declarations are seeded AS DECLARED, which is what an examiner over
273
+ * this container should read, and the writes go through as HOST writes
274
+ * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the
275
+ * engine's own doorway and passes the flag too: the engine has already sorted
276
+ * story from host by then - a story write was refused earlier, a host write is
277
+ * the only kind that arrives - and a resolver takes a name and a value with no
278
+ * room to say which. A game wanting a rule of its own binds its own resolver
279
+ * rather than this one; the ports' container (Unreal's UStoryletWorld) draws
280
+ * the same line, with HostSet never refused and StorySet asking the game's own
281
+ * read-only list. */
249
282
  declare function createWorldContainer(bundle: Bundle): WorldContainer;
250
283
 
251
284
  export { type BundleInspector, type BundleInspectorOptions, type LiveBundleResult, type LiveFrame, type LiveLink, type LiveLinkOptions, type LiveSocketLike, type PropertyInspector, type PropertyInspectorOptions, type WorldContainer, applyLiveBundle, boardFrame, createBundleInspector, createLiveLink, createPropertyInspector, createStateLogger, createWorldContainer, deserializeState, ensureInspectorStyle, formatLogEntry, formatPropertySummary, formatScopeLabel, loadState, saveState, serializeState, snapshotState };
package/dist/index.d.ts CHANGED
@@ -4,10 +4,17 @@ export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, State
4
4
  import { PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
5
5
 
6
6
  /** The full flattened snapshot of ONE FLOW's view - the shared partitions
7
- * plus that flow's own - straight off the save envelope, so "what the
8
- * snapshot sees" is by construction "what a save persists". @world is not
9
- * here for the same reason it is not in the envelope: the host owns that
10
- * container and mounts/saves it itself (createWorldContainer). */
7
+ * plus that flow's own - plus its turns / cooldowns / board. @world is not
8
+ * here for the same reason it is not in a save envelope: the host owns that
9
+ * container and mounts/saves it itself (createWorldContainer).
10
+ *
11
+ * Taken off the BAGS, which is what a save envelope is made of, rather than
12
+ * off the envelope itself. The two used to be interchangeable; from 4.4 they
13
+ * are not, because a property ADDRESS names its owner by gameId while the
14
+ * envelope stays keyed by internal id (a save has to survive a rename). The
15
+ * bags carry the address, so reading them is what keeps this snapshot and
16
+ * the live logger's lines in ONE path space - which is the invariant the
17
+ * whole diff rests on. */
11
18
  declare function snapshotState(engine: Engine, flow: Flow): StateSnapshot;
12
19
  /** The storylets state logger: the kernel core mounted on the SHARED bags
13
20
  * (engine.listBags()) and one flow's own (flow.listBags()) - the same
@@ -81,7 +88,10 @@ interface BundleInspector {
81
88
  description: BundleDescription;
82
89
  destroy(): void;
83
90
  }
84
- /** "name: type = default", plus enum/flags options where declared. */
91
+ /** "name: type = default", plus enum/flags options where declared, plus
92
+ * "(durable)" where the declaration says the value outlives a run
93
+ * (design/engine-server.md 4.2). Nothing is added for the ordinary
94
+ * run-scoped property: that is what a property is. */
85
95
  declare function formatPropertySummary(p: PropertySummary): string;
86
96
  /** The scope label a declaration block files under ("world", "box box",
87
97
  * "tag docks (zone)"). */
@@ -237,7 +247,11 @@ interface WorldContainer {
237
247
  /** Pass as `new Engine(bundle, { world: container.resolver })`. */
238
248
  resolver: ScopeResolver;
239
249
  /** The kernel bag itself (subscribe, audit, rows live there) - mount it
240
- * into a state logger or examiner beside the engine's own bags. */
250
+ * into a state logger or examiner beside the engine's own bags. Writing it
251
+ * DIRECTLY is writing the kernel, so a `writable: false` declaration asks
252
+ * the kernel's question: pass `{ host: true }` to say the game is speaking
253
+ * (`bag.set(name, value, { host: true })`). Through `resolver` or an
254
+ * engine's setProperty that is already answered. */
241
255
  bag: PropertyBag$1;
242
256
  /** The current values, for saving beside the engine's envelope. */
243
257
  values(): PropertyBag;
@@ -245,7 +259,26 @@ interface WorldContainer {
245
259
  * declarations keep their defaults - the same drift rule as loadGame. */
246
260
  load(values: PropertyBag): void;
247
261
  }
248
- /** A world container seeded from the bundle's @world declarations. */
262
+ /** A world container seeded from the bundle's @world declarations.
263
+ *
264
+ * The container is the GAME's state, so it WRITES - even a declaration
265
+ * carrying `writable: false`. That flag is the STORY's promise not to write
266
+ * the value (Reboot.md 10), and the engine keeps it where the story writes: an
267
+ * outcome is refused against the engine's read-only table before it ever
268
+ * reaches this resolver. Enforcing it here as well refused the HOST too - the
269
+ * clock the game must move, the harness driving the value it is testing
270
+ * against - which is the opposite of what the flag says.
271
+ *
272
+ * So the declarations are seeded AS DECLARED, which is what an examiner over
273
+ * this container should read, and the writes go through as HOST writes
274
+ * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the
275
+ * engine's own doorway and passes the flag too: the engine has already sorted
276
+ * story from host by then - a story write was refused earlier, a host write is
277
+ * the only kind that arrives - and a resolver takes a name and a value with no
278
+ * room to say which. A game wanting a rule of its own binds its own resolver
279
+ * rather than this one; the ports' container (Unreal's UStoryletWorld) draws
280
+ * the same line, with HostSet never refused and StorySet asking the game's own
281
+ * read-only list. */
249
282
  declare function createWorldContainer(bundle: Bundle): WorldContainer;
250
283
 
251
284
  export { type BundleInspector, type BundleInspectorOptions, type LiveBundleResult, type LiveFrame, type LiveLink, type LiveLinkOptions, type LiveSocketLike, type PropertyInspector, type PropertyInspectorOptions, type WorldContainer, applyLiveBundle, boardFrame, createBundleInspector, createLiveLink, createPropertyInspector, createStateLogger, createWorldContainer, deserializeState, ensureInspectorStyle, formatLogEntry, formatPropertySummary, formatScopeLabel, loadState, saveState, serializeState, snapshotState };
package/dist/index.js CHANGED
@@ -95,10 +95,14 @@ var PropertyBag = class _PropertyBag {
95
95
  }
96
96
  /** Write a property. Engine writes (the default) notify subscribers;
97
97
  * pass `silent: true` for a host write, which reaches only the audit
98
- * hook. Throws on a read-only property. Returns the change. */
98
+ * hook. Throws on a read-only property unless the caller says it is the
99
+ * HOST (`host: true`), for whom `writable: false` was never a rule - it is
100
+ * the story's promise, not the game's. `silent` and `host` are separate on
101
+ * purpose: one is about who hears the write, the other about who may make
102
+ * it. Returns the change. */
99
103
  set(name, value, opts) {
100
104
  const n = this.norm(name);
101
- if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
105
+ if (!opts?.host && this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
102
106
  const change = {
103
107
  name: n,
104
108
  prev: this.values[n],
@@ -200,19 +204,13 @@ function defaultFor(d) {
200
204
 
201
205
  // src/logger.ts
202
206
  function snapshotState(engine, flow) {
203
- const env = engine.saveGame();
204
- const flowSave = env.flows[flow.id];
205
207
  const out = {};
206
- const bag = (prefix, values) => {
207
- for (const [name, value] of Object.entries(values ?? {})) out[`${prefix}.${name}`] = value;
208
- };
209
- bag("story", env.shared.props.story);
210
- bag("story", flowSave?.props.story);
211
- for (const kind of ["box", "deck", "hand", "value"]) {
212
- for (const [id, values] of Object.entries(env.shared.props[kind])) bag(`${kind}.${id}`, values);
213
- for (const [id, values] of Object.entries(flowSave?.props[kind] ?? {})) bag(`${kind}.${id}`, values);
208
+ for (const { bag } of [...engine.listBags(), ...flow.listBags()]) {
209
+ for (const row of bag.rows()) {
210
+ if (row.value !== void 0) out[row.path] = row.value;
211
+ }
214
212
  }
215
- Object.assign(out, extraState(env.flows[flow.id]));
213
+ Object.assign(out, extraState(engine.saveGame().flows[flow.id]));
216
214
  return out;
217
215
  }
218
216
  function extraState(saved) {
@@ -666,7 +664,8 @@ import { describeBundle } from "@storylet-studio/runtime";
666
664
  var showVal2 = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
667
665
  function formatPropertySummary(p) {
668
666
  const options = p.values !== void 0 && p.values.length > 0 ? ` [${p.values.join(", ")}]` : "";
669
- return `${p.name}: ${p.type} = ${showVal2(p.default)}${options}`;
667
+ const durable = p.durable === true ? " (durable)" : "";
668
+ return `${p.name}: ${p.type} = ${showVal2(p.default)}${options}${durable}`;
670
669
  }
671
670
  function formatScopeLabel(scope) {
672
671
  if (scope.scope === "world" || scope.scope === "story") return scope.scope;
@@ -722,7 +721,8 @@ function createBundleInspector(bundle, opts = {}) {
722
721
  }
723
722
  for (const hand of description.hands) {
724
723
  const template = hand.template !== void 0 ? `, template ${hand.template}` : "";
725
- line(handsBody, `${hand.gameId}: box ${hand.box}, slots ${hand.slots}${template}` + (hand.title !== void 0 ? ` - ${hand.title}` : ""));
724
+ const moves = hand.movable === void 0 ? "" : `, moves ${hand.movable.map((m) => `${m.group} from ${m.from}`).join(" and ")}`;
725
+ line(handsBody, `${hand.gameId}: box ${hand.box}, slots ${hand.slots}${template}${moves}` + (hand.title !== void 0 ? ` - ${hand.title}` : ""));
726
726
  }
727
727
  const tagsBody = fold(el, "Tags by box (peek criteria)", open);
728
728
  tagsBody.className = "sl-tags";
@@ -751,7 +751,7 @@ function createBundleInspector(bundle, opts = {}) {
751
751
  mapsBody.className = "sl-maps";
752
752
  line(mapsBody, "Geometry the build was asked to carry. The engine ignores it.", "sl-line sl-note");
753
753
  for (const map of description.maps) {
754
- line(mapsBody, `${map.box} - ${map.group}: zones ${map.zones}, pictures ${map.backgrounds}`);
754
+ line(mapsBody, `${map.box} - ${map.group}: zones ${map.zones}, pictures ${map.backgrounds}, sites ${map.sites}`);
755
755
  }
756
756
  }
757
757
  const countsBody = fold(el, "Counts", open);
@@ -759,7 +759,7 @@ function createBundleInspector(bundle, opts = {}) {
759
759
  line(countsBody, `boxes ${totals.boxes} - decks ${totals.decks} - cards ${totals.cards}`);
760
760
  line(countsBody, `hands ${totals.hands} - templates ${totals.templates} - tag groups ${totals.tagGroups}`);
761
761
  for (const box of description.boxes) {
762
- line(countsBody, `${box.gameId}: decks ${box.counts.decks}, cards ${box.counts.cards}, hands ${box.counts.hands}, templates ${box.counts.templates}, tag groups ${box.counts.tagGroups}, ranking.specificity ${box.ranking.specificity}`);
762
+ line(countsBody, `${box.gameId}: decks ${box.counts.decks}, cards ${box.counts.cards}, hands ${box.counts.hands}, templates ${box.counts.templates}, tag groups ${box.counts.tagGroups}, ranking.specificity ${box.ranking.specificity}` + (box.turn !== void 0 ? `, turn = ${box.turn.seconds}s` : "") + (box.durableCards !== void 0 ? `, durable cards ${box.durableCards}` : ""));
763
763
  }
764
764
  (opts.container ?? document.body).append(el);
765
765
  return {
@@ -968,7 +968,7 @@ function createWorldContainer(bundle) {
968
968
  resolver: {
969
969
  get: (n) => bag.get(n),
970
970
  set: (n, v) => {
971
- bag.set(n, v);
971
+ bag.set(n, v, { host: true });
972
972
  }
973
973
  },
974
974
  bag,