@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.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/package.json +8 -2
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
|
|
24
|
+
function createStateLogger(engine, flow, opts = {}) {
|
|
232
25
|
const id = flow.id;
|
|
233
26
|
const live = () => engine.getFlow(id);
|
|
234
|
-
return
|
|
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
|
|
439
|
-
row.el.style.display =
|
|
440
|
-
any = any ||
|
|
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
|
|
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
|
-
|
|
781
|
+
createKernelStateLogger,
|
|
992
782
|
createLiveLink,
|
|
993
783
|
createPropertyInspector,
|
|
994
|
-
|
|
784
|
+
createStateLogger,
|
|
995
785
|
createWorldContainer,
|
|
996
786
|
deserializeState,
|
|
997
787
|
diffState,
|