@storylet-studio/play-helpers 0.4.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/README.md +55 -0
- package/dist/index.cjs +1043 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +251 -0
- package/dist/index.d.ts +251 -0
- package/dist/index.js +999 -0
- package/dist/index.js.map +1 -0
- package/dist/storyletengine.min.js +33 -0
- package/dist/storyletengine.min.js.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
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
|
+
/** Write a property. Engine writes (the default) notify subscribers;
|
|
97
|
+
* pass `silent: true` for a host write, which reaches only the audit
|
|
98
|
+
* hook. Throws on a read-only property. Returns the change. */
|
|
99
|
+
set(name, value, opts) {
|
|
100
|
+
const n = this.norm(name);
|
|
101
|
+
if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
102
|
+
const change = {
|
|
103
|
+
name: n,
|
|
104
|
+
prev: this.values[n],
|
|
105
|
+
next: value,
|
|
106
|
+
silent: opts?.silent ?? false,
|
|
107
|
+
reason: opts?.reason
|
|
108
|
+
};
|
|
109
|
+
this.values[n] = value;
|
|
110
|
+
for (const audit of this.auditors) audit(change);
|
|
111
|
+
if (!change.silent) for (const fn of this.subscribers) fn(change);
|
|
112
|
+
return change;
|
|
113
|
+
}
|
|
114
|
+
/** Notified of engine (non-silent) writes. Returns the unsubscribe. */
|
|
115
|
+
subscribe(fn) {
|
|
116
|
+
this.subscribers.add(fn);
|
|
117
|
+
return () => this.subscribers.delete(fn);
|
|
118
|
+
}
|
|
119
|
+
/** Notified of EVERY write, silent or not. Returns the unsubscribe. */
|
|
120
|
+
onAudit(fn) {
|
|
121
|
+
this.auditors.add(fn);
|
|
122
|
+
return () => this.auditors.delete(fn);
|
|
123
|
+
}
|
|
124
|
+
/** Examiner rows: the declared surface only (stray values are storage,
|
|
125
|
+
* not surface). */
|
|
126
|
+
rows() {
|
|
127
|
+
return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name, this.pathPrefix));
|
|
128
|
+
}
|
|
129
|
+
declarations() {
|
|
130
|
+
return [...this.decls.values()];
|
|
131
|
+
}
|
|
132
|
+
/** The one sanctioned copy door: values deep-copied, declarations
|
|
133
|
+
* duplicated, the normalisation policy carried, subscriptions NOT
|
|
134
|
+
* carried. */
|
|
135
|
+
clone() {
|
|
136
|
+
const c = new _PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });
|
|
137
|
+
c.decls = new Map(this.decls);
|
|
138
|
+
Object.assign(c.values, structuredClone(this.values));
|
|
139
|
+
return c;
|
|
140
|
+
}
|
|
141
|
+
/** Clear and re-seed from new declarations, in place (the values record
|
|
142
|
+
* keeps its identity, so contexts built over it stay valid). */
|
|
143
|
+
reseed(declarations) {
|
|
144
|
+
for (const k of Object.keys(this.values)) delete this.values[k];
|
|
145
|
+
this.decls.clear();
|
|
146
|
+
this.seed(declarations);
|
|
147
|
+
}
|
|
148
|
+
/** Bare values, ready to embed in a product's save. */
|
|
149
|
+
save() {
|
|
150
|
+
return structuredClone(this.values);
|
|
151
|
+
}
|
|
152
|
+
/** Lay saved values over the current ones (call after a fresh seed:
|
|
153
|
+
* orphans land as strays, new declarations keep their defaults; the
|
|
154
|
+
* product decides whether to prune). Does not fire events. */
|
|
155
|
+
load(values) {
|
|
156
|
+
for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
function rowFor(d, value, writable, name, pathPrefix = "") {
|
|
160
|
+
const rowName = name ?? d.name.toLowerCase();
|
|
161
|
+
return {
|
|
162
|
+
name: rowName,
|
|
163
|
+
path: pathPrefix + rowName,
|
|
164
|
+
type: d.type,
|
|
165
|
+
value,
|
|
166
|
+
default: d.default ?? defaultFor(d),
|
|
167
|
+
...d.values !== void 0 ? { values: d.values } : {},
|
|
168
|
+
// `stages` was added to the row so an examiner could offer a quality's ladder
|
|
169
|
+
// instead of a free-text box, and then never populated here: every quality row
|
|
170
|
+
// this function built came out without one. Fixed 2026-09-02.
|
|
171
|
+
...d.stages !== void 0 ? { stages: d.stages } : {},
|
|
172
|
+
writable: writable ?? d.writable ?? true
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function defaultFor(d) {
|
|
176
|
+
if (d.default !== void 0) return d.default;
|
|
177
|
+
switch (d.type) {
|
|
178
|
+
case "boolean":
|
|
179
|
+
return false;
|
|
180
|
+
case "number":
|
|
181
|
+
return 0;
|
|
182
|
+
case "string":
|
|
183
|
+
return "";
|
|
184
|
+
case "enum":
|
|
185
|
+
return d.values?.[0] ?? "";
|
|
186
|
+
case "flags":
|
|
187
|
+
return [];
|
|
188
|
+
// A quality starts at the first rung of its ladder.
|
|
189
|
+
case "quality":
|
|
190
|
+
return d.stages?.[0] ?? "";
|
|
191
|
+
// Unreachable for a well-typed declaration, and deliberately present anyway: a bundle
|
|
192
|
+
// is DATA, and a hand-edited or newer-than-this-build one can carry a type string the
|
|
193
|
+
// union does not have. Falling off the switch would seed `undefined`, which is not a
|
|
194
|
+
// ScalarValue and travels a long way before it fails. Patterplay's copy of this had the
|
|
195
|
+
// guard and this one did not, which is the drift you only find by removing a duplicate.
|
|
196
|
+
default:
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/logger.ts
|
|
202
|
+
function snapshotState(engine, flow) {
|
|
203
|
+
const env = engine.saveGame();
|
|
204
|
+
const flowSave = env.flows[flow.id];
|
|
205
|
+
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);
|
|
214
|
+
}
|
|
215
|
+
Object.assign(out, extraState(env.flows[flow.id]));
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
function extraState(saved) {
|
|
219
|
+
const out = {};
|
|
220
|
+
if (saved === void 0) return out;
|
|
221
|
+
for (const [boxId, turn] of Object.entries(saved.turns)) out[`turn:${boxId}`] = turn;
|
|
222
|
+
for (const [cardId, at] of Object.entries(saved.cooldowns)) out[`cooldown:${cardId}`] = at;
|
|
223
|
+
for (const [handId, cards] of Object.entries(saved.board)) out[`board:${handId}`] = [...cards];
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
function createStateLogger2(engine, flow, opts = {}) {
|
|
227
|
+
const id = flow.id;
|
|
228
|
+
const live = () => engine.getFlow(id);
|
|
229
|
+
return createStateLogger({
|
|
230
|
+
// A BagMount's `prefix` ("story", "deck.<id>") is the engine's label for the mount;
|
|
231
|
+
// the kernel composes paths from the BAG's own pathPrefix ("story.", "deck.<id>.")
|
|
232
|
+
// and needs none passed. Same strings, one owner.
|
|
233
|
+
mounts: () => [...engine.listBags(), ...live()?.listBags() ?? []].map(({ bag }) => ({ bag })),
|
|
234
|
+
extra: () => extraState(engine.saveGame().flows[id])
|
|
235
|
+
}, opts);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ../model/src/index.ts
|
|
239
|
+
var SAVE_SCHEMA = "storylets/save@1";
|
|
240
|
+
var SAVEFILE_SCHEMA = "storylets/savefile@1";
|
|
241
|
+
|
|
242
|
+
// src/save.ts
|
|
243
|
+
function serializeState(engine, world) {
|
|
244
|
+
return JSON.stringify(saveState(engine, world), null, 2);
|
|
245
|
+
}
|
|
246
|
+
function saveState(engine, world) {
|
|
247
|
+
return {
|
|
248
|
+
schema: SAVEFILE_SCHEMA,
|
|
249
|
+
engine: engine.saveGame(),
|
|
250
|
+
...world !== void 0 ? { world } : {}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function loadState(engine, file) {
|
|
254
|
+
if (!file || typeof file !== "object" || file.schema !== SAVEFILE_SCHEMA || file.engine?.schema !== SAVE_SCHEMA) {
|
|
255
|
+
throw new Error(`not a storylets save (expected schema "${SAVEFILE_SCHEMA}")`);
|
|
256
|
+
}
|
|
257
|
+
engine.loadGame(file.engine);
|
|
258
|
+
return file.world;
|
|
259
|
+
}
|
|
260
|
+
function deserializeState(engine, json) {
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(json);
|
|
264
|
+
} catch {
|
|
265
|
+
throw new Error("not valid JSON");
|
|
266
|
+
}
|
|
267
|
+
return loadState(engine, parsed);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/inspector.ts
|
|
271
|
+
var STYLE_ID = "sl-inspector-style";
|
|
272
|
+
var CSS = `
|
|
273
|
+
.sl-insp { font: 12px system-ui, sans-serif; color: var(--ink, #222); background: var(--surface, #fafafa);
|
|
274
|
+
border: 1px solid var(--line, #ccc); border-radius: 8px; padding: 10px 12px; max-width: 26rem; }
|
|
275
|
+
.sl-insp h3 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
276
|
+
color: var(--muted, #666); }
|
|
277
|
+
.sl-insp .sl-head { display: flex; align-items: baseline; gap: 6px; }
|
|
278
|
+
.sl-insp .sl-head h3 { flex: 1; }
|
|
279
|
+
.sl-insp .sl-save, .sl-insp .sl-load { font: inherit; font-size: 11px; padding: 1px 6px; cursor: pointer; }
|
|
280
|
+
.sl-insp .sl-filter { display: block; width: 100%; box-sizing: border-box; margin: 0 0 6px; }
|
|
281
|
+
.sl-insp .sl-group { margin: 8px 0 2px; font-weight: 600; font-size: 11px; color: var(--muted, #666); }
|
|
282
|
+
.sl-insp .sl-section { margin: 10px 0 2px; font-weight: 600; font-size: 11px; text-transform: uppercase;
|
|
283
|
+
letter-spacing: 0.06em; color: var(--muted, #666); }
|
|
284
|
+
.sl-insp .sl-line { padding: 1px 0; }
|
|
285
|
+
.sl-insp .sl-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
286
|
+
.sl-insp .sl-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
287
|
+
.sl-insp input[type="text"], .sl-insp input[type="number"], .sl-insp select {
|
|
288
|
+
font: inherit; width: 9rem; padding: 1px 4px; }
|
|
289
|
+
.sl-insp .sl-reset { border: 0; background: none; cursor: pointer; color: var(--muted, #666); }
|
|
290
|
+
.sl-insp .sl-reset:disabled { opacity: 0.3; cursor: default; }
|
|
291
|
+
.sl-insp .sl-logbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 2px 0; }
|
|
292
|
+
.sl-insp .sl-logbar label { display: inline-flex; align-items: center; gap: 2px; }
|
|
293
|
+
.sl-insp .sl-logbar button { font: inherit; font-size: 11px; padding: 1px 6px; cursor: pointer; }
|
|
294
|
+
.sl-insp .sl-log { font-family: ui-monospace, monospace; font-size: 11px; max-height: 12rem;
|
|
295
|
+
overflow: auto; white-space: pre; border: 1px solid var(--line, #ccc); padding: 4px 6px; }
|
|
296
|
+
.sl-insp details.sl-fold > summary { cursor: pointer; margin: 10px 0 2px; font-weight: 600;
|
|
297
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted, #666); }
|
|
298
|
+
.sl-insp .sl-ident { font-family: ui-monospace, monospace; font-size: 11px; }
|
|
299
|
+
.sl-insp .sl-ident b { font-weight: 600; }
|
|
300
|
+
.sl-insp .sl-note { color: var(--muted, #666); }
|
|
301
|
+
`;
|
|
302
|
+
function ensureInspectorStyle() {
|
|
303
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
304
|
+
const style = document.createElement("style");
|
|
305
|
+
style.id = STYLE_ID;
|
|
306
|
+
style.textContent = CSS;
|
|
307
|
+
document.head.append(style);
|
|
308
|
+
}
|
|
309
|
+
var eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
310
|
+
var LOG_KINDS = ["deal", "play", "write", "evict", "turns", "diagnostic"];
|
|
311
|
+
var LOG_KIND_LABELS = {
|
|
312
|
+
deal: "Deal",
|
|
313
|
+
play: "Play",
|
|
314
|
+
write: "Write",
|
|
315
|
+
evict: "Evict",
|
|
316
|
+
turns: "Turns",
|
|
317
|
+
diagnostic: "Diag"
|
|
318
|
+
};
|
|
319
|
+
var logKindOf = (e) => e.type === "peek" ? "deal" : e.type;
|
|
320
|
+
var showVal = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
|
|
321
|
+
function formatLogEntry(e) {
|
|
322
|
+
return formatLogBody(e, "flow" in e && e.flow ? `${e.flow} ` : "");
|
|
323
|
+
}
|
|
324
|
+
function formatLogBody(e, flow) {
|
|
325
|
+
const stamp = (e.turn !== void 0 ? `[${e.turn}] ` : "[-] ") + flow;
|
|
326
|
+
switch (e.type) {
|
|
327
|
+
case "deal": {
|
|
328
|
+
const dealt = e.cards.filter((c) => c.verdict === "dealt").map((c) => c.id);
|
|
329
|
+
return `${stamp}deal ${e.hand}: ${dealt.length > 0 ? dealt.join(", ") : "(none)"} (${e.cards.length} considered)`;
|
|
330
|
+
}
|
|
331
|
+
case "peek": {
|
|
332
|
+
const crit = Object.entries(e.criteria).map(([g, t]) => `${g}=${t}`).join(", ");
|
|
333
|
+
const listed = e.cards.filter((c) => c.verdict === "dealt").map((c) => c.id);
|
|
334
|
+
return `${stamp}peek ${e.box}${crit ? ` [${crit}]` : ""}: ${listed.length > 0 ? listed.join(", ") : "(none)"} (${e.cards.length} considered)`;
|
|
335
|
+
}
|
|
336
|
+
case "evict":
|
|
337
|
+
return `${stamp}evict ${e.card} from ${e.hand} (${e.reason})`;
|
|
338
|
+
case "play":
|
|
339
|
+
return `${stamp}play ${e.card} -> ${e.outcome}`;
|
|
340
|
+
case "write":
|
|
341
|
+
return `${stamp}write ${e.path}: ${showVal(e.prev)} -> ${showVal(e.value)}`;
|
|
342
|
+
case "turns":
|
|
343
|
+
return `${stamp}turns ${e.box} -> ${e.turn}`;
|
|
344
|
+
default:
|
|
345
|
+
return `${stamp}diagnostic ${e.where}: ${e.message}`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var groupOf = (path) => {
|
|
349
|
+
const parts = path.split(".");
|
|
350
|
+
return parts.length === 3 ? `${parts[0]} ${parts[1]}` : parts[0];
|
|
351
|
+
};
|
|
352
|
+
function createPropertyInspector(engine, flow, opts = {}) {
|
|
353
|
+
ensureInspectorStyle();
|
|
354
|
+
let liveFlow = flow;
|
|
355
|
+
const live = () => liveFlow;
|
|
356
|
+
const el = document.createElement("div");
|
|
357
|
+
el.className = "sl-insp";
|
|
358
|
+
const head = document.createElement("div");
|
|
359
|
+
head.className = "sl-head";
|
|
360
|
+
const h = document.createElement("h3");
|
|
361
|
+
h.textContent = opts.title ?? "Runtime state";
|
|
362
|
+
head.append(h);
|
|
363
|
+
const saveBtn = document.createElement("button");
|
|
364
|
+
saveBtn.type = "button";
|
|
365
|
+
saveBtn.className = "sl-save";
|
|
366
|
+
saveBtn.textContent = "Save state";
|
|
367
|
+
saveBtn.addEventListener("click", () => {
|
|
368
|
+
const blob = new Blob([serializeState(engine)], { type: "application/json" });
|
|
369
|
+
const url = URL.createObjectURL(blob);
|
|
370
|
+
const a = document.createElement("a");
|
|
371
|
+
a.href = url;
|
|
372
|
+
a.download = "save.storyletsave";
|
|
373
|
+
a.click();
|
|
374
|
+
URL.revokeObjectURL(url);
|
|
375
|
+
});
|
|
376
|
+
head.append(saveBtn);
|
|
377
|
+
const filePicker = document.createElement("input");
|
|
378
|
+
filePicker.type = "file";
|
|
379
|
+
filePicker.accept = ".storyletsave,application/json";
|
|
380
|
+
filePicker.hidden = true;
|
|
381
|
+
filePicker.addEventListener("change", () => {
|
|
382
|
+
const file = filePicker.files?.[0];
|
|
383
|
+
if (!file) return;
|
|
384
|
+
const reader = new FileReader();
|
|
385
|
+
reader.onload = () => {
|
|
386
|
+
try {
|
|
387
|
+
deserializeState(engine, String(reader.result));
|
|
388
|
+
liveFlow = engine.getFlow(flow.id) ?? engine.openFlow(flow.id);
|
|
389
|
+
refresh();
|
|
390
|
+
} catch (e) {
|
|
391
|
+
console.error("storylets inspector: load failed:", e instanceof Error ? e.message : e);
|
|
392
|
+
}
|
|
393
|
+
filePicker.value = "";
|
|
394
|
+
};
|
|
395
|
+
reader.readAsText(file);
|
|
396
|
+
});
|
|
397
|
+
const loadBtn = document.createElement("button");
|
|
398
|
+
loadBtn.type = "button";
|
|
399
|
+
loadBtn.className = "sl-load";
|
|
400
|
+
loadBtn.textContent = "Load state";
|
|
401
|
+
loadBtn.addEventListener("click", () => filePicker.click());
|
|
402
|
+
head.append(loadBtn, filePicker);
|
|
403
|
+
el.append(head);
|
|
404
|
+
const filter = document.createElement("input");
|
|
405
|
+
filter.type = "text";
|
|
406
|
+
filter.className = "sl-filter";
|
|
407
|
+
filter.placeholder = "Filter properties";
|
|
408
|
+
el.append(filter);
|
|
409
|
+
const editors = [];
|
|
410
|
+
const groups = [];
|
|
411
|
+
let lastGroup = "";
|
|
412
|
+
for (const row of live().listProperties()) {
|
|
413
|
+
const group = groupOf(row.path);
|
|
414
|
+
if (group !== lastGroup) {
|
|
415
|
+
const g = document.createElement("div");
|
|
416
|
+
g.className = "sl-group";
|
|
417
|
+
g.textContent = group;
|
|
418
|
+
el.append(g);
|
|
419
|
+
groups.push({ el: g, rows: [] });
|
|
420
|
+
lastGroup = group;
|
|
421
|
+
}
|
|
422
|
+
const rowEl = buildRow(live, row, editors);
|
|
423
|
+
groups[groups.length - 1].rows.push({ el: rowEl, text: `${row.name} ${row.path}`.toLowerCase() });
|
|
424
|
+
el.append(rowEl);
|
|
425
|
+
}
|
|
426
|
+
filter.addEventListener("input", () => {
|
|
427
|
+
const q = filter.value.trim().toLowerCase();
|
|
428
|
+
for (const group of groups) {
|
|
429
|
+
let any = false;
|
|
430
|
+
for (const row of group.rows) {
|
|
431
|
+
const show2 = q === "" || row.text.includes(q);
|
|
432
|
+
row.el.style.display = show2 ? "" : "none";
|
|
433
|
+
any = any || show2;
|
|
434
|
+
}
|
|
435
|
+
group.el.style.display = any ? "" : "none";
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
const turnsHead = document.createElement("div");
|
|
439
|
+
turnsHead.className = "sl-section";
|
|
440
|
+
turnsHead.textContent = "Turns (per box)";
|
|
441
|
+
const turnsBody = document.createElement("div");
|
|
442
|
+
turnsBody.className = "sl-turns";
|
|
443
|
+
const boardHead = document.createElement("div");
|
|
444
|
+
boardHead.className = "sl-section";
|
|
445
|
+
boardHead.textContent = "Board";
|
|
446
|
+
const boardBody = document.createElement("div");
|
|
447
|
+
boardBody.className = "sl-board";
|
|
448
|
+
el.append(turnsHead, turnsBody, boardHead, boardBody);
|
|
449
|
+
const check = (text, onChange, cls) => {
|
|
450
|
+
const label = document.createElement("label");
|
|
451
|
+
label.className = cls;
|
|
452
|
+
const box = document.createElement("input");
|
|
453
|
+
box.type = "checkbox";
|
|
454
|
+
box.checked = true;
|
|
455
|
+
box.addEventListener("change", () => onChange(box.checked));
|
|
456
|
+
label.append(box, text);
|
|
457
|
+
return label;
|
|
458
|
+
};
|
|
459
|
+
const buildLogPanel = (which, caption, entriesOf, clear, empty) => {
|
|
460
|
+
const head2 = document.createElement("div");
|
|
461
|
+
head2.className = `sl-section sl-${which}`;
|
|
462
|
+
head2.textContent = caption;
|
|
463
|
+
const bar = document.createElement("div");
|
|
464
|
+
bar.className = `sl-logbar sl-${which}`;
|
|
465
|
+
const body = document.createElement("div");
|
|
466
|
+
body.className = `sl-log sl-${which}`;
|
|
467
|
+
const kindOn = /* @__PURE__ */ new Map();
|
|
468
|
+
const visibleLines = () => entriesOf().filter((e) => kindOn.get(logKindOf(e)) !== false).map(formatLogEntry);
|
|
469
|
+
let autoscroll = true;
|
|
470
|
+
let stampSeen = "";
|
|
471
|
+
const render = (force = false) => {
|
|
472
|
+
const entries = entriesOf();
|
|
473
|
+
const stamp = `${entries.length}:${entries.length > 0 ? entries[entries.length - 1].seq : -1}`;
|
|
474
|
+
if (!force && stamp === stampSeen) return;
|
|
475
|
+
stampSeen = stamp;
|
|
476
|
+
const lines = visibleLines();
|
|
477
|
+
body.textContent = lines.length > 0 ? lines.join("\n") : empty;
|
|
478
|
+
if (autoscroll) body.scrollTop = body.scrollHeight;
|
|
479
|
+
};
|
|
480
|
+
for (const kind of LOG_KINDS) {
|
|
481
|
+
kindOn.set(kind, true);
|
|
482
|
+
bar.append(check(LOG_KIND_LABELS[kind], (on) => {
|
|
483
|
+
kindOn.set(kind, on);
|
|
484
|
+
render(true);
|
|
485
|
+
}, "sl-logkind"));
|
|
486
|
+
}
|
|
487
|
+
bar.append(check("Autoscroll", (on) => {
|
|
488
|
+
autoscroll = on;
|
|
489
|
+
}, "sl-logscroll"));
|
|
490
|
+
const copyBtn = document.createElement("button");
|
|
491
|
+
copyBtn.type = "button";
|
|
492
|
+
copyBtn.className = "sl-logcopy";
|
|
493
|
+
copyBtn.textContent = "Copy";
|
|
494
|
+
copyBtn.title = "Copy the visible (filtered) log to the clipboard";
|
|
495
|
+
copyBtn.addEventListener("click", () => {
|
|
496
|
+
void navigator.clipboard?.writeText(visibleLines().join("\n"));
|
|
497
|
+
});
|
|
498
|
+
const clearBtn = document.createElement("button");
|
|
499
|
+
clearBtn.type = "button";
|
|
500
|
+
clearBtn.className = "sl-logclear";
|
|
501
|
+
clearBtn.textContent = "Clear";
|
|
502
|
+
clearBtn.title = "Drop the retained log entries (cosmetic - no game state changes)";
|
|
503
|
+
clearBtn.addEventListener("click", () => {
|
|
504
|
+
clear();
|
|
505
|
+
render(true);
|
|
506
|
+
});
|
|
507
|
+
bar.append(copyBtn, clearBtn);
|
|
508
|
+
el.append(head2, bar, body);
|
|
509
|
+
return { render };
|
|
510
|
+
};
|
|
511
|
+
const flowLog = buildLogPanel(
|
|
512
|
+
"flow",
|
|
513
|
+
"Log",
|
|
514
|
+
() => live().log(),
|
|
515
|
+
() => live().clearLog(),
|
|
516
|
+
"(empty - new Engine(bundle, { log: true }) retains the flow log)"
|
|
517
|
+
);
|
|
518
|
+
const runLog = buildLogPanel(
|
|
519
|
+
"run",
|
|
520
|
+
"Run log (every flow)",
|
|
521
|
+
() => engine.log(),
|
|
522
|
+
() => engine.clearLog(),
|
|
523
|
+
"(empty - new Engine(bundle, { log: true }) retains the run log)"
|
|
524
|
+
);
|
|
525
|
+
const renderLog = (force = false) => {
|
|
526
|
+
runLog.render(force);
|
|
527
|
+
flowLog.render(force);
|
|
528
|
+
};
|
|
529
|
+
const line2 = (parent, text) => {
|
|
530
|
+
const div = document.createElement("div");
|
|
531
|
+
div.className = "sl-line";
|
|
532
|
+
div.textContent = text;
|
|
533
|
+
parent.append(div);
|
|
534
|
+
};
|
|
535
|
+
const readLive = () => {
|
|
536
|
+
turnsBody.textContent = "";
|
|
537
|
+
for (const box of live().listBoxes()) {
|
|
538
|
+
line2(turnsBody, `${box.title ?? box.gameId}: turn ${box.turn}`);
|
|
539
|
+
}
|
|
540
|
+
boardBody.textContent = "";
|
|
541
|
+
for (const [hand, cards] of Object.entries(live().board())) {
|
|
542
|
+
const names = cards.map((c) => c.title ?? c.gameId);
|
|
543
|
+
line2(boardBody, `${hand}: ${names.length > 0 ? names.join(", ") : "(empty)"}`);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const refresh = () => {
|
|
547
|
+
for (const e of editors) e.read();
|
|
548
|
+
readLive();
|
|
549
|
+
renderLog();
|
|
550
|
+
};
|
|
551
|
+
readLive();
|
|
552
|
+
renderLog(true);
|
|
553
|
+
let timer;
|
|
554
|
+
const pollMs = opts.pollMs ?? 250;
|
|
555
|
+
if (pollMs > 0) timer = setInterval(refresh, pollMs);
|
|
556
|
+
(opts.container ?? document.body).append(el);
|
|
557
|
+
return {
|
|
558
|
+
el,
|
|
559
|
+
refresh,
|
|
560
|
+
destroy() {
|
|
561
|
+
if (timer !== void 0) clearInterval(timer);
|
|
562
|
+
el.remove();
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function buildRow(live, row, editors) {
|
|
567
|
+
const div = document.createElement("div");
|
|
568
|
+
div.className = "sl-row";
|
|
569
|
+
const name = document.createElement("span");
|
|
570
|
+
name.className = "sl-name";
|
|
571
|
+
name.textContent = row.name;
|
|
572
|
+
name.title = row.path;
|
|
573
|
+
const current = () => {
|
|
574
|
+
try {
|
|
575
|
+
return live().getProperty(row.path);
|
|
576
|
+
} catch {
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
let sync = () => {
|
|
581
|
+
};
|
|
582
|
+
const commit = (value) => {
|
|
583
|
+
live().setProperty(row.path, value);
|
|
584
|
+
sync();
|
|
585
|
+
};
|
|
586
|
+
const reset = document.createElement("button");
|
|
587
|
+
reset.type = "button";
|
|
588
|
+
reset.className = "sl-reset";
|
|
589
|
+
reset.textContent = "\u21BA";
|
|
590
|
+
reset.title = "Reset to default";
|
|
591
|
+
reset.addEventListener("click", () => commit(row.default));
|
|
592
|
+
let widget;
|
|
593
|
+
let read;
|
|
594
|
+
const focused = (w) => document.activeElement === w;
|
|
595
|
+
switch (row.type) {
|
|
596
|
+
case "boolean": {
|
|
597
|
+
const input = document.createElement("input");
|
|
598
|
+
input.type = "checkbox";
|
|
599
|
+
input.addEventListener("change", () => commit(input.checked));
|
|
600
|
+
widget = input;
|
|
601
|
+
read = () => {
|
|
602
|
+
if (!focused(input)) input.checked = current() === true;
|
|
603
|
+
};
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case "number": {
|
|
607
|
+
const input = document.createElement("input");
|
|
608
|
+
input.type = "number";
|
|
609
|
+
input.addEventListener("change", () => commit(Number(input.value)));
|
|
610
|
+
widget = input;
|
|
611
|
+
read = () => {
|
|
612
|
+
if (!focused(input)) input.value = String(current() ?? 0);
|
|
613
|
+
};
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "enum":
|
|
617
|
+
case "quality": {
|
|
618
|
+
const select = document.createElement("select");
|
|
619
|
+
for (const v of (row.type === "quality" ? row.stages : row.values) ?? []) {
|
|
620
|
+
const o = document.createElement("option");
|
|
621
|
+
o.value = v;
|
|
622
|
+
o.textContent = v;
|
|
623
|
+
select.append(o);
|
|
624
|
+
}
|
|
625
|
+
select.addEventListener("change", () => commit(select.value));
|
|
626
|
+
widget = select;
|
|
627
|
+
read = () => {
|
|
628
|
+
if (!focused(select)) select.value = String(current() ?? "");
|
|
629
|
+
};
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case "flags": {
|
|
633
|
+
const input = document.createElement("input");
|
|
634
|
+
input.type = "text";
|
|
635
|
+
input.placeholder = "comma, separated, flags";
|
|
636
|
+
input.addEventListener("change", () => commit(input.value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)));
|
|
637
|
+
widget = input;
|
|
638
|
+
read = () => {
|
|
639
|
+
if (!focused(input)) input.value = (current() ?? []).join(", ");
|
|
640
|
+
};
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
default: {
|
|
644
|
+
const input = document.createElement("input");
|
|
645
|
+
input.type = "text";
|
|
646
|
+
input.addEventListener("change", () => commit(input.value));
|
|
647
|
+
widget = input;
|
|
648
|
+
read = () => {
|
|
649
|
+
if (!focused(input)) input.value = String(current() ?? "");
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const readAll = () => {
|
|
654
|
+
read();
|
|
655
|
+
reset.disabled = eq(current(), row.default);
|
|
656
|
+
};
|
|
657
|
+
sync = readAll;
|
|
658
|
+
readAll();
|
|
659
|
+
editors.push({ row, read: readAll });
|
|
660
|
+
div.append(name, widget, reset);
|
|
661
|
+
return div;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/bundle-inspector.ts
|
|
665
|
+
import { describeBundle } from "@storylet-studio/runtime";
|
|
666
|
+
var showVal2 = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
|
|
667
|
+
function formatPropertySummary(p) {
|
|
668
|
+
const options = p.values !== void 0 && p.values.length > 0 ? ` [${p.values.join(", ")}]` : "";
|
|
669
|
+
return `${p.name}: ${p.type} = ${showVal2(p.default)}${options}`;
|
|
670
|
+
}
|
|
671
|
+
function formatScopeLabel(scope) {
|
|
672
|
+
if (scope.scope === "world" || scope.scope === "story") return scope.scope;
|
|
673
|
+
const group = scope.group !== void 0 ? ` (${scope.group})` : "";
|
|
674
|
+
return `${scope.scope} ${scope.owner}${group}`;
|
|
675
|
+
}
|
|
676
|
+
var line = (parent, text, cls = "sl-line") => {
|
|
677
|
+
const div = document.createElement("div");
|
|
678
|
+
div.className = cls;
|
|
679
|
+
div.textContent = text;
|
|
680
|
+
parent.append(div);
|
|
681
|
+
return div;
|
|
682
|
+
};
|
|
683
|
+
var fold = (parent, label, open) => {
|
|
684
|
+
const details = document.createElement("details");
|
|
685
|
+
details.className = "sl-fold";
|
|
686
|
+
details.open = open;
|
|
687
|
+
const summary = document.createElement("summary");
|
|
688
|
+
summary.textContent = label;
|
|
689
|
+
details.append(summary);
|
|
690
|
+
const body = document.createElement("div");
|
|
691
|
+
details.append(body);
|
|
692
|
+
parent.append(details);
|
|
693
|
+
return body;
|
|
694
|
+
};
|
|
695
|
+
function createBundleInspector(bundle, opts = {}) {
|
|
696
|
+
ensureInspectorStyle();
|
|
697
|
+
const description = describeBundle(bundle);
|
|
698
|
+
const open = opts.open ?? true;
|
|
699
|
+
const el = document.createElement("div");
|
|
700
|
+
el.className = "sl-insp";
|
|
701
|
+
const head = document.createElement("div");
|
|
702
|
+
head.className = "sl-head";
|
|
703
|
+
const h = document.createElement("h3");
|
|
704
|
+
h.textContent = opts.title ?? "Bundle";
|
|
705
|
+
head.append(h);
|
|
706
|
+
el.append(head);
|
|
707
|
+
const { identity, totals } = description;
|
|
708
|
+
const ident = document.createElement("div");
|
|
709
|
+
ident.className = "sl-ident";
|
|
710
|
+
el.append(ident);
|
|
711
|
+
line(ident, `${identity.project} ${identity.version}`, "sl-line");
|
|
712
|
+
line(ident, `schema ${identity.schema}`, "sl-line sl-note");
|
|
713
|
+
line(
|
|
714
|
+
ident,
|
|
715
|
+
`hash ${identity.hash === "" ? "(none)" : identity.hash} - metadata ${identity.metadata}`,
|
|
716
|
+
"sl-line sl-note"
|
|
717
|
+
);
|
|
718
|
+
const handsBody = fold(el, "Hands (deal)", open);
|
|
719
|
+
handsBody.className = "sl-hands";
|
|
720
|
+
if (description.hands.length === 0) {
|
|
721
|
+
line(handsBody, "(no hands - this bundle is peek-only)", "sl-line sl-note");
|
|
722
|
+
}
|
|
723
|
+
for (const hand of description.hands) {
|
|
724
|
+
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}` : ""));
|
|
726
|
+
}
|
|
727
|
+
const tagsBody = fold(el, "Tags by box (peek criteria)", open);
|
|
728
|
+
tagsBody.className = "sl-tags";
|
|
729
|
+
for (const box of description.boxes) {
|
|
730
|
+
line(tagsBody, `${box.title ?? box.gameId}`, "sl-group");
|
|
731
|
+
if (box.tagGroups.length === 0) {
|
|
732
|
+
line(tagsBody, " (no tag groups)", "sl-line sl-note");
|
|
733
|
+
}
|
|
734
|
+
for (const group of box.tagGroups) {
|
|
735
|
+
line(tagsBody, ` ${group.gameId}: ${group.tags.length > 0 ? group.tags.join(", ") : "(no tags)"}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const propsBody = fold(el, "Properties (declared)", open);
|
|
739
|
+
propsBody.className = "sl-props";
|
|
740
|
+
for (const scope of description.properties) {
|
|
741
|
+
line(propsBody, formatScopeLabel(scope), "sl-group");
|
|
742
|
+
if (scope.properties.length === 0) {
|
|
743
|
+
line(propsBody, " (none declared)", "sl-line sl-note");
|
|
744
|
+
}
|
|
745
|
+
for (const p of scope.properties) {
|
|
746
|
+
line(propsBody, ` ${formatPropertySummary(p)}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (description.maps.length > 0) {
|
|
750
|
+
const mapsBody = fold(el, "Maps (carried, not read)", open);
|
|
751
|
+
mapsBody.className = "sl-maps";
|
|
752
|
+
line(mapsBody, "Geometry the build was asked to carry. The engine ignores it.", "sl-line sl-note");
|
|
753
|
+
for (const map of description.maps) {
|
|
754
|
+
line(mapsBody, `${map.box} - ${map.group}: zones ${map.zones}, pictures ${map.backgrounds}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const countsBody = fold(el, "Counts", open);
|
|
758
|
+
countsBody.className = "sl-counts";
|
|
759
|
+
line(countsBody, `boxes ${totals.boxes} - decks ${totals.decks} - cards ${totals.cards}`);
|
|
760
|
+
line(countsBody, `hands ${totals.hands} - templates ${totals.templates} - tag groups ${totals.tagGroups}`);
|
|
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}`);
|
|
763
|
+
}
|
|
764
|
+
(opts.container ?? document.body).append(el);
|
|
765
|
+
return {
|
|
766
|
+
el,
|
|
767
|
+
description,
|
|
768
|
+
destroy() {
|
|
769
|
+
el.remove();
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/live-link.ts
|
|
775
|
+
var OPEN = 1;
|
|
776
|
+
var QUEUE_CAP = 512;
|
|
777
|
+
var DEFAULT_URL = "ws://127.0.0.1:4472";
|
|
778
|
+
var BOARD_EVENTS = /* @__PURE__ */ new Set(["deal", "play", "evict", "turns"]);
|
|
779
|
+
function boardFrame(flow) {
|
|
780
|
+
const id = flow.id;
|
|
781
|
+
const hands = {};
|
|
782
|
+
for (const [hand, cards] of Object.entries(flow.board())) hands[hand] = cards.map((c) => c.gameId);
|
|
783
|
+
const turns = {};
|
|
784
|
+
for (const box of flow.listBoxes()) turns[box.gameId] = box.turn;
|
|
785
|
+
return { t: "board", flow: id, hands, turns };
|
|
786
|
+
}
|
|
787
|
+
function createLiveLink(opts) {
|
|
788
|
+
const url = opts.url ?? DEFAULT_URL;
|
|
789
|
+
const Ctor = opts.WebSocket ?? globalThis.WebSocket;
|
|
790
|
+
let queue = [];
|
|
791
|
+
let sock = null;
|
|
792
|
+
let closed = false;
|
|
793
|
+
let build = opts.build;
|
|
794
|
+
let engine = null;
|
|
795
|
+
let unsubscribe = null;
|
|
796
|
+
let announced = /* @__PURE__ */ new Set();
|
|
797
|
+
if (!Ctor) {
|
|
798
|
+
return { attach() {
|
|
799
|
+
}, detach() {
|
|
800
|
+
}, setBuild() {
|
|
801
|
+
}, close() {
|
|
802
|
+
closed = true;
|
|
803
|
+
} };
|
|
804
|
+
}
|
|
805
|
+
const flush = () => {
|
|
806
|
+
if (!sock || sock.readyState !== OPEN) return;
|
|
807
|
+
for (const m of queue) {
|
|
808
|
+
try {
|
|
809
|
+
sock.send(m);
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
queue = [];
|
|
814
|
+
};
|
|
815
|
+
const post = (frame) => {
|
|
816
|
+
if (closed) return;
|
|
817
|
+
queue.push(JSON.stringify(frame));
|
|
818
|
+
if (queue.length > QUEUE_CAP) queue.splice(0, queue.length - QUEUE_CAP);
|
|
819
|
+
flush();
|
|
820
|
+
};
|
|
821
|
+
const liveFlows = () => {
|
|
822
|
+
try {
|
|
823
|
+
return engine ? engine.flows() : [];
|
|
824
|
+
} catch {
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const sendHello = () => {
|
|
829
|
+
const flows = liveFlows();
|
|
830
|
+
const hello = { t: "hello", v: 2, build, flows: flows.map((f) => f.id) };
|
|
831
|
+
if (opts.project !== void 0) hello.project = opts.project;
|
|
832
|
+
const first = flows[0];
|
|
833
|
+
if (first) {
|
|
834
|
+
try {
|
|
835
|
+
hello.boxes = first.listBoxes().map((b) => b.gameId);
|
|
836
|
+
} catch {
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
announced = new Set(flows.map((f) => f.id));
|
|
840
|
+
try {
|
|
841
|
+
sock?.send(JSON.stringify(hello));
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
const postBoard = (flow) => {
|
|
846
|
+
try {
|
|
847
|
+
post(boardFrame(flow));
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const syncFlows = () => {
|
|
852
|
+
const now = liveFlows();
|
|
853
|
+
const ids = new Set(now.map((f) => f.id));
|
|
854
|
+
for (const f of now) {
|
|
855
|
+
if (announced.has(f.id)) continue;
|
|
856
|
+
announced.add(f.id);
|
|
857
|
+
post({ t: "flowOpen", flow: f.id });
|
|
858
|
+
postBoard(f);
|
|
859
|
+
}
|
|
860
|
+
for (const id of [...announced]) {
|
|
861
|
+
if (ids.has(id)) continue;
|
|
862
|
+
announced.delete(id);
|
|
863
|
+
post({ t: "flowClose", flow: id });
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
const onTrace = (flowId, event) => {
|
|
867
|
+
try {
|
|
868
|
+
syncFlows();
|
|
869
|
+
post({ t: "trace", flow: flowId, event });
|
|
870
|
+
if (BOARD_EVENTS.has(event.type)) {
|
|
871
|
+
const f = engine?.getFlow(flowId);
|
|
872
|
+
if (f) postBoard(f);
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
try {
|
|
878
|
+
sock = new Ctor(url);
|
|
879
|
+
sock.addEventListener("open", () => {
|
|
880
|
+
sendHello();
|
|
881
|
+
flush();
|
|
882
|
+
});
|
|
883
|
+
sock.addEventListener("message", (ev) => {
|
|
884
|
+
if (!opts.onBundle || typeof ev.data !== "string") return;
|
|
885
|
+
try {
|
|
886
|
+
const msg = JSON.parse(ev.data);
|
|
887
|
+
if (msg.t === "bundle" && typeof msg.build === "string" && typeof msg.data === "string") {
|
|
888
|
+
opts.onBundle({ build: msg.build, data: msg.data });
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
sock.addEventListener("error", () => {
|
|
894
|
+
});
|
|
895
|
+
sock.addEventListener("close", () => {
|
|
896
|
+
sock = null;
|
|
897
|
+
closed = true;
|
|
898
|
+
queue = [];
|
|
899
|
+
});
|
|
900
|
+
} catch {
|
|
901
|
+
sock = null;
|
|
902
|
+
closed = true;
|
|
903
|
+
}
|
|
904
|
+
const detach = () => {
|
|
905
|
+
unsubscribe?.();
|
|
906
|
+
unsubscribe = null;
|
|
907
|
+
engine = null;
|
|
908
|
+
announced = /* @__PURE__ */ new Set();
|
|
909
|
+
};
|
|
910
|
+
return {
|
|
911
|
+
attach(next) {
|
|
912
|
+
if (closed) return;
|
|
913
|
+
detach();
|
|
914
|
+
engine = next;
|
|
915
|
+
try {
|
|
916
|
+
unsubscribe = next.subscribeTrace(onTrace);
|
|
917
|
+
} catch {
|
|
918
|
+
engine = null;
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
announced = new Set(liveFlows().map((f) => f.id));
|
|
922
|
+
for (const f of liveFlows()) postBoard(f);
|
|
923
|
+
},
|
|
924
|
+
detach,
|
|
925
|
+
setBuild(next) {
|
|
926
|
+
if (closed || next === build) return;
|
|
927
|
+
build = next;
|
|
928
|
+
if (sock && sock.readyState === OPEN) {
|
|
929
|
+
sendHello();
|
|
930
|
+
for (const f of liveFlows()) postBoard(f);
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
close() {
|
|
934
|
+
closed = true;
|
|
935
|
+
detach();
|
|
936
|
+
queue = [];
|
|
937
|
+
try {
|
|
938
|
+
sock?.close();
|
|
939
|
+
} catch {
|
|
940
|
+
}
|
|
941
|
+
sock = null;
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/refresh.ts
|
|
947
|
+
import { Engine } from "@storylet-studio/runtime";
|
|
948
|
+
function applyLiveBundle(engine, bundleJson, opts = {}) {
|
|
949
|
+
let bundle;
|
|
950
|
+
try {
|
|
951
|
+
bundle = JSON.parse(bundleJson);
|
|
952
|
+
} catch {
|
|
953
|
+
return { ok: false, error: "pushed bundle is not valid JSON" };
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
const next = new Engine(bundle, opts);
|
|
957
|
+
next.loadGame(engine.saveGame());
|
|
958
|
+
return { ok: true, engine: next, bundle };
|
|
959
|
+
} catch (e) {
|
|
960
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/world.ts
|
|
965
|
+
function createWorldContainer(bundle) {
|
|
966
|
+
const bag = new PropertyBag(bundle.world.properties, { normalise: (n) => n });
|
|
967
|
+
return {
|
|
968
|
+
resolver: {
|
|
969
|
+
get: (n) => bag.get(n),
|
|
970
|
+
set: (n, v) => {
|
|
971
|
+
bag.set(n, v);
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
bag,
|
|
975
|
+
values: () => bag.values,
|
|
976
|
+
load: (values) => bag.load(values)
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
export {
|
|
980
|
+
applyLiveBundle,
|
|
981
|
+
boardFrame,
|
|
982
|
+
createBundleInspector,
|
|
983
|
+
createStateLogger as createKernelStateLogger,
|
|
984
|
+
createLiveLink,
|
|
985
|
+
createPropertyInspector,
|
|
986
|
+
createStateLogger2 as createStateLogger,
|
|
987
|
+
createWorldContainer,
|
|
988
|
+
deserializeState,
|
|
989
|
+
diffState,
|
|
990
|
+
ensureInspectorStyle,
|
|
991
|
+
formatLogEntry,
|
|
992
|
+
formatPropertySummary,
|
|
993
|
+
formatScopeLabel,
|
|
994
|
+
loadState,
|
|
995
|
+
saveState,
|
|
996
|
+
serializeState,
|
|
997
|
+
snapshotState
|
|
998
|
+
};
|
|
999
|
+
//# sourceMappingURL=index.js.map
|