@storylet-studio/play-helpers 0.8.0 → 0.8.2

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
@@ -23,13 +23,13 @@ __export(index_exports, {
23
23
  applyLiveBundle: () => applyLiveBundle,
24
24
  boardFrame: () => boardFrame,
25
25
  createBundleInspector: () => createBundleInspector,
26
- createKernelStateLogger: () => createStateLogger,
26
+ createKernelStateLogger: () => import_scoperegistry.createStateLogger,
27
27
  createLiveLink: () => createLiveLink,
28
28
  createPropertyInspector: () => createPropertyInspector,
29
- createStateLogger: () => createStateLogger2,
29
+ createStateLogger: () => createStateLogger,
30
30
  createWorldContainer: () => createWorldContainer,
31
31
  deserializeState: () => deserializeState,
32
- diffState: () => diffState,
32
+ diffState: () => import_scoperegistry.diffState,
33
33
  ensureInspectorStyle: () => ensureInspectorStyle,
34
34
  formatLogEntry: () => formatLogEntry,
35
35
  formatPropertySummary: () => formatPropertySummary,
@@ -41,218 +41,8 @@ __export(index_exports, {
41
41
  });
42
42
  module.exports = __toCommonJS(index_exports);
43
43
 
44
- // ../../../expr/packages/scoperegistry/src/state-logger.ts
45
- function diffState(prev, next) {
46
- const changes = [];
47
- const paths = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
48
- for (const path of [...paths].sort()) {
49
- const from = prev[path], to = next[path];
50
- if (JSON.stringify(from) !== JSON.stringify(to)) changes.push({ path, from, to });
51
- }
52
- return changes;
53
- }
54
- var show = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
55
- var prefixOf = (m) => m.pathPrefix ?? m.bag.pathPrefix;
56
- function createStateLogger(adapter, opts = {}) {
57
- const sink = opts.sink ?? ((line2) => console.log(line2));
58
- const label = opts.label ?? "";
59
- const emit = (c) => {
60
- sink(`${label}${c.path}: ${show(c.from)} -> ${show(c.to)}`);
61
- };
62
- const full = () => {
63
- const out = {};
64
- for (const m of adapter.mounts()) {
65
- const prefix = prefixOf(m);
66
- for (const [name, value] of Object.entries(m.bag.values)) out[prefix + name] = value;
67
- }
68
- Object.assign(out, adapter.extra?.() ?? {});
69
- return structuredClone(out);
70
- };
71
- let baseline = full();
72
- let pushed = [];
73
- let mounted = [];
74
- const hook = (prefix, bag) => bag.onAudit((change) => {
75
- const c = structuredClone({ path: prefix + change.name, from: change.prev, to: change.next });
76
- emit(c);
77
- pushed.push(c);
78
- baseline[c.path] = structuredClone(change.next);
79
- });
80
- const mount = () => {
81
- const mounts = adapter.mounts();
82
- const same = mounted.length === mounts.length && mounts.every((m, i) => mounted[i].bag === m.bag);
83
- if (same) return;
84
- for (const m of mounted) m.off();
85
- mounted = mounts.map((m) => ({ bag: m.bag, off: hook(prefixOf(m), m.bag) }));
86
- };
87
- mount();
88
- return {
89
- snapshot: full,
90
- capture() {
91
- const next = full();
92
- const diffed = diffState(baseline, next);
93
- for (const c of diffed) emit(c);
94
- const changes = [...pushed, ...diffed];
95
- pushed = [];
96
- baseline = next;
97
- mount();
98
- return changes;
99
- },
100
- dispose() {
101
- for (const m of mounted) m.off();
102
- mounted = [];
103
- pushed = [];
104
- }
105
- };
106
- }
107
-
108
- // ../../../expr/packages/scoperegistry/src/index.ts
109
- var PropertyBag = class _PropertyBag {
110
- /** The live values record (stable identity across reseed, so an
111
- * EvalContext built over it stays valid). Read-path for evaluation;
112
- * writes go through `set` so the firing rule applies. */
113
- values = {};
114
- decls = /* @__PURE__ */ new Map();
115
- subscribers = /* @__PURE__ */ new Set();
116
- auditors = /* @__PURE__ */ new Set();
117
- /** Name normalisation policy: lowercase by default (the registry's
118
- * long-standing contract); a product whose names are case-significant
119
- * passes identity. */
120
- norm;
121
- /** The address prefix this bag's rows carry, separator included (`@`,
122
- * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */
123
- pathPrefix;
124
- constructor(declarations = [], opts) {
125
- this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
126
- this.pathPrefix = opts?.pathPrefix ?? "";
127
- this.seed(declarations);
128
- }
129
- seed(declarations) {
130
- for (const d of declarations) {
131
- const name = this.norm(d.name);
132
- this.decls.set(name, d);
133
- this.values[name] = structuredClone(d.default ?? defaultFor(d));
134
- }
135
- }
136
- get(name) {
137
- return this.values[this.norm(name)];
138
- }
139
- /** A name as this bag keys it: its normalisation policy applied. The registry
140
- * uses it to key quality ladders and the validation schema the bag's own way,
141
- * so a case-significant (identity) bag is not quietly folded to lower case
142
- * one layer up. */
143
- normalise(name) {
144
- return this.norm(name);
145
- }
146
- /** Write a property. Engine writes (the default) notify subscribers;
147
- * pass `silent: true` for a host write, which reaches only the audit
148
- * hook. Throws on a read-only property unless the caller says it is the
149
- * HOST (`host: true`), for whom `writable: false` was never a rule - it is
150
- * the story's promise, not the game's. `silent` and `host` are separate on
151
- * purpose: one is about who hears the write, the other about who may make
152
- * it. Returns the change. */
153
- set(name, value, opts) {
154
- const n = this.norm(name);
155
- if (!opts?.host && this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
156
- const change = {
157
- name: n,
158
- prev: this.values[n],
159
- next: value,
160
- silent: opts?.silent ?? false,
161
- reason: opts?.reason
162
- };
163
- this.values[n] = value;
164
- for (const audit of this.auditors) audit(change);
165
- if (!change.silent) for (const fn of this.subscribers) fn(change);
166
- return change;
167
- }
168
- /** Notified of engine (non-silent) writes. Returns the unsubscribe. */
169
- subscribe(fn) {
170
- this.subscribers.add(fn);
171
- return () => this.subscribers.delete(fn);
172
- }
173
- /** Notified of EVERY write, silent or not. Returns the unsubscribe. */
174
- onAudit(fn) {
175
- this.auditors.add(fn);
176
- return () => this.auditors.delete(fn);
177
- }
178
- /** Examiner rows: the declared surface only (stray values are storage,
179
- * not surface). */
180
- rows() {
181
- return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name, this.pathPrefix));
182
- }
183
- declarations() {
184
- return [...this.decls.values()];
185
- }
186
- /** The one sanctioned copy door: values deep-copied, declarations
187
- * duplicated, the normalisation policy carried, subscriptions NOT
188
- * carried. */
189
- clone() {
190
- const c = new _PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });
191
- c.decls = new Map(this.decls);
192
- Object.assign(c.values, structuredClone(this.values));
193
- return c;
194
- }
195
- /** Clear and re-seed from new declarations, in place (the values record
196
- * keeps its identity, so contexts built over it stay valid). */
197
- reseed(declarations) {
198
- for (const k of Object.keys(this.values)) delete this.values[k];
199
- this.decls.clear();
200
- this.seed(declarations);
201
- }
202
- /** Bare values, ready to embed in a product's save. */
203
- save() {
204
- return structuredClone(this.values);
205
- }
206
- /** Lay saved values over the current ones (call after a fresh seed:
207
- * orphans land as strays, new declarations keep their defaults; the
208
- * product decides whether to prune). Does not fire events. */
209
- load(values) {
210
- for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
211
- }
212
- };
213
- function rowFor(d, value, writable, name, pathPrefix = "") {
214
- const rowName = name ?? d.name.toLowerCase();
215
- return {
216
- name: rowName,
217
- path: pathPrefix + rowName,
218
- type: d.type,
219
- value,
220
- default: d.default ?? defaultFor(d),
221
- ...d.values !== void 0 ? { values: d.values } : {},
222
- // `stages` was added to the row so an examiner could offer a quality's ladder
223
- // instead of a free-text box, and then never populated here: every quality row
224
- // this function built came out without one. Fixed 2026-09-02.
225
- ...d.stages !== void 0 ? { stages: d.stages } : {},
226
- writable: writable ?? d.writable ?? true
227
- };
228
- }
229
- function defaultFor(d) {
230
- if (d.default !== void 0) return d.default;
231
- switch (d.type) {
232
- case "boolean":
233
- return false;
234
- case "number":
235
- return 0;
236
- case "string":
237
- return "";
238
- case "enum":
239
- return d.values?.[0] ?? "";
240
- case "flags":
241
- return [];
242
- // A quality starts at the first rung of its ladder.
243
- case "quality":
244
- return d.stages?.[0] ?? "";
245
- // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle
246
- // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the
247
- // union does not have. Falling off the switch would seed `undefined`, which is not a
248
- // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the
249
- // guard and this one did not, which is the drift you only find by removing a duplicate.
250
- default:
251
- return false;
252
- }
253
- }
254
-
255
44
  // src/logger.ts
45
+ var import_scoperegistry = require("@wildwinter/scoperegistry");
256
46
  function snapshotState(engine, flow) {
257
47
  const out = {};
258
48
  for (const { bag } of [...engine.listBags(), ...flow.listBags()]) {
@@ -271,10 +61,10 @@ function extraState(saved) {
271
61
  for (const [handId, cards] of Object.entries(saved.board)) out[`board:${handId}`] = [...cards];
272
62
  return out;
273
63
  }
274
- function createStateLogger2(engine, flow, opts = {}) {
64
+ function createStateLogger(engine, flow, opts = {}) {
275
65
  const id = flow.id;
276
66
  const live = () => engine.getFlow(id);
277
- return createStateLogger({
67
+ return (0, import_scoperegistry.createStateLogger)({
278
68
  // A BagMount's `prefix` ("story", "deck.<id>") is the engine's label for the mount;
279
69
  // the kernel composes paths from the BAG's own pathPrefix ("story.", "deck.<id>.")
280
70
  // and needs none passed. Same strings, one owner.
@@ -283,25 +73,21 @@ function createStateLogger2(engine, flow, opts = {}) {
283
73
  }, opts);
284
74
  }
285
75
 
286
- // ../model/src/index.ts
287
- var SAVE_SCHEMA = "storylets/save@2";
288
- var SAVE_SCHEMA_V1 = "storylets/save@1";
289
- var SAVEFILE_SCHEMA = "storylets/savefile@1";
290
-
291
76
  // src/save.ts
77
+ var import_model = require("@storylet-studio/model");
292
78
  function serializeState(engine, world) {
293
79
  return JSON.stringify(saveState(engine, world), null, 2);
294
80
  }
295
81
  function saveState(engine, world) {
296
82
  return {
297
- schema: SAVEFILE_SCHEMA,
83
+ schema: import_model.SAVEFILE_SCHEMA,
298
84
  engine: engine.saveGame(),
299
85
  ...world !== void 0 ? { world } : {}
300
86
  };
301
87
  }
302
88
  function loadState(engine, file) {
303
- if (!file || typeof file !== "object" || file.schema !== SAVEFILE_SCHEMA || file.engine?.schema !== SAVE_SCHEMA && file.engine?.schema !== SAVE_SCHEMA_V1) {
304
- throw new Error(`not a storylets save (expected schema "${SAVEFILE_SCHEMA}")`);
89
+ if (!file || typeof file !== "object" || file.schema !== import_model.SAVEFILE_SCHEMA || file.engine?.schema !== import_model.SAVE_SCHEMA && file.engine?.schema !== import_model.SAVE_SCHEMA_V1) {
90
+ throw new Error(`not a storylets save (expected schema "${import_model.SAVEFILE_SCHEMA}")`);
305
91
  }
306
92
  engine.loadGame(file.engine);
307
93
  return file.world;
@@ -478,9 +264,9 @@ function createPropertyInspector(engine, flow, opts = {}) {
478
264
  for (const group of groups) {
479
265
  let any = false;
480
266
  for (const row of group.rows) {
481
- const show2 = q === "" || row.text.includes(q);
482
- row.el.style.display = show2 ? "" : "none";
483
- any = any || show2;
267
+ const show = q === "" || row.text.includes(q);
268
+ row.el.style.display = show ? "" : "none";
269
+ any = any || show;
484
270
  }
485
271
  group.el.style.display = any ? "" : "none";
486
272
  }
@@ -1013,8 +799,9 @@ function applyLiveBundle(engine, bundleJson, opts = {}) {
1013
799
  }
1014
800
 
1015
801
  // src/world.ts
802
+ var import_scoperegistry2 = require("@wildwinter/scoperegistry");
1016
803
  function createWorldContainer(bundle) {
1017
- const bag = new PropertyBag(bundle.world.properties, { normalise: (n) => n });
804
+ const bag = new import_scoperegistry2.PropertyBag(bundle.world.properties, { normalise: (n) => n });
1018
805
  return {
1019
806
  resolver: {
1020
807
  get: (n) => bag.get(n),