@patterkit/runtime 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +13 -2
- package/dist/index.cjs +162 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -2
- package/dist/index.d.ts +40 -2
- package/dist/index.js +138 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,32 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.24.0] - 2026-10-11
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Flow capture and restore**: `engine.captureFlow(id)` takes one flow out as a `FlowState`, plain data in the
|
|
41
|
+
save's own spelling (its snapshot, a `version`, and its own values as a version 2 save's per-flow `scopes`
|
|
42
|
+
and `sceneBags`, without its id), and `engine.restoreFlow(id, state)` opens it again under any id, as
|
|
43
|
+
`loadGame` restores each flow. Shared state isn't in it, so a restore rewinds nothing else, and anything a
|
|
44
|
+
load left waiting under the id is dropped first, as when a flow starts. Refused over an open id, while a
|
|
45
|
+
checkpoint is open, and for a state with no cursor, changing nothing; capturing is allowed in a checkpoint.
|
|
46
|
+
The conformance corpus holds every runtime to it.
|
|
47
|
+
|
|
48
|
+
## [0.23.0] - 2026-10-10
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **Line padding**: the writer's pause after a line, in seconds. A `line` or `text` step carries `padAfter`,
|
|
53
|
+
resolved from the beat's own value, else the nearest `padAfterDefault` above it (its snippet, each group
|
|
54
|
+
around it, innermost first, its block, its scene), else the project's, else the built-in 0.6. Negative is a
|
|
55
|
+
cut-in, the next line (or game event) starting that long before this one ends; it is clamped to zero only on
|
|
56
|
+
a snippet's last line or text beat, where nothing certain follows. A choice option's `prompt` carries its
|
|
57
|
+
`padAfter` too, resolved through its option and never clamped, for a game that voices the prompts itself.
|
|
58
|
+
The outline and beat sequence carry `padAfter` and, when the beat sets its own, `ownPadAfter`.
|
|
59
|
+
`DEFAULT_PAD_AFTER` (0.6) is exported. What to do with it is the game's: the docs give the timing
|
|
60
|
+
rules a writer expects.
|
|
61
|
+
|
|
36
62
|
## [0.22.0] - 2026-10-10
|
|
37
63
|
|
|
38
64
|
### Added
|
package/README.md
CHANGED
|
@@ -44,6 +44,13 @@ for (;;) {
|
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
A `line` step carries `id`, `text`, `character`, `characterName`, `direction`, and, when the line has a
|
|
48
|
+
speaker qualifier, `qualifier` (its `gameId`, `os`) and `qualifierName` (its shown name, `O.S.`). Line and
|
|
49
|
+
text steps both carry `padAfter`, the writer's pause after the beat in seconds, already resolved; a negative
|
|
50
|
+
one means the next line cuts in. Line, text, and game event steps can carry `gameData` and `tags`. The
|
|
51
|
+
[step shapes](https://patterkit.dev/play/engine/#step-shapes) give the full list and the timing rules a game
|
|
52
|
+
should follow.
|
|
53
|
+
|
|
47
54
|
`new Engine(bundle, options)` takes `{ rng?, seed?, locale?, hostScopes?, registry?, replayPromptOnChoose?, closedCaptions? }`:
|
|
48
55
|
- `hostScopes` binds the scopes your game owns, by token, each `{ get(name), set?(name, value) }`: for World
|
|
49
56
|
Properties, `hostScopes: { world: { get, set } }`. Leave `world` unbound and the runtime self-backs `@world`
|
|
@@ -60,8 +67,8 @@ state, visit counts, and PRNG are untouched - so a game's "language" setting can
|
|
|
60
67
|
rebuilding the engine or losing the player's place.
|
|
61
68
|
|
|
62
69
|
The above is the **Embedded** build (strings ship inside the `.patterc`). An **IDs-only** build ships no
|
|
63
|
-
strings: `step.text` is the beat **ID
|
|
64
|
-
its own system, then applies `{@ref}` replacement with **`flow.interpolate(yourString)`**. See the
|
|
70
|
+
strings: `step.text` is the beat **ID**, and `step.characterName` and `step.qualifierName` are omitted, so
|
|
71
|
+
your game localises the IDs in its own system, then applies `{@ref}` replacement with **`flow.interpolate(yourString)`**. See the
|
|
65
72
|
[Localisation guide](https://patterkit.dev/play/localisation/) for both modes across all four runtimes.
|
|
66
73
|
|
|
67
74
|
## Drop-in (`<script>`)
|
|
@@ -103,6 +110,10 @@ value); `engine.loadGame(blob)` restores it. Saves from before property values m
|
|
|
103
110
|
registry (version 2) still load. [@patterkit/play-helpers](../play-helpers) wraps these as
|
|
104
111
|
`serializeState` / `deserializeState` with a tagged envelope.
|
|
105
112
|
|
|
113
|
+
To set one flow aside instead, `engine.captureFlow(id)` returns its part of a save as plain data (its
|
|
114
|
+
position, PRNG, visits, selectors, and its own property values, without its id); close it after capturing,
|
|
115
|
+
and `engine.restoreFlow(newId, state)` opens it again later, under any id, without rewinding shared state.
|
|
116
|
+
|
|
106
117
|
## One registry per game
|
|
107
118
|
|
|
108
119
|
A game has one `ScopeRegistry` holding every property from every engine, except values the game
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Checkpoint: () => Checkpoint,
|
|
24
|
+
DEFAULT_PAD_AFTER: () => import_model5.DEFAULT_PAD_AFTER,
|
|
24
25
|
Engine: () => Engine,
|
|
25
26
|
Flow: () => Flow,
|
|
26
27
|
buildTagIndex: () => buildTagIndex,
|
|
@@ -37,7 +38,7 @@ var import_expr_specificity = require("@wildwinter/expr-specificity");
|
|
|
37
38
|
var import_scoperegistry = require("@wildwinter/scoperegistry");
|
|
38
39
|
var import_scoperegistry2 = require("@wildwinter/scoperegistry");
|
|
39
40
|
var import_dialect = require("@patterkit/dialect");
|
|
40
|
-
var
|
|
41
|
+
var import_model2 = require("@patterkit/model");
|
|
41
42
|
|
|
42
43
|
// src/tags.ts
|
|
43
44
|
function dedupe(tags) {
|
|
@@ -73,8 +74,47 @@ function buildTagIndex(bundle) {
|
|
|
73
74
|
return index;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// src/pads.ts
|
|
78
|
+
var import_model = require("@patterkit/model");
|
|
79
|
+
var spoken = (b) => b.kind === "line" || b.kind === "text";
|
|
80
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
81
|
+
function buildPadIndex(bundle) {
|
|
82
|
+
const index = /* @__PURE__ */ new Map();
|
|
83
|
+
const set = (beat, inherited, clamp) => {
|
|
84
|
+
const own = beat.kind === "gameEvent" ? void 0 : num(beat.padAfter);
|
|
85
|
+
let resolved = own ?? inherited;
|
|
86
|
+
if (clamp && resolved < 0) resolved = 0;
|
|
87
|
+
index.set(beat.id, own !== void 0 ? { own, resolved } : { resolved });
|
|
88
|
+
};
|
|
89
|
+
const visit = (node, inherited) => {
|
|
90
|
+
const def = num(node.padAfterDefault) ?? inherited;
|
|
91
|
+
if (node.type === "group") {
|
|
92
|
+
if (node.prompt) set(node.prompt, def, false);
|
|
93
|
+
for (const child of node.children) visit(child, def);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const beats = node.beats ?? [];
|
|
97
|
+
let last = -1;
|
|
98
|
+
beats.forEach((b, i) => {
|
|
99
|
+
if (spoken(b)) last = i;
|
|
100
|
+
});
|
|
101
|
+
beats.forEach((b, i) => {
|
|
102
|
+
if (spoken(b)) set(b, def, i === last);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const projectDefault = num(bundle.padAfterDefault) ?? import_model.DEFAULT_PAD_AFTER;
|
|
106
|
+
for (const scene of Object.values(bundle.scenes)) {
|
|
107
|
+
const sceneDefault = num(scene.padAfterDefault) ?? projectDefault;
|
|
108
|
+
for (const block of scene.blocks) {
|
|
109
|
+
const blockDefault = num(block.padAfterDefault) ?? sceneDefault;
|
|
110
|
+
for (const child of block.children) visit(child, blockDefault);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return index;
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
// src/engine.ts
|
|
77
|
-
var
|
|
117
|
+
var import_model3 = require("@patterkit/model");
|
|
78
118
|
var astCache = /* @__PURE__ */ new WeakMap();
|
|
79
119
|
var tagIndexCache = /* @__PURE__ */ new WeakMap();
|
|
80
120
|
function tagIndexFor(bundle) {
|
|
@@ -85,11 +125,21 @@ function tagIndexFor(bundle) {
|
|
|
85
125
|
}
|
|
86
126
|
return index;
|
|
87
127
|
}
|
|
128
|
+
var padIndexCache = /* @__PURE__ */ new WeakMap();
|
|
129
|
+
function padIndexFor(bundle) {
|
|
130
|
+
let index = padIndexCache.get(bundle);
|
|
131
|
+
if (!index) {
|
|
132
|
+
index = buildPadIndex(bundle);
|
|
133
|
+
padIndexCache.set(bundle, index);
|
|
134
|
+
}
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
88
137
|
function tagsOf(host, id) {
|
|
89
138
|
return host.tagIndex.get(id)?.slice();
|
|
90
139
|
}
|
|
91
140
|
var OWNER = "Patter";
|
|
92
141
|
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
142
|
+
var unesc = (key) => key.replace(/%2F/g, "/").replace(/%25/g, "%");
|
|
93
143
|
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
94
144
|
var keys = {
|
|
95
145
|
/** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
|
|
@@ -157,13 +207,13 @@ var Engine = class _Engine {
|
|
|
157
207
|
const blockIndex = /* @__PURE__ */ new Map();
|
|
158
208
|
const blockById = /* @__PURE__ */ new Map();
|
|
159
209
|
for (const [sceneId, scene] of Object.entries(bundle.scenes)) {
|
|
160
|
-
this.sceneGameIdToId.set((0,
|
|
210
|
+
this.sceneGameIdToId.set((0, import_model2.effectiveGameId)(scene), sceneId);
|
|
161
211
|
const blockAddrs = /* @__PURE__ */ new Map();
|
|
162
212
|
for (const block of scene.blocks) {
|
|
163
213
|
blockIndex.set(block.id, { sceneId });
|
|
164
214
|
blockById.set(block.id, block);
|
|
165
|
-
blockAddrs.set((0,
|
|
166
|
-
(0,
|
|
215
|
+
blockAddrs.set((0, import_model2.effectiveGameId)(block), block.id);
|
|
216
|
+
(0, import_model2.walkNodes)(block.children, (n) => nodeIndex.set(n.id, n));
|
|
167
217
|
}
|
|
168
218
|
this.blockGameIdToId.set(sceneId, blockAddrs);
|
|
169
219
|
}
|
|
@@ -220,6 +270,7 @@ var Engine = class _Engine {
|
|
|
220
270
|
blockGameIdToId: this.blockGameIdToId,
|
|
221
271
|
// same instances the engine resolves with
|
|
222
272
|
tagIndex: tagIndexFor(bundle),
|
|
273
|
+
padIndex: padIndexFor(bundle),
|
|
223
274
|
registry,
|
|
224
275
|
ownsRegistry,
|
|
225
276
|
patterBag,
|
|
@@ -239,9 +290,9 @@ var Engine = class _Engine {
|
|
|
239
290
|
replayPromptOnChoose: options.replayPromptOnChoose ?? false,
|
|
240
291
|
captionsOn: options.closedCaptions ?? true,
|
|
241
292
|
// captions shown by default (full text)
|
|
242
|
-
captionOpen: (bundle.closedCaptions ??
|
|
243
|
-
captionClose: (bundle.closedCaptions ??
|
|
244
|
-
captionCharacter: bundle.closedCaptions?.character ||
|
|
293
|
+
captionOpen: (bundle.closedCaptions ?? import_model2.DEFAULT_CAPTION_DELIMITERS).open,
|
|
294
|
+
captionClose: (bundle.closedCaptions ?? import_model2.DEFAULT_CAPTION_DELIMITERS).close,
|
|
295
|
+
captionCharacter: bundle.closedCaptions?.character || import_model2.DEFAULT_CAPTION_CHARACTER,
|
|
245
296
|
// absent/empty -> SFX
|
|
246
297
|
refSplitCache: /* @__PURE__ */ new Map(),
|
|
247
298
|
refSplitRevision: registry.revision
|
|
@@ -466,11 +517,11 @@ var Engine = class _Engine {
|
|
|
466
517
|
* The inverse of the resolve helpers - for a host that wants to display / log the address. */
|
|
467
518
|
sceneAddress(sceneId) {
|
|
468
519
|
const scene = this.host.bundle.scenes[sceneId];
|
|
469
|
-
return scene ? (0,
|
|
520
|
+
return scene ? (0, import_model2.effectiveGameId)(scene) : void 0;
|
|
470
521
|
}
|
|
471
522
|
blockAddress(blockId) {
|
|
472
523
|
const block = this.host.blockById.get(blockId);
|
|
473
|
-
return block ? (0,
|
|
524
|
+
return block ? (0, import_model2.effectiveGameId)(block) : void 0;
|
|
474
525
|
}
|
|
475
526
|
/**
|
|
476
527
|
* Author tags (#215) accumulated for a beat by id: its own tags unioned with every ancestor's
|
|
@@ -553,13 +604,13 @@ var Engine = class _Engine {
|
|
|
553
604
|
getOutline() {
|
|
554
605
|
return Object.values(this.host.bundle.scenes).map((scene) => ({
|
|
555
606
|
id: scene.id,
|
|
556
|
-
...(0,
|
|
607
|
+
...(0, import_model2.effectiveGameId)(scene) ? { gameId: (0, import_model2.effectiveGameId)(scene) } : {},
|
|
557
608
|
name: scene.name,
|
|
558
609
|
...gameDataField(scene.gameData),
|
|
559
610
|
...this.tagsField(scene.id),
|
|
560
611
|
blocks: scene.blocks.map((block) => ({
|
|
561
612
|
id: block.id,
|
|
562
|
-
...(0,
|
|
613
|
+
...(0, import_model2.effectiveGameId)(block) ? { gameId: (0, import_model2.effectiveGameId)(block) } : {},
|
|
563
614
|
name: block.name,
|
|
564
615
|
...gameDataField(block.gameData),
|
|
565
616
|
...this.tagsField(block.id),
|
|
@@ -576,7 +627,7 @@ var Engine = class _Engine {
|
|
|
576
627
|
const out = [];
|
|
577
628
|
for (const scene of Object.values(this.host.bundle.scenes)) {
|
|
578
629
|
for (const block of scene.blocks) {
|
|
579
|
-
(0,
|
|
630
|
+
(0, import_model2.walkNodes)(block.children, (n) => {
|
|
580
631
|
if (n.type !== "snippet") return;
|
|
581
632
|
for (const beat of n.beats ?? []) {
|
|
582
633
|
out.push({ sceneId: scene.id, blockId: block.id, snippetId: n.id, beat: this.beatInfo(beat) });
|
|
@@ -613,17 +664,20 @@ var Engine = class _Engine {
|
|
|
613
664
|
if (beat.kind === "line") {
|
|
614
665
|
if (beat.character !== void 0) {
|
|
615
666
|
info.character = beat.character;
|
|
616
|
-
const name = this.host.defaultStrings[(0,
|
|
667
|
+
const name = this.host.defaultStrings[(0, import_model2.castStringKey)(beat.character)] ?? this.host.castDisplay.get(beat.character);
|
|
617
668
|
if (name !== void 0) info.characterName = name;
|
|
618
669
|
}
|
|
619
670
|
if (beat.direction !== void 0) info.direction = beat.direction;
|
|
620
671
|
if (beat.qualifier !== void 0) {
|
|
621
672
|
info.qualifier = beat.qualifier;
|
|
622
|
-
const name = this.host.defaultStrings[(0,
|
|
673
|
+
const name = this.host.defaultStrings[(0, import_model2.qualifierStringKey)(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
|
|
623
674
|
if (name !== void 0) info.qualifierName = name;
|
|
624
675
|
}
|
|
625
676
|
}
|
|
626
677
|
if (beat.kind === "line" || beat.kind === "text") {
|
|
678
|
+
const pad = this.host.padIndex.get(beat.id);
|
|
679
|
+
info.padAfter = pad?.resolved ?? import_model2.DEFAULT_PAD_AFTER;
|
|
680
|
+
if (pad?.own !== void 0) info.ownPadAfter = pad.own;
|
|
627
681
|
const source = this.host.defaultStrings[beat.id];
|
|
628
682
|
if (source !== void 0) info.text = source;
|
|
629
683
|
}
|
|
@@ -714,6 +768,45 @@ var Engine = class _Engine {
|
|
|
714
768
|
refuseInCheckpoint(what) {
|
|
715
769
|
if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
|
|
716
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
|
|
773
|
+
* `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
|
|
774
|
+
* state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
|
|
775
|
+
* drops its values. Only reads, so it is allowed while a checkpoint is open.
|
|
776
|
+
*/
|
|
777
|
+
captureFlow(id) {
|
|
778
|
+
const flow = this.flowsById.get(id);
|
|
779
|
+
if (!flow) throw new Error(`no open flow '${id}' to capture`);
|
|
780
|
+
return { version: import_model3.SAVE_VERSION, ...flow.snapshot(), ...flowValuesFrom(id, this.host.registry.save()) };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
|
|
784
|
+
* registry as a load's do, and the flow is restored as a load restores each flow. `id` needn't be the id it was
|
|
785
|
+
* captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
|
|
786
|
+
* that id is open, or while a checkpoint is.
|
|
787
|
+
*/
|
|
788
|
+
restoreFlow(id, state) {
|
|
789
|
+
this.refuseInCheckpoint("restoreFlow");
|
|
790
|
+
const version = state.version;
|
|
791
|
+
if (version !== 2 && version !== import_model3.SAVE_VERSION) throw new Error(`unsupported flow state version: ${String(version)}`);
|
|
792
|
+
const cursor = state.cursor;
|
|
793
|
+
if (typeof cursor !== "object" || cursor === null) throw new Error("malformed flow state: no cursor");
|
|
794
|
+
if (this.flowsById.has(id)) throw new Error(`a flow '${id}' is already open; close it before restoring another under that id`);
|
|
795
|
+
this.assertExternalScopes();
|
|
796
|
+
const parked = keys.flow(id);
|
|
797
|
+
this.host.registry.discardParked(parked);
|
|
798
|
+
this.host.registry.load(flowSections(id, state), { keepParked: true });
|
|
799
|
+
const flow = new Flow(id, this.host, this.defaultSeed);
|
|
800
|
+
try {
|
|
801
|
+
flow.restore(state);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
flow.close();
|
|
804
|
+
this.host.registry.discardParked(parked);
|
|
805
|
+
throw e;
|
|
806
|
+
}
|
|
807
|
+
this.flowsById.set(id, flow);
|
|
808
|
+
return flow;
|
|
809
|
+
}
|
|
717
810
|
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
718
811
|
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
719
812
|
closeFlow(id) {
|
|
@@ -849,7 +942,7 @@ var Engine = class _Engine {
|
|
|
849
942
|
const flows = {};
|
|
850
943
|
for (const [id, flow] of this.flowsById) flows[id] = flow.snapshot();
|
|
851
944
|
return {
|
|
852
|
-
version:
|
|
945
|
+
version: import_model3.SAVE_VERSION,
|
|
853
946
|
...this.host.ownsRegistry ? { registry: this.host.registry.save() } : {},
|
|
854
947
|
sharedVisits: Object.fromEntries(this.host.sharedVisits),
|
|
855
948
|
sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
|
|
@@ -866,7 +959,7 @@ var Engine = class _Engine {
|
|
|
866
959
|
loadGame(save) {
|
|
867
960
|
this.refuseInCheckpoint("loadGame");
|
|
868
961
|
const version = save.version;
|
|
869
|
-
if (version !== 2 && version !==
|
|
962
|
+
if (version !== 2 && version !== import_model3.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
870
963
|
const flows = save.flows;
|
|
871
964
|
if (typeof flows !== "object" || flows === null || Array.isArray(flows)) throw new Error("malformed save: no flows");
|
|
872
965
|
this.assertExternalScopes();
|
|
@@ -1417,7 +1510,8 @@ var Flow = class {
|
|
|
1417
1510
|
const node = this.host.nodeIndex.get(o.id);
|
|
1418
1511
|
if (!node) continue;
|
|
1419
1512
|
byId.set(o.id, node);
|
|
1420
|
-
|
|
1513
|
+
const { prompt: saved, ...rest } = o;
|
|
1514
|
+
options.push({ ...rest, ...saved ? { prompt: { ...saved, padAfter: saved.padAfter ?? this.promptPadOf(node) } } : {} });
|
|
1421
1515
|
}
|
|
1422
1516
|
if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
|
|
1423
1517
|
}
|
|
@@ -1426,7 +1520,7 @@ var Flow = class {
|
|
|
1426
1520
|
if (this.pendingPromptOwnerId) {
|
|
1427
1521
|
const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
|
|
1428
1522
|
const beat = owner ? this.authoredPromptOf(owner) : void 0;
|
|
1429
|
-
if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
|
|
1523
|
+
if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt, padAfter: c.pendingPrompt.padAfter ?? this.padOf(beat.id) } } : {} };
|
|
1430
1524
|
else this.pendingPromptOwnerId = null;
|
|
1431
1525
|
}
|
|
1432
1526
|
}
|
|
@@ -1796,7 +1890,7 @@ var Flow = class {
|
|
|
1796
1890
|
case "gameEvent":
|
|
1797
1891
|
return { type: "gameEvent", id: beat.id, gameData: beat.gameData, ...withTags };
|
|
1798
1892
|
case "text":
|
|
1799
|
-
return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), gameData: beat.gameData, ...withTags };
|
|
1893
|
+
return { type: "text", id: beat.id, text: this.interpolate(this.resolveString(beat.id)), padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
|
|
1800
1894
|
case "line": {
|
|
1801
1895
|
const raw = this.resolveString(beat.id);
|
|
1802
1896
|
const off = !this.host.captionsOn;
|
|
@@ -1812,6 +1906,8 @@ var Flow = class {
|
|
|
1812
1906
|
direction: silent ? void 0 : beat.direction,
|
|
1813
1907
|
qualifier: silent ? void 0 : beat.qualifier,
|
|
1814
1908
|
qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
|
|
1909
|
+
padAfter: this.padOf(beat.id),
|
|
1910
|
+
// a silent line still fires, so it still carries its pause
|
|
1815
1911
|
gameData: beat.gameData,
|
|
1816
1912
|
...withTags
|
|
1817
1913
|
};
|
|
@@ -1850,6 +1946,7 @@ var Flow = class {
|
|
|
1850
1946
|
const beat = this.promptBeatOf(node);
|
|
1851
1947
|
if (!beat) return void 0;
|
|
1852
1948
|
const text = this.interpolate(this.resolveString(beat.id));
|
|
1949
|
+
const padAfter = this.padOf(beat.id);
|
|
1853
1950
|
return beat.kind === "line" ? {
|
|
1854
1951
|
kind: "line",
|
|
1855
1952
|
text: this.captionLine(text),
|
|
@@ -1857,8 +1954,9 @@ var Flow = class {
|
|
|
1857
1954
|
characterName: this.resolveCharacterName(beat.character),
|
|
1858
1955
|
direction: beat.direction,
|
|
1859
1956
|
qualifier: beat.qualifier,
|
|
1860
|
-
qualifierName: this.resolveQualifierName(beat.qualifier)
|
|
1861
|
-
|
|
1957
|
+
qualifierName: this.resolveQualifierName(beat.qualifier),
|
|
1958
|
+
padAfter
|
|
1959
|
+
} : { kind: "text", text, padAfter };
|
|
1862
1960
|
}
|
|
1863
1961
|
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
1864
1962
|
authoredPromptOf(node) {
|
|
@@ -1869,7 +1967,7 @@ var Flow = class {
|
|
|
1869
1967
|
promptResult(beat, shown) {
|
|
1870
1968
|
const tags = tagsOf(this.host, beat.id);
|
|
1871
1969
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1872
|
-
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1970
|
+
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, padAfter: this.padOf(beat.id), gameData: beat.gameData, ...withTags };
|
|
1873
1971
|
return {
|
|
1874
1972
|
type: "line",
|
|
1875
1973
|
id: beat.id,
|
|
@@ -1879,6 +1977,7 @@ var Flow = class {
|
|
|
1879
1977
|
direction: shown.direction,
|
|
1880
1978
|
qualifier: shown.qualifier,
|
|
1881
1979
|
qualifierName: shown.qualifierName,
|
|
1980
|
+
padAfter: this.padOf(beat.id),
|
|
1882
1981
|
gameData: beat.gameData,
|
|
1883
1982
|
...withTags
|
|
1884
1983
|
};
|
|
@@ -1892,7 +1991,7 @@ var Flow = class {
|
|
|
1892
1991
|
/** The first snippet with a line/text beat within a child list, depth-first in authored order. */
|
|
1893
1992
|
firstTextSnippetIn(children) {
|
|
1894
1993
|
let found;
|
|
1895
|
-
(0,
|
|
1994
|
+
(0, import_model2.walkNodes)(children, (n) => {
|
|
1896
1995
|
if (!found && n.type === "snippet" && (n.beats ?? []).some((b) => b.kind === "line" || b.kind === "text")) {
|
|
1897
1996
|
found = n;
|
|
1898
1997
|
}
|
|
@@ -1912,16 +2011,27 @@ var Flow = class {
|
|
|
1912
2011
|
resolveCharacterName(character) {
|
|
1913
2012
|
if (character === void 0) return void 0;
|
|
1914
2013
|
if (this.host.emitIds) return void 0;
|
|
1915
|
-
const key = (0,
|
|
2014
|
+
const key = (0, import_model2.castStringKey)(character);
|
|
1916
2015
|
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
|
|
1917
2016
|
}
|
|
2017
|
+
/** The resolved pause of the beat an option shows as its prompt (its authored prompt, else its first content
|
|
2018
|
+
* line), for a saved option that predates prompts carrying one. */
|
|
2019
|
+
promptPadOf(node) {
|
|
2020
|
+
const beat = this.promptBeatOf(node);
|
|
2021
|
+
return beat ? this.padOf(beat.id) : import_model2.DEFAULT_PAD_AFTER;
|
|
2022
|
+
}
|
|
2023
|
+
/** A line or text beat's resolved pause (line padding): its own `padAfter`, else the nearest default above
|
|
2024
|
+
* it, else the project's, else the built-in one; clamped to zero on a snippet's last line. */
|
|
2025
|
+
padOf(beatId) {
|
|
2026
|
+
return this.host.padIndex.get(beatId)?.resolved ?? import_model2.DEFAULT_PAD_AFTER;
|
|
2027
|
+
}
|
|
1918
2028
|
/** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
|
|
1919
2029
|
* active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
|
|
1920
2030
|
* maps the `qualifier` gameId itself). */
|
|
1921
2031
|
resolveQualifierName(qualifier) {
|
|
1922
2032
|
if (qualifier === void 0) return void 0;
|
|
1923
2033
|
if (this.host.emitIds) return void 0;
|
|
1924
|
-
const key = (0,
|
|
2034
|
+
const key = (0, import_model2.qualifierStringKey)(qualifier);
|
|
1925
2035
|
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.qualifierDisplay.get(qualifier);
|
|
1926
2036
|
}
|
|
1927
2037
|
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
@@ -2004,7 +2114,7 @@ var Flow = class {
|
|
|
2004
2114
|
}
|
|
2005
2115
|
};
|
|
2006
2116
|
function collectCast(nodes, out) {
|
|
2007
|
-
(0,
|
|
2117
|
+
(0, import_model2.walkNodes)(nodes, (n) => {
|
|
2008
2118
|
if (n.type === "group") {
|
|
2009
2119
|
if (n.prompt?.kind === "line" && n.prompt.character) out.add(n.prompt.character);
|
|
2010
2120
|
return;
|
|
@@ -2022,6 +2132,7 @@ function savedPrompt(p) {
|
|
|
2022
2132
|
if (p.direction !== void 0) out.direction = p.direction;
|
|
2023
2133
|
if (p.qualifier !== void 0) out.qualifier = p.qualifier;
|
|
2024
2134
|
if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
|
|
2135
|
+
if (typeof p.padAfter === "number") out.padAfter = p.padAfter;
|
|
2025
2136
|
return out;
|
|
2026
2137
|
}
|
|
2027
2138
|
function serialiseSelectors(map) {
|
|
@@ -2099,9 +2210,23 @@ function sectionsFromV2(save) {
|
|
|
2099
2210
|
const out = {};
|
|
2100
2211
|
if (save.shared?.patter) out["patter"] = save.shared.patter;
|
|
2101
2212
|
for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
|
|
2102
|
-
for (const [id, f] of Object.entries(save.flows ?? {}))
|
|
2103
|
-
|
|
2104
|
-
|
|
2213
|
+
for (const [id, f] of Object.entries(save.flows ?? {})) Object.assign(out, flowSections(id, f));
|
|
2214
|
+
return out;
|
|
2215
|
+
}
|
|
2216
|
+
function flowSections(id, f) {
|
|
2217
|
+
const out = {};
|
|
2218
|
+
if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
|
|
2219
|
+
for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
|
|
2220
|
+
return out;
|
|
2221
|
+
}
|
|
2222
|
+
function flowValuesFrom(id, saved) {
|
|
2223
|
+
const out = { scopes: {}, sceneBags: {} };
|
|
2224
|
+
const prefix = keys.flow(id);
|
|
2225
|
+
for (const [key, values] of Object.entries(saved)) {
|
|
2226
|
+
if (!key.startsWith(prefix)) continue;
|
|
2227
|
+
const rest = key.slice(prefix.length);
|
|
2228
|
+
if (rest === "patter") out.scopes.patter = values;
|
|
2229
|
+
else if (rest.startsWith("scene/")) out.sceneBags[unesc(rest.slice("scene/".length))] = values;
|
|
2105
2230
|
}
|
|
2106
2231
|
return out;
|
|
2107
2232
|
}
|
|
@@ -2112,8 +2237,11 @@ function truthy(v) {
|
|
|
2112
2237
|
return v.length > 0;
|
|
2113
2238
|
}
|
|
2114
2239
|
|
|
2240
|
+
// src/index.ts
|
|
2241
|
+
var import_model5 = require("@patterkit/model");
|
|
2242
|
+
|
|
2115
2243
|
// src/describe.ts
|
|
2116
|
-
var
|
|
2244
|
+
var import_model4 = require("@patterkit/model");
|
|
2117
2245
|
var isShared = (d, scopeDefault) => d.shared ?? scopeDefault;
|
|
2118
2246
|
function summariseProperty(d, scopeDefault) {
|
|
2119
2247
|
return {
|
|
@@ -2166,11 +2294,11 @@ function describeBundle(bundle) {
|
|
|
2166
2294
|
const sceneProps = [];
|
|
2167
2295
|
for (const scene of Object.values(bundle.scenes)) {
|
|
2168
2296
|
counts.scenes++;
|
|
2169
|
-
const gameId = (0,
|
|
2297
|
+
const gameId = (0, import_model4.effectiveGameId)(scene);
|
|
2170
2298
|
addresses.push({
|
|
2171
2299
|
gameId,
|
|
2172
2300
|
name: scene.name,
|
|
2173
|
-
blocks: scene.blocks.map((b) => ({ gameId: (0,
|
|
2301
|
+
blocks: scene.blocks.map((b) => ({ gameId: (0, import_model4.effectiveGameId)(b), name: b.name }))
|
|
2174
2302
|
});
|
|
2175
2303
|
for (const block of scene.blocks) countBlock(block, counts);
|
|
2176
2304
|
if (scene.sceneProps?.length) {
|
|
@@ -2235,6 +2363,7 @@ function effectiveGameData(fields, node) {
|
|
|
2235
2363
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2236
2364
|
0 && (module.exports = {
|
|
2237
2365
|
Checkpoint,
|
|
2366
|
+
DEFAULT_PAD_AFTER,
|
|
2238
2367
|
Engine,
|
|
2239
2368
|
Flow,
|
|
2240
2369
|
buildTagIndex,
|