@storylet-studio/play-helpers 0.13.0 → 0.15.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/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Engine, Flow, LogEntry, EngineLogEntry, BundleDescription, PropertySummary, PropertyScopeSummary, TraceEvent, EngineOptions } from '@storylet-studio/runtime';
2
- import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag as PropertyBag$1 } from '@wildwinter/scoperegistry';
2
+ import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag } from '@wildwinter/scoperegistry';
3
3
  export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot, createStateLogger as createKernelStateLogger, diffState } from '@wildwinter/scoperegistry';
4
- import { LoadReport, PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
4
+ import { PropertyBag as PropertyBag$1, Bundle, LoadReport, SaveFile } from '@storylet-studio/model';
5
5
  import { ScopeResolver } from '@wildwinter/expr';
6
6
 
7
7
  /** The full flattened snapshot of ONE FLOW's view - the shared partitions
@@ -24,9 +24,48 @@ declare function snapshotState(engine: Engine, flow: Flow): StateSnapshot;
24
24
  * world container's bag through createKernelStateLogger itself. */
25
25
  declare function createStateLogger(engine: Engine, flow: Flow, opts?: StateLoggerOptions): StateLogger;
26
26
 
27
+ interface WorldContainer {
28
+ /** Pass as `new Engine(bundle, { hostScopes: { world: container.resolver } })`. */
29
+ resolver: ScopeResolver;
30
+ /** The kernel bag itself (subscribe, audit, rows live there) - mount it
31
+ * into a state logger or examiner beside the engine's own bags. Writing it
32
+ * DIRECTLY is writing the kernel, so a `writable: false` declaration asks
33
+ * the kernel's question: pass `{ host: true }` to say the game is speaking
34
+ * (`bag.set(name, value, { host: true })`). Through `resolver` or an
35
+ * engine's setProperty that is already answered. */
36
+ bag: PropertyBag;
37
+ /** The current values, for saving beside the engine's envelope. */
38
+ values(): PropertyBag$1;
39
+ /** Restore saved values over fresh defaults: orphaned keys drop, new
40
+ * declarations keep their defaults - the same drift rule as loadGame. */
41
+ load(values: PropertyBag$1): void;
42
+ }
43
+ /** A container seeded from one host scope's declarations in the bundle:
44
+ * `@world`'s unless `token` names another (`"player"`).
45
+ *
46
+ * The container is the GAME's state, so it WRITES - even a declaration
47
+ * carrying `writable: false`. That flag is the STORY's promise not to write
48
+ * the value (Reboot.md 10), and the engine keeps it where the story writes: an
49
+ * outcome is refused against the engine's read-only table before it ever
50
+ * reaches this resolver. Enforcing it here as well refused the HOST too - the
51
+ * clock the game must move, the harness driving the value it is testing
52
+ * against - which is the opposite of what the flag says.
53
+ *
54
+ * So the declarations are seeded AS DECLARED, which is what an examiner over
55
+ * this container should read, and the writes go through as HOST writes
56
+ * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the
57
+ * engine's own doorway and passes the flag too: the engine has already sorted
58
+ * story from host by then - a story write was refused earlier, a host write is
59
+ * the only kind that arrives - and a resolver takes a name and a value with no
60
+ * room to say which. A game wanting a rule of its own binds its own resolver
61
+ * rather than this one; the ports' container (Unreal's UStoryletWorld) draws
62
+ * the same line, with HostSet never refused and StorySet asking the game's own
63
+ * read-only list. */
64
+ declare function createWorldContainer(bundle: Bundle, token?: string): WorldContainer;
65
+
27
66
  /** The current engine state (and the host's @world values, if given) as
28
67
  * pretty-printed .storyletsave JSON. */
29
- declare function serializeState(engine: Engine, world?: PropertyBag): string;
68
+ declare function serializeState(engine: Engine, world?: PropertyBag$1): string;
30
69
  /**
31
70
  * Capture the whole engine (and the host's @world values, if it keeps any) as
32
71
  * the tagged save-file OBJECT.
@@ -40,7 +79,7 @@ declare function serializeState(engine: Engine, world?: PropertyBag): string;
40
79
  * things across the four Storylets runtimes, and neither matched the family.
41
80
  * Godot and Unreal already had Patter's shape; these two were brought to it.
42
81
  */
43
- declare function saveState(engine: Engine, world?: PropertyBag): SaveFile;
82
+ declare function saveState(engine: Engine, world?: PropertyBag$1): SaveFile;
44
83
  /** What a load may be told to do besides restoring. */
45
84
  interface LoadStateOptions {
46
85
  /** Handed the engine's LoadReport for this file as the load happens: what
@@ -49,6 +88,13 @@ interface LoadStateOptions {
49
88
  * `engine.previewLoad` return. A callback rather than a return value
50
89
  * because these functions already return the file's @world values. */
51
90
  onReport?: (report: LoadReport) => void;
91
+ /** The host's @world container (`createWorldContainer`, or anything with
92
+ * the same `values` and `load`). Given, the file's @world values go into it
93
+ * BEFORE the engine loads, so the load's check of each held card reads the
94
+ * saved world rather than the one the game had; a refused load puts the
95
+ * container back as it was. Without it the values are only returned, and
96
+ * the game must apply them before loading for the same check. */
97
+ world?: Pick<WorldContainer, "values" | "load">;
52
98
  }
53
99
  /** Restore a {@link saveState} file into an engine. EVERY FLOW IS REBUILT, so
54
100
  * the Flow handles you held before are inert: re-take them with
@@ -57,13 +103,14 @@ interface LoadStateOptions {
57
103
  * the failure lands later, as `play()` refusing a card as "not dealt". (The
58
104
  * engine's `onReplacedFlow` hook reports exactly this.) Throws on a foreign or malformed
59
105
  * file, and the runtime's own project check still applies. Returns the file's
60
- * @world values, if any - the HOST applies them to its container; the engine
106
+ * @world values, if any. Pass the host's container as `opts.world` and they
107
+ * are applied before the load (see LoadStateOptions.world); the engine itself
61
108
  * never touches them. */
62
- declare function loadState(engine: Engine, file: SaveFile, opts?: LoadStateOptions): PropertyBag | undefined;
109
+ declare function loadState(engine: Engine, file: SaveFile, opts?: LoadStateOptions): PropertyBag$1 | undefined;
63
110
  /** Parse + restore a {@link serializeState} string: the TEXT twin of
64
111
  * loadState, as Patterplay pairs them. Throws on malformed JSON, a foreign
65
112
  * file or a project mismatch. Returns the file's @world values for the host. */
66
- declare function deserializeState(engine: Engine, json: string, opts?: LoadStateOptions): PropertyBag | undefined;
113
+ declare function deserializeState(engine: Engine, json: string, opts?: LoadStateOptions): PropertyBag$1 | undefined;
67
114
 
68
115
  interface PropertyInspectorOptions {
69
116
  /** Mount point; defaults to document.body. */
@@ -103,8 +150,8 @@ interface BundleInspector {
103
150
  * (design/engine-server.md 4.2). Nothing is added for the ordinary
104
151
  * run-scoped property: that is what a property is. */
105
152
  declare function formatPropertySummary(p: PropertySummary): string;
106
- /** The scope label a declaration block files under ("world", "box box",
107
- * "tag docks (zone)"). */
153
+ /** The scope label a declaration block files under ("world", "player", "box box",
154
+ * "tag docks (zone)"). A host scope is named by its token, as @world is. */
108
155
  declare function formatScopeLabel(scope: PropertyScopeSummary): string;
109
156
  /** Render a read-only summary of a compiled bundle: what an integrator may
110
157
  * call, with no session and no game running. */
@@ -240,47 +287,9 @@ type LiveBundleResult =
240
287
  * the game's registry the old engine hands its keys to the replacement, which a
241
288
  * plain save and load into a second engine cannot do (the two would clash).
242
289
  *
243
- * The engine remembers the options it was built with (seed, log, world,
290
+ * The engine remembers the options it was built with (seed, log, hostScopes,
244
291
  * registry), so `opts` is only for overriding one of them for the replacement.
245
292
  */
246
293
  declare function applyLiveBundle(engine: Engine, bundleJson: string, opts?: EngineOptions): LiveBundleResult;
247
294
 
248
- interface WorldContainer {
249
- /** Pass as `new Engine(bundle, { world: container.resolver })`. */
250
- resolver: ScopeResolver;
251
- /** The kernel bag itself (subscribe, audit, rows live there) - mount it
252
- * into a state logger or examiner beside the engine's own bags. Writing it
253
- * DIRECTLY is writing the kernel, so a `writable: false` declaration asks
254
- * the kernel's question: pass `{ host: true }` to say the game is speaking
255
- * (`bag.set(name, value, { host: true })`). Through `resolver` or an
256
- * engine's setProperty that is already answered. */
257
- bag: PropertyBag$1;
258
- /** The current values, for saving beside the engine's envelope. */
259
- values(): PropertyBag;
260
- /** Restore saved values over fresh defaults: orphaned keys drop, new
261
- * declarations keep their defaults - the same drift rule as loadGame. */
262
- load(values: PropertyBag): void;
263
- }
264
- /** A world container seeded from the bundle's @world declarations.
265
- *
266
- * The container is the GAME's state, so it WRITES - even a declaration
267
- * carrying `writable: false`. That flag is the STORY's promise not to write
268
- * the value (Reboot.md 10), and the engine keeps it where the story writes: an
269
- * outcome is refused against the engine's read-only table before it ever
270
- * reaches this resolver. Enforcing it here as well refused the HOST too - the
271
- * clock the game must move, the harness driving the value it is testing
272
- * against - which is the opposite of what the flag says.
273
- *
274
- * So the declarations are seeded AS DECLARED, which is what an examiner over
275
- * this container should read, and the writes go through as HOST writes
276
- * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the
277
- * engine's own doorway and passes the flag too: the engine has already sorted
278
- * story from host by then - a story write was refused earlier, a host write is
279
- * the only kind that arrives - and a resolver takes a name and a value with no
280
- * room to say which. A game wanting a rule of its own binds its own resolver
281
- * rather than this one; the ports' container (Unreal's UStoryletWorld) draws
282
- * the same line, with HostSet never refused and StorySet asking the game's own
283
- * read-only list. */
284
- declare function createWorldContainer(bundle: Bundle): WorldContainer;
285
-
286
295
  export { type BundleInspector, type BundleInspectorOptions, type LiveBundleResult, type LiveFrame, type LiveLink, type LiveLinkOptions, type LiveLinkState, type LiveSocketLike, type LoadStateOptions, type PropertyInspector, type PropertyInspectorOptions, type WorldContainer, applyLiveBundle, boardFrame, createBundleInspector, createLiveLink, createPropertyInspector, createStateLogger, createWorldContainer, deserializeState, ensureInspectorStyle, formatLogEntry, formatPropertySummary, formatScopeLabel, loadState, saveState, serializeState, snapshotState };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Engine, Flow, LogEntry, EngineLogEntry, BundleDescription, PropertySummary, PropertyScopeSummary, TraceEvent, EngineOptions } from '@storylet-studio/runtime';
2
- import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag as PropertyBag$1 } from '@wildwinter/scoperegistry';
2
+ import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag } from '@wildwinter/scoperegistry';
3
3
  export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot, createStateLogger as createKernelStateLogger, diffState } from '@wildwinter/scoperegistry';
4
- import { LoadReport, PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
4
+ import { PropertyBag as PropertyBag$1, Bundle, LoadReport, SaveFile } from '@storylet-studio/model';
5
5
  import { ScopeResolver } from '@wildwinter/expr';
6
6
 
7
7
  /** The full flattened snapshot of ONE FLOW's view - the shared partitions
@@ -24,9 +24,48 @@ declare function snapshotState(engine: Engine, flow: Flow): StateSnapshot;
24
24
  * world container's bag through createKernelStateLogger itself. */
25
25
  declare function createStateLogger(engine: Engine, flow: Flow, opts?: StateLoggerOptions): StateLogger;
26
26
 
27
+ interface WorldContainer {
28
+ /** Pass as `new Engine(bundle, { hostScopes: { world: container.resolver } })`. */
29
+ resolver: ScopeResolver;
30
+ /** The kernel bag itself (subscribe, audit, rows live there) - mount it
31
+ * into a state logger or examiner beside the engine's own bags. Writing it
32
+ * DIRECTLY is writing the kernel, so a `writable: false` declaration asks
33
+ * the kernel's question: pass `{ host: true }` to say the game is speaking
34
+ * (`bag.set(name, value, { host: true })`). Through `resolver` or an
35
+ * engine's setProperty that is already answered. */
36
+ bag: PropertyBag;
37
+ /** The current values, for saving beside the engine's envelope. */
38
+ values(): PropertyBag$1;
39
+ /** Restore saved values over fresh defaults: orphaned keys drop, new
40
+ * declarations keep their defaults - the same drift rule as loadGame. */
41
+ load(values: PropertyBag$1): void;
42
+ }
43
+ /** A container seeded from one host scope's declarations in the bundle:
44
+ * `@world`'s unless `token` names another (`"player"`).
45
+ *
46
+ * The container is the GAME's state, so it WRITES - even a declaration
47
+ * carrying `writable: false`. That flag is the STORY's promise not to write
48
+ * the value (Reboot.md 10), and the engine keeps it where the story writes: an
49
+ * outcome is refused against the engine's read-only table before it ever
50
+ * reaches this resolver. Enforcing it here as well refused the HOST too - the
51
+ * clock the game must move, the harness driving the value it is testing
52
+ * against - which is the opposite of what the flag says.
53
+ *
54
+ * So the declarations are seeded AS DECLARED, which is what an examiner over
55
+ * this container should read, and the writes go through as HOST writes
56
+ * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the
57
+ * engine's own doorway and passes the flag too: the engine has already sorted
58
+ * story from host by then - a story write was refused earlier, a host write is
59
+ * the only kind that arrives - and a resolver takes a name and a value with no
60
+ * room to say which. A game wanting a rule of its own binds its own resolver
61
+ * rather than this one; the ports' container (Unreal's UStoryletWorld) draws
62
+ * the same line, with HostSet never refused and StorySet asking the game's own
63
+ * read-only list. */
64
+ declare function createWorldContainer(bundle: Bundle, token?: string): WorldContainer;
65
+
27
66
  /** The current engine state (and the host's @world values, if given) as
28
67
  * pretty-printed .storyletsave JSON. */
29
- declare function serializeState(engine: Engine, world?: PropertyBag): string;
68
+ declare function serializeState(engine: Engine, world?: PropertyBag$1): string;
30
69
  /**
31
70
  * Capture the whole engine (and the host's @world values, if it keeps any) as
32
71
  * the tagged save-file OBJECT.
@@ -40,7 +79,7 @@ declare function serializeState(engine: Engine, world?: PropertyBag): string;
40
79
  * things across the four Storylets runtimes, and neither matched the family.
41
80
  * Godot and Unreal already had Patter's shape; these two were brought to it.
42
81
  */
43
- declare function saveState(engine: Engine, world?: PropertyBag): SaveFile;
82
+ declare function saveState(engine: Engine, world?: PropertyBag$1): SaveFile;
44
83
  /** What a load may be told to do besides restoring. */
45
84
  interface LoadStateOptions {
46
85
  /** Handed the engine's LoadReport for this file as the load happens: what
@@ -49,6 +88,13 @@ interface LoadStateOptions {
49
88
  * `engine.previewLoad` return. A callback rather than a return value
50
89
  * because these functions already return the file's @world values. */
51
90
  onReport?: (report: LoadReport) => void;
91
+ /** The host's @world container (`createWorldContainer`, or anything with
92
+ * the same `values` and `load`). Given, the file's @world values go into it
93
+ * BEFORE the engine loads, so the load's check of each held card reads the
94
+ * saved world rather than the one the game had; a refused load puts the
95
+ * container back as it was. Without it the values are only returned, and
96
+ * the game must apply them before loading for the same check. */
97
+ world?: Pick<WorldContainer, "values" | "load">;
52
98
  }
53
99
  /** Restore a {@link saveState} file into an engine. EVERY FLOW IS REBUILT, so
54
100
  * the Flow handles you held before are inert: re-take them with
@@ -57,13 +103,14 @@ interface LoadStateOptions {
57
103
  * the failure lands later, as `play()` refusing a card as "not dealt". (The
58
104
  * engine's `onReplacedFlow` hook reports exactly this.) Throws on a foreign or malformed
59
105
  * file, and the runtime's own project check still applies. Returns the file's
60
- * @world values, if any - the HOST applies them to its container; the engine
106
+ * @world values, if any. Pass the host's container as `opts.world` and they
107
+ * are applied before the load (see LoadStateOptions.world); the engine itself
61
108
  * never touches them. */
62
- declare function loadState(engine: Engine, file: SaveFile, opts?: LoadStateOptions): PropertyBag | undefined;
109
+ declare function loadState(engine: Engine, file: SaveFile, opts?: LoadStateOptions): PropertyBag$1 | undefined;
63
110
  /** Parse + restore a {@link serializeState} string: the TEXT twin of
64
111
  * loadState, as Patterplay pairs them. Throws on malformed JSON, a foreign
65
112
  * file or a project mismatch. Returns the file's @world values for the host. */
66
- declare function deserializeState(engine: Engine, json: string, opts?: LoadStateOptions): PropertyBag | undefined;
113
+ declare function deserializeState(engine: Engine, json: string, opts?: LoadStateOptions): PropertyBag$1 | undefined;
67
114
 
68
115
  interface PropertyInspectorOptions {
69
116
  /** Mount point; defaults to document.body. */
@@ -103,8 +150,8 @@ interface BundleInspector {
103
150
  * (design/engine-server.md 4.2). Nothing is added for the ordinary
104
151
  * run-scoped property: that is what a property is. */
105
152
  declare function formatPropertySummary(p: PropertySummary): string;
106
- /** The scope label a declaration block files under ("world", "box box",
107
- * "tag docks (zone)"). */
153
+ /** The scope label a declaration block files under ("world", "player", "box box",
154
+ * "tag docks (zone)"). A host scope is named by its token, as @world is. */
108
155
  declare function formatScopeLabel(scope: PropertyScopeSummary): string;
109
156
  /** Render a read-only summary of a compiled bundle: what an integrator may
110
157
  * call, with no session and no game running. */
@@ -240,47 +287,9 @@ type LiveBundleResult =
240
287
  * the game's registry the old engine hands its keys to the replacement, which a
241
288
  * plain save and load into a second engine cannot do (the two would clash).
242
289
  *
243
- * The engine remembers the options it was built with (seed, log, world,
290
+ * The engine remembers the options it was built with (seed, log, hostScopes,
244
291
  * registry), so `opts` is only for overriding one of them for the replacement.
245
292
  */
246
293
  declare function applyLiveBundle(engine: Engine, bundleJson: string, opts?: EngineOptions): LiveBundleResult;
247
294
 
248
- interface WorldContainer {
249
- /** Pass as `new Engine(bundle, { world: container.resolver })`. */
250
- resolver: ScopeResolver;
251
- /** The kernel bag itself (subscribe, audit, rows live there) - mount it
252
- * into a state logger or examiner beside the engine's own bags. Writing it
253
- * DIRECTLY is writing the kernel, so a `writable: false` declaration asks
254
- * the kernel's question: pass `{ host: true }` to say the game is speaking
255
- * (`bag.set(name, value, { host: true })`). Through `resolver` or an
256
- * engine's setProperty that is already answered. */
257
- bag: PropertyBag$1;
258
- /** The current values, for saving beside the engine's envelope. */
259
- values(): PropertyBag;
260
- /** Restore saved values over fresh defaults: orphaned keys drop, new
261
- * declarations keep their defaults - the same drift rule as loadGame. */
262
- load(values: PropertyBag): void;
263
- }
264
- /** A world container seeded from the bundle's @world declarations.
265
- *
266
- * The container is the GAME's state, so it WRITES - even a declaration
267
- * carrying `writable: false`. That flag is the STORY's promise not to write
268
- * the value (Reboot.md 10), and the engine keeps it where the story writes: an
269
- * outcome is refused against the engine's read-only table before it ever
270
- * reaches this resolver. Enforcing it here as well refused the HOST too - the
271
- * clock the game must move, the harness driving the value it is testing
272
- * against - which is the opposite of what the flag says.
273
- *
274
- * So the declarations are seeded AS DECLARED, which is what an examiner over
275
- * this container should read, and the writes go through as HOST writes
276
- * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the
277
- * engine's own doorway and passes the flag too: the engine has already sorted
278
- * story from host by then - a story write was refused earlier, a host write is
279
- * the only kind that arrives - and a resolver takes a name and a value with no
280
- * room to say which. A game wanting a rule of its own binds its own resolver
281
- * rather than this one; the ports' container (Unreal's UStoryletWorld) draws
282
- * the same line, with HostSet never refused and StorySet asking the game's own
283
- * read-only list. */
284
- declare function createWorldContainer(bundle: Bundle): WorldContainer;
285
-
286
295
  export { type BundleInspector, type BundleInspectorOptions, type LiveBundleResult, type LiveFrame, type LiveLink, type LiveLinkOptions, type LiveLinkState, type LiveSocketLike, type LoadStateOptions, type PropertyInspector, type PropertyInspectorOptions, type WorldContainer, applyLiveBundle, boardFrame, createBundleInspector, createLiveLink, createPropertyInspector, createStateLogger, createWorldContainer, deserializeState, ensureInspectorStyle, formatLogEntry, formatPropertySummary, formatScopeLabel, loadState, saveState, serializeState, snapshotState };
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ function createStateLogger(engine, flow, opts = {}) {
37
37
  }
38
38
 
39
39
  // src/save.ts
40
- import { SAVEFILE_SCHEMA, SAVE_SCHEMA, SAVE_SCHEMA_V1 } from "@storylet-studio/model";
40
+ import { SAVEFILE_SCHEMA, SAVE_SCHEMA, SAVE_SCHEMA_V1, SAVE_SCHEMA_V2 } from "@storylet-studio/model";
41
41
  function serializeState(engine, world) {
42
42
  return JSON.stringify(saveState(engine, world), null, 2);
43
43
  }
@@ -49,10 +49,19 @@ function saveState(engine, world) {
49
49
  };
50
50
  }
51
51
  function loadState(engine, file, opts = {}) {
52
- if (!file || typeof file !== "object" || file.schema !== SAVEFILE_SCHEMA || file.engine?.schema !== SAVE_SCHEMA && file.engine?.schema !== SAVE_SCHEMA_V1) {
52
+ if (!file || typeof file !== "object" || file.schema !== SAVEFILE_SCHEMA || file.engine?.schema !== SAVE_SCHEMA && file.engine?.schema !== SAVE_SCHEMA_V2 && file.engine?.schema !== SAVE_SCHEMA_V1) {
53
53
  throw new Error(`not a storylets save (expected schema "${SAVEFILE_SCHEMA}")`);
54
54
  }
55
- const report = engine.loadGame(file.engine);
55
+ const container = opts.world;
56
+ const before = container !== void 0 ? structuredClone(container.values()) : void 0;
57
+ if (container !== void 0 && file.world !== void 0) container.load(file.world);
58
+ let report;
59
+ try {
60
+ report = engine.loadGame(file.engine);
61
+ } catch (e) {
62
+ if (container !== void 0 && before !== void 0) container.load(before);
63
+ throw e;
64
+ }
56
65
  opts.onReport?.(report);
57
66
  return file.world;
58
67
  }
@@ -122,7 +131,7 @@ var LOG_KIND_LABELS = {
122
131
  turns: "Turns",
123
132
  diagnostic: "Diag"
124
133
  };
125
- var logKindOf = (e) => e.type === "peek" ? "deal" : e.type;
134
+ var logKindOf = (e) => e.type === "peek" ? "deal" : e.type === "discard" ? "play" : e.type;
126
135
  var showVal = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
127
136
  function formatLogEntry(e) {
128
137
  return formatLogBody(e, "flow" in e && e.flow ? `${e.flow} ` : "");
@@ -148,6 +157,9 @@ function formatLogBody(e, flow) {
148
157
  return `${stamp}write ${e.path}: ${showVal(e.prev)} -> ${showVal(e.value)}`;
149
158
  case "turns":
150
159
  return `${stamp}turns ${e.box} -> ${e.turn}`;
160
+ // A card the game took out of its hand unplayed: filed with the plays.
161
+ case "discard":
162
+ return `${stamp}discard ${e.card} from ${e.hand}`;
151
163
  default:
152
164
  return `${stamp}diagnostic ${e.where}: ${e.message}`;
153
165
  }
@@ -496,6 +508,7 @@ function formatPropertySummary(p) {
496
508
  }
497
509
  function formatScopeLabel(scope) {
498
510
  if (scope.scope === "world" || scope.scope === "story") return scope.scope;
511
+ if (scope.scope === "host") return scope.owner;
499
512
  const group = scope.group !== void 0 ? ` (${scope.group})` : "";
500
513
  return `${scope.scope} ${scope.owner}${group}`;
501
514
  }
@@ -587,7 +600,7 @@ function createBundleInspector(bundle, opts = {}) {
587
600
  line(countsBody, `boxes ${totals.boxes} - decks ${totals.decks} - cards ${totals.cards}`);
588
601
  line(countsBody, `hands ${totals.hands} - templates ${totals.templates} - tag groups ${totals.tagGroups}`);
589
602
  for (const box of description.boxes) {
590
- 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}` + (box.turn !== void 0 ? `, turn = ${box.turn.seconds}s` : "") + (box.durableCards !== void 0 ? `, durable cards ${box.durableCards}` : ""));
603
+ 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}` + (box.turn === void 0 ? "" : box.turn.seconds !== void 0 ? `, turn = ${box.turn.seconds}s` : ", turn moved by the game") + (box.durableCards !== void 0 ? `, durable cards ${box.durableCards}` : ""));
591
604
  }
592
605
  (opts.container ?? document.body).append(el);
593
606
  return {
@@ -603,7 +616,7 @@ function createBundleInspector(bundle, opts = {}) {
603
616
  var OPEN = 1;
604
617
  var QUEUE_CAP = 512;
605
618
  var DEFAULT_URL = "ws://127.0.0.1:4472";
606
- var BOARD_EVENTS = /* @__PURE__ */ new Set(["deal", "play", "evict", "turns"]);
619
+ var BOARD_EVENTS = /* @__PURE__ */ new Set(["deal", "play", "discard", "evict", "turns"]);
607
620
  function boardFrame(flow) {
608
621
  const id = flow.id;
609
622
  const hands = {};
@@ -794,8 +807,9 @@ function applyLiveBundle(engine, bundleJson, opts = {}) {
794
807
 
795
808
  // src/world.ts
796
809
  import { PropertyBag as StateBag } from "@wildwinter/scoperegistry";
797
- function createWorldContainer(bundle) {
798
- const bag = new StateBag(bundle.world.properties, { normalise: (n) => n });
810
+ import { hostScopeDecls } from "@storylet-studio/model";
811
+ function createWorldContainer(bundle, token = "world") {
812
+ const bag = new StateBag(hostScopeDecls(bundle, token), { normalise: (n) => n });
799
813
  return {
800
814
  resolver: {
801
815
  get: (n) => bag.get(n),