@patterkit/runtime 0.20.0 → 0.22.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 +27 -0
- package/dist/index.cjs +54 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +55 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,33 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.22.0] - 2026-10-10
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Speaker qualifiers**: a line can say how it is delivered, a screenplay's `TAM (O.S.)`, without becoming a
|
|
41
|
+
second character. A `line` step, and a line-kind choice prompt, carries `qualifier`, the qualifier's `gameId`
|
|
42
|
+
(`vo`, `os`, `radio`, or the project's own), and `qualifierName`, its shown name, resolved as `characterName`
|
|
43
|
+
is: the active locale's `qualifier:<gameId>` string, then the default locale's, then the authored name.
|
|
44
|
+
`qualifierName` is absent in an IDs-only build, and both are dropped on a line silenced by closed captions.
|
|
45
|
+
Saves and the outline and beat sequence carry them too. What a qualifier means is the game's to decide.
|
|
46
|
+
|
|
47
|
+
## [0.21.0] - 2026-10-07
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **A new `Engine` on a bundle another engine already uses no longer rebuilds the tag index.** It is built once
|
|
52
|
+
per bundle and shared, so a tool that starts a fresh engine for every run (coverage does) starts each in about
|
|
53
|
+
a third of the time: 0.5 ms rather than 1.6 ms on a 100-scene project.
|
|
54
|
+
- **The conformance corpus covers more, and every runtime is held to it**: the outline and beat sequence, the
|
|
55
|
+
audio resolver's joins, `listProperties`, what a checkpoint refuses while it is open, a save taken inside one,
|
|
56
|
+
and a block named under another scene.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Tags handed out are the caller's own.** `tagsForBeat`, `tagsForScene`, `tagsForBlock`, and a delivered
|
|
61
|
+
line's `tags` are copies, so a game that edits one changes nothing the engine says afterwards.
|
|
62
|
+
|
|
36
63
|
## [0.20.0] - 2026-10-06
|
|
37
64
|
|
|
38
65
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -76,6 +76,18 @@ function buildTagIndex(bundle) {
|
|
|
76
76
|
// src/engine.ts
|
|
77
77
|
var import_model2 = require("@patterkit/model");
|
|
78
78
|
var astCache = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
var tagIndexCache = /* @__PURE__ */ new WeakMap();
|
|
80
|
+
function tagIndexFor(bundle) {
|
|
81
|
+
let index = tagIndexCache.get(bundle);
|
|
82
|
+
if (!index) {
|
|
83
|
+
index = buildTagIndex(bundle);
|
|
84
|
+
tagIndexCache.set(bundle, index);
|
|
85
|
+
}
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
function tagsOf(host, id) {
|
|
89
|
+
return host.tagIndex.get(id)?.slice();
|
|
90
|
+
}
|
|
79
91
|
var OWNER = "Patter";
|
|
80
92
|
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
81
93
|
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
@@ -138,6 +150,8 @@ var Engine = class _Engine {
|
|
|
138
150
|
}
|
|
139
151
|
const castDisplay = /* @__PURE__ */ new Map();
|
|
140
152
|
for (const c of bundle.cast ?? []) if (c.displayName) castDisplay.set(c.name, c.displayName);
|
|
153
|
+
const qualifierDisplay = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const q of bundle.qualifiers ?? []) if (q?.gameId) qualifierDisplay.set(q.gameId, q.name);
|
|
141
155
|
this.defaultSeed = (options.seed ?? 2654435769) >>> 0;
|
|
142
156
|
const nodeIndex = /* @__PURE__ */ new Map();
|
|
143
157
|
const blockIndex = /* @__PURE__ */ new Map();
|
|
@@ -198,13 +212,14 @@ var Engine = class _Engine {
|
|
|
198
212
|
strings,
|
|
199
213
|
defaultStrings,
|
|
200
214
|
castDisplay,
|
|
215
|
+
qualifierDisplay,
|
|
201
216
|
nodeIndex,
|
|
202
217
|
blockIndex,
|
|
203
218
|
blockById,
|
|
204
219
|
sceneGameIdToId: this.sceneGameIdToId,
|
|
205
220
|
blockGameIdToId: this.blockGameIdToId,
|
|
206
221
|
// same instances the engine resolves with
|
|
207
|
-
tagIndex:
|
|
222
|
+
tagIndex: tagIndexFor(bundle),
|
|
208
223
|
registry,
|
|
209
224
|
ownsRegistry,
|
|
210
225
|
patterBag,
|
|
@@ -463,18 +478,18 @@ var Engine = class _Engine {
|
|
|
463
478
|
* delivered step carries. Empty array for an unknown id or a beat with no tags anywhere up the chain.
|
|
464
479
|
*/
|
|
465
480
|
tagsForBeat(beatId) {
|
|
466
|
-
return this.host
|
|
481
|
+
return tagsOf(this.host, beatId) ?? [];
|
|
467
482
|
}
|
|
468
483
|
/** A scene's own tags (by internal id or gameId address). Empty when none / unknown. */
|
|
469
484
|
tagsForScene(sceneRef) {
|
|
470
485
|
const id = this.resolveSceneRef(sceneRef);
|
|
471
|
-
return (id != null ? this.host
|
|
486
|
+
return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
|
|
472
487
|
}
|
|
473
488
|
/** A block's accumulated tags (scene + block), by scene + block ref (id or gameId). Empty when none / unknown. */
|
|
474
489
|
tagsForBlock(sceneRef, blockRef) {
|
|
475
490
|
const sceneId = this.resolveSceneRef(sceneRef);
|
|
476
491
|
const id = this.resolveBlockRef(sceneId, blockRef);
|
|
477
|
-
return (id != null ? this.host
|
|
492
|
+
return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
|
|
478
493
|
}
|
|
479
494
|
/**
|
|
480
495
|
* A scene's own author gameData (by internal id or gameId address): the RAW sparse overrides, exactly
|
|
@@ -593,7 +608,7 @@ var Engine = class _Engine {
|
|
|
593
608
|
}
|
|
594
609
|
/** One beat's static data (source locale), the same shape a delivered step carries. */
|
|
595
610
|
beatInfo(beat) {
|
|
596
|
-
const tags = this.host
|
|
611
|
+
const tags = tagsOf(this.host, beat.id);
|
|
597
612
|
const info = { id: beat.id, kind: beat.kind };
|
|
598
613
|
if (beat.kind === "line") {
|
|
599
614
|
if (beat.character !== void 0) {
|
|
@@ -602,6 +617,11 @@ var Engine = class _Engine {
|
|
|
602
617
|
if (name !== void 0) info.characterName = name;
|
|
603
618
|
}
|
|
604
619
|
if (beat.direction !== void 0) info.direction = beat.direction;
|
|
620
|
+
if (beat.qualifier !== void 0) {
|
|
621
|
+
info.qualifier = beat.qualifier;
|
|
622
|
+
const name = this.host.defaultStrings[(0, import_model.qualifierStringKey)(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
|
|
623
|
+
if (name !== void 0) info.qualifierName = name;
|
|
624
|
+
}
|
|
605
625
|
}
|
|
606
626
|
if (beat.kind === "line" || beat.kind === "text") {
|
|
607
627
|
const source = this.host.defaultStrings[beat.id];
|
|
@@ -613,7 +633,7 @@ var Engine = class _Engine {
|
|
|
613
633
|
}
|
|
614
634
|
/** A `{ tags }` fragment for an id, present only when the id has accumulated tags (keeps output tidy). */
|
|
615
635
|
tagsField(id) {
|
|
616
|
-
const tags = this.host
|
|
636
|
+
const tags = tagsOf(this.host, id);
|
|
617
637
|
return tags && tags.length ? { tags } : {};
|
|
618
638
|
}
|
|
619
639
|
/** Retrieve an open flow by id (undefined if none / closed). */
|
|
@@ -750,8 +770,6 @@ var Engine = class _Engine {
|
|
|
750
770
|
this.host.registry.set(scope, name, value, { host: true });
|
|
751
771
|
if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
|
|
752
772
|
}
|
|
753
|
-
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
754
|
-
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
755
773
|
/** The run's decisions, in order, each naming the flow it happened in. Empty unless the
|
|
756
774
|
* run was opened with `log: true`. A flow's own log stays flow-local; this is the only
|
|
757
775
|
* place a story spanning several flows reads as one sequence. */
|
|
@@ -783,6 +801,8 @@ var Engine = class _Engine {
|
|
|
783
801
|
getVisitCounts() {
|
|
784
802
|
return Object.fromEntries(this.host.sharedVisits);
|
|
785
803
|
}
|
|
804
|
+
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
805
|
+
* value, declared default (for reset), and enum options. Every runtime has the same call. */
|
|
786
806
|
listProperties() {
|
|
787
807
|
return this.host.patterSharedDecls.map((d) => ({
|
|
788
808
|
name: d.name,
|
|
@@ -1770,7 +1790,7 @@ var Flow = class {
|
|
|
1770
1790
|
};
|
|
1771
1791
|
// -- Strings / beats ------------------------------------------------------
|
|
1772
1792
|
beatResult(beat) {
|
|
1773
|
-
const tags = this.host
|
|
1793
|
+
const tags = tagsOf(this.host, beat.id);
|
|
1774
1794
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1775
1795
|
switch (beat.kind) {
|
|
1776
1796
|
case "gameEvent":
|
|
@@ -1790,6 +1810,8 @@ var Flow = class {
|
|
|
1790
1810
|
character: silent ? void 0 : beat.character,
|
|
1791
1811
|
characterName: silent ? void 0 : this.resolveCharacterName(beat.character),
|
|
1792
1812
|
direction: silent ? void 0 : beat.direction,
|
|
1813
|
+
qualifier: silent ? void 0 : beat.qualifier,
|
|
1814
|
+
qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
|
|
1793
1815
|
gameData: beat.gameData,
|
|
1794
1816
|
...withTags
|
|
1795
1817
|
};
|
|
@@ -1828,7 +1850,15 @@ var Flow = class {
|
|
|
1828
1850
|
const beat = this.promptBeatOf(node);
|
|
1829
1851
|
if (!beat) return void 0;
|
|
1830
1852
|
const text = this.interpolate(this.resolveString(beat.id));
|
|
1831
|
-
return beat.kind === "line" ? {
|
|
1853
|
+
return beat.kind === "line" ? {
|
|
1854
|
+
kind: "line",
|
|
1855
|
+
text: this.captionLine(text),
|
|
1856
|
+
character: beat.character,
|
|
1857
|
+
characterName: this.resolveCharacterName(beat.character),
|
|
1858
|
+
direction: beat.direction,
|
|
1859
|
+
qualifier: beat.qualifier,
|
|
1860
|
+
qualifierName: this.resolveQualifierName(beat.qualifier)
|
|
1861
|
+
} : { kind: "text", text };
|
|
1832
1862
|
}
|
|
1833
1863
|
/** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
|
|
1834
1864
|
authoredPromptOf(node) {
|
|
@@ -1837,7 +1867,7 @@ var Flow = class {
|
|
|
1837
1867
|
/** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
|
|
1838
1868
|
* choice showed. */
|
|
1839
1869
|
promptResult(beat, shown) {
|
|
1840
|
-
const tags = this.host
|
|
1870
|
+
const tags = tagsOf(this.host, beat.id);
|
|
1841
1871
|
const withTags = tags && tags.length ? { tags } : {};
|
|
1842
1872
|
if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
|
|
1843
1873
|
return {
|
|
@@ -1847,6 +1877,8 @@ var Flow = class {
|
|
|
1847
1877
|
character: shown.character,
|
|
1848
1878
|
characterName: shown.characterName,
|
|
1849
1879
|
direction: shown.direction,
|
|
1880
|
+
qualifier: shown.qualifier,
|
|
1881
|
+
qualifierName: shown.qualifierName,
|
|
1850
1882
|
gameData: beat.gameData,
|
|
1851
1883
|
...withTags
|
|
1852
1884
|
};
|
|
@@ -1883,6 +1915,15 @@ var Flow = class {
|
|
|
1883
1915
|
const key = (0, import_model.castStringKey)(character);
|
|
1884
1916
|
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
|
|
1885
1917
|
}
|
|
1918
|
+
/** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
|
|
1919
|
+
* active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
|
|
1920
|
+
* maps the `qualifier` gameId itself). */
|
|
1921
|
+
resolveQualifierName(qualifier) {
|
|
1922
|
+
if (qualifier === void 0) return void 0;
|
|
1923
|
+
if (this.host.emitIds) return void 0;
|
|
1924
|
+
const key = (0, import_model.qualifierStringKey)(qualifier);
|
|
1925
|
+
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.qualifierDisplay.get(qualifier);
|
|
1926
|
+
}
|
|
1886
1927
|
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
1887
1928
|
splitRef(ref) {
|
|
1888
1929
|
return splitHostRef(this.host, ref);
|
|
@@ -1979,6 +2020,8 @@ function savedPrompt(p) {
|
|
|
1979
2020
|
if (p.character !== void 0) out.character = p.character;
|
|
1980
2021
|
if (p.characterName !== void 0) out.characterName = p.characterName;
|
|
1981
2022
|
if (p.direction !== void 0) out.direction = p.direction;
|
|
2023
|
+
if (p.qualifier !== void 0) out.qualifier = p.qualifier;
|
|
2024
|
+
if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
|
|
1982
2025
|
return out;
|
|
1983
2026
|
}
|
|
1984
2027
|
function serialiseSelectors(map) {
|