@storylet-studio/play-helpers 0.8.1 → 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.d.cts CHANGED
@@ -2,6 +2,7 @@ import { Engine, Flow, LogEntry, EngineLogEntry, BundleDescription, PropertySumm
2
2
  import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag as PropertyBag$1 } from '@wildwinter/scoperegistry';
3
3
  export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot, createStateLogger as createKernelStateLogger, diffState } from '@wildwinter/scoperegistry';
4
4
  import { PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
5
+ import { ScopeResolver } from '@wildwinter/expr';
5
6
 
6
7
  /** The full flattened snapshot of ONE FLOW's view - the shared partitions
7
8
  * plus that flow's own - plus its turns / cooldowns / board. @world is not
@@ -224,24 +225,6 @@ type LiveBundleResult =
224
225
  */
225
226
  declare function applyLiveBundle(engine: Engine, bundleJson: string, opts?: EngineOptions): LiveBundleResult;
226
227
 
227
- type ScalarValue = boolean | number | string | string[];
228
-
229
- /**
230
- * A scope backed by a host resolver rather than a static bag - the basis for
231
- * *foreign* scopes (e.g. `@game` / `@world`) whose values live in a host or
232
- * another engine and are read (and optionally written) at runtime. The
233
- * evaluator treats a missing property the same as a bag does (the scope's
234
- * missing-policy decides false-vs-throw). A scope entirely absent from the
235
- * EvalContext still resolves to false regardless.
236
- */
237
- interface ScopeResolver {
238
- /** Read a property's value, or undefined if the scope does not have it. */
239
- get(name: string): ScalarValue | undefined;
240
- /** Write a property (omit for a read-only scope). The core never calls this;
241
- * it is for host/runtime effect application (e.g. a state container's `set`). */
242
- set?(name: string, value: ScalarValue): void;
243
- }
244
-
245
228
  interface WorldContainer {
246
229
  /** Pass as `new Engine(bundle, { world: container.resolver })`. */
247
230
  resolver: ScopeResolver;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Engine, Flow, LogEntry, EngineLogEntry, BundleDescription, PropertySumm
2
2
  import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag as PropertyBag$1 } from '@wildwinter/scoperegistry';
3
3
  export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot, createStateLogger as createKernelStateLogger, diffState } from '@wildwinter/scoperegistry';
4
4
  import { PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
5
+ import { ScopeResolver } from '@wildwinter/expr';
5
6
 
6
7
  /** The full flattened snapshot of ONE FLOW's view - the shared partitions
7
8
  * plus that flow's own - plus its turns / cooldowns / board. @world is not
@@ -224,24 +225,6 @@ type LiveBundleResult =
224
225
  */
225
226
  declare function applyLiveBundle(engine: Engine, bundleJson: string, opts?: EngineOptions): LiveBundleResult;
226
227
 
227
- type ScalarValue = boolean | number | string | string[];
228
-
229
- /**
230
- * A scope backed by a host resolver rather than a static bag - the basis for
231
- * *foreign* scopes (e.g. `@game` / `@world`) whose values live in a host or
232
- * another engine and are read (and optionally written) at runtime. The
233
- * evaluator treats a missing property the same as a bag does (the scope's
234
- * missing-policy decides false-vs-throw). A scope entirely absent from the
235
- * EvalContext still resolves to false regardless.
236
- */
237
- interface ScopeResolver {
238
- /** Read a property's value, or undefined if the scope does not have it. */
239
- get(name: string): ScalarValue | undefined;
240
- /** Write a property (omit for a read-only scope). The core never calls this;
241
- * it is for host/runtime effect application (e.g. a state container's `set`). */
242
- set?(name: string, value: ScalarValue): void;
243
- }
244
-
245
228
  interface WorldContainer {
246
229
  /** Pass as `new Engine(bundle, { world: container.resolver })`. */
247
230
  resolver: ScopeResolver;
package/dist/index.js CHANGED
@@ -1,215 +1,8 @@
1
- // ../../../expr/packages/scoperegistry/src/state-logger.ts
2
- function diffState(prev, next) {
3
- const changes = [];
4
- const paths = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
5
- for (const path of [...paths].sort()) {
6
- const from = prev[path], to = next[path];
7
- if (JSON.stringify(from) !== JSON.stringify(to)) changes.push({ path, from, to });
8
- }
9
- return changes;
10
- }
11
- var show = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
12
- var prefixOf = (m) => m.pathPrefix ?? m.bag.pathPrefix;
13
- function createStateLogger(adapter, opts = {}) {
14
- const sink = opts.sink ?? ((line2) => console.log(line2));
15
- const label = opts.label ?? "";
16
- const emit = (c) => {
17
- sink(`${label}${c.path}: ${show(c.from)} -> ${show(c.to)}`);
18
- };
19
- const full = () => {
20
- const out = {};
21
- for (const m of adapter.mounts()) {
22
- const prefix = prefixOf(m);
23
- for (const [name, value] of Object.entries(m.bag.values)) out[prefix + name] = value;
24
- }
25
- Object.assign(out, adapter.extra?.() ?? {});
26
- return structuredClone(out);
27
- };
28
- let baseline = full();
29
- let pushed = [];
30
- let mounted = [];
31
- const hook = (prefix, bag) => bag.onAudit((change) => {
32
- const c = structuredClone({ path: prefix + change.name, from: change.prev, to: change.next });
33
- emit(c);
34
- pushed.push(c);
35
- baseline[c.path] = structuredClone(change.next);
36
- });
37
- const mount = () => {
38
- const mounts = adapter.mounts();
39
- const same = mounted.length === mounts.length && mounts.every((m, i) => mounted[i].bag === m.bag);
40
- if (same) return;
41
- for (const m of mounted) m.off();
42
- mounted = mounts.map((m) => ({ bag: m.bag, off: hook(prefixOf(m), m.bag) }));
43
- };
44
- mount();
45
- return {
46
- snapshot: full,
47
- capture() {
48
- const next = full();
49
- const diffed = diffState(baseline, next);
50
- for (const c of diffed) emit(c);
51
- const changes = [...pushed, ...diffed];
52
- pushed = [];
53
- baseline = next;
54
- mount();
55
- return changes;
56
- },
57
- dispose() {
58
- for (const m of mounted) m.off();
59
- mounted = [];
60
- pushed = [];
61
- }
62
- };
63
- }
64
-
65
- // ../../../expr/packages/scoperegistry/src/index.ts
66
- var PropertyBag = class _PropertyBag {
67
- /** The live values record (stable identity across reseed, so an
68
- * EvalContext built over it stays valid). Read-path for evaluation;
69
- * writes go through `set` so the firing rule applies. */
70
- values = {};
71
- decls = /* @__PURE__ */ new Map();
72
- subscribers = /* @__PURE__ */ new Set();
73
- auditors = /* @__PURE__ */ new Set();
74
- /** Name normalisation policy: lowercase by default (the registry's
75
- * long-standing contract); a product whose names are case-significant
76
- * passes identity. */
77
- norm;
78
- /** The address prefix this bag's rows carry, separator included (`@`,
79
- * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */
80
- pathPrefix;
81
- constructor(declarations = [], opts) {
82
- this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
83
- this.pathPrefix = opts?.pathPrefix ?? "";
84
- this.seed(declarations);
85
- }
86
- seed(declarations) {
87
- for (const d of declarations) {
88
- const name = this.norm(d.name);
89
- this.decls.set(name, d);
90
- this.values[name] = structuredClone(d.default ?? defaultFor(d));
91
- }
92
- }
93
- get(name) {
94
- return this.values[this.norm(name)];
95
- }
96
- /** A name as this bag keys it: its normalisation policy applied. The registry
97
- * uses it to key quality ladders and the validation schema the bag's own way,
98
- * so a case-significant (identity) bag is not quietly folded to lower case
99
- * one layer up. */
100
- normalise(name) {
101
- return this.norm(name);
102
- }
103
- /** Write a property. Engine writes (the default) notify subscribers;
104
- * pass `silent: true` for a host write, which reaches only the audit
105
- * hook. Throws on a read-only property unless the caller says it is the
106
- * HOST (`host: true`), for whom `writable: false` was never a rule - it is
107
- * the story's promise, not the game's. `silent` and `host` are separate on
108
- * purpose: one is about who hears the write, the other about who may make
109
- * it. Returns the change. */
110
- set(name, value, opts) {
111
- const n = this.norm(name);
112
- if (!opts?.host && this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
113
- const change = {
114
- name: n,
115
- prev: this.values[n],
116
- next: value,
117
- silent: opts?.silent ?? false,
118
- reason: opts?.reason
119
- };
120
- this.values[n] = value;
121
- for (const audit of this.auditors) audit(change);
122
- if (!change.silent) for (const fn of this.subscribers) fn(change);
123
- return change;
124
- }
125
- /** Notified of engine (non-silent) writes. Returns the unsubscribe. */
126
- subscribe(fn) {
127
- this.subscribers.add(fn);
128
- return () => this.subscribers.delete(fn);
129
- }
130
- /** Notified of EVERY write, silent or not. Returns the unsubscribe. */
131
- onAudit(fn) {
132
- this.auditors.add(fn);
133
- return () => this.auditors.delete(fn);
134
- }
135
- /** Examiner rows: the declared surface only (stray values are storage,
136
- * not surface). */
137
- rows() {
138
- return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name, this.pathPrefix));
139
- }
140
- declarations() {
141
- return [...this.decls.values()];
142
- }
143
- /** The one sanctioned copy door: values deep-copied, declarations
144
- * duplicated, the normalisation policy carried, subscriptions NOT
145
- * carried. */
146
- clone() {
147
- const c = new _PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });
148
- c.decls = new Map(this.decls);
149
- Object.assign(c.values, structuredClone(this.values));
150
- return c;
151
- }
152
- /** Clear and re-seed from new declarations, in place (the values record
153
- * keeps its identity, so contexts built over it stay valid). */
154
- reseed(declarations) {
155
- for (const k of Object.keys(this.values)) delete this.values[k];
156
- this.decls.clear();
157
- this.seed(declarations);
158
- }
159
- /** Bare values, ready to embed in a product's save. */
160
- save() {
161
- return structuredClone(this.values);
162
- }
163
- /** Lay saved values over the current ones (call after a fresh seed:
164
- * orphans land as strays, new declarations keep their defaults; the
165
- * product decides whether to prune). Does not fire events. */
166
- load(values) {
167
- for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
168
- }
169
- };
170
- function rowFor(d, value, writable, name, pathPrefix = "") {
171
- const rowName = name ?? d.name.toLowerCase();
172
- return {
173
- name: rowName,
174
- path: pathPrefix + rowName,
175
- type: d.type,
176
- value,
177
- default: d.default ?? defaultFor(d),
178
- ...d.values !== void 0 ? { values: d.values } : {},
179
- // `stages` was added to the row so an examiner could offer a quality's ladder
180
- // instead of a free-text box, and then never populated here: every quality row
181
- // this function built came out without one. Fixed 2026-09-02.
182
- ...d.stages !== void 0 ? { stages: d.stages } : {},
183
- writable: writable ?? d.writable ?? true
184
- };
185
- }
186
- function defaultFor(d) {
187
- if (d.default !== void 0) return d.default;
188
- switch (d.type) {
189
- case "boolean":
190
- return false;
191
- case "number":
192
- return 0;
193
- case "string":
194
- return "";
195
- case "enum":
196
- return d.values?.[0] ?? "";
197
- case "flags":
198
- return [];
199
- // A quality starts at the first rung of its ladder.
200
- case "quality":
201
- return d.stages?.[0] ?? "";
202
- // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle
203
- // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the
204
- // union does not have. Falling off the switch would seed `undefined`, which is not a
205
- // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the
206
- // guard and this one did not, which is the drift you only find by removing a duplicate.
207
- default:
208
- return false;
209
- }
210
- }
211
-
212
1
  // src/logger.ts
2
+ import {
3
+ createStateLogger as createKernelStateLogger,
4
+ diffState
5
+ } from "@wildwinter/scoperegistry";
213
6
  function snapshotState(engine, flow) {
214
7
  const out = {};
215
8
  for (const { bag } of [...engine.listBags(), ...flow.listBags()]) {
@@ -228,10 +21,10 @@ function extraState(saved) {
228
21
  for (const [handId, cards] of Object.entries(saved.board)) out[`board:${handId}`] = [...cards];
229
22
  return out;
230
23
  }
231
- function createStateLogger2(engine, flow, opts = {}) {
24
+ function createStateLogger(engine, flow, opts = {}) {
232
25
  const id = flow.id;
233
26
  const live = () => engine.getFlow(id);
234
- return createStateLogger({
27
+ return createKernelStateLogger({
235
28
  // A BagMount's `prefix` ("story", "deck.<id>") is the engine's label for the mount;
236
29
  // the kernel composes paths from the BAG's own pathPrefix ("story.", "deck.<id>.")
237
30
  // and needs none passed. Same strings, one owner.
@@ -240,12 +33,8 @@ function createStateLogger2(engine, flow, opts = {}) {
240
33
  }, opts);
241
34
  }
242
35
 
243
- // ../model/src/index.ts
244
- var SAVE_SCHEMA = "storylets/save@2";
245
- var SAVE_SCHEMA_V1 = "storylets/save@1";
246
- var SAVEFILE_SCHEMA = "storylets/savefile@1";
247
-
248
36
  // src/save.ts
37
+ import { SAVEFILE_SCHEMA, SAVE_SCHEMA, SAVE_SCHEMA_V1 } from "@storylet-studio/model";
249
38
  function serializeState(engine, world) {
250
39
  return JSON.stringify(saveState(engine, world), null, 2);
251
40
  }
@@ -435,9 +224,9 @@ function createPropertyInspector(engine, flow, opts = {}) {
435
224
  for (const group of groups) {
436
225
  let any = false;
437
226
  for (const row of group.rows) {
438
- const show2 = q === "" || row.text.includes(q);
439
- row.el.style.display = show2 ? "" : "none";
440
- any = any || show2;
227
+ const show = q === "" || row.text.includes(q);
228
+ row.el.style.display = show ? "" : "none";
229
+ any = any || show;
441
230
  }
442
231
  group.el.style.display = any ? "" : "none";
443
232
  }
@@ -970,8 +759,9 @@ function applyLiveBundle(engine, bundleJson, opts = {}) {
970
759
  }
971
760
 
972
761
  // src/world.ts
762
+ import { PropertyBag as StateBag } from "@wildwinter/scoperegistry";
973
763
  function createWorldContainer(bundle) {
974
- const bag = new PropertyBag(bundle.world.properties, { normalise: (n) => n });
764
+ const bag = new StateBag(bundle.world.properties, { normalise: (n) => n });
975
765
  return {
976
766
  resolver: {
977
767
  get: (n) => bag.get(n),
@@ -988,10 +778,10 @@ export {
988
778
  applyLiveBundle,
989
779
  boardFrame,
990
780
  createBundleInspector,
991
- createStateLogger as createKernelStateLogger,
781
+ createKernelStateLogger,
992
782
  createLiveLink,
993
783
  createPropertyInspector,
994
- createStateLogger2 as createStateLogger,
784
+ createStateLogger,
995
785
  createWorldContainer,
996
786
  deserializeState,
997
787
  diffState,