@patterkit/runtime 0.16.0 → 0.18.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 +31 -0
- package/dist/index.cjs +80 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +80 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -459,6 +459,10 @@ declare class Engine {
|
|
|
459
459
|
*
|
|
460
460
|
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
461
461
|
* call to reach for when you want a speaker's variation state to carry on.
|
|
462
|
+
*
|
|
463
|
+
* THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
|
|
464
|
+
* scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
|
|
465
|
+
* exactly as it was.
|
|
462
466
|
*/
|
|
463
467
|
openFlow(id: string, opts?: OpenFlowOptions): Flow;
|
|
464
468
|
/**
|
|
@@ -490,6 +494,13 @@ declare class Engine {
|
|
|
490
494
|
* it opens (or the save as it loads), before anything changes. By then a game has built all of its
|
|
491
495
|
* engines, whatever order it built them in. The same message on every runtime. */
|
|
492
496
|
private assertExternalScopes;
|
|
497
|
+
/**
|
|
498
|
+
* `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
|
|
499
|
+
* scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
|
|
500
|
+
* id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
|
|
501
|
+
* is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
|
|
502
|
+
*/
|
|
503
|
+
private resolveOpenAddress;
|
|
493
504
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
494
505
|
private resolveSceneRef;
|
|
495
506
|
/** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
|
|
@@ -673,10 +684,13 @@ declare class Flow {
|
|
|
673
684
|
private activeSnippet;
|
|
674
685
|
private beatIndex;
|
|
675
686
|
private pendingChoice;
|
|
676
|
-
/** When `replayPromptOnChoose`, the chosen option's prompt
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
*
|
|
687
|
+
/** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
|
|
688
|
+
* beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
|
|
689
|
+
* `shown` is missing only after loading a save written before saves carried it: the beat is then
|
|
690
|
+
* resolved when it is delivered, as it always was. */
|
|
691
|
+
private pendingPrompt;
|
|
692
|
+
/** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
|
|
693
|
+
* advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
|
|
680
694
|
private pendingPromptOwnerId;
|
|
681
695
|
private selectors;
|
|
682
696
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
@@ -896,6 +910,11 @@ declare class Flow {
|
|
|
896
910
|
* option's first content line. NO look-ahead. Undefined only when even that is absent.
|
|
897
911
|
*/
|
|
898
912
|
private promptFor;
|
|
913
|
+
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
914
|
+
private authoredPromptOf;
|
|
915
|
+
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
916
|
+
* choice showed. */
|
|
917
|
+
private promptResult;
|
|
899
918
|
/** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
|
|
900
919
|
private promptBeatOf;
|
|
901
920
|
/** The first snippet with a line/text beat within a child list, depth-first in authored order. */
|
package/dist/index.d.ts
CHANGED
|
@@ -459,6 +459,10 @@ declare class Engine {
|
|
|
459
459
|
*
|
|
460
460
|
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
461
461
|
* call to reach for when you want a speaker's variation state to carry on.
|
|
462
|
+
*
|
|
463
|
+
* THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
|
|
464
|
+
* scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
|
|
465
|
+
* exactly as it was.
|
|
462
466
|
*/
|
|
463
467
|
openFlow(id: string, opts?: OpenFlowOptions): Flow;
|
|
464
468
|
/**
|
|
@@ -490,6 +494,13 @@ declare class Engine {
|
|
|
490
494
|
* it opens (or the save as it loads), before anything changes. By then a game has built all of its
|
|
491
495
|
* engines, whatever order it built them in. The same message on every runtime. */
|
|
492
496
|
private assertExternalScopes;
|
|
497
|
+
/**
|
|
498
|
+
* `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
|
|
499
|
+
* scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
|
|
500
|
+
* id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
|
|
501
|
+
* is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
|
|
502
|
+
*/
|
|
503
|
+
private resolveOpenAddress;
|
|
493
504
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
494
505
|
private resolveSceneRef;
|
|
495
506
|
/** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
|
|
@@ -673,10 +684,13 @@ declare class Flow {
|
|
|
673
684
|
private activeSnippet;
|
|
674
685
|
private beatIndex;
|
|
675
686
|
private pendingChoice;
|
|
676
|
-
/** When `replayPromptOnChoose`, the chosen option's prompt
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
*
|
|
687
|
+
/** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
|
|
688
|
+
* beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
|
|
689
|
+
* `shown` is missing only after loading a save written before saves carried it: the beat is then
|
|
690
|
+
* resolved when it is delivered, as it always was. */
|
|
691
|
+
private pendingPrompt;
|
|
692
|
+
/** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
|
|
693
|
+
* advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
|
|
680
694
|
private pendingPromptOwnerId;
|
|
681
695
|
private selectors;
|
|
682
696
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
@@ -896,6 +910,11 @@ declare class Flow {
|
|
|
896
910
|
* option's first content line. NO look-ahead. Undefined only when even that is absent.
|
|
897
911
|
*/
|
|
898
912
|
private promptFor;
|
|
913
|
+
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
914
|
+
private authoredPromptOf;
|
|
915
|
+
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
916
|
+
* choice showed. */
|
|
917
|
+
private promptResult;
|
|
899
918
|
/** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
|
|
900
919
|
private promptBeatOf;
|
|
901
920
|
/** The first snippet with a line/text beat within a child list, depth-first in authored order. */
|
package/dist/index.js
CHANGED
|
@@ -299,11 +299,14 @@ var Engine = class _Engine {
|
|
|
299
299
|
*
|
|
300
300
|
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
301
301
|
* call to reach for when you want a speaker's variation state to carry on.
|
|
302
|
+
*
|
|
303
|
+
* THROWS on an address that does not resolve (an unknown scene, or a block that is not in the named
|
|
304
|
+
* scene), before anything changes: nothing is opened, and a flow already open under `id` carries on
|
|
305
|
+
* exactly as it was.
|
|
302
306
|
*/
|
|
303
307
|
openFlow(id, opts = {}) {
|
|
304
308
|
this.assertExternalScopes();
|
|
305
|
-
const sceneId = this.
|
|
306
|
-
const blockId = this.resolveBlockRef(sceneId, opts.block);
|
|
309
|
+
const { sceneId, blockId } = this.resolveOpenAddress(opts.scene, opts.block);
|
|
307
310
|
const prior = this.flowsById.get(id);
|
|
308
311
|
const journal = this.host.journal;
|
|
309
312
|
if (journal && prior && !prior.isClosed) throw new Error(`openFlow would replace the open flow '${id}', which can't be undone while a checkpoint is open`);
|
|
@@ -364,6 +367,28 @@ var Engine = class _Engine {
|
|
|
364
367
|
}
|
|
365
368
|
}
|
|
366
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* `openFlow`'s address as internal ids, or a throw when it does not resolve. With a scene, the block is
|
|
372
|
+
* scene-scoped exactly as {@link Flow.goto} resolves it (a gameId address in that scene, or the internal
|
|
373
|
+
* id of a block in that scene), so a block from another scene does not resolve. With no scene, the block
|
|
374
|
+
* is an internal id from any scene. Neither: the bundle's first scene. The same rule on every runtime.
|
|
375
|
+
*/
|
|
376
|
+
resolveOpenAddress(scene, block) {
|
|
377
|
+
if (scene !== void 0) {
|
|
378
|
+
const sceneId = this.resolveSceneRef(scene);
|
|
379
|
+
if (!this.host.bundle.scenes[sceneId]) throw new Error(`unknown scene: ${scene}`);
|
|
380
|
+
if (block === void 0) return { sceneId };
|
|
381
|
+
const blockId = this.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
|
|
382
|
+
if (blockId === void 0) throw new Error(`unknown block: ${block}`);
|
|
383
|
+
return { sceneId, blockId };
|
|
384
|
+
}
|
|
385
|
+
if (block !== void 0) {
|
|
386
|
+
if (!this.host.blockIndex.has(block)) throw new Error(`unknown block: ${block}`);
|
|
387
|
+
return { blockId: block };
|
|
388
|
+
}
|
|
389
|
+
if (Object.keys(this.host.bundle.scenes).length === 0) throw new Error("no scenes in bundle");
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
367
392
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
368
393
|
resolveSceneRef(ref) {
|
|
369
394
|
if (ref == null) return void 0;
|
|
@@ -818,10 +843,13 @@ var Flow = class {
|
|
|
818
843
|
activeSnippet = null;
|
|
819
844
|
beatIndex = 0;
|
|
820
845
|
pendingChoice = null;
|
|
821
|
-
/** When `replayPromptOnChoose`, the chosen option's prompt
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
*
|
|
846
|
+
/** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
|
|
847
|
+
* beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
|
|
848
|
+
* `shown` is missing only after loading a save written before saves carried it: the beat is then
|
|
849
|
+
* resolved when it is delivered, as it always was. */
|
|
850
|
+
pendingPrompt = null;
|
|
851
|
+
/** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
|
|
852
|
+
* advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
|
|
825
853
|
pendingPromptOwnerId = null;
|
|
826
854
|
selectors = /* @__PURE__ */ new Map();
|
|
827
855
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
@@ -983,7 +1011,7 @@ var Flow = class {
|
|
|
983
1011
|
if (scene === "END") {
|
|
984
1012
|
this.started = true;
|
|
985
1013
|
this.pendingChoice = null;
|
|
986
|
-
this.
|
|
1014
|
+
this.pendingPrompt = null;
|
|
987
1015
|
this.pendingPromptOwnerId = null;
|
|
988
1016
|
this.activeSnippet = null;
|
|
989
1017
|
this.beatIndex = 0;
|
|
@@ -1003,7 +1031,7 @@ var Flow = class {
|
|
|
1003
1031
|
return true;
|
|
1004
1032
|
}
|
|
1005
1033
|
this.pendingChoice = null;
|
|
1006
|
-
this.
|
|
1034
|
+
this.pendingPrompt = null;
|
|
1007
1035
|
this.pendingPromptOwnerId = null;
|
|
1008
1036
|
this.activeSnippet = null;
|
|
1009
1037
|
this.beatIndex = 0;
|
|
@@ -1030,7 +1058,7 @@ var Flow = class {
|
|
|
1030
1058
|
this.activeSnippet = null;
|
|
1031
1059
|
this.beatIndex = 0;
|
|
1032
1060
|
this.pendingChoice = null;
|
|
1033
|
-
this.
|
|
1061
|
+
this.pendingPrompt = null;
|
|
1034
1062
|
this.pendingPromptOwnerId = null;
|
|
1035
1063
|
}
|
|
1036
1064
|
/** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
|
|
@@ -1040,10 +1068,10 @@ var Flow = class {
|
|
|
1040
1068
|
const journal = this.host.journal;
|
|
1041
1069
|
if (!journal || journal.flows.has(this)) return;
|
|
1042
1070
|
journal.flows.add(this);
|
|
1043
|
-
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice,
|
|
1071
|
+
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId } = this;
|
|
1044
1072
|
const stack = this.stack.map((f) => ({ ...f }));
|
|
1045
1073
|
journal.undo.push(() => {
|
|
1046
|
-
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice,
|
|
1074
|
+
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId, stack });
|
|
1047
1075
|
});
|
|
1048
1076
|
}
|
|
1049
1077
|
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
@@ -1061,11 +1089,11 @@ var Flow = class {
|
|
|
1061
1089
|
if (this.closed) return { type: "end" };
|
|
1062
1090
|
if (!this.started) throw new Error("flow has not been started");
|
|
1063
1091
|
this.touch();
|
|
1064
|
-
if (this.
|
|
1065
|
-
const
|
|
1066
|
-
this.
|
|
1092
|
+
if (this.pendingPrompt) {
|
|
1093
|
+
const p = this.pendingPrompt;
|
|
1094
|
+
this.pendingPrompt = null;
|
|
1067
1095
|
this.pendingPromptOwnerId = null;
|
|
1068
|
-
return this.beatResult(
|
|
1096
|
+
return p.shown ? this.promptResult(p.beat, p.shown) : this.beatResult(p.beat);
|
|
1069
1097
|
}
|
|
1070
1098
|
this.settle();
|
|
1071
1099
|
if (this.flowEnded) return { type: "end" };
|
|
@@ -1184,8 +1212,9 @@ var Flow = class {
|
|
|
1184
1212
|
const node = choice.byId.get(id);
|
|
1185
1213
|
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1186
1214
|
this.pendingChoice = null;
|
|
1187
|
-
|
|
1188
|
-
this.
|
|
1215
|
+
const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
|
|
1216
|
+
this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
|
|
1217
|
+
this.pendingPromptOwnerId = this.pendingPrompt ? node.id : null;
|
|
1189
1218
|
this.enterChild(node);
|
|
1190
1219
|
}
|
|
1191
1220
|
isEnded() {
|
|
@@ -1251,6 +1280,8 @@ var Flow = class {
|
|
|
1251
1280
|
beatIndex: this.beatIndex,
|
|
1252
1281
|
pendingChoice: this.pendingChoice ? { groupId: this.pendingChoice.groupId, options: this.pendingChoice.options.map((o) => ({ ...o })) } : null,
|
|
1253
1282
|
pendingPromptOwnerId: this.pendingPromptOwnerId,
|
|
1283
|
+
// The prompt still to be replayed, as the choice showed it: present only in the choose -> advance window.
|
|
1284
|
+
...this.pendingPrompt?.shown ? { pendingPrompt: savedPrompt(this.pendingPrompt.shown) } : {},
|
|
1254
1285
|
selectors: serialiseSelectors(this.selectors)
|
|
1255
1286
|
}
|
|
1256
1287
|
};
|
|
@@ -1301,12 +1332,13 @@ var Flow = class {
|
|
|
1301
1332
|
}
|
|
1302
1333
|
if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
|
|
1303
1334
|
}
|
|
1304
|
-
this.
|
|
1335
|
+
this.pendingPrompt = null;
|
|
1305
1336
|
this.pendingPromptOwnerId = c.pendingPromptOwnerId ?? null;
|
|
1306
1337
|
if (this.pendingPromptOwnerId) {
|
|
1307
1338
|
const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
|
|
1308
|
-
|
|
1309
|
-
if (
|
|
1339
|
+
const beat = owner ? this.authoredPromptOf(owner) : void 0;
|
|
1340
|
+
if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
|
|
1341
|
+
else this.pendingPromptOwnerId = null;
|
|
1310
1342
|
}
|
|
1311
1343
|
}
|
|
1312
1344
|
// -- Scene / block / node entry ------------------------------------------
|
|
@@ -1701,6 +1733,27 @@ var Flow = class {
|
|
|
1701
1733
|
const text = this.interpolate(this.resolveString(beat.id));
|
|
1702
1734
|
return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
|
|
1703
1735
|
}
|
|
1736
|
+
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
1737
|
+
authoredPromptOf(node) {
|
|
1738
|
+
return node.type === "group" ? node.prompt : void 0;
|
|
1739
|
+
}
|
|
1740
|
+
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
1741
|
+
* choice showed. */
|
|
1742
|
+
promptResult(beat, shown) {
|
|
1743
|
+
const tags = this.host.tagIndex.get(beat.id);
|
|
1744
|
+
const withTags = tags && tags.length ? { tags } : {};
|
|
1745
|
+
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1746
|
+
return {
|
|
1747
|
+
type: "line",
|
|
1748
|
+
id: beat.id,
|
|
1749
|
+
text: shown.text,
|
|
1750
|
+
character: shown.character,
|
|
1751
|
+
characterName: shown.characterName,
|
|
1752
|
+
direction: shown.direction,
|
|
1753
|
+
gameData: beat.gameData,
|
|
1754
|
+
...withTags
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1704
1757
|
/** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
|
|
1705
1758
|
promptBeatOf(node) {
|
|
1706
1759
|
if (node.type === "group" && node.prompt) return node.prompt;
|
|
@@ -1824,6 +1877,13 @@ function collectCast(nodes, out) {
|
|
|
1824
1877
|
function gameDataField(gameData) {
|
|
1825
1878
|
return gameData && Object.keys(gameData).length ? { gameData } : {};
|
|
1826
1879
|
}
|
|
1880
|
+
function savedPrompt(p) {
|
|
1881
|
+
const out = { kind: p.kind, text: p.text };
|
|
1882
|
+
if (p.character !== void 0) out.character = p.character;
|
|
1883
|
+
if (p.characterName !== void 0) out.characterName = p.characterName;
|
|
1884
|
+
if (p.direction !== void 0) out.direction = p.direction;
|
|
1885
|
+
return out;
|
|
1886
|
+
}
|
|
1827
1887
|
function serialiseSelectors(map) {
|
|
1828
1888
|
const out = {};
|
|
1829
1889
|
for (const [id, st] of map) {
|