@patterkit/runtime 0.17.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 +20 -0
- package/dist/index.cjs +53 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +53 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,26 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.18.0] - 2026-10-05
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`replayPromptOnChoose` speaks back only an authored prompt, and exactly what the choice showed.** A game that
|
|
41
|
+
turns the option on will see the difference, which is why this is a minor release rather than a patch:
|
|
42
|
+
- An option whose prompt the choice borrowed from its own first content line (a bare-snippet option,
|
|
43
|
+
or an Option group with no prompt of its own) is no longer spoken back. That line used to play twice
|
|
44
|
+
in a row, once as the replay and once as content. It now plays once.
|
|
45
|
+
- The replayed prompt carries the text, `character`, `characterName`, and `direction` the choice
|
|
46
|
+
displayed, instead of resolving the prompt again when it is delivered. So if choosing runs effects
|
|
47
|
+
first (an Option group that picks a snippet runs its onEnter at once), or the language changes
|
|
48
|
+
between `choose()` and the next `advance()`, the replay still says what the player saw. In a voiced
|
|
49
|
+
project a replayed line prompt is now interpolated, as the choice showed it.
|
|
50
|
+
- A save taken between `choose()` and the next `advance()` carries that prompt as shown, in a new
|
|
51
|
+
optional `pendingPrompt` on the flow's cursor, so the replay after a load is the same. Every
|
|
52
|
+
Patterplay runtime writes and reads it. A save from an earlier version, without it, still loads and
|
|
53
|
+
resolves the prompt when it is delivered, as before.
|
|
54
|
+
The save shape's `FlowCursor.pendingPrompt` is typed in `@patterkit/model`.
|
|
55
|
+
|
|
36
56
|
## [0.17.0] - 2026-10-05
|
|
37
57
|
|
|
38
58
|
### Fixed
|
package/dist/index.cjs
CHANGED
|
@@ -876,10 +876,13 @@ var Flow = class {
|
|
|
876
876
|
activeSnippet = null;
|
|
877
877
|
beatIndex = 0;
|
|
878
878
|
pendingChoice = null;
|
|
879
|
-
/** When `replayPromptOnChoose`, the chosen option's prompt
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
*
|
|
879
|
+
/** When `replayPromptOnChoose`, the chosen option's AUTHORED prompt to deliver before its content: its
|
|
880
|
+
* beat (id, gameData, tags) and the prompt exactly as the choice showed it (text and speaker fields).
|
|
881
|
+
* `shown` is missing only after loading a save written before saves carried it: the beat is then
|
|
882
|
+
* resolved when it is delivered, as it always was. */
|
|
883
|
+
pendingPrompt = null;
|
|
884
|
+
/** The chosen option that owns `pendingPrompt`, so a save taken between choose() and the next
|
|
885
|
+
* advance() can find the prompt beat again on load (it isn't otherwise reachable by id). */
|
|
883
886
|
pendingPromptOwnerId = null;
|
|
884
887
|
selectors = /* @__PURE__ */ new Map();
|
|
885
888
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
@@ -1041,7 +1044,7 @@ var Flow = class {
|
|
|
1041
1044
|
if (scene === "END") {
|
|
1042
1045
|
this.started = true;
|
|
1043
1046
|
this.pendingChoice = null;
|
|
1044
|
-
this.
|
|
1047
|
+
this.pendingPrompt = null;
|
|
1045
1048
|
this.pendingPromptOwnerId = null;
|
|
1046
1049
|
this.activeSnippet = null;
|
|
1047
1050
|
this.beatIndex = 0;
|
|
@@ -1061,7 +1064,7 @@ var Flow = class {
|
|
|
1061
1064
|
return true;
|
|
1062
1065
|
}
|
|
1063
1066
|
this.pendingChoice = null;
|
|
1064
|
-
this.
|
|
1067
|
+
this.pendingPrompt = null;
|
|
1065
1068
|
this.pendingPromptOwnerId = null;
|
|
1066
1069
|
this.activeSnippet = null;
|
|
1067
1070
|
this.beatIndex = 0;
|
|
@@ -1088,7 +1091,7 @@ var Flow = class {
|
|
|
1088
1091
|
this.activeSnippet = null;
|
|
1089
1092
|
this.beatIndex = 0;
|
|
1090
1093
|
this.pendingChoice = null;
|
|
1091
|
-
this.
|
|
1094
|
+
this.pendingPrompt = null;
|
|
1092
1095
|
this.pendingPromptOwnerId = null;
|
|
1093
1096
|
}
|
|
1094
1097
|
/** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
|
|
@@ -1098,10 +1101,10 @@ var Flow = class {
|
|
|
1098
1101
|
const journal = this.host.journal;
|
|
1099
1102
|
if (!journal || journal.flows.has(this)) return;
|
|
1100
1103
|
journal.flows.add(this);
|
|
1101
|
-
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice,
|
|
1104
|
+
const { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId } = this;
|
|
1102
1105
|
const stack = this.stack.map((f) => ({ ...f }));
|
|
1103
1106
|
journal.undo.push(() => {
|
|
1104
|
-
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice,
|
|
1107
|
+
Object.assign(this, { rngState, started, flowEnded, currentSceneId, activeSnippet, beatIndex, pendingChoice, pendingPrompt, pendingPromptOwnerId, stack });
|
|
1105
1108
|
});
|
|
1106
1109
|
}
|
|
1107
1110
|
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
@@ -1119,11 +1122,11 @@ var Flow = class {
|
|
|
1119
1122
|
if (this.closed) return { type: "end" };
|
|
1120
1123
|
if (!this.started) throw new Error("flow has not been started");
|
|
1121
1124
|
this.touch();
|
|
1122
|
-
if (this.
|
|
1123
|
-
const
|
|
1124
|
-
this.
|
|
1125
|
+
if (this.pendingPrompt) {
|
|
1126
|
+
const p = this.pendingPrompt;
|
|
1127
|
+
this.pendingPrompt = null;
|
|
1125
1128
|
this.pendingPromptOwnerId = null;
|
|
1126
|
-
return this.beatResult(
|
|
1129
|
+
return p.shown ? this.promptResult(p.beat, p.shown) : this.beatResult(p.beat);
|
|
1127
1130
|
}
|
|
1128
1131
|
this.settle();
|
|
1129
1132
|
if (this.flowEnded) return { type: "end" };
|
|
@@ -1242,8 +1245,9 @@ var Flow = class {
|
|
|
1242
1245
|
const node = choice.byId.get(id);
|
|
1243
1246
|
this.emit({ type: "chose", group: choice.groupId, option: id });
|
|
1244
1247
|
this.pendingChoice = null;
|
|
1245
|
-
|
|
1246
|
-
this.
|
|
1248
|
+
const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
|
|
1249
|
+
this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
|
|
1250
|
+
this.pendingPromptOwnerId = this.pendingPrompt ? node.id : null;
|
|
1247
1251
|
this.enterChild(node);
|
|
1248
1252
|
}
|
|
1249
1253
|
isEnded() {
|
|
@@ -1309,6 +1313,8 @@ var Flow = class {
|
|
|
1309
1313
|
beatIndex: this.beatIndex,
|
|
1310
1314
|
pendingChoice: this.pendingChoice ? { groupId: this.pendingChoice.groupId, options: this.pendingChoice.options.map((o) => ({ ...o })) } : null,
|
|
1311
1315
|
pendingPromptOwnerId: this.pendingPromptOwnerId,
|
|
1316
|
+
// The prompt still to be replayed, as the choice showed it: present only in the choose -> advance window.
|
|
1317
|
+
...this.pendingPrompt?.shown ? { pendingPrompt: savedPrompt(this.pendingPrompt.shown) } : {},
|
|
1312
1318
|
selectors: serialiseSelectors(this.selectors)
|
|
1313
1319
|
}
|
|
1314
1320
|
};
|
|
@@ -1359,12 +1365,13 @@ var Flow = class {
|
|
|
1359
1365
|
}
|
|
1360
1366
|
if (options.length > 0) this.pendingChoice = { groupId: c.pendingChoice.groupId, options, byId };
|
|
1361
1367
|
}
|
|
1362
|
-
this.
|
|
1368
|
+
this.pendingPrompt = null;
|
|
1363
1369
|
this.pendingPromptOwnerId = c.pendingPromptOwnerId ?? null;
|
|
1364
1370
|
if (this.pendingPromptOwnerId) {
|
|
1365
1371
|
const owner = this.host.nodeIndex.get(this.pendingPromptOwnerId);
|
|
1366
|
-
|
|
1367
|
-
if (
|
|
1372
|
+
const beat = owner ? this.authoredPromptOf(owner) : void 0;
|
|
1373
|
+
if (beat) this.pendingPrompt = { beat, ...c.pendingPrompt ? { shown: { ...c.pendingPrompt } } : {} };
|
|
1374
|
+
else this.pendingPromptOwnerId = null;
|
|
1368
1375
|
}
|
|
1369
1376
|
}
|
|
1370
1377
|
// -- Scene / block / node entry ------------------------------------------
|
|
@@ -1759,6 +1766,27 @@ var Flow = class {
|
|
|
1759
1766
|
const text = this.interpolate(this.resolveString(beat.id));
|
|
1760
1767
|
return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
|
|
1761
1768
|
}
|
|
1769
|
+
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
1770
|
+
authoredPromptOf(node) {
|
|
1771
|
+
return node.type === "group" ? node.prompt : void 0;
|
|
1772
|
+
}
|
|
1773
|
+
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
1774
|
+
* choice showed. */
|
|
1775
|
+
promptResult(beat, shown) {
|
|
1776
|
+
const tags = this.host.tagIndex.get(beat.id);
|
|
1777
|
+
const withTags = tags && tags.length ? { tags } : {};
|
|
1778
|
+
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1779
|
+
return {
|
|
1780
|
+
type: "line",
|
|
1781
|
+
id: beat.id,
|
|
1782
|
+
text: shown.text,
|
|
1783
|
+
character: shown.character,
|
|
1784
|
+
characterName: shown.characterName,
|
|
1785
|
+
direction: shown.direction,
|
|
1786
|
+
gameData: beat.gameData,
|
|
1787
|
+
...withTags
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1762
1790
|
/** The prompt BEAT of an option: the Option group's `prompt`, else (tolerance) its first content line. */
|
|
1763
1791
|
promptBeatOf(node) {
|
|
1764
1792
|
if (node.type === "group" && node.prompt) return node.prompt;
|
|
@@ -1882,6 +1910,13 @@ function collectCast(nodes, out) {
|
|
|
1882
1910
|
function gameDataField(gameData) {
|
|
1883
1911
|
return gameData && Object.keys(gameData).length ? { gameData } : {};
|
|
1884
1912
|
}
|
|
1913
|
+
function savedPrompt(p) {
|
|
1914
|
+
const out = { kind: p.kind, text: p.text };
|
|
1915
|
+
if (p.character !== void 0) out.character = p.character;
|
|
1916
|
+
if (p.characterName !== void 0) out.characterName = p.characterName;
|
|
1917
|
+
if (p.direction !== void 0) out.direction = p.direction;
|
|
1918
|
+
return out;
|
|
1919
|
+
}
|
|
1885
1920
|
function serialiseSelectors(map) {
|
|
1886
1921
|
const out = {};
|
|
1887
1922
|
for (const [id, st] of map) {
|