@patterkit/play-helpers 0.4.0 → 0.5.1

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 CHANGED
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- SAVE_SCHEMA: () => SAVE_SCHEMA,
23
+ SAVE_SCHEMA: () => import_model.SAVE_SCHEMA,
24
24
  applyLiveBundle: () => applyLiveBundle,
25
25
  createAudioResolver: () => createAudioResolver,
26
26
  createBundleInspector: () => createBundleInspector,
@@ -28,7 +28,7 @@ __export(index_exports, {
28
28
  createPropertyInspector: () => createPropertyInspector,
29
29
  createStateLogger: () => createStateLogger,
30
30
  deserializeState: () => deserializeState,
31
- diffState: () => diffState,
31
+ diffState: () => import_scoperegistry.diffState,
32
32
  getProperty: () => getProperty,
33
33
  loadState: () => loadState,
34
34
  saveState: () => saveState,
@@ -40,13 +40,13 @@ __export(index_exports, {
40
40
  module.exports = __toCommonJS(index_exports);
41
41
 
42
42
  // src/save.ts
43
- var SAVE_SCHEMA = "patter/save@0";
43
+ var import_model = require("@patterkit/model");
44
44
  function saveState(engine) {
45
- return { schema: SAVE_SCHEMA, save: engine.saveGame() };
45
+ return { schema: import_model.SAVE_SCHEMA, save: engine.saveGame() };
46
46
  }
47
47
  function loadState(engine, env) {
48
- if (!env || env.schema !== SAVE_SCHEMA || !env.save) {
49
- throw new Error(`loadState: not a ${SAVE_SCHEMA} envelope`);
48
+ if (!env || env.schema !== import_model.SAVE_SCHEMA || !env.save) {
49
+ throw new Error(`loadState: not a ${import_model.SAVE_SCHEMA} envelope`);
50
50
  }
51
51
  engine.loadGame(env.save);
52
52
  }
@@ -69,7 +69,7 @@ function setProperties(engine, values) {
69
69
  }
70
70
 
71
71
  // src/logger.ts
72
- var eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
72
+ var import_scoperegistry = require("@wildwinter/scoperegistry");
73
73
  function snapshotState(engine) {
74
74
  const save = engine.saveGame();
75
75
  const out = {};
@@ -85,16 +85,6 @@ function snapshotState(engine) {
85
85
  }
86
86
  return out;
87
87
  }
88
- function diffState(prev, next) {
89
- const changes = [];
90
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)])].sort();
91
- for (const path of keys) {
92
- const from = prev[path], to = next[path];
93
- if (!eq(from, to)) changes.push({ path, ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} });
94
- }
95
- return changes;
96
- }
97
- var fmt = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
98
88
  function describeStep(step) {
99
89
  switch (step.type) {
100
90
  case "line":
@@ -110,21 +100,29 @@ function describeStep(step) {
110
100
  }
111
101
  }
112
102
  var gd = (data) => data ? ` gameData=${JSON.stringify(data)}` : "";
103
+ function visitState(engine) {
104
+ const save = engine.saveGame();
105
+ const out = {};
106
+ for (const [id, n] of Object.entries(save.sharedVisits)) out[`visit:${id}`] = n;
107
+ for (const [fid, snap] of Object.entries(save.flows)) {
108
+ for (const [id, n] of Object.entries(snap.visits)) out[`${fid}/visit:${id}`] = n;
109
+ }
110
+ return out;
111
+ }
113
112
  function createStateLogger(engine, opts = {}) {
114
- const sink = opts.sink ?? ((line) => console.log(line));
115
113
  const tag = opts.label ? `[${opts.label}] ` : "";
116
- let baseline = snapshotState(engine);
114
+ const kernel = (0, import_scoperegistry.createStateLogger)({
115
+ // Re-read on every capture: openFlow and loadGame both replace bags, and the kernel
116
+ // re-mounts whatever it is handed.
117
+ mounts: () => [...engine.listBags(), ...engine.flows().flatMap((f) => f.listBags())],
118
+ extra: () => visitState(engine)
119
+ }, { sink: opts.sink, label: tag });
117
120
  return {
118
121
  snapshot: () => snapshotState(engine),
119
- capture() {
120
- const next = snapshotState(engine);
121
- const changes = diffState(baseline, next);
122
- baseline = next;
123
- for (const c of changes) sink(`${tag}${c.path}: ${fmt(c.from)} -> ${fmt(c.to)}`);
124
- return changes;
125
- },
122
+ capture: () => kernel.capture(),
123
+ dispose: () => kernel.dispose(),
126
124
  logStep(step) {
127
- sink(`${tag}${describeStep(step)}`);
125
+ (opts.sink ?? ((l) => console.log(l)))(`${tag}${describeStep(step)}`);
128
126
  }
129
127
  };
130
128
  }
package/dist/index.d.cts CHANGED
@@ -1,11 +1,9 @@
1
- import { SaveGame, Engine, StepResult, Bundle as Bundle$1, BundleDescription } from '@patterkit/runtime';
1
+ import { Engine, StepResult, Bundle, BundleDescription } from '@patterkit/runtime';
2
+ import { SaveEnvelope, Bundle as Bundle$1 } from '@patterkit/model';
3
+ export { SAVE_SCHEMA, SaveEnvelope } from '@patterkit/model';
4
+ import { StateSnapshot, StateChange, ScalarValue } from '@wildwinter/scoperegistry';
5
+ export { StateChange, StateSnapshot, diffState } from '@wildwinter/scoperegistry';
2
6
 
3
- declare const SAVE_SCHEMA = "patter/save@0";
4
- interface SaveEnvelope {
5
- schema: typeof SAVE_SCHEMA;
6
- /** The engine save-game: shared `@patter`/`@scene` state, visit counts, and every live flow. */
7
- save: SaveGame;
8
- }
9
7
  /** Capture the whole game as a tagged envelope (wraps `engine.saveGame()`). */
10
8
  declare function saveState(engine: Engine): SaveEnvelope;
11
9
  /** Restore a {@link saveState} envelope into an engine (fresh or live). Throws on a foreign/blank blob. */
@@ -24,20 +22,14 @@ declare function setProperty(engine: Engine, ref: string, value: PropertyValue):
24
22
  /** Set many at once, e.g. `setProperties(engine, { "@hp": 10, "@scene.locked": false })`. */
25
23
  declare function setProperties(engine: Engine, values: Record<string, PropertyValue>): void;
26
24
 
27
- /** A flattened runtime-state value (a Patter scalar: number / boolean / string / string[] flags). */
28
- type StateValue = NonNullable<ReturnType<Engine["getProperty"]>>;
29
- /** A flattened snapshot: dotted path -> value. Paths: `@patter.x`, `@scene:scene.x`, `visit:nodeId`,
30
- * and `flow/...` for a flow's not-shared locals. */
31
- type StateSnapshot = Record<string, StateValue>;
32
- interface StateChange {
33
- path: string;
34
- from?: StateValue;
35
- to?: StateValue;
36
- }
37
- /** Flatten the engine's whole-game state into a path -> value map (shared scopes + every live flow). */
25
+ /** A flattened runtime-state value: a Patter scalar. The kernel calls this `ScalarValue`;
26
+ * the alias stays because it is exported API and reads in Patter's own vocabulary. */
27
+ type StateValue = ScalarValue;
28
+ /** Flatten the engine's whole-game state into a path -> value map (shared scopes + every live
29
+ * flow), off `saveGame()`. The logger no longer diffs this - it mounts the bags directly - but
30
+ * it stays as the public "what is the state right now" call, and as the definition of the path
31
+ * space the mounts compose. */
38
32
  declare function snapshotState(engine: Engine): StateSnapshot;
39
- /** The sorted set of paths that differ between two snapshots (added / removed / changed). */
40
- declare function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[];
41
33
  interface StateLoggerOptions {
42
34
  /** Where lines go; defaults to `console.log`. */
43
35
  sink?: (line: string) => void;
@@ -47,12 +39,16 @@ interface StateLoggerOptions {
47
39
  interface StateLogger {
48
40
  /** The current flattened state (no logging). */
49
41
  snapshot(): StateSnapshot;
50
- /** Diff since the last capture, log each change, and re-baseline. Returns the changes. */
42
+ /** Everything since the last capture: the property writes already logged as they landed,
43
+ * plus the visit counts, diffed and re-baselined. */
51
44
  capture(): StateChange[];
52
45
  /** Trace one played step (line / text / game-event / choice / end), including any `gameData`. */
53
46
  logStep(step: StepResult): void;
47
+ /** Unhook the bag auditors. The logger is inert afterwards. */
48
+ dispose(): void;
54
49
  }
55
- /** Create a state logger over an engine. Call `capture()` after each `advance`/`choose` to log mutations. */
50
+ /** Create a state logger over an engine. Property writes log as they land; call `capture()`
51
+ * after each `advance`/`choose` to pick up the visit counts and re-baseline. */
56
52
  declare function createStateLogger(engine: Engine, opts?: StateLoggerOptions): StateLogger;
57
53
 
58
54
  /** A minimal structural type for a WebSocket implementation (browsers + Node 21+ have a global one). */
@@ -107,7 +103,7 @@ interface LiveBundleResult {
107
103
  /** The engine to keep using: the same instance for a "text" swap, a replacement for "structure". */
108
104
  engine: Engine;
109
105
  /** The parsed pushed bundle - hold on to it for the next apply's comparison. */
110
- bundle: Bundle$1;
106
+ bundle: Bundle;
111
107
  /** Which tier applied: "text" (strings-only, nothing restarted) or "structure" (full hot swap). */
112
108
  kind: "text" | "structure";
113
109
  }
@@ -117,7 +113,7 @@ interface LiveBundleResult {
117
113
  * unparseable JSON - a structural swap's edge cases are absorbed by the §9.8 drift policy inside
118
114
  * `hotSwap`, which never throws for ordinary edits.
119
115
  */
120
- declare function applyLiveBundle(engine: Engine, current: Bundle$1, data: string): LiveBundleResult;
116
+ declare function applyLiveBundle(engine: Engine, current: Bundle, data: string): LiveBundleResult;
121
117
 
122
118
  interface PropertyInspectorOptions {
123
119
  /** Where to mount the panel. If omitted, append the returned `el` yourself. */
@@ -137,324 +133,6 @@ interface PropertyInspector {
137
133
  }
138
134
  declare function createPropertyInspector(engine: Engine, opts?: PropertyInspectorOptions): PropertyInspector;
139
135
 
140
- type BinaryOp = "==" | "!=" | ">" | ">=" | "<" | "<=" | "+" | "-" | "*" | "/" | "and" | "or";
141
- type UnaryOp = "not" | "neg";
142
- type AstNode = ["b", boolean] | ["n", number] | ["s", string] | ["sv", string, string] | ["u", UnaryOp, AstNode] | ["bin", BinaryOp, AstNode, AstNode] | ["call", string, ...AstNode[]] | ["fd", "+" | "-", string];
143
-
144
- type ScalarValue = boolean | number | string | string[];
145
- /** Developer-defined host metadata (spec §17). Opaque to Patter. */
146
- type GameData = Record<string, unknown>;
147
- /** A scene/block id, or the reserved "END". */
148
- type JumpTarget = string;
149
- interface Jump {
150
- to: JumpTarget;
151
- /** "jump" (one-way, default) or "call" (jump-and-return via the flow callstack). */
152
- mode?: "jump" | "call";
153
- }
154
- interface LineBeat {
155
- id: string;
156
- kind: "line";
157
- /** Speaker; must be a member of the project cast (validated). */
158
- character?: string;
159
- /** Performance direction, language-neutral (never localised). */
160
- direction?: string;
161
- gameData?: GameData;
162
- /** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
163
- * At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
164
- * snippet → beat). Each tag is letters/digits/symbols with NO comma and NO whitespace; deduped. */
165
- tags?: string[];
166
- }
167
- /**
168
- * Authorial voice / narration - speaker-less prose (spec §2). Never voiced; its
169
- * localised text always permits inline `{@name}` interpolation (spec §16). This
170
- * is the on-screen-text role (e.g. "A door slams!") - distinct from a game-event
171
- * beat, which is a pure engine instruction with no localised content.
172
- */
173
- interface TextBeat {
174
- id: string;
175
- kind: "text";
176
- gameData?: GameData;
177
- /** Author tags (#215). See LineBeat.tags. */
178
- tags?: string[];
179
- }
180
- /**
181
- * A GAME EVENT (spec §2): a pure engine instruction with no player-facing text - just `gameData` the host
182
- * reads when the beat plays (comments / docs attach via the authoring file by id). Named "game event"
183
- * rather than "action" because a screenplay's "action" is prose, which is our text beat. Player-facing
184
- * words are a line or text beat instead.
185
- */
186
- interface GameEventBeat {
187
- id: string;
188
- kind: "gameEvent";
189
- gameData?: GameData;
190
- /** Author tags (#215). See LineBeat.tags. */
191
- tags?: string[];
192
- }
193
- type Beat = LineBeat | TextBeat | GameEventBeat;
194
- type Selector = "run" | "branch" | "sequence" | "choice";
195
- /** How a `sequence` walks its children. `specificity` = **Best match**: pick the eligible
196
- * child whose condition most specifically fits the current state (the most atomic constraints
197
- * actively holding it true); equally-specific ties break by the seeded shuffle. A child with no
198
- * condition scores zero, so it acts as the filler that wins only when nothing more specific is
199
- * eligible. Composes with `exhaust`: `repeat` re-scores every visit (re-pickable, the Best-match
200
- * default), `once` uses each pick up so the group slides down to the filler (graceful degradation). */
201
- type SelectorOrder = "sequential" | "shuffle" | "specificity";
202
- /** What a `sequence` does after one full pass through its children. */
203
- type SelectorExhaust = "once" | "repeat" | "stick";
204
- /**
205
- * `sequence` selector config (spec §4). One stateful picker with two orthogonal
206
- * axes subsumes Ink's stopping / cycle / once / shuffle and their combinations.
207
- * Defaults: `order: "sequential"`, `exhaust: "once"`. `shuffle` draws without
208
- * replacement and never repeats a line back-to-back (built in).
209
- */
210
- interface SequenceOptions {
211
- order?: SelectorOrder;
212
- exhaust?: SelectorExhaust;
213
- }
214
- /** An option's prompt beat (spec §5): a single line | text beat - the choice text. */
215
- type PromptBeat = LineBeat | TextBeat;
216
- type PropertyType = "boolean" | "number" | "string" | "flags" | "enum" | "quality";
217
- interface PropertyDecl {
218
- name: string;
219
- type: PropertyType;
220
- default?: ScalarValue;
221
- /**
222
- * The orthogonal *sharing* axis (spec §7): is this property's value shared across
223
- * all flows (one world value) or kept per-flow? It does NOT change the reference
224
- * syntax - sharing is set here, on the declaration, not by a different scope token.
225
- * The default depends on the scope it is declared in: a **global** property
226
- * (project `properties` -> `@patter`) defaults to **shared**; a **scene-local**
227
- * property (scene `sceneProps` -> `@scene`) defaults to **not shared** (per-flow).
228
- */
229
- shared?: boolean;
230
- /**
231
- * Persistence axis, for **scene-local (`@scene`) properties** (spec §7). Default
232
- * `false`: the value PERSISTS across scene re-entries (like every other property).
233
- * `true`: the value is **reseeded to its default on every scene entry** - "fresh
234
- * each playthrough" (Ink's `temp`). Orthogonal to `shared`. Ignored on global
235
- * (`@patter`) properties, which always persist for the life of the piece.
236
- */
237
- temporary?: boolean;
238
- /** For enum / flags. */
239
- values?: string[];
240
- /** For quality: the ORDERED ladder of stage names (the shared engine's quality type, expr 0.4.0).
241
- * Ordering operators compare by position in this list and `advance()` steps along it, so the order
242
- * is the meaning - unlike `values`, whose order is presentation only. Default seeds to the first
243
- * stage when no `default` is declared. */
244
- stages?: string[];
245
- /** Free-text author note documenting what this property is for (authoring only; shown as a hint). */
246
- purpose?: string;
247
- }
248
- /**
249
- * A property of a host / world scope (`@world`, `@game`, ...). The same shape the
250
- * `@wildwinter/scoperegistry` `scopeRegistrySpec` uses, declared structurally here
251
- * so the model stays free of a runtime dependency. `default` seeds the standalone
252
- * runtime's self-backed bag (see `HostScopeSpec`); `writable: false` makes the
253
- * property read-only to the story (validated at compile time).
254
- */
255
- interface HostScopeDecl {
256
- name: string;
257
- type: PropertyType;
258
- values?: string[];
259
- /** For quality: the ordered stage ladder (see `PropertyDecl.stages`). */
260
- stages?: string[];
261
- default?: ScalarValue;
262
- writable?: boolean;
263
- /** Free-text author note documenting the property (authoring only; shown as a hint). */
264
- purpose?: string;
265
- }
266
- /** One scope (`token`) in a project's host-scope registry. */
267
- interface HostScopeSpec {
268
- /** The scope token after `@` (e.g. `"world"`). Must not collide with Patter's own
269
- * `patter` / `scene` / `flow`. */
270
- token: string;
271
- /** Scope-level read/write default for its declarations (default true). */
272
- writable?: boolean;
273
- /** Property declarations; omit for an opaque scope (any name, unchecked). */
274
- declarations?: HostScopeDecl[];
275
- }
276
- /**
277
- * A project's host-scope registry: the `scopeRegistrySpec` it OWNS (spec
278
- * design/scope-registry.md §6). Makes `@world` (and any host scope) first-class
279
- * in a standalone project: the compiler validates references into it, the runtime
280
- * self-backs it from declaration defaults when no host resolver claims the token,
281
- * and coverage drives its values. Structurally identical to scoperegistry's
282
- * `ScopeRegistrySpec` so it threads straight through the compiler.
283
- */
284
- interface HostScopeRegistry {
285
- version: number;
286
- scopes: HostScopeSpec[];
287
- }
288
- /** A character's grammatical gender, for localisation. Translators need it to inflect the speaker's own
289
- * lines in gendered languages (adjectives, participles, pronouns), which the source text alone often
290
- * cannot tell them. Absent means "not specified". Authoring-only: it never reaches the runtime bundle,
291
- * but it IS carried into the localisation handoff formats as translator context (spec §14).
292
- *
293
- * Free text, not a closed set: real languages need more than three genders (common/utrum, animate/
294
- * inanimate, and so on) and translators name them differently. `COMMON_GENDERS` seeds the editor's
295
- * auto-suggest so the everyday values stay spelled consistently; anything else is still valid. */
296
- type GrammaticalGender = string;
297
- interface CastMember {
298
- /** Canonical speaker name (matched by a beat's `character`); language-neutral key. */
299
- name: string;
300
- /** Localised player-facing name - a localisation id (project-level strings). */
301
- displayName?: string;
302
- /** Grammatical gender for translators (see `GrammaticalGender`); absent = not specified. */
303
- gender?: GrammaticalGender;
304
- /** Free-text production notes about the character (casting, voice, intent) - authoring only. */
305
- notes?: string;
306
- /** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
307
- * Authoring-only (not shipped in the runtime bundle). */
308
- actor?: string;
309
- gameData?: GameData;
310
- }
311
- /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
312
- * `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
313
- * game never carries a real person's name or a writer's private notes. The compiler copies the shipping
314
- * fields across explicitly (an allow-list), which is what actually keeps a new authoring field out of
315
- * the bundle; this type states the resulting contract for anyone reading a bundle. */
316
- type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
317
- /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
318
- * Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
319
- type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
320
- /** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
321
- * Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
322
- type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
323
- /** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
324
- interface GameDataField {
325
- /** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
326
- name: string;
327
- type: GameDataFieldType;
328
- /** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
329
- * a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
330
- default?: ScalarValue;
331
- /** Allowed values when `type` is "enum". */
332
- values?: string[];
333
- /** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
334
- purpose?: string;
335
- }
336
- /** Author-defined gameData fields, grouped by the node type they attach to (project-level schema). */
337
- type GameDataFields = Partial<Record<GameDataNodeKind, GameDataField[]>>;
338
- /** A compiled expression envelope: canonical source + pre-derived tagged-tuple AST. */
339
- interface Expression {
340
- src: string;
341
- ast: AstNode;
342
- }
343
- type CompiledEffect = {
344
- kind: "set";
345
- target: string;
346
- value: Expression;
347
- };
348
- interface CompiledSnippet {
349
- id: string;
350
- type: "snippet";
351
- condition?: Expression;
352
- beats?: Beat[];
353
- onEnter?: CompiledEffect[];
354
- onExit?: CompiledEffect[];
355
- gameData?: GameData;
356
- tags?: string[];
357
- jump?: Jump;
358
- secretUntilEligible?: boolean;
359
- /** Option-position: repeatable (spec §5). Default false = once-only. */
360
- sticky?: boolean;
361
- /** Option-position: the choice's fallback, auto-followed when last (spec §5). */
362
- fallback?: boolean;
363
- }
364
- interface CompiledGroup {
365
- id: string;
366
- type: "group";
367
- condition?: Expression;
368
- /** Default (omitted) = `"run"`. */
369
- selector?: Selector;
370
- /** Selector cursor shared across flows (default false = per-flow). */
371
- shared?: boolean;
372
- options?: SequenceOptions;
373
- children: Array<CompiledGroup | CompiledSnippet>;
374
- gameData?: GameData;
375
- tags?: string[];
376
- /** Option-position fields (spec §5) - only when a direct child of a `choice`. */
377
- prompt?: PromptBeat;
378
- secretUntilEligible?: boolean;
379
- /** Repeatable (spec §5). Default false = once-only. */
380
- sticky?: boolean;
381
- /** The choice's fallback, auto-followed when last (spec §5). */
382
- fallback?: boolean;
383
- }
384
- interface CompiledBlock {
385
- id: string;
386
- type: "block";
387
- name: string;
388
- /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
389
- gameId?: string;
390
- children: Array<CompiledGroup | CompiledSnippet>;
391
- gameData?: GameData;
392
- tags?: string[];
393
- }
394
- interface CompiledScene {
395
- id: string;
396
- type: "scene";
397
- name: string;
398
- /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
399
- gameId?: string;
400
- gameData?: GameData;
401
- tags?: string[];
402
- onEntry?: CompiledEffect[];
403
- sceneProps?: PropertyDecl[];
404
- blocks: CompiledBlock[];
405
- }
406
- interface Bundle {
407
- schema: string;
408
- /** `hash` fingerprints the WHOLE bundle (binds saves, gates staleness); `structureHash` is the same
409
- * fingerprint with the string tables left out, so same structureHash + a different hash = a
410
- * text-only edit, safe to hot-swap in place (live bundle refresh). */
411
- content: {
412
- project: string;
413
- version?: string;
414
- hash?: string;
415
- structureHash?: string;
416
- };
417
- voiced: boolean;
418
- locales: {
419
- default: string;
420
- included: string[];
421
- };
422
- /** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
423
- cast?: BundleCastMember[];
424
- properties?: PropertyDecl[];
425
- /** Host / world scope declarations, baked from the project so the runtime can self-back a declared
426
- * scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
427
- scopeRegistry?: HostScopeRegistry;
428
- gameDataFields?: GameDataFields;
429
- scenes: Record<string, CompiledScene>;
430
- /** locale -> (beatId -> text). In "embedded" localisation this carries every included locale; in "ids"
431
- * it is EMPTY (the runtime emits beat IDs), unless `localisation.sourceDebug` embedded the source locale
432
- * for debug playback. `content.hash` is computed over the FULL strings regardless, so the staleness gate
433
- * is unaffected. */
434
- strings: Record<string, Record<string, string>>;
435
- /** How strings ship + resolve (spec §11). Absent = "embedded" (back-compat default): the runtime resolves
436
- * `strings` per locale. "ids": the runtime emits beat IDs and the game localises them itself; `sourceDebug`
437
- * means the source locale is embedded purely for debug playback and the runtime should flag the build as
438
- * not shippable. */
439
- localisation?: {
440
- mode: "embedded" | "ids";
441
- sourceDebug?: boolean;
442
- };
443
- /** Closed-caption delimiters baked from the project (#214). Absent = the default `(` / `)`; the
444
- * runtime strips spans between them from line text when a game disables captions. */
445
- closedCaptions?: CaptionDelimiters;
446
- }
447
- /** Closed-caption configuration (#214). `open`/`close` wrap a caption cue inside a dialogue line (both
448
- * non-empty; they MAY be the same token, e.g. `*…*`). `character` names a cast member whose lines are a
449
- * pure caption: when captions are off, ALL of that character's dialogue (and its speaker label) is
450
- * omitted - delimiters or not - leaving a silent line that still fires (so audio plays). Absent / empty
451
- * `character` resolves to the default `SFX` (you "disable" it simply by never using that speaker). */
452
- interface CaptionDelimiters {
453
- open: string;
454
- close: string;
455
- character?: string;
456
- }
457
-
458
136
  interface BundleInspectorOptions {
459
137
  /** Where to mount the panel. If omitted, append the returned `el` yourself. */
460
138
  container?: HTMLElement;
@@ -477,7 +155,7 @@ interface BundleInspector {
477
155
  * Build a read-only panel describing a compiled bundle: what it is, what game
478
156
  * code may call on it, and what the host must supply.
479
157
  */
480
- declare function createBundleInspector(bundle: Bundle, opts?: BundleInspectorOptions): BundleInspector;
158
+ declare function createBundleInspector(bundle: Bundle$1, opts?: BundleInspectorOptions): BundleInspector;
481
159
 
482
160
  interface AudioResolver {
483
161
  /** The full path/URL of a beat's winning audio take, or null when it has none. */
@@ -490,4 +168,4 @@ interface AudioResolver {
490
168
  */
491
169
  declare function createAudioResolver(manifestJson: string, basePath: string): AudioResolver;
492
170
 
493
- export { type AudioResolver, type BundleInspector, type BundleInspectorOptions, type BundleSection, type DebugLink, type DebugLinkOptions, type DebugSocketLike, type LiveBundleResult, type PropertyInspector, type PropertyInspectorOptions, type PropertyValue, SAVE_SCHEMA, type SaveEnvelope, type StateChange, type StateLogger, type StateLoggerOptions, type StateSnapshot, type StateValue, applyLiveBundle, createAudioResolver, createBundleInspector, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, diffState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
171
+ export { type AudioResolver, type BundleInspector, type BundleInspectorOptions, type BundleSection, type DebugLink, type DebugLinkOptions, type DebugSocketLike, type LiveBundleResult, type PropertyInspector, type PropertyInspectorOptions, type PropertyValue, type StateLogger, type StateLoggerOptions, type StateValue, applyLiveBundle, createAudioResolver, createBundleInspector, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,9 @@
1
- import { SaveGame, Engine, StepResult, Bundle as Bundle$1, BundleDescription } from '@patterkit/runtime';
1
+ import { Engine, StepResult, Bundle, BundleDescription } from '@patterkit/runtime';
2
+ import { SaveEnvelope, Bundle as Bundle$1 } from '@patterkit/model';
3
+ export { SAVE_SCHEMA, SaveEnvelope } from '@patterkit/model';
4
+ import { StateSnapshot, StateChange, ScalarValue } from '@wildwinter/scoperegistry';
5
+ export { StateChange, StateSnapshot, diffState } from '@wildwinter/scoperegistry';
2
6
 
3
- declare const SAVE_SCHEMA = "patter/save@0";
4
- interface SaveEnvelope {
5
- schema: typeof SAVE_SCHEMA;
6
- /** The engine save-game: shared `@patter`/`@scene` state, visit counts, and every live flow. */
7
- save: SaveGame;
8
- }
9
7
  /** Capture the whole game as a tagged envelope (wraps `engine.saveGame()`). */
10
8
  declare function saveState(engine: Engine): SaveEnvelope;
11
9
  /** Restore a {@link saveState} envelope into an engine (fresh or live). Throws on a foreign/blank blob. */
@@ -24,20 +22,14 @@ declare function setProperty(engine: Engine, ref: string, value: PropertyValue):
24
22
  /** Set many at once, e.g. `setProperties(engine, { "@hp": 10, "@scene.locked": false })`. */
25
23
  declare function setProperties(engine: Engine, values: Record<string, PropertyValue>): void;
26
24
 
27
- /** A flattened runtime-state value (a Patter scalar: number / boolean / string / string[] flags). */
28
- type StateValue = NonNullable<ReturnType<Engine["getProperty"]>>;
29
- /** A flattened snapshot: dotted path -> value. Paths: `@patter.x`, `@scene:scene.x`, `visit:nodeId`,
30
- * and `flow/...` for a flow's not-shared locals. */
31
- type StateSnapshot = Record<string, StateValue>;
32
- interface StateChange {
33
- path: string;
34
- from?: StateValue;
35
- to?: StateValue;
36
- }
37
- /** Flatten the engine's whole-game state into a path -> value map (shared scopes + every live flow). */
25
+ /** A flattened runtime-state value: a Patter scalar. The kernel calls this `ScalarValue`;
26
+ * the alias stays because it is exported API and reads in Patter's own vocabulary. */
27
+ type StateValue = ScalarValue;
28
+ /** Flatten the engine's whole-game state into a path -> value map (shared scopes + every live
29
+ * flow), off `saveGame()`. The logger no longer diffs this - it mounts the bags directly - but
30
+ * it stays as the public "what is the state right now" call, and as the definition of the path
31
+ * space the mounts compose. */
38
32
  declare function snapshotState(engine: Engine): StateSnapshot;
39
- /** The sorted set of paths that differ between two snapshots (added / removed / changed). */
40
- declare function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[];
41
33
  interface StateLoggerOptions {
42
34
  /** Where lines go; defaults to `console.log`. */
43
35
  sink?: (line: string) => void;
@@ -47,12 +39,16 @@ interface StateLoggerOptions {
47
39
  interface StateLogger {
48
40
  /** The current flattened state (no logging). */
49
41
  snapshot(): StateSnapshot;
50
- /** Diff since the last capture, log each change, and re-baseline. Returns the changes. */
42
+ /** Everything since the last capture: the property writes already logged as they landed,
43
+ * plus the visit counts, diffed and re-baselined. */
51
44
  capture(): StateChange[];
52
45
  /** Trace one played step (line / text / game-event / choice / end), including any `gameData`. */
53
46
  logStep(step: StepResult): void;
47
+ /** Unhook the bag auditors. The logger is inert afterwards. */
48
+ dispose(): void;
54
49
  }
55
- /** Create a state logger over an engine. Call `capture()` after each `advance`/`choose` to log mutations. */
50
+ /** Create a state logger over an engine. Property writes log as they land; call `capture()`
51
+ * after each `advance`/`choose` to pick up the visit counts and re-baseline. */
56
52
  declare function createStateLogger(engine: Engine, opts?: StateLoggerOptions): StateLogger;
57
53
 
58
54
  /** A minimal structural type for a WebSocket implementation (browsers + Node 21+ have a global one). */
@@ -107,7 +103,7 @@ interface LiveBundleResult {
107
103
  /** The engine to keep using: the same instance for a "text" swap, a replacement for "structure". */
108
104
  engine: Engine;
109
105
  /** The parsed pushed bundle - hold on to it for the next apply's comparison. */
110
- bundle: Bundle$1;
106
+ bundle: Bundle;
111
107
  /** Which tier applied: "text" (strings-only, nothing restarted) or "structure" (full hot swap). */
112
108
  kind: "text" | "structure";
113
109
  }
@@ -117,7 +113,7 @@ interface LiveBundleResult {
117
113
  * unparseable JSON - a structural swap's edge cases are absorbed by the §9.8 drift policy inside
118
114
  * `hotSwap`, which never throws for ordinary edits.
119
115
  */
120
- declare function applyLiveBundle(engine: Engine, current: Bundle$1, data: string): LiveBundleResult;
116
+ declare function applyLiveBundle(engine: Engine, current: Bundle, data: string): LiveBundleResult;
121
117
 
122
118
  interface PropertyInspectorOptions {
123
119
  /** Where to mount the panel. If omitted, append the returned `el` yourself. */
@@ -137,324 +133,6 @@ interface PropertyInspector {
137
133
  }
138
134
  declare function createPropertyInspector(engine: Engine, opts?: PropertyInspectorOptions): PropertyInspector;
139
135
 
140
- type BinaryOp = "==" | "!=" | ">" | ">=" | "<" | "<=" | "+" | "-" | "*" | "/" | "and" | "or";
141
- type UnaryOp = "not" | "neg";
142
- type AstNode = ["b", boolean] | ["n", number] | ["s", string] | ["sv", string, string] | ["u", UnaryOp, AstNode] | ["bin", BinaryOp, AstNode, AstNode] | ["call", string, ...AstNode[]] | ["fd", "+" | "-", string];
143
-
144
- type ScalarValue = boolean | number | string | string[];
145
- /** Developer-defined host metadata (spec §17). Opaque to Patter. */
146
- type GameData = Record<string, unknown>;
147
- /** A scene/block id, or the reserved "END". */
148
- type JumpTarget = string;
149
- interface Jump {
150
- to: JumpTarget;
151
- /** "jump" (one-way, default) or "call" (jump-and-return via the flow callstack). */
152
- mode?: "jump" | "call";
153
- }
154
- interface LineBeat {
155
- id: string;
156
- kind: "line";
157
- /** Speaker; must be a member of the project cast (validated). */
158
- character?: string;
159
- /** Performance direction, language-neutral (never localised). */
160
- direction?: string;
161
- gameData?: GameData;
162
- /** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
163
- * At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
164
- * snippet → beat). Each tag is letters/digits/symbols with NO comma and NO whitespace; deduped. */
165
- tags?: string[];
166
- }
167
- /**
168
- * Authorial voice / narration - speaker-less prose (spec §2). Never voiced; its
169
- * localised text always permits inline `{@name}` interpolation (spec §16). This
170
- * is the on-screen-text role (e.g. "A door slams!") - distinct from a game-event
171
- * beat, which is a pure engine instruction with no localised content.
172
- */
173
- interface TextBeat {
174
- id: string;
175
- kind: "text";
176
- gameData?: GameData;
177
- /** Author tags (#215). See LineBeat.tags. */
178
- tags?: string[];
179
- }
180
- /**
181
- * A GAME EVENT (spec §2): a pure engine instruction with no player-facing text - just `gameData` the host
182
- * reads when the beat plays (comments / docs attach via the authoring file by id). Named "game event"
183
- * rather than "action" because a screenplay's "action" is prose, which is our text beat. Player-facing
184
- * words are a line or text beat instead.
185
- */
186
- interface GameEventBeat {
187
- id: string;
188
- kind: "gameEvent";
189
- gameData?: GameData;
190
- /** Author tags (#215). See LineBeat.tags. */
191
- tags?: string[];
192
- }
193
- type Beat = LineBeat | TextBeat | GameEventBeat;
194
- type Selector = "run" | "branch" | "sequence" | "choice";
195
- /** How a `sequence` walks its children. `specificity` = **Best match**: pick the eligible
196
- * child whose condition most specifically fits the current state (the most atomic constraints
197
- * actively holding it true); equally-specific ties break by the seeded shuffle. A child with no
198
- * condition scores zero, so it acts as the filler that wins only when nothing more specific is
199
- * eligible. Composes with `exhaust`: `repeat` re-scores every visit (re-pickable, the Best-match
200
- * default), `once` uses each pick up so the group slides down to the filler (graceful degradation). */
201
- type SelectorOrder = "sequential" | "shuffle" | "specificity";
202
- /** What a `sequence` does after one full pass through its children. */
203
- type SelectorExhaust = "once" | "repeat" | "stick";
204
- /**
205
- * `sequence` selector config (spec §4). One stateful picker with two orthogonal
206
- * axes subsumes Ink's stopping / cycle / once / shuffle and their combinations.
207
- * Defaults: `order: "sequential"`, `exhaust: "once"`. `shuffle` draws without
208
- * replacement and never repeats a line back-to-back (built in).
209
- */
210
- interface SequenceOptions {
211
- order?: SelectorOrder;
212
- exhaust?: SelectorExhaust;
213
- }
214
- /** An option's prompt beat (spec §5): a single line | text beat - the choice text. */
215
- type PromptBeat = LineBeat | TextBeat;
216
- type PropertyType = "boolean" | "number" | "string" | "flags" | "enum" | "quality";
217
- interface PropertyDecl {
218
- name: string;
219
- type: PropertyType;
220
- default?: ScalarValue;
221
- /**
222
- * The orthogonal *sharing* axis (spec §7): is this property's value shared across
223
- * all flows (one world value) or kept per-flow? It does NOT change the reference
224
- * syntax - sharing is set here, on the declaration, not by a different scope token.
225
- * The default depends on the scope it is declared in: a **global** property
226
- * (project `properties` -> `@patter`) defaults to **shared**; a **scene-local**
227
- * property (scene `sceneProps` -> `@scene`) defaults to **not shared** (per-flow).
228
- */
229
- shared?: boolean;
230
- /**
231
- * Persistence axis, for **scene-local (`@scene`) properties** (spec §7). Default
232
- * `false`: the value PERSISTS across scene re-entries (like every other property).
233
- * `true`: the value is **reseeded to its default on every scene entry** - "fresh
234
- * each playthrough" (Ink's `temp`). Orthogonal to `shared`. Ignored on global
235
- * (`@patter`) properties, which always persist for the life of the piece.
236
- */
237
- temporary?: boolean;
238
- /** For enum / flags. */
239
- values?: string[];
240
- /** For quality: the ORDERED ladder of stage names (the shared engine's quality type, expr 0.4.0).
241
- * Ordering operators compare by position in this list and `advance()` steps along it, so the order
242
- * is the meaning - unlike `values`, whose order is presentation only. Default seeds to the first
243
- * stage when no `default` is declared. */
244
- stages?: string[];
245
- /** Free-text author note documenting what this property is for (authoring only; shown as a hint). */
246
- purpose?: string;
247
- }
248
- /**
249
- * A property of a host / world scope (`@world`, `@game`, ...). The same shape the
250
- * `@wildwinter/scoperegistry` `scopeRegistrySpec` uses, declared structurally here
251
- * so the model stays free of a runtime dependency. `default` seeds the standalone
252
- * runtime's self-backed bag (see `HostScopeSpec`); `writable: false` makes the
253
- * property read-only to the story (validated at compile time).
254
- */
255
- interface HostScopeDecl {
256
- name: string;
257
- type: PropertyType;
258
- values?: string[];
259
- /** For quality: the ordered stage ladder (see `PropertyDecl.stages`). */
260
- stages?: string[];
261
- default?: ScalarValue;
262
- writable?: boolean;
263
- /** Free-text author note documenting the property (authoring only; shown as a hint). */
264
- purpose?: string;
265
- }
266
- /** One scope (`token`) in a project's host-scope registry. */
267
- interface HostScopeSpec {
268
- /** The scope token after `@` (e.g. `"world"`). Must not collide with Patter's own
269
- * `patter` / `scene` / `flow`. */
270
- token: string;
271
- /** Scope-level read/write default for its declarations (default true). */
272
- writable?: boolean;
273
- /** Property declarations; omit for an opaque scope (any name, unchecked). */
274
- declarations?: HostScopeDecl[];
275
- }
276
- /**
277
- * A project's host-scope registry: the `scopeRegistrySpec` it OWNS (spec
278
- * design/scope-registry.md §6). Makes `@world` (and any host scope) first-class
279
- * in a standalone project: the compiler validates references into it, the runtime
280
- * self-backs it from declaration defaults when no host resolver claims the token,
281
- * and coverage drives its values. Structurally identical to scoperegistry's
282
- * `ScopeRegistrySpec` so it threads straight through the compiler.
283
- */
284
- interface HostScopeRegistry {
285
- version: number;
286
- scopes: HostScopeSpec[];
287
- }
288
- /** A character's grammatical gender, for localisation. Translators need it to inflect the speaker's own
289
- * lines in gendered languages (adjectives, participles, pronouns), which the source text alone often
290
- * cannot tell them. Absent means "not specified". Authoring-only: it never reaches the runtime bundle,
291
- * but it IS carried into the localisation handoff formats as translator context (spec §14).
292
- *
293
- * Free text, not a closed set: real languages need more than three genders (common/utrum, animate/
294
- * inanimate, and so on) and translators name them differently. `COMMON_GENDERS` seeds the editor's
295
- * auto-suggest so the everyday values stay spelled consistently; anything else is still valid. */
296
- type GrammaticalGender = string;
297
- interface CastMember {
298
- /** Canonical speaker name (matched by a beat's `character`); language-neutral key. */
299
- name: string;
300
- /** Localised player-facing name - a localisation id (project-level strings). */
301
- displayName?: string;
302
- /** Grammatical gender for translators (see `GrammaticalGender`); absent = not specified. */
303
- gender?: GrammaticalGender;
304
- /** Free-text production notes about the character (casting, voice, intent) - authoring only. */
305
- notes?: string;
306
- /** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
307
- * Authoring-only (not shipped in the runtime bundle). */
308
- actor?: string;
309
- gameData?: GameData;
310
- }
311
- /** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
312
- * `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
313
- * game never carries a real person's name or a writer's private notes. The compiler copies the shipping
314
- * fields across explicitly (an allow-list), which is what actually keeps a new authoring field out of
315
- * the bundle; this type states the resulting contract for anyone reading a bundle. */
316
- type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
317
- /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
318
- * Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
319
- type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
320
- /** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
321
- * Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
322
- type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
323
- /** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
324
- interface GameDataField {
325
- /** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
326
- name: string;
327
- type: GameDataFieldType;
328
- /** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
329
- * a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
330
- default?: ScalarValue;
331
- /** Allowed values when `type` is "enum". */
332
- values?: string[];
333
- /** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
334
- purpose?: string;
335
- }
336
- /** Author-defined gameData fields, grouped by the node type they attach to (project-level schema). */
337
- type GameDataFields = Partial<Record<GameDataNodeKind, GameDataField[]>>;
338
- /** A compiled expression envelope: canonical source + pre-derived tagged-tuple AST. */
339
- interface Expression {
340
- src: string;
341
- ast: AstNode;
342
- }
343
- type CompiledEffect = {
344
- kind: "set";
345
- target: string;
346
- value: Expression;
347
- };
348
- interface CompiledSnippet {
349
- id: string;
350
- type: "snippet";
351
- condition?: Expression;
352
- beats?: Beat[];
353
- onEnter?: CompiledEffect[];
354
- onExit?: CompiledEffect[];
355
- gameData?: GameData;
356
- tags?: string[];
357
- jump?: Jump;
358
- secretUntilEligible?: boolean;
359
- /** Option-position: repeatable (spec §5). Default false = once-only. */
360
- sticky?: boolean;
361
- /** Option-position: the choice's fallback, auto-followed when last (spec §5). */
362
- fallback?: boolean;
363
- }
364
- interface CompiledGroup {
365
- id: string;
366
- type: "group";
367
- condition?: Expression;
368
- /** Default (omitted) = `"run"`. */
369
- selector?: Selector;
370
- /** Selector cursor shared across flows (default false = per-flow). */
371
- shared?: boolean;
372
- options?: SequenceOptions;
373
- children: Array<CompiledGroup | CompiledSnippet>;
374
- gameData?: GameData;
375
- tags?: string[];
376
- /** Option-position fields (spec §5) - only when a direct child of a `choice`. */
377
- prompt?: PromptBeat;
378
- secretUntilEligible?: boolean;
379
- /** Repeatable (spec §5). Default false = once-only. */
380
- sticky?: boolean;
381
- /** The choice's fallback, auto-followed when last (spec §5). */
382
- fallback?: boolean;
383
- }
384
- interface CompiledBlock {
385
- id: string;
386
- type: "block";
387
- name: string;
388
- /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
389
- gameId?: string;
390
- children: Array<CompiledGroup | CompiledSnippet>;
391
- gameData?: GameData;
392
- tags?: string[];
393
- }
394
- interface CompiledScene {
395
- id: string;
396
- type: "scene";
397
- name: string;
398
- /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
399
- gameId?: string;
400
- gameData?: GameData;
401
- tags?: string[];
402
- onEntry?: CompiledEffect[];
403
- sceneProps?: PropertyDecl[];
404
- blocks: CompiledBlock[];
405
- }
406
- interface Bundle {
407
- schema: string;
408
- /** `hash` fingerprints the WHOLE bundle (binds saves, gates staleness); `structureHash` is the same
409
- * fingerprint with the string tables left out, so same structureHash + a different hash = a
410
- * text-only edit, safe to hot-swap in place (live bundle refresh). */
411
- content: {
412
- project: string;
413
- version?: string;
414
- hash?: string;
415
- structureHash?: string;
416
- };
417
- voiced: boolean;
418
- locales: {
419
- default: string;
420
- included: string[];
421
- };
422
- /** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
423
- cast?: BundleCastMember[];
424
- properties?: PropertyDecl[];
425
- /** Host / world scope declarations, baked from the project so the runtime can self-back a declared
426
- * scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
427
- scopeRegistry?: HostScopeRegistry;
428
- gameDataFields?: GameDataFields;
429
- scenes: Record<string, CompiledScene>;
430
- /** locale -> (beatId -> text). In "embedded" localisation this carries every included locale; in "ids"
431
- * it is EMPTY (the runtime emits beat IDs), unless `localisation.sourceDebug` embedded the source locale
432
- * for debug playback. `content.hash` is computed over the FULL strings regardless, so the staleness gate
433
- * is unaffected. */
434
- strings: Record<string, Record<string, string>>;
435
- /** How strings ship + resolve (spec §11). Absent = "embedded" (back-compat default): the runtime resolves
436
- * `strings` per locale. "ids": the runtime emits beat IDs and the game localises them itself; `sourceDebug`
437
- * means the source locale is embedded purely for debug playback and the runtime should flag the build as
438
- * not shippable. */
439
- localisation?: {
440
- mode: "embedded" | "ids";
441
- sourceDebug?: boolean;
442
- };
443
- /** Closed-caption delimiters baked from the project (#214). Absent = the default `(` / `)`; the
444
- * runtime strips spans between them from line text when a game disables captions. */
445
- closedCaptions?: CaptionDelimiters;
446
- }
447
- /** Closed-caption configuration (#214). `open`/`close` wrap a caption cue inside a dialogue line (both
448
- * non-empty; they MAY be the same token, e.g. `*…*`). `character` names a cast member whose lines are a
449
- * pure caption: when captions are off, ALL of that character's dialogue (and its speaker label) is
450
- * omitted - delimiters or not - leaving a silent line that still fires (so audio plays). Absent / empty
451
- * `character` resolves to the default `SFX` (you "disable" it simply by never using that speaker). */
452
- interface CaptionDelimiters {
453
- open: string;
454
- close: string;
455
- character?: string;
456
- }
457
-
458
136
  interface BundleInspectorOptions {
459
137
  /** Where to mount the panel. If omitted, append the returned `el` yourself. */
460
138
  container?: HTMLElement;
@@ -477,7 +155,7 @@ interface BundleInspector {
477
155
  * Build a read-only panel describing a compiled bundle: what it is, what game
478
156
  * code may call on it, and what the host must supply.
479
157
  */
480
- declare function createBundleInspector(bundle: Bundle, opts?: BundleInspectorOptions): BundleInspector;
158
+ declare function createBundleInspector(bundle: Bundle$1, opts?: BundleInspectorOptions): BundleInspector;
481
159
 
482
160
  interface AudioResolver {
483
161
  /** The full path/URL of a beat's winning audio take, or null when it has none. */
@@ -490,4 +168,4 @@ interface AudioResolver {
490
168
  */
491
169
  declare function createAudioResolver(manifestJson: string, basePath: string): AudioResolver;
492
170
 
493
- export { type AudioResolver, type BundleInspector, type BundleInspectorOptions, type BundleSection, type DebugLink, type DebugLinkOptions, type DebugSocketLike, type LiveBundleResult, type PropertyInspector, type PropertyInspectorOptions, type PropertyValue, SAVE_SCHEMA, type SaveEnvelope, type StateChange, type StateLogger, type StateLoggerOptions, type StateSnapshot, type StateValue, applyLiveBundle, createAudioResolver, createBundleInspector, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, diffState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
171
+ export { type AudioResolver, type BundleInspector, type BundleInspectorOptions, type BundleSection, type DebugLink, type DebugLinkOptions, type DebugSocketLike, type LiveBundleResult, type PropertyInspector, type PropertyInspectorOptions, type PropertyValue, type StateLogger, type StateLoggerOptions, type StateValue, applyLiveBundle, createAudioResolver, createBundleInspector, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/save.ts
2
- var SAVE_SCHEMA = "patter/save@0";
2
+ import { SAVE_SCHEMA } from "@patterkit/model";
3
3
  function saveState(engine) {
4
4
  return { schema: SAVE_SCHEMA, save: engine.saveGame() };
5
5
  }
@@ -28,7 +28,7 @@ function setProperties(engine, values) {
28
28
  }
29
29
 
30
30
  // src/logger.ts
31
- var eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
31
+ import { createStateLogger as createKernelStateLogger, diffState } from "@wildwinter/scoperegistry";
32
32
  function snapshotState(engine) {
33
33
  const save = engine.saveGame();
34
34
  const out = {};
@@ -44,16 +44,6 @@ function snapshotState(engine) {
44
44
  }
45
45
  return out;
46
46
  }
47
- function diffState(prev, next) {
48
- const changes = [];
49
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)])].sort();
50
- for (const path of keys) {
51
- const from = prev[path], to = next[path];
52
- if (!eq(from, to)) changes.push({ path, ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} });
53
- }
54
- return changes;
55
- }
56
- var fmt = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
57
47
  function describeStep(step) {
58
48
  switch (step.type) {
59
49
  case "line":
@@ -69,21 +59,29 @@ function describeStep(step) {
69
59
  }
70
60
  }
71
61
  var gd = (data) => data ? ` gameData=${JSON.stringify(data)}` : "";
62
+ function visitState(engine) {
63
+ const save = engine.saveGame();
64
+ const out = {};
65
+ for (const [id, n] of Object.entries(save.sharedVisits)) out[`visit:${id}`] = n;
66
+ for (const [fid, snap] of Object.entries(save.flows)) {
67
+ for (const [id, n] of Object.entries(snap.visits)) out[`${fid}/visit:${id}`] = n;
68
+ }
69
+ return out;
70
+ }
72
71
  function createStateLogger(engine, opts = {}) {
73
- const sink = opts.sink ?? ((line) => console.log(line));
74
72
  const tag = opts.label ? `[${opts.label}] ` : "";
75
- let baseline = snapshotState(engine);
73
+ const kernel = createKernelStateLogger({
74
+ // Re-read on every capture: openFlow and loadGame both replace bags, and the kernel
75
+ // re-mounts whatever it is handed.
76
+ mounts: () => [...engine.listBags(), ...engine.flows().flatMap((f) => f.listBags())],
77
+ extra: () => visitState(engine)
78
+ }, { sink: opts.sink, label: tag });
76
79
  return {
77
80
  snapshot: () => snapshotState(engine),
78
- capture() {
79
- const next = snapshotState(engine);
80
- const changes = diffState(baseline, next);
81
- baseline = next;
82
- for (const c of changes) sink(`${tag}${c.path}: ${fmt(c.from)} -> ${fmt(c.to)}`);
83
- return changes;
84
- },
81
+ capture: () => kernel.capture(),
82
+ dispose: () => kernel.dispose(),
85
83
  logStep(step) {
86
- sink(`${tag}${describeStep(step)}`);
84
+ (opts.sink ?? ((l) => console.log(l)))(`${tag}${describeStep(step)}`);
87
85
  }
88
86
  };
89
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/play-helpers",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Thin game-integration helpers around @patterkit/runtime: save/load serialisation, runtime property setters, a state logger, the Patterpad Live Link client + hot reload, a property inspector, and audio resolution. The Patterplay JS companion.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,8 @@
34
34
  "build": "tsup src/index.ts --format esm,cjs --dts --clean"
35
35
  },
36
36
  "dependencies": {
37
- "@patterkit/runtime": "0.9.0"
37
+ "@patterkit/model": "^0.5.0",
38
+ "@patterkit/runtime": "0.11.0",
39
+ "@wildwinter/scoperegistry": "^0.5.0"
38
40
  }
39
41
  }