@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 +25 -27
- package/dist/index.d.cts +22 -344
- package/dist/index.d.ts +22 -344
- package/dist/index.js +20 -22
- package/package.json +4 -2
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
37
|
+
"@patterkit/model": "^0.5.0",
|
|
38
|
+
"@patterkit/runtime": "0.11.0",
|
|
39
|
+
"@wildwinter/scoperegistry": "^0.5.0"
|
|
38
40
|
}
|
|
39
41
|
}
|