@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 +15 -228
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -18
- package/dist/index.d.ts +1 -18
- package/dist/index.js +14 -224
- package/dist/index.js.map +1 -1
- package/dist/storyletengine.min.js.map +1 -1
- package/package.json +9 -3
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: () =>
|
|
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
|
|
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
|
|
482
|
-
row.el.style.display =
|
|
483
|
-
any = any ||
|
|
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),
|