@yagejs-addons/dialogue 0.1.0 → 0.2.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/dist/DialogueController-BMeNLi0v.d.cts +1204 -0
- package/dist/DialogueController-Cs5IUc-u.d.ts +1204 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/chunk-CU47RPEB.js +410 -0
- package/dist/chunk-CU47RPEB.js.map +1 -0
- package/dist/chunk-GJQKZCOL.js +983 -0
- package/dist/chunk-GJQKZCOL.js.map +1 -0
- package/dist/index.cjs +3441 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +591 -0
- package/dist/index.d.ts +591 -0
- package/dist/index.js +2048 -0
- package/dist/index.js.map +1 -0
- package/dist/presenters.cjs +3149 -0
- package/dist/presenters.cjs.map +1 -0
- package/dist/presenters.d.cts +1817 -0
- package/dist/presenters.d.ts +1817 -0
- package/dist/presenters.js +2920 -0
- package/dist/presenters.js.map +1 -0
- package/dist/types-DSbBSlh7.d.cts +375 -0
- package/dist/types-DSbBSlh7.d.ts +375 -0
- package/dist/yaml.cjs +726 -0
- package/dist/yaml.cjs.map +1 -0
- package/dist/yaml.d.cts +23 -0
- package/dist/yaml.d.ts +23 -0
- package/dist/yaml.js +37 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +4 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
CompositeInputBinding: () => CompositeInputBinding,
|
|
25
|
+
DEFAULT_ACTIONS: () => DEFAULT_ACTIONS,
|
|
26
|
+
DialogueAutoAdvanceEvent: () => DialogueAutoAdvanceEvent,
|
|
27
|
+
DialogueChoiceMadeEvent: () => DialogueChoiceMadeEvent,
|
|
28
|
+
DialogueChoiceShownEvent: () => DialogueChoiceShownEvent,
|
|
29
|
+
DialogueCommandEvent: () => DialogueCommandEvent,
|
|
30
|
+
DialogueController: () => DialogueController,
|
|
31
|
+
DialogueEndedEvent: () => DialogueEndedEvent,
|
|
32
|
+
DialogueExprError: () => DialogueExprError,
|
|
33
|
+
DialogueLineEvent: () => DialogueLineEvent,
|
|
34
|
+
DialoguePlayError: () => DialoguePlayError,
|
|
35
|
+
DialogueRevealCompletedEvent: () => DialogueRevealCompletedEvent,
|
|
36
|
+
DialogueRevealMarkerEvent: () => DialogueRevealMarkerEvent,
|
|
37
|
+
DialogueRunner: () => DialogueRunner,
|
|
38
|
+
DialogueScriptError: () => DialogueScriptError,
|
|
39
|
+
DialogueSelectionChangedEvent: () => DialogueSelectionChangedEvent,
|
|
40
|
+
DialogueSession: () => DialogueSession,
|
|
41
|
+
DialogueSkipUsedEvent: () => DialogueSkipUsedEvent,
|
|
42
|
+
DialogueStartedEvent: () => DialogueStartedEvent,
|
|
43
|
+
EMPTY_PARSED: () => EMPTY_PARSED,
|
|
44
|
+
FULL_ACTIONS: () => FULL_ACTIONS,
|
|
45
|
+
IdentityI18n: () => IdentityI18n,
|
|
46
|
+
KeyboardInputBinding: () => KeyboardInputBinding,
|
|
47
|
+
LineReveal: () => LineReveal,
|
|
48
|
+
MemoryVariableStorage: () => MemoryVariableStorage,
|
|
49
|
+
PointerInputBinding: () => PointerInputBinding,
|
|
50
|
+
cells: () => cells,
|
|
51
|
+
compose: () => compose,
|
|
52
|
+
createVoiceChannel: () => createVoiceChannel,
|
|
53
|
+
defineScript: () => defineScript,
|
|
54
|
+
evalCondition: () => evalCondition,
|
|
55
|
+
evaluate: () => evaluate,
|
|
56
|
+
fullControls: () => fullControls,
|
|
57
|
+
interpolate: () => interpolate,
|
|
58
|
+
isExpr: () => isExpr,
|
|
59
|
+
loadCompact: () => loadCompact,
|
|
60
|
+
loadScript: () => loadScript,
|
|
61
|
+
materialize: () => materialize,
|
|
62
|
+
parseCompact: () => parseCompact,
|
|
63
|
+
parseExpr: () => parseExpr,
|
|
64
|
+
parseMarkup: () => parseMarkup,
|
|
65
|
+
splitGraphemes: () => splitGraphemes,
|
|
66
|
+
stripMarkup: () => stripMarkup
|
|
67
|
+
});
|
|
68
|
+
module.exports = __toCommonJS(index_exports);
|
|
69
|
+
|
|
70
|
+
// src/core/markup.ts
|
|
71
|
+
var EMPTY_PARSED = Object.freeze({
|
|
72
|
+
runs: Object.freeze([]),
|
|
73
|
+
tokens: Object.freeze([]),
|
|
74
|
+
length: 0
|
|
75
|
+
});
|
|
76
|
+
var NAMED_COLORS = {
|
|
77
|
+
black: 0,
|
|
78
|
+
white: 16777215,
|
|
79
|
+
red: 16734810,
|
|
80
|
+
green: 9232491,
|
|
81
|
+
blue: 7316441,
|
|
82
|
+
yellow: 16769126,
|
|
83
|
+
gold: 16765530,
|
|
84
|
+
orange: 16097640,
|
|
85
|
+
purple: 13214975,
|
|
86
|
+
pink: 16752331,
|
|
87
|
+
gray: 10133670,
|
|
88
|
+
grey: 10133670
|
|
89
|
+
};
|
|
90
|
+
function parseColor(raw) {
|
|
91
|
+
const v = raw.trim().toLowerCase();
|
|
92
|
+
if (v.startsWith("#")) {
|
|
93
|
+
const hex = v.slice(1);
|
|
94
|
+
if (/^[0-9a-f]{6}$/.test(hex)) return parseInt(hex, 16);
|
|
95
|
+
if (/^[0-9a-f]{3}$/.test(hex)) {
|
|
96
|
+
const r = hex[0];
|
|
97
|
+
const g = hex[1];
|
|
98
|
+
const b = hex[2];
|
|
99
|
+
return parseInt(`${r}${r}${g}${g}${b}${b}`, 16);
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
if (/^0x[0-9a-f]{6}$/.test(v)) return parseInt(v.slice(2), 16);
|
|
104
|
+
return NAMED_COLORS[v];
|
|
105
|
+
}
|
|
106
|
+
__name(parseColor, "parseColor");
|
|
107
|
+
function effectiveStyle(stack) {
|
|
108
|
+
let style = {};
|
|
109
|
+
for (const f of stack) style = mergeStyle(style, f.override);
|
|
110
|
+
return style;
|
|
111
|
+
}
|
|
112
|
+
__name(effectiveStyle, "effectiveStyle");
|
|
113
|
+
function mergeStyle(parent, child) {
|
|
114
|
+
const merged = {
|
|
115
|
+
...parent,
|
|
116
|
+
...stripUndefined(child)
|
|
117
|
+
};
|
|
118
|
+
if (child.speed !== void 0) {
|
|
119
|
+
merged.speed = (parent.speed ?? 1) * child.speed;
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
122
|
+
}
|
|
123
|
+
__name(mergeStyle, "mergeStyle");
|
|
124
|
+
function stripUndefined(s) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const k of Object.keys(s)) {
|
|
127
|
+
if (s[k] !== void 0) out[k] = s[k];
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
__name(stripUndefined, "stripUndefined");
|
|
132
|
+
var TAG_RE = /\[(\/?)([a-zA-Z]+)(?:=([^\s\]/]*))?((?:\s+[A-Za-z_][\w-]*=[^\s\]/]*)*)(\/)?\]/g;
|
|
133
|
+
var GRAPHEME_SEGMENTER = new Intl.Segmenter();
|
|
134
|
+
function splitGraphemes(text) {
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const s of GRAPHEME_SEGMENTER.segment(text)) out.push(s.segment);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
__name(splitGraphemes, "splitGraphemes");
|
|
140
|
+
function parseMarkup(input) {
|
|
141
|
+
const runs = [];
|
|
142
|
+
const tokens = [];
|
|
143
|
+
const stack = [];
|
|
144
|
+
let charCount = 0;
|
|
145
|
+
let buffer = "";
|
|
146
|
+
const flush = /* @__PURE__ */ __name(() => {
|
|
147
|
+
if (buffer.length === 0) return;
|
|
148
|
+
const graphemeCount = splitGraphemes(buffer).length;
|
|
149
|
+
runs.push({ text: buffer, style: effectiveStyle(stack), graphemeCount });
|
|
150
|
+
charCount += graphemeCount;
|
|
151
|
+
buffer = "";
|
|
152
|
+
}, "flush");
|
|
153
|
+
let lastIndex = 0;
|
|
154
|
+
TAG_RE.lastIndex = 0;
|
|
155
|
+
let m;
|
|
156
|
+
while ((m = TAG_RE.exec(input)) !== null) {
|
|
157
|
+
let backslashes = 0;
|
|
158
|
+
for (let i = m.index - 1; i >= lastIndex && input[i] === "\\"; i--) backslashes++;
|
|
159
|
+
if (backslashes % 2 === 1) {
|
|
160
|
+
buffer += unescape(input.slice(lastIndex, m.index - 1)) + m[0];
|
|
161
|
+
lastIndex = TAG_RE.lastIndex;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const literal = input.slice(lastIndex, m.index);
|
|
165
|
+
buffer += unescape(literal);
|
|
166
|
+
lastIndex = TAG_RE.lastIndex;
|
|
167
|
+
const closing = m[1] === "/";
|
|
168
|
+
const name = m[2].toLowerCase();
|
|
169
|
+
const arg = m[3];
|
|
170
|
+
const propsStr = m[4];
|
|
171
|
+
const selfClosing = m[5] === "/";
|
|
172
|
+
if (propsStr && !selfClosing) {
|
|
173
|
+
buffer += m[0];
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (closing) {
|
|
177
|
+
flush();
|
|
178
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
179
|
+
if (stack[i].name === name) {
|
|
180
|
+
stack.splice(i, 1);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (selfClosing) {
|
|
187
|
+
flush();
|
|
188
|
+
if (name === "pause") {
|
|
189
|
+
const ms = Number(arg ?? "0");
|
|
190
|
+
if (Number.isFinite(ms) && ms > 0) {
|
|
191
|
+
tokens.push({ kind: "pause", atChar: charCount, ms });
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
tokens.push({
|
|
195
|
+
kind: "marker",
|
|
196
|
+
atChar: charCount,
|
|
197
|
+
name,
|
|
198
|
+
props: markerProps(name, arg, propsStr)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const override = styleForTag(name, arg);
|
|
204
|
+
if (override) {
|
|
205
|
+
flush();
|
|
206
|
+
stack.push({ name, override });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
buffer += unescape(input.slice(lastIndex));
|
|
210
|
+
flush();
|
|
211
|
+
return { runs: mergeAdjacent(runs), tokens, length: charCount };
|
|
212
|
+
}
|
|
213
|
+
__name(parseMarkup, "parseMarkup");
|
|
214
|
+
function markerProps(name, arg, propsStr) {
|
|
215
|
+
const props = {};
|
|
216
|
+
if (arg !== void 0) props[name] = arg;
|
|
217
|
+
if (propsStr) {
|
|
218
|
+
for (const tok of propsStr.trim().split(/\s+/)) {
|
|
219
|
+
const eq = tok.indexOf("=");
|
|
220
|
+
if (eq > 0) props[tok.slice(0, eq).toLowerCase()] = tok.slice(eq + 1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return props;
|
|
224
|
+
}
|
|
225
|
+
__name(markerProps, "markerProps");
|
|
226
|
+
function styleForTag(name, arg) {
|
|
227
|
+
switch (name) {
|
|
228
|
+
case "b":
|
|
229
|
+
case "bold":
|
|
230
|
+
return { bold: true };
|
|
231
|
+
case "i":
|
|
232
|
+
case "italic":
|
|
233
|
+
return { italic: true };
|
|
234
|
+
case "color":
|
|
235
|
+
case "c": {
|
|
236
|
+
const color = arg ? parseColor(arg) : void 0;
|
|
237
|
+
return color !== void 0 ? { color } : null;
|
|
238
|
+
}
|
|
239
|
+
case "speed": {
|
|
240
|
+
const s = Number(arg);
|
|
241
|
+
return Number.isFinite(s) && s > 0 ? { speed: s } : null;
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
return { effect: name };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
__name(styleForTag, "styleForTag");
|
|
248
|
+
function unescape(s) {
|
|
249
|
+
return s.replace(/\\([[\]\\])/g, "$1");
|
|
250
|
+
}
|
|
251
|
+
__name(unescape, "unescape");
|
|
252
|
+
function mergeAdjacent(runs) {
|
|
253
|
+
const out = [];
|
|
254
|
+
for (const run of runs) {
|
|
255
|
+
const prev = out[out.length - 1];
|
|
256
|
+
if (prev && sameStyle(prev.style, run.style)) {
|
|
257
|
+
out[out.length - 1] = {
|
|
258
|
+
text: prev.text + run.text,
|
|
259
|
+
style: prev.style,
|
|
260
|
+
graphemeCount: prev.graphemeCount + run.graphemeCount
|
|
261
|
+
};
|
|
262
|
+
} else {
|
|
263
|
+
out.push(run);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
__name(mergeAdjacent, "mergeAdjacent");
|
|
269
|
+
function sameStyle(a, b) {
|
|
270
|
+
return !!a.bold === !!b.bold && !!a.italic === !!b.italic && a.color === b.color && a.effect === b.effect && (a.speed ?? 1) === (b.speed ?? 1);
|
|
271
|
+
}
|
|
272
|
+
__name(sameStyle, "sameStyle");
|
|
273
|
+
function stripMarkup(input) {
|
|
274
|
+
return parseMarkup(input).runs.map((r) => r.text).join("");
|
|
275
|
+
}
|
|
276
|
+
__name(stripMarkup, "stripMarkup");
|
|
277
|
+
function firstUnknownTag(input) {
|
|
278
|
+
TAG_RE.lastIndex = 0;
|
|
279
|
+
let lastIndex = 0;
|
|
280
|
+
let m;
|
|
281
|
+
while ((m = TAG_RE.exec(input)) !== null) {
|
|
282
|
+
let backslashes = 0;
|
|
283
|
+
for (let i = m.index - 1; i >= lastIndex && input[i] === "\\"; i--) backslashes++;
|
|
284
|
+
lastIndex = TAG_RE.lastIndex;
|
|
285
|
+
if (backslashes % 2 === 1) continue;
|
|
286
|
+
if (m[1] === "/" || m[4] || m[5] === "/") continue;
|
|
287
|
+
const name = m[2].toLowerCase();
|
|
288
|
+
if (styleForTag(name, m[3]) === null) return name;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
__name(firstUnknownTag, "firstUnknownTag");
|
|
293
|
+
|
|
294
|
+
// src/core/LineReveal.ts
|
|
295
|
+
var LineReveal = class {
|
|
296
|
+
/** @param charsPerSec base reveal rate (graphemes/second), scaled by the
|
|
297
|
+
* hold, per-line, and per-run multipliers. */
|
|
298
|
+
constructor(charsPerSec) {
|
|
299
|
+
this.charsPerSec = charsPerSec;
|
|
300
|
+
}
|
|
301
|
+
charsPerSec;
|
|
302
|
+
static {
|
|
303
|
+
__name(this, "LineReveal");
|
|
304
|
+
}
|
|
305
|
+
parsed;
|
|
306
|
+
/** Reveal cursor, in graphemes (fractional while typing). */
|
|
307
|
+
cursor = 0;
|
|
308
|
+
pauseTimer = 0;
|
|
309
|
+
/** Next un-drained token in `parsed.tokens` (one ordered cursor over pauses +
|
|
310
|
+
* markers — source order is drain order). */
|
|
311
|
+
tokenIdx = 0;
|
|
312
|
+
/** Graphemes already ticked (so each grapheme ticks exactly once). */
|
|
313
|
+
tickCount = 0;
|
|
314
|
+
/** Hold-to-fast-forward rate (1 = normal). */
|
|
315
|
+
speedMul = 1;
|
|
316
|
+
/** Per-line `say.speed` multiplier (1 = base). */
|
|
317
|
+
lineSpeed = 1;
|
|
318
|
+
done = false;
|
|
319
|
+
completed = false;
|
|
320
|
+
/** Fired exactly once when the line finishes revealing. The consuming view
|
|
321
|
+
* wires this to the session-owned reveal listener (NOT a public mutable
|
|
322
|
+
* field a game could clobber). */
|
|
323
|
+
onComplete;
|
|
324
|
+
/** Per-grapheme ticks + inline markers, wired by the consuming view to the
|
|
325
|
+
* session-owned beat listener (like {@link onComplete}, never a public field). */
|
|
326
|
+
onBeat;
|
|
327
|
+
/**
|
|
328
|
+
* Register the completion listener — fires once per line, the moment the
|
|
329
|
+
* cursor reaches the end (or synchronously from {@link begin} for an empty
|
|
330
|
+
* line, or from {@link complete}). Pass `undefined` to clear.
|
|
331
|
+
*/
|
|
332
|
+
setCompletionListener(listener) {
|
|
333
|
+
this.onComplete = listener;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Register the reveal-beat listener — per-grapheme ticks and inline markers,
|
|
337
|
+
* in char order, the moment the cursor reaches each. Session-owned (set once,
|
|
338
|
+
* like {@link setCompletionListener}); pass `undefined` to clear.
|
|
339
|
+
*/
|
|
340
|
+
setBeatListener(listener) {
|
|
341
|
+
this.onBeat = listener;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Start revealing a new line. Resets the cursor, pauses, and the hold
|
|
345
|
+
* multiplier (a stale fast-forward must not leak into the next line — an
|
|
346
|
+
* active binding re-asserts it on its next poll). An **empty** line
|
|
347
|
+
* (`parsed.length === 0`) is complete immediately and fires the completion
|
|
348
|
+
* listener synchronously, matching the no-typewriter contract.
|
|
349
|
+
*/
|
|
350
|
+
begin(parsed, lineSpeed = 1) {
|
|
351
|
+
this.parsed = parsed;
|
|
352
|
+
this.lineSpeed = lineSpeed > 0 ? lineSpeed : 1;
|
|
353
|
+
this.cursor = 0;
|
|
354
|
+
this.pauseTimer = 0;
|
|
355
|
+
this.tokenIdx = 0;
|
|
356
|
+
this.tickCount = 0;
|
|
357
|
+
this.speedMul = 1;
|
|
358
|
+
this.done = parsed.length === 0;
|
|
359
|
+
this.completed = false;
|
|
360
|
+
this.drainTokens();
|
|
361
|
+
if (this.done) this.finish();
|
|
362
|
+
}
|
|
363
|
+
/** Hold-to-fast-forward multiplier (1 = normal, e.g. 4 while skip is held). */
|
|
364
|
+
setSpeedMultiplier(m) {
|
|
365
|
+
this.speedMul = Math.max(1, m);
|
|
366
|
+
}
|
|
367
|
+
/** Advance the reveal cursor by `dt` (ms). Honours armed pauses and per-run
|
|
368
|
+
* speed; fires completion once the cursor reaches the end. No-op after the
|
|
369
|
+
* line is done or before the first {@link begin}. */
|
|
370
|
+
update(dt) {
|
|
371
|
+
const parsed = this.parsed;
|
|
372
|
+
if (!parsed || this.done) return;
|
|
373
|
+
if (this.pauseTimer > 0) {
|
|
374
|
+
this.pauseTimer = Math.max(0, this.pauseTimer - dt);
|
|
375
|
+
} else {
|
|
376
|
+
this.drainTokens();
|
|
377
|
+
if (this.pauseTimer === 0) {
|
|
378
|
+
const rate = this.charsPerSec * this.speedMul * this.lineSpeed * this.runSpeedAt(this.cursor);
|
|
379
|
+
this.cursor = Math.min(parsed.length, this.cursor + rate * dt / 1e3);
|
|
380
|
+
this.drainTokens();
|
|
381
|
+
this.emitTicks();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (this.cursor >= parsed.length && this.pauseTimer === 0) this.finish();
|
|
385
|
+
}
|
|
386
|
+
/** Reveal everything now (skip-to-end on a click/tap). Drains any not-yet-fired
|
|
387
|
+
* markers in order so their consequences still happen (`viaSkip=true` lets a
|
|
388
|
+
* host suppress a loud one-shot) and blows straight through pending pauses (a
|
|
389
|
+
* skip ignores holds), but DISCARDS pending ticks — replaying dozens of
|
|
390
|
+
* typewriter blips at once would machine-gun. Fires completion. */
|
|
391
|
+
complete() {
|
|
392
|
+
const parsed = this.parsed;
|
|
393
|
+
if (!parsed) return;
|
|
394
|
+
this.cursor = parsed.length;
|
|
395
|
+
this.pauseTimer = 0;
|
|
396
|
+
this.drainTokens(true);
|
|
397
|
+
this.tickCount = parsed.length;
|
|
398
|
+
this.finish();
|
|
399
|
+
}
|
|
400
|
+
/** Revealed grapheme count (fractional while typing). The view floors this to
|
|
401
|
+
* map onto its glyph prefix table. */
|
|
402
|
+
get revealed() {
|
|
403
|
+
return this.cursor;
|
|
404
|
+
}
|
|
405
|
+
/** True once the line is fully revealed (also true for an empty line). */
|
|
406
|
+
isComplete() {
|
|
407
|
+
return this.done;
|
|
408
|
+
}
|
|
409
|
+
/** True while glyphs are still appearing. */
|
|
410
|
+
isRevealing() {
|
|
411
|
+
return !this.done;
|
|
412
|
+
}
|
|
413
|
+
finish() {
|
|
414
|
+
this.done = true;
|
|
415
|
+
if (this.completed) return;
|
|
416
|
+
this.completed = true;
|
|
417
|
+
this.onComplete?.();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Drain tokens whose offset the cursor has reached, IN SOURCE ORDER. A `marker`
|
|
421
|
+
* emits a beat; a `pause` arms the hold, clamps the cursor to its offset, and
|
|
422
|
+
* STOPS the drain for this frame (a one-frame advance can overshoot the offset,
|
|
423
|
+
* so the clamp keeps glyphs past the beat from popping in early, and a later
|
|
424
|
+
* token waits until the hold resumes). `viaSkip` (from {@link complete}) tags
|
|
425
|
+
* drained markers and blows straight through pauses without holding. Monotonic
|
|
426
|
+
* `tokenIdx` → each token is handled exactly once.
|
|
427
|
+
*/
|
|
428
|
+
drainTokens(viaSkip = false) {
|
|
429
|
+
const tokens = this.parsed?.tokens;
|
|
430
|
+
if (!tokens) return;
|
|
431
|
+
while (this.tokenIdx < tokens.length && this.cursor >= tokens[this.tokenIdx].atChar) {
|
|
432
|
+
const tok = tokens[this.tokenIdx];
|
|
433
|
+
this.tokenIdx++;
|
|
434
|
+
if (tok.kind === "marker") {
|
|
435
|
+
this.onBeat?.({ kind: "marker", marker: tok, viaSkip });
|
|
436
|
+
} else if (!viaSkip && tok.ms > 0) {
|
|
437
|
+
this.pauseTimer = tok.ms;
|
|
438
|
+
this.cursor = Math.min(this.cursor, tok.atChar);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Emit a `tick` for each grapheme newly revealed since the last call (raw —
|
|
444
|
+
* no whitespace test; the host filters). Multiple in order on a large-dt
|
|
445
|
+
* frame; `tickCount` is monotonic so none repeat. */
|
|
446
|
+
emitTicks() {
|
|
447
|
+
const next = Math.floor(this.cursor);
|
|
448
|
+
for (let i = this.tickCount; i < next; i++) {
|
|
449
|
+
this.onBeat?.({ kind: "tick", index: i });
|
|
450
|
+
}
|
|
451
|
+
this.tickCount = next;
|
|
452
|
+
}
|
|
453
|
+
/** Reveal speed multiplier for whichever run the cursor currently sits in. */
|
|
454
|
+
runSpeedAt(reveal) {
|
|
455
|
+
const parsed = this.parsed;
|
|
456
|
+
if (!parsed) return 1;
|
|
457
|
+
const at = Math.floor(reveal);
|
|
458
|
+
let acc = 0;
|
|
459
|
+
for (const run of parsed.runs) {
|
|
460
|
+
if (at < acc + run.graphemeCount) return run.style.speed ?? 1;
|
|
461
|
+
acc += run.graphemeCount;
|
|
462
|
+
}
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/core/vars.ts
|
|
468
|
+
function materialize(storage) {
|
|
469
|
+
const out = {};
|
|
470
|
+
for (const [name, value] of storage.entries()) out[name] = value;
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
__name(materialize, "materialize");
|
|
474
|
+
var MemoryVariableStorage = class {
|
|
475
|
+
static {
|
|
476
|
+
__name(this, "MemoryVariableStorage");
|
|
477
|
+
}
|
|
478
|
+
map = /* @__PURE__ */ new Map();
|
|
479
|
+
constructor(initial) {
|
|
480
|
+
if (initial) for (const [name, value] of Object.entries(initial)) this.map.set(name, value);
|
|
481
|
+
}
|
|
482
|
+
get(name) {
|
|
483
|
+
return this.map.get(name);
|
|
484
|
+
}
|
|
485
|
+
set(name, value) {
|
|
486
|
+
this.map.set(name, value);
|
|
487
|
+
}
|
|
488
|
+
has(name) {
|
|
489
|
+
return this.map.has(name);
|
|
490
|
+
}
|
|
491
|
+
entries() {
|
|
492
|
+
return this.map.entries();
|
|
493
|
+
}
|
|
494
|
+
/** Drop everything — host-controlled reset (variables persist across plays by default). */
|
|
495
|
+
clear() {
|
|
496
|
+
this.map.clear();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
function cells(defs) {
|
|
500
|
+
const read = /* @__PURE__ */ __name((name) => {
|
|
501
|
+
const cell = defs[name];
|
|
502
|
+
return typeof cell === "function" ? cell() : cell.get();
|
|
503
|
+
}, "read");
|
|
504
|
+
return {
|
|
505
|
+
get(name) {
|
|
506
|
+
return Object.hasOwn(defs, name) ? read(name) : void 0;
|
|
507
|
+
},
|
|
508
|
+
set(name, value) {
|
|
509
|
+
if (!Object.hasOwn(defs, name)) {
|
|
510
|
+
throw new Error(`dialogue: cells() has no accessor for "${name}"`);
|
|
511
|
+
}
|
|
512
|
+
const cell = defs[name];
|
|
513
|
+
if (typeof cell === "function" || cell.set === void 0) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`dialogue: "${name}" is read-only (a cells getter without a setter)`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
cell.set(value);
|
|
519
|
+
},
|
|
520
|
+
has(name) {
|
|
521
|
+
return Object.hasOwn(defs, name);
|
|
522
|
+
},
|
|
523
|
+
*entries() {
|
|
524
|
+
for (const name of Object.keys(defs)) yield [name, read(name)];
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
__name(cells, "cells");
|
|
529
|
+
function compose(...storages) {
|
|
530
|
+
if (storages.length === 0) {
|
|
531
|
+
throw new Error("dialogue: compose() needs at least one storage");
|
|
532
|
+
}
|
|
533
|
+
const last = storages[storages.length - 1];
|
|
534
|
+
return {
|
|
535
|
+
get(name) {
|
|
536
|
+
for (const s of storages) if (s.has(name)) return s.get(name);
|
|
537
|
+
return void 0;
|
|
538
|
+
},
|
|
539
|
+
set(name, value) {
|
|
540
|
+
for (const s of storages) {
|
|
541
|
+
if (s.has(name)) {
|
|
542
|
+
s.set(name, value);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
last.set(name, value);
|
|
547
|
+
},
|
|
548
|
+
has(name) {
|
|
549
|
+
return storages.some((s) => s.has(name));
|
|
550
|
+
},
|
|
551
|
+
*entries() {
|
|
552
|
+
const seen = /* @__PURE__ */ new Set();
|
|
553
|
+
for (const s of storages) {
|
|
554
|
+
for (const [name, value] of s.entries()) {
|
|
555
|
+
if (!seen.has(name)) {
|
|
556
|
+
seen.add(name);
|
|
557
|
+
yield [name, value];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
__name(compose, "compose");
|
|
565
|
+
|
|
566
|
+
// src/core/expr.ts
|
|
567
|
+
function createScope(storage, functions) {
|
|
568
|
+
return {
|
|
569
|
+
get: /* @__PURE__ */ __name((name) => storage.get(name) ?? null, "get"),
|
|
570
|
+
call: /* @__PURE__ */ __name((fn, args) => {
|
|
571
|
+
const f = functions[fn];
|
|
572
|
+
if (!f) throw new Error(`dialogue: no function "${fn}" is installed`);
|
|
573
|
+
return f(...args);
|
|
574
|
+
}, "call"),
|
|
575
|
+
vars: /* @__PURE__ */ __name(() => materialize(storage), "vars")
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
__name(createScope, "createScope");
|
|
579
|
+
function holds(condition, scope) {
|
|
580
|
+
return condition === void 0 ? true : evalCondition(condition, scope);
|
|
581
|
+
}
|
|
582
|
+
__name(holds, "holds");
|
|
583
|
+
function evalCondition(condition, scope) {
|
|
584
|
+
if (typeof condition === "function") return condition(scope.vars());
|
|
585
|
+
if (typeof condition === "string") return truthy(scope.get(condition));
|
|
586
|
+
if (isExpr(condition)) return truthy(evaluate(condition, scope));
|
|
587
|
+
const { var: name, op, value } = condition;
|
|
588
|
+
if (op === "truthy") return truthy(scope.get(name));
|
|
589
|
+
if (op === "falsy") return !truthy(scope.get(name));
|
|
590
|
+
return truthy(
|
|
591
|
+
evaluate(
|
|
592
|
+
{
|
|
593
|
+
kind: "binary",
|
|
594
|
+
op,
|
|
595
|
+
left: { kind: "varRef", name },
|
|
596
|
+
right: { kind: "literal", value }
|
|
597
|
+
},
|
|
598
|
+
scope
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
__name(evalCondition, "evalCondition");
|
|
603
|
+
function evaluate(expr, scope) {
|
|
604
|
+
switch (expr.kind) {
|
|
605
|
+
case "literal":
|
|
606
|
+
return expr.value;
|
|
607
|
+
case "varRef":
|
|
608
|
+
return scope.get(expr.name);
|
|
609
|
+
case "group":
|
|
610
|
+
return evaluate(expr.expr, scope);
|
|
611
|
+
case "call":
|
|
612
|
+
return scope.call(
|
|
613
|
+
expr.fn,
|
|
614
|
+
(expr.args ?? []).map((a) => evaluate(a, scope))
|
|
615
|
+
);
|
|
616
|
+
case "unary":
|
|
617
|
+
return applyUnary(expr.op, evaluate(expr.operand, scope));
|
|
618
|
+
case "binary": {
|
|
619
|
+
const { op } = expr;
|
|
620
|
+
if (op === "and" || op === "&&") {
|
|
621
|
+
return truthy(evaluate(expr.left, scope)) && truthy(evaluate(expr.right, scope));
|
|
622
|
+
}
|
|
623
|
+
if (op === "or" || op === "||") {
|
|
624
|
+
return truthy(evaluate(expr.left, scope)) || truthy(evaluate(expr.right, scope));
|
|
625
|
+
}
|
|
626
|
+
return applyBinary(op, evaluate(expr.left, scope), evaluate(expr.right, scope));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
__name(evaluate, "evaluate");
|
|
631
|
+
function isExpr(value) {
|
|
632
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
633
|
+
}
|
|
634
|
+
__name(isExpr, "isExpr");
|
|
635
|
+
function truthy(value) {
|
|
636
|
+
return Boolean(value);
|
|
637
|
+
}
|
|
638
|
+
__name(truthy, "truthy");
|
|
639
|
+
function applyUnary(op, v) {
|
|
640
|
+
switch (op) {
|
|
641
|
+
case "not":
|
|
642
|
+
case "!":
|
|
643
|
+
return !truthy(v);
|
|
644
|
+
case "-":
|
|
645
|
+
return -num(v);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
__name(applyUnary, "applyUnary");
|
|
649
|
+
function applyBinary(op, l, r) {
|
|
650
|
+
switch (op) {
|
|
651
|
+
case "==":
|
|
652
|
+
case "eq":
|
|
653
|
+
case "is":
|
|
654
|
+
return l === r;
|
|
655
|
+
case "!=":
|
|
656
|
+
case "neq":
|
|
657
|
+
return l !== r;
|
|
658
|
+
case ">":
|
|
659
|
+
case "gt":
|
|
660
|
+
return num(l) > num(r);
|
|
661
|
+
case "<":
|
|
662
|
+
case "lt":
|
|
663
|
+
return num(l) < num(r);
|
|
664
|
+
case ">=":
|
|
665
|
+
case "gte":
|
|
666
|
+
return num(l) >= num(r);
|
|
667
|
+
case "<=":
|
|
668
|
+
case "lte":
|
|
669
|
+
return num(l) <= num(r);
|
|
670
|
+
// `and`/`or` (and `&&`/`||`) are short-circuited in evaluate() and never
|
|
671
|
+
// reach here; `xor` needs both operands, so it stays.
|
|
672
|
+
case "xor":
|
|
673
|
+
case "^":
|
|
674
|
+
return truthy(l) !== truthy(r);
|
|
675
|
+
case "+":
|
|
676
|
+
return typeof l === "string" || typeof r === "string" ? `${str(l)}${str(r)}` : num(l) + num(r);
|
|
677
|
+
case "-":
|
|
678
|
+
return num(l) - num(r);
|
|
679
|
+
case "*":
|
|
680
|
+
return num(l) * num(r);
|
|
681
|
+
case "/":
|
|
682
|
+
return num(l) / num(r);
|
|
683
|
+
case "%":
|
|
684
|
+
return num(l) % num(r);
|
|
685
|
+
default:
|
|
686
|
+
throw new Error(`dialogue: unknown binary operator "${op}"`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
__name(applyBinary, "applyBinary");
|
|
690
|
+
function num(v) {
|
|
691
|
+
return typeof v === "number" ? v : Number(v);
|
|
692
|
+
}
|
|
693
|
+
__name(num, "num");
|
|
694
|
+
function str(v) {
|
|
695
|
+
return v === null ? "" : String(v);
|
|
696
|
+
}
|
|
697
|
+
__name(str, "str");
|
|
698
|
+
|
|
699
|
+
// src/core/i18n.ts
|
|
700
|
+
var IdentityI18n = class {
|
|
701
|
+
constructor(locale = "en") {
|
|
702
|
+
this.locale = locale;
|
|
703
|
+
}
|
|
704
|
+
locale;
|
|
705
|
+
static {
|
|
706
|
+
__name(this, "IdentityI18n");
|
|
707
|
+
}
|
|
708
|
+
t(_key, fallback, params) {
|
|
709
|
+
return params ? interpolate(fallback, params) : fallback;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var TOKEN = /\{(\w+)\}/g;
|
|
713
|
+
function interpolate(text, params) {
|
|
714
|
+
return text.replace(
|
|
715
|
+
TOKEN,
|
|
716
|
+
(whole, name) => Object.hasOwn(params, name) ? String(params[name]) : whole
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
__name(interpolate, "interpolate");
|
|
720
|
+
function tokensIn(text) {
|
|
721
|
+
const names = /* @__PURE__ */ new Set();
|
|
722
|
+
for (const m of text.matchAll(TOKEN)) names.add(m[1]);
|
|
723
|
+
return [...names];
|
|
724
|
+
}
|
|
725
|
+
__name(tokensIn, "tokensIn");
|
|
726
|
+
|
|
727
|
+
// src/core/validate.ts
|
|
728
|
+
var DialogueScriptError = class extends Error {
|
|
729
|
+
static {
|
|
730
|
+
__name(this, "DialogueScriptError");
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var DialoguePlayError = class extends Error {
|
|
734
|
+
static {
|
|
735
|
+
__name(this, "DialoguePlayError");
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
var NUMERIC_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
|
|
739
|
+
var NUMERIC_EXPR_OPS = /* @__PURE__ */ new Set([
|
|
740
|
+
">",
|
|
741
|
+
"<",
|
|
742
|
+
">=",
|
|
743
|
+
"<=",
|
|
744
|
+
"gt",
|
|
745
|
+
"lt",
|
|
746
|
+
"gte",
|
|
747
|
+
"lte",
|
|
748
|
+
"-",
|
|
749
|
+
"*",
|
|
750
|
+
"/",
|
|
751
|
+
"%"
|
|
752
|
+
]);
|
|
753
|
+
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set(["set"]);
|
|
754
|
+
function operandRequirement(op) {
|
|
755
|
+
if (op === "+") return "numberOrString";
|
|
756
|
+
return NUMERIC_EXPR_OPS.has(op) ? "number" : null;
|
|
757
|
+
}
|
|
758
|
+
__name(operandRequirement, "operandRequirement");
|
|
759
|
+
var analysisCache = /* @__PURE__ */ new WeakMap();
|
|
760
|
+
function analyzeScript(script) {
|
|
761
|
+
const cached = analysisCache.get(script);
|
|
762
|
+
if (cached) return cached;
|
|
763
|
+
const analysis = computeAnalysis(script);
|
|
764
|
+
analysisCache.set(script, analysis);
|
|
765
|
+
return analysis;
|
|
766
|
+
}
|
|
767
|
+
__name(analyzeScript, "analyzeScript");
|
|
768
|
+
function computeAnalysis(script) {
|
|
769
|
+
const declaredTypes = /* @__PURE__ */ new Map();
|
|
770
|
+
for (const [name, value] of Object.entries(script.declare ?? {})) {
|
|
771
|
+
declaredTypes.set(name, valueType(value));
|
|
772
|
+
}
|
|
773
|
+
const readVars = /* @__PURE__ */ new Set();
|
|
774
|
+
const setTargets = /* @__PURE__ */ new Set();
|
|
775
|
+
const calledFunctions = /* @__PURE__ */ new Set();
|
|
776
|
+
const commandTypes = /* @__PURE__ */ new Set();
|
|
777
|
+
const collectExpr = /* @__PURE__ */ __name((expr, where) => {
|
|
778
|
+
switch (expr.kind) {
|
|
779
|
+
case "literal":
|
|
780
|
+
return;
|
|
781
|
+
case "varRef":
|
|
782
|
+
readVars.add(expr.name);
|
|
783
|
+
return;
|
|
784
|
+
case "call":
|
|
785
|
+
calledFunctions.add(expr.fn);
|
|
786
|
+
for (const arg of expr.args ?? []) collectExpr(arg, where);
|
|
787
|
+
return;
|
|
788
|
+
case "group":
|
|
789
|
+
collectExpr(expr.expr, where);
|
|
790
|
+
return;
|
|
791
|
+
case "unary":
|
|
792
|
+
collectExpr(expr.operand, where);
|
|
793
|
+
return;
|
|
794
|
+
case "binary":
|
|
795
|
+
collectExpr(expr.left, where);
|
|
796
|
+
collectExpr(expr.right, where);
|
|
797
|
+
checkBinaryOperands(expr, where);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
}, "collectExpr");
|
|
801
|
+
const checkBinaryOperands = /* @__PURE__ */ __name((expr, where) => {
|
|
802
|
+
const req = operandRequirement(expr.op);
|
|
803
|
+
if (!req) return;
|
|
804
|
+
const expected = req === "numberOrString" ? "a number or string" : "a number";
|
|
805
|
+
for (const operand of [expr.left, expr.right]) {
|
|
806
|
+
if (operand.kind === "literal") {
|
|
807
|
+
const t = valueType(operand.value);
|
|
808
|
+
if (t === "number" || req === "numberOrString" && t === "string") continue;
|
|
809
|
+
throw new DialogueScriptError(
|
|
810
|
+
`${where}: operator "${expr.op}" expects ${expected}, got ${t}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (operand.kind === "varRef" && req === "number") {
|
|
814
|
+
const t = declaredTypes.get(operand.name);
|
|
815
|
+
if (t !== void 0 && t !== "number" && t !== "null") {
|
|
816
|
+
throw new DialogueScriptError(
|
|
817
|
+
`${where}: operator "${expr.op}" needs a number; "${operand.name}" is ${t}`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}, "checkBinaryOperands");
|
|
823
|
+
const checkTokens = /* @__PURE__ */ __name((text) => {
|
|
824
|
+
if (!text) return;
|
|
825
|
+
for (const token of tokensIn(text)) readVars.add(token);
|
|
826
|
+
}, "checkTokens");
|
|
827
|
+
const checkCondition = /* @__PURE__ */ __name((condition, where) => {
|
|
828
|
+
if (condition === void 0 || typeof condition === "function") return;
|
|
829
|
+
if (typeof condition === "string") {
|
|
830
|
+
readVars.add(condition);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (isExpr(condition)) {
|
|
834
|
+
collectExpr(condition, where);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
readVars.add(condition.var);
|
|
838
|
+
if (NUMERIC_OPS.has(condition.op)) {
|
|
839
|
+
const t = declaredTypes.get(condition.var);
|
|
840
|
+
if (t !== void 0 && t !== "number" && t !== "null") {
|
|
841
|
+
throw new DialogueScriptError(
|
|
842
|
+
`${where}: operator "${condition.op}" needs a number; "${condition.var}" is ${t}`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (typeof condition.value !== "number") {
|
|
846
|
+
throw new DialogueScriptError(
|
|
847
|
+
`${where}: operator "${condition.op}" compares against a number, got ${typeof condition.value}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}, "checkCondition");
|
|
852
|
+
const checkSetLiteralType = /* @__PURE__ */ __name((target, value, where) => {
|
|
853
|
+
const declared = declaredTypes.get(target);
|
|
854
|
+
if (declared !== void 0 && declared !== "null" && value !== null && typeof value !== declared) {
|
|
855
|
+
throw new DialogueScriptError(
|
|
856
|
+
`${where}: set "${target}" expects ${declared}, got ${typeof value}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}, "checkSetLiteralType");
|
|
860
|
+
const checkCommands = /* @__PURE__ */ __name((commands, where) => {
|
|
861
|
+
for (const cmd of commands ?? []) {
|
|
862
|
+
if (cmd.type === "set") {
|
|
863
|
+
const target = cmd["var"];
|
|
864
|
+
if (typeof target !== "string") {
|
|
865
|
+
throw new DialogueScriptError(`${where}: set command has no string "var"`);
|
|
866
|
+
}
|
|
867
|
+
setTargets.add(target);
|
|
868
|
+
const value = cmd["value"];
|
|
869
|
+
if (value === void 0) {
|
|
870
|
+
throw new DialogueScriptError(`${where}: set "${target}" has no value`);
|
|
871
|
+
}
|
|
872
|
+
if (isExpr(value)) {
|
|
873
|
+
collectExpr(value, where);
|
|
874
|
+
if (value.kind === "literal") checkSetLiteralType(target, value.value, where);
|
|
875
|
+
} else {
|
|
876
|
+
checkSetLiteralType(target, value, where);
|
|
877
|
+
}
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (!BUILTIN_COMMANDS.has(cmd.type)) commandTypes.add(cmd.type);
|
|
881
|
+
}
|
|
882
|
+
}, "checkCommands");
|
|
883
|
+
for (const speaker of Object.values(script.speakers ?? {})) {
|
|
884
|
+
checkTokens(speaker.name);
|
|
885
|
+
}
|
|
886
|
+
for (const node of Object.values(script.nodes)) {
|
|
887
|
+
const where = `node "${node.id}"`;
|
|
888
|
+
for (const step of node.steps) {
|
|
889
|
+
switch (step.kind) {
|
|
890
|
+
case "say": {
|
|
891
|
+
const s = step;
|
|
892
|
+
checkTokens(s.text);
|
|
893
|
+
checkCommands(s.commands, `${where} say`);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
case "choice": {
|
|
897
|
+
const c = step;
|
|
898
|
+
checkTokens(c.text);
|
|
899
|
+
for (const opt of c.options) {
|
|
900
|
+
checkTokens(opt.text);
|
|
901
|
+
checkTokens(opt.disabledReason);
|
|
902
|
+
checkCondition(opt.condition, `${where} choice option "${opt.text}"`);
|
|
903
|
+
checkCommands(opt.commands, `${where} choice option "${opt.text}"`);
|
|
904
|
+
}
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
case "command": {
|
|
908
|
+
const cs = step;
|
|
909
|
+
checkCommands(cs.commands, `${where} command`);
|
|
910
|
+
checkCondition(cs.condition, `${where} command`);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
default:
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return { declaredTypes, readVars, setTargets, calledFunctions, commandTypes };
|
|
919
|
+
}
|
|
920
|
+
__name(computeAnalysis, "computeAnalysis");
|
|
921
|
+
function validatePlay(analysis, env) {
|
|
922
|
+
for (const [name, type] of analysis.declaredTypes) {
|
|
923
|
+
if (type === "null" || !env.storage.has(name)) continue;
|
|
924
|
+
const current = env.storage.get(name);
|
|
925
|
+
if (current !== void 0 && current !== null && typeof current !== type) {
|
|
926
|
+
throw new DialoguePlayError(
|
|
927
|
+
`declared default for "${name}" is ${type} but storage already holds ${typeof current}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
for (const name of analysis.readVars) {
|
|
932
|
+
if (!analysis.declaredTypes.has(name) && !env.storage.has(name) && !analysis.setTargets.has(name)) {
|
|
933
|
+
throw new DialoguePlayError(
|
|
934
|
+
`script reads "${name}" but nothing provides it (no declared default, no storage value, no \`set\`; for an argument read use a function call)`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const fn of analysis.calledFunctions) {
|
|
939
|
+
if (!Object.hasOwn(env.functions, fn)) {
|
|
940
|
+
throw new DialoguePlayError(
|
|
941
|
+
`script calls function "${fn}" but no such function is installed`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const target of analysis.setTargets) {
|
|
946
|
+
if (Object.hasOwn(env.functions, target)) {
|
|
947
|
+
throw new DialoguePlayError(
|
|
948
|
+
`set target "${target}" is a function (read-only); functions cannot be assigned`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (env.fallbackCommand === void 0) {
|
|
953
|
+
const unhandled = [...analysis.commandTypes].filter((t) => !Object.hasOwn(env.commands, t));
|
|
954
|
+
if (unhandled.length > 0) {
|
|
955
|
+
throw new DialoguePlayError(
|
|
956
|
+
`no handler for command type(s): ${unhandled.join(", ")} (add to commands, or set fallbackCommand)`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
__name(validatePlay, "validatePlay");
|
|
962
|
+
function valueType(value) {
|
|
963
|
+
if (value === null) return "null";
|
|
964
|
+
if (typeof value === "string") return "string";
|
|
965
|
+
if (typeof value === "number") return "number";
|
|
966
|
+
return "boolean";
|
|
967
|
+
}
|
|
968
|
+
__name(valueType, "valueType");
|
|
969
|
+
|
|
970
|
+
// src/core/expr-parse.ts
|
|
971
|
+
var DialogueExprError = class extends DialogueScriptError {
|
|
972
|
+
static {
|
|
973
|
+
__name(this, "DialogueExprError");
|
|
974
|
+
}
|
|
975
|
+
line;
|
|
976
|
+
col;
|
|
977
|
+
constructor(message, line, col) {
|
|
978
|
+
super(`${message} (at ${line}:${col})`);
|
|
979
|
+
this.name = "DialogueExprError";
|
|
980
|
+
this.line = line;
|
|
981
|
+
this.col = col;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
function parseExpr(src) {
|
|
985
|
+
const tokens = tokenize(src);
|
|
986
|
+
return new Parser(tokens).parse();
|
|
987
|
+
}
|
|
988
|
+
__name(parseExpr, "parseExpr");
|
|
989
|
+
var KEYWORDS = {
|
|
990
|
+
and: "&&",
|
|
991
|
+
or: "||",
|
|
992
|
+
not: "!",
|
|
993
|
+
xor: "xor",
|
|
994
|
+
is: "==",
|
|
995
|
+
eq: "==",
|
|
996
|
+
neq: "!=",
|
|
997
|
+
gt: ">",
|
|
998
|
+
lt: "<",
|
|
999
|
+
gte: ">=",
|
|
1000
|
+
lte: "<=",
|
|
1001
|
+
true: "true",
|
|
1002
|
+
false: "false",
|
|
1003
|
+
null: "null"
|
|
1004
|
+
};
|
|
1005
|
+
var isDigit = /* @__PURE__ */ __name((c) => c >= "0" && c <= "9", "isDigit");
|
|
1006
|
+
var isIdentStart = /* @__PURE__ */ __name((c) => c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_" || c === "$", "isIdentStart");
|
|
1007
|
+
var isIdentPart = /* @__PURE__ */ __name((c) => isIdentStart(c) || isDigit(c) || c === ".", "isIdentPart");
|
|
1008
|
+
function tokenize(src) {
|
|
1009
|
+
const tokens = [];
|
|
1010
|
+
let i = 0;
|
|
1011
|
+
let line = 1;
|
|
1012
|
+
let col = 1;
|
|
1013
|
+
const advance = /* @__PURE__ */ __name((n = 1) => {
|
|
1014
|
+
for (let k = 0; k < n; k++) {
|
|
1015
|
+
if (src[i] === "\n") {
|
|
1016
|
+
line++;
|
|
1017
|
+
col = 1;
|
|
1018
|
+
} else {
|
|
1019
|
+
col++;
|
|
1020
|
+
}
|
|
1021
|
+
i++;
|
|
1022
|
+
}
|
|
1023
|
+
}, "advance");
|
|
1024
|
+
while (i < src.length) {
|
|
1025
|
+
const c = src[i];
|
|
1026
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
1027
|
+
advance();
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const startLine = line;
|
|
1031
|
+
const startCol = col;
|
|
1032
|
+
if (isDigit(c)) {
|
|
1033
|
+
let text = "";
|
|
1034
|
+
while (i < src.length && isDigit(src[i])) {
|
|
1035
|
+
text += src[i];
|
|
1036
|
+
advance();
|
|
1037
|
+
}
|
|
1038
|
+
if (src[i] === "." && isDigit(src[i + 1] ?? "")) {
|
|
1039
|
+
text += ".";
|
|
1040
|
+
advance();
|
|
1041
|
+
while (i < src.length && isDigit(src[i])) {
|
|
1042
|
+
text += src[i];
|
|
1043
|
+
advance();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
tokens.push({ kind: "number", value: Number(text), line: startLine, col: startCol });
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (c === "'" || c === '"') {
|
|
1050
|
+
const quote = c;
|
|
1051
|
+
advance();
|
|
1052
|
+
let text = "";
|
|
1053
|
+
while (i < src.length && src[i] !== quote) {
|
|
1054
|
+
if (src[i] === "\\") {
|
|
1055
|
+
advance();
|
|
1056
|
+
if (i >= src.length) break;
|
|
1057
|
+
text += unescape2(src[i]);
|
|
1058
|
+
} else {
|
|
1059
|
+
text += src[i];
|
|
1060
|
+
}
|
|
1061
|
+
advance();
|
|
1062
|
+
}
|
|
1063
|
+
if (src[i] !== quote) {
|
|
1064
|
+
throw new DialogueExprError("unterminated string literal", startLine, startCol);
|
|
1065
|
+
}
|
|
1066
|
+
advance();
|
|
1067
|
+
tokens.push({ kind: "string", value: text, line: startLine, col: startCol });
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
if (isIdentStart(c)) {
|
|
1071
|
+
let text = "";
|
|
1072
|
+
while (i < src.length && isIdentPart(src[i])) {
|
|
1073
|
+
text += src[i];
|
|
1074
|
+
advance();
|
|
1075
|
+
}
|
|
1076
|
+
const keyword = KEYWORDS[text];
|
|
1077
|
+
if (keyword === void 0) {
|
|
1078
|
+
tokens.push({ kind: "ident", value: text, line: startLine, col: startCol });
|
|
1079
|
+
} else if (keyword === "true" || keyword === "false" || keyword === "null") {
|
|
1080
|
+
const value = keyword === "true" ? true : keyword === "false" ? false : null;
|
|
1081
|
+
tokens.push({ kind: keyword, value, line: startLine, col: startCol });
|
|
1082
|
+
} else {
|
|
1083
|
+
tokens.push({ kind: keyword, line: startLine, col: startCol });
|
|
1084
|
+
}
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const two = src.slice(i, i + 2);
|
|
1088
|
+
if (two === "&&" || two === "||" || two === "==" || two === "!=" || two === ">=" || two === "<=") {
|
|
1089
|
+
tokens.push({ kind: two, line: startLine, col: startCol });
|
|
1090
|
+
advance(2);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (c === ">" || c === "<" || c === "!" || c === "+" || c === "-" || c === "(" || c === ")" || c === ",") {
|
|
1094
|
+
tokens.push({ kind: c, line: startLine, col: startCol });
|
|
1095
|
+
advance();
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
throw new DialogueExprError(`unexpected character "${c}"`, startLine, startCol);
|
|
1099
|
+
}
|
|
1100
|
+
tokens.push({ kind: "eof", line, col });
|
|
1101
|
+
return tokens;
|
|
1102
|
+
}
|
|
1103
|
+
__name(tokenize, "tokenize");
|
|
1104
|
+
function unescape2(c) {
|
|
1105
|
+
switch (c) {
|
|
1106
|
+
case "n":
|
|
1107
|
+
return "\n";
|
|
1108
|
+
case "t":
|
|
1109
|
+
return " ";
|
|
1110
|
+
case "r":
|
|
1111
|
+
return "\r";
|
|
1112
|
+
default:
|
|
1113
|
+
return c;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
__name(unescape2, "unescape");
|
|
1117
|
+
var INFIX_BP = {
|
|
1118
|
+
"||": 1,
|
|
1119
|
+
"&&": 2,
|
|
1120
|
+
"==": 3,
|
|
1121
|
+
"!=": 3,
|
|
1122
|
+
">": 3,
|
|
1123
|
+
"<": 3,
|
|
1124
|
+
">=": 3,
|
|
1125
|
+
"<=": 3,
|
|
1126
|
+
"+": 4,
|
|
1127
|
+
"-": 4
|
|
1128
|
+
};
|
|
1129
|
+
var Parser = class {
|
|
1130
|
+
constructor(tokens) {
|
|
1131
|
+
this.tokens = tokens;
|
|
1132
|
+
}
|
|
1133
|
+
tokens;
|
|
1134
|
+
static {
|
|
1135
|
+
__name(this, "Parser");
|
|
1136
|
+
}
|
|
1137
|
+
pos = 0;
|
|
1138
|
+
parse() {
|
|
1139
|
+
const expr = this.parseBinary(0);
|
|
1140
|
+
const t = this.peek();
|
|
1141
|
+
if (t.kind !== "eof") {
|
|
1142
|
+
throw new DialogueExprError(`unexpected trailing token "${describe(t)}"`, t.line, t.col);
|
|
1143
|
+
}
|
|
1144
|
+
return expr;
|
|
1145
|
+
}
|
|
1146
|
+
parseBinary(minBp) {
|
|
1147
|
+
let left = this.parseUnary();
|
|
1148
|
+
for (; ; ) {
|
|
1149
|
+
const t = this.peek();
|
|
1150
|
+
const bp = INFIX_BP[t.kind];
|
|
1151
|
+
if (bp === void 0 || bp < minBp) break;
|
|
1152
|
+
this.next();
|
|
1153
|
+
const right = this.parseBinary(bp + 1);
|
|
1154
|
+
left = { kind: "binary", op: t.kind, left, right };
|
|
1155
|
+
}
|
|
1156
|
+
return left;
|
|
1157
|
+
}
|
|
1158
|
+
parseUnary() {
|
|
1159
|
+
const t = this.peek();
|
|
1160
|
+
if (t.kind === "!" || t.kind === "-") {
|
|
1161
|
+
this.next();
|
|
1162
|
+
const operand = this.parseUnary();
|
|
1163
|
+
return { kind: "unary", op: t.kind === "!" ? "!" : "-", operand };
|
|
1164
|
+
}
|
|
1165
|
+
return this.parsePrimary();
|
|
1166
|
+
}
|
|
1167
|
+
parsePrimary() {
|
|
1168
|
+
const t = this.peek();
|
|
1169
|
+
switch (t.kind) {
|
|
1170
|
+
case "number":
|
|
1171
|
+
case "string":
|
|
1172
|
+
this.next();
|
|
1173
|
+
return { kind: "literal", value: t.value };
|
|
1174
|
+
case "true":
|
|
1175
|
+
case "false":
|
|
1176
|
+
case "null":
|
|
1177
|
+
this.next();
|
|
1178
|
+
return { kind: "literal", value: t.value };
|
|
1179
|
+
case "ident": {
|
|
1180
|
+
this.next();
|
|
1181
|
+
const name = t.value;
|
|
1182
|
+
if (this.peek().kind === "(") {
|
|
1183
|
+
this.next();
|
|
1184
|
+
const args = this.parseArgs();
|
|
1185
|
+
return { kind: "call", fn: name, args };
|
|
1186
|
+
}
|
|
1187
|
+
return { kind: "varRef", name };
|
|
1188
|
+
}
|
|
1189
|
+
case "(": {
|
|
1190
|
+
this.next();
|
|
1191
|
+
const inner = this.parseBinary(0);
|
|
1192
|
+
this.expect(")");
|
|
1193
|
+
return { kind: "group", expr: inner };
|
|
1194
|
+
}
|
|
1195
|
+
default:
|
|
1196
|
+
throw new DialogueExprError(`unexpected ${describe(t)}`, t.line, t.col);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
/** Parse a comma-separated argument list; the opening `(` is already consumed. */
|
|
1200
|
+
parseArgs() {
|
|
1201
|
+
const args = [];
|
|
1202
|
+
if (this.peek().kind === ")") {
|
|
1203
|
+
this.next();
|
|
1204
|
+
return args;
|
|
1205
|
+
}
|
|
1206
|
+
for (; ; ) {
|
|
1207
|
+
args.push(this.parseBinary(0));
|
|
1208
|
+
const t = this.peek();
|
|
1209
|
+
if (t.kind === ",") {
|
|
1210
|
+
this.next();
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (t.kind === ")") {
|
|
1214
|
+
this.next();
|
|
1215
|
+
return args;
|
|
1216
|
+
}
|
|
1217
|
+
throw new DialogueExprError(`expected "," or ")" in argument list, got ${describe(t)}`, t.line, t.col);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
expect(kind) {
|
|
1221
|
+
const t = this.peek();
|
|
1222
|
+
if (t.kind !== kind) {
|
|
1223
|
+
throw new DialogueExprError(`expected "${kind}", got ${describe(t)}`, t.line, t.col);
|
|
1224
|
+
}
|
|
1225
|
+
this.next();
|
|
1226
|
+
}
|
|
1227
|
+
peek() {
|
|
1228
|
+
return this.tokens[this.pos];
|
|
1229
|
+
}
|
|
1230
|
+
next() {
|
|
1231
|
+
return this.tokens[this.pos++];
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
function describe(t) {
|
|
1235
|
+
if (t.kind === "eof") return "end of input";
|
|
1236
|
+
if (t.kind === "ident") return `"${String(t.value)}"`;
|
|
1237
|
+
if (t.kind === "string") return `string "${String(t.value)}"`;
|
|
1238
|
+
if (t.kind === "number") return `number ${String(t.value)}`;
|
|
1239
|
+
return `"${t.kind}"`;
|
|
1240
|
+
}
|
|
1241
|
+
__name(describe, "describe");
|
|
1242
|
+
|
|
1243
|
+
// src/core/formats/canonical.ts
|
|
1244
|
+
function loadScript(raw) {
|
|
1245
|
+
if (!raw || typeof raw !== "object") {
|
|
1246
|
+
throw new DialogueScriptError("script must be an object");
|
|
1247
|
+
}
|
|
1248
|
+
if (!raw.id) throw new DialogueScriptError("script.id is required");
|
|
1249
|
+
if (!raw.nodes || typeof raw.nodes !== "object") {
|
|
1250
|
+
throw new DialogueScriptError(`script "${raw.id}" has no nodes`);
|
|
1251
|
+
}
|
|
1252
|
+
const nodeIds = Object.keys(raw.nodes);
|
|
1253
|
+
if (nodeIds.length === 0) {
|
|
1254
|
+
throw new DialogueScriptError(`script "${raw.id}" has no nodes`);
|
|
1255
|
+
}
|
|
1256
|
+
const start = raw.start ?? nodeIds[0];
|
|
1257
|
+
if (!raw.nodes[start]) {
|
|
1258
|
+
throw new DialogueScriptError(`start node "${start}" not found in "${raw.id}"`);
|
|
1259
|
+
}
|
|
1260
|
+
for (const [key, speaker] of Object.entries(raw.speakers ?? {})) {
|
|
1261
|
+
if (speaker.id !== key) {
|
|
1262
|
+
throw new DialogueScriptError(
|
|
1263
|
+
`speaker key "${key}" != speaker.id "${speaker.id}"`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const [id, node] of Object.entries(raw.nodes)) {
|
|
1268
|
+
validateNode(raw, id, node);
|
|
1269
|
+
}
|
|
1270
|
+
const resolved = resolveExpressions(raw);
|
|
1271
|
+
const script = Object.freeze({ ...resolved, start });
|
|
1272
|
+
analyzeScript(script);
|
|
1273
|
+
return script;
|
|
1274
|
+
}
|
|
1275
|
+
__name(loadScript, "loadScript");
|
|
1276
|
+
function validateNode(script, id, node) {
|
|
1277
|
+
if (node.id !== id) {
|
|
1278
|
+
throw new DialogueScriptError(`node key "${id}" != node.id "${node.id}"`);
|
|
1279
|
+
}
|
|
1280
|
+
if (!Array.isArray(node.steps) || node.steps.length === 0) {
|
|
1281
|
+
throw new DialogueScriptError(`node "${id}" has no steps`);
|
|
1282
|
+
}
|
|
1283
|
+
for (const step of node.steps) validateStep(script, id, step);
|
|
1284
|
+
}
|
|
1285
|
+
__name(validateNode, "validateNode");
|
|
1286
|
+
function validateStep(script, nodeId, step) {
|
|
1287
|
+
const targetExists = /* @__PURE__ */ __name((t) => {
|
|
1288
|
+
if (t !== void 0 && !script.nodes[t]) {
|
|
1289
|
+
throw new DialogueScriptError(
|
|
1290
|
+
`node "${nodeId}": jump target "${t}" does not exist`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
}, "targetExists");
|
|
1294
|
+
const speakerExists = /* @__PURE__ */ __name((s) => {
|
|
1295
|
+
if (s !== void 0 && !script.speakers?.[s]) {
|
|
1296
|
+
throw new DialogueScriptError(
|
|
1297
|
+
`node "${nodeId}": speaker "${s}" is not in script.speakers`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
}, "speakerExists");
|
|
1301
|
+
switch (step.kind) {
|
|
1302
|
+
case "say":
|
|
1303
|
+
if (typeof step.text !== "string") {
|
|
1304
|
+
throw new DialogueScriptError(`node "${nodeId}": say.text must be a string`);
|
|
1305
|
+
}
|
|
1306
|
+
speakerExists(step.speaker);
|
|
1307
|
+
break;
|
|
1308
|
+
case "choice":
|
|
1309
|
+
if (!Array.isArray(step.options) || step.options.length === 0) {
|
|
1310
|
+
throw new DialogueScriptError(`node "${nodeId}": choice has no options`);
|
|
1311
|
+
}
|
|
1312
|
+
speakerExists(step.speaker);
|
|
1313
|
+
for (const opt of step.options) targetExists(opt.target);
|
|
1314
|
+
break;
|
|
1315
|
+
case "command":
|
|
1316
|
+
targetExists(step.target);
|
|
1317
|
+
break;
|
|
1318
|
+
case "goto":
|
|
1319
|
+
if (step.target === void 0) {
|
|
1320
|
+
throw new DialogueScriptError(`node "${nodeId}": goto has no target`);
|
|
1321
|
+
}
|
|
1322
|
+
targetExists(step.target);
|
|
1323
|
+
break;
|
|
1324
|
+
case "end":
|
|
1325
|
+
break;
|
|
1326
|
+
default:
|
|
1327
|
+
throw new DialogueScriptError(
|
|
1328
|
+
`node "${nodeId}": unknown step kind "${step.kind}"`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
__name(validateStep, "validateStep");
|
|
1333
|
+
function resolveExpressions(script) {
|
|
1334
|
+
let nodesChanged = false;
|
|
1335
|
+
const nodes = {};
|
|
1336
|
+
for (const [id, node] of Object.entries(script.nodes)) {
|
|
1337
|
+
let stepsChanged = false;
|
|
1338
|
+
const steps = node.steps.map((step) => {
|
|
1339
|
+
const next = resolveStep(step);
|
|
1340
|
+
if (next !== step) stepsChanged = true;
|
|
1341
|
+
return next;
|
|
1342
|
+
});
|
|
1343
|
+
if (stepsChanged) {
|
|
1344
|
+
nodes[id] = { ...node, steps };
|
|
1345
|
+
nodesChanged = true;
|
|
1346
|
+
} else {
|
|
1347
|
+
nodes[id] = node;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return nodesChanged ? { ...script, nodes } : script;
|
|
1351
|
+
}
|
|
1352
|
+
__name(resolveExpressions, "resolveExpressions");
|
|
1353
|
+
function resolveStep(step) {
|
|
1354
|
+
switch (step.kind) {
|
|
1355
|
+
case "say":
|
|
1356
|
+
return resolveSay(step);
|
|
1357
|
+
case "choice":
|
|
1358
|
+
return resolveChoice(step);
|
|
1359
|
+
case "command":
|
|
1360
|
+
return resolveCommandStep(step);
|
|
1361
|
+
case "goto":
|
|
1362
|
+
case "end":
|
|
1363
|
+
return step;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
__name(resolveStep, "resolveStep");
|
|
1367
|
+
function resolveSay(step) {
|
|
1368
|
+
const commands = rewriteCommands(step.commands);
|
|
1369
|
+
return commands ? { ...step, commands } : step;
|
|
1370
|
+
}
|
|
1371
|
+
__name(resolveSay, "resolveSay");
|
|
1372
|
+
function resolveCommandStep(step) {
|
|
1373
|
+
const condition = rewriteCondition(step.condition);
|
|
1374
|
+
const commands = rewriteCommands(step.commands);
|
|
1375
|
+
if (!condition && !commands) return step;
|
|
1376
|
+
return {
|
|
1377
|
+
...step,
|
|
1378
|
+
...condition ? { condition } : {},
|
|
1379
|
+
...commands ? { commands } : {}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
__name(resolveCommandStep, "resolveCommandStep");
|
|
1383
|
+
function resolveChoice(step) {
|
|
1384
|
+
let changed = false;
|
|
1385
|
+
const options = step.options.map((opt) => {
|
|
1386
|
+
const next = rewriteOption(opt);
|
|
1387
|
+
if (next) {
|
|
1388
|
+
changed = true;
|
|
1389
|
+
return next;
|
|
1390
|
+
}
|
|
1391
|
+
return opt;
|
|
1392
|
+
});
|
|
1393
|
+
return changed ? { ...step, options } : step;
|
|
1394
|
+
}
|
|
1395
|
+
__name(resolveChoice, "resolveChoice");
|
|
1396
|
+
function rewriteOption(opt) {
|
|
1397
|
+
const condition = rewriteCondition(opt.condition);
|
|
1398
|
+
const commands = rewriteCommands(opt.commands);
|
|
1399
|
+
if (!condition && !commands) return void 0;
|
|
1400
|
+
return {
|
|
1401
|
+
...opt,
|
|
1402
|
+
...condition ? { condition } : {},
|
|
1403
|
+
...commands ? { commands } : {}
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
__name(rewriteOption, "rewriteOption");
|
|
1407
|
+
function rewriteCondition(condition) {
|
|
1408
|
+
return typeof condition === "string" ? parseExpr(condition) : void 0;
|
|
1409
|
+
}
|
|
1410
|
+
__name(rewriteCondition, "rewriteCondition");
|
|
1411
|
+
function rewriteCommands(commands) {
|
|
1412
|
+
if (!commands) return void 0;
|
|
1413
|
+
let changed = false;
|
|
1414
|
+
const out = commands.map((cmd) => {
|
|
1415
|
+
if (cmd.type === "set" && typeof cmd.value === "string") {
|
|
1416
|
+
changed = true;
|
|
1417
|
+
return { ...cmd, value: parseExpr(cmd.value) };
|
|
1418
|
+
}
|
|
1419
|
+
return cmd;
|
|
1420
|
+
});
|
|
1421
|
+
return changed ? out : void 0;
|
|
1422
|
+
}
|
|
1423
|
+
__name(rewriteCommands, "rewriteCommands");
|
|
1424
|
+
|
|
1425
|
+
// src/core/formats/compact.ts
|
|
1426
|
+
function parseCompact(text) {
|
|
1427
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
1428
|
+
const speakers = {};
|
|
1429
|
+
lines.forEach((raw, i) => {
|
|
1430
|
+
const line = raw.trim();
|
|
1431
|
+
if (!line.startsWith("@")) return;
|
|
1432
|
+
const def = parseSpeaker(line, i + 1);
|
|
1433
|
+
if (Object.hasOwn(speakers, def.id)) {
|
|
1434
|
+
fail(i + 1, `duplicate speaker "${def.id}"`);
|
|
1435
|
+
}
|
|
1436
|
+
speakers[def.id] = def;
|
|
1437
|
+
});
|
|
1438
|
+
let id;
|
|
1439
|
+
const nodes = {};
|
|
1440
|
+
const nodeOrder = [];
|
|
1441
|
+
let current = null;
|
|
1442
|
+
let choiceRun = null;
|
|
1443
|
+
const declares = {};
|
|
1444
|
+
const flushChoice = /* @__PURE__ */ __name(() => {
|
|
1445
|
+
if (choiceRun && current) current.steps.push({ kind: "choice", options: choiceRun });
|
|
1446
|
+
choiceRun = null;
|
|
1447
|
+
}, "flushChoice");
|
|
1448
|
+
lines.forEach((raw, i) => {
|
|
1449
|
+
const lineNo = i + 1;
|
|
1450
|
+
const line = raw.trim();
|
|
1451
|
+
if (line === "" || line.startsWith("//")) return;
|
|
1452
|
+
if (line.startsWith("@")) return;
|
|
1453
|
+
if (line.startsWith("#")) {
|
|
1454
|
+
flushChoice();
|
|
1455
|
+
const newId = line.slice(1).trim();
|
|
1456
|
+
if (!newId) fail(lineNo, "'#' script id directive needs an id");
|
|
1457
|
+
if (id !== void 0) fail(lineNo, `duplicate '#' script id directive (already "${id}")`);
|
|
1458
|
+
id = newId;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (line.startsWith("::")) {
|
|
1462
|
+
flushChoice();
|
|
1463
|
+
const nodeId = line.slice(2).trim();
|
|
1464
|
+
if (!nodeId) fail(lineNo, "':: ' node directive needs an id");
|
|
1465
|
+
if (Object.hasOwn(nodes, nodeId)) fail(lineNo, `duplicate node "${nodeId}"`);
|
|
1466
|
+
current = { id: nodeId, steps: [] };
|
|
1467
|
+
nodes[nodeId] = current;
|
|
1468
|
+
nodeOrder.push(nodeId);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (line.startsWith("?")) {
|
|
1472
|
+
if (!current) fail(lineNo, "choice '?' appears before any ':: <node>'");
|
|
1473
|
+
(choiceRun ??= []).push(parseChoice(line.slice(1).trim(), lineNo));
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
flushChoice();
|
|
1477
|
+
const decl = parseDeclare(line);
|
|
1478
|
+
if (decl) {
|
|
1479
|
+
declares[decl.name] = decl.value;
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const node = current;
|
|
1483
|
+
if (!node) fail(lineNo, `dialogue line appears before any ':: <node>' ("${line}")`);
|
|
1484
|
+
if (line.startsWith("->")) {
|
|
1485
|
+
node.steps.push(parseGoto(line, lineNo));
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const set = parseSet(line);
|
|
1489
|
+
if (set) {
|
|
1490
|
+
node.steps.push(set);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const cmd = parseDo(line, lineNo);
|
|
1494
|
+
if (cmd) {
|
|
1495
|
+
node.steps.push(cmd);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (line === "end") {
|
|
1499
|
+
node.steps.push({ kind: "end" });
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
node.steps.push(parseSay(line, lineNo, speakers));
|
|
1503
|
+
});
|
|
1504
|
+
flushChoice();
|
|
1505
|
+
if (id === void 0) throw new DialogueScriptError("compact: missing '# <id>' script directive");
|
|
1506
|
+
if (nodeOrder.length === 0) {
|
|
1507
|
+
throw new DialogueScriptError(`compact: script "${id}" has no ':: <node>' nodes`);
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
id,
|
|
1511
|
+
start: nodeOrder[0],
|
|
1512
|
+
nodes,
|
|
1513
|
+
...Object.keys(speakers).length > 0 ? { speakers } : {},
|
|
1514
|
+
...Object.keys(declares).length > 0 ? { declare: declares } : {}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
__name(parseCompact, "parseCompact");
|
|
1518
|
+
function loadCompact(text) {
|
|
1519
|
+
return loadScript(parseCompact(text));
|
|
1520
|
+
}
|
|
1521
|
+
__name(loadCompact, "loadCompact");
|
|
1522
|
+
function parseSpeaker(line, lineNo) {
|
|
1523
|
+
const tokens = line.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
1524
|
+
const id = tokens[0];
|
|
1525
|
+
if (!id) fail(lineNo, "'@' speaker directive needs an id");
|
|
1526
|
+
let nameTokens = tokens.slice(1);
|
|
1527
|
+
let color;
|
|
1528
|
+
const last = nameTokens[nameTokens.length - 1];
|
|
1529
|
+
if (last !== void 0 && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(last)) {
|
|
1530
|
+
color = hexColor(last);
|
|
1531
|
+
nameTokens = nameTokens.slice(0, -1);
|
|
1532
|
+
}
|
|
1533
|
+
return {
|
|
1534
|
+
id,
|
|
1535
|
+
name: nameTokens.length > 0 ? nameTokens.join(" ") : id,
|
|
1536
|
+
...color !== void 0 ? { color } : {}
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
__name(parseSpeaker, "parseSpeaker");
|
|
1540
|
+
function hexColor(token) {
|
|
1541
|
+
let hex = token.slice(1);
|
|
1542
|
+
if (hex.length === 3) hex = hex.replace(/./g, "$&$&");
|
|
1543
|
+
return parseInt(hex, 16);
|
|
1544
|
+
}
|
|
1545
|
+
__name(hexColor, "hexColor");
|
|
1546
|
+
function parseGoto(line, lineNo) {
|
|
1547
|
+
const m = /^->\s*(\S+)(?:\s+if:\s*(.+))?\s*$/.exec(line);
|
|
1548
|
+
if (!m) fail(lineNo, "'->' goto needs a target node id (optionally `-> node if: cond`)");
|
|
1549
|
+
const target = m[1];
|
|
1550
|
+
if (m[2] !== void 0) {
|
|
1551
|
+
return { kind: "command", commands: [], condition: parseExpr(m[2].trim()), target };
|
|
1552
|
+
}
|
|
1553
|
+
return { kind: "goto", target };
|
|
1554
|
+
}
|
|
1555
|
+
__name(parseGoto, "parseGoto");
|
|
1556
|
+
function parseDeclare(line) {
|
|
1557
|
+
const m = /^declare\s+([A-Za-z_$][A-Za-z0-9_.$]*)\s*=\s*(\S.*)$/.exec(line);
|
|
1558
|
+
if (!m) return null;
|
|
1559
|
+
return { name: m[1], value: scalar(m[2].trim()) };
|
|
1560
|
+
}
|
|
1561
|
+
__name(parseDeclare, "parseDeclare");
|
|
1562
|
+
function parseSet(line) {
|
|
1563
|
+
const m = /^set\s+([A-Za-z_$][A-Za-z0-9_.$]*)\s*=\s*(\S.*)$/.exec(line);
|
|
1564
|
+
if (!m) return null;
|
|
1565
|
+
return {
|
|
1566
|
+
kind: "command",
|
|
1567
|
+
commands: [{ type: "set", var: m[1], value: setValue(m[2].trim()) }]
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
__name(parseSet, "parseSet");
|
|
1571
|
+
function setValue(rhs) {
|
|
1572
|
+
const literal = numberBoolNull(rhs);
|
|
1573
|
+
return literal === NOT_LITERAL ? parseExpr(rhs) : literal;
|
|
1574
|
+
}
|
|
1575
|
+
__name(setValue, "setValue");
|
|
1576
|
+
function parseDo(line, lineNo) {
|
|
1577
|
+
if (!/^do(\s|$)/.test(line)) return null;
|
|
1578
|
+
const tokens = splitArgs(line.slice(2).trim());
|
|
1579
|
+
const type = tokens[0];
|
|
1580
|
+
if (type === void 0 || !/^[A-Za-z_][A-Za-z0-9_-]*$/.test(type)) return null;
|
|
1581
|
+
const command = { type };
|
|
1582
|
+
let typeKeyCollision = false;
|
|
1583
|
+
for (const tok of tokens.slice(1)) {
|
|
1584
|
+
if (tok.startsWith("#")) {
|
|
1585
|
+
const flag = tok.slice(1);
|
|
1586
|
+
if (!flag) return null;
|
|
1587
|
+
if (flag === "type") typeKeyCollision = true;
|
|
1588
|
+
else command[flag] = true;
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const eq = tok.indexOf("=");
|
|
1592
|
+
if (eq <= 0) return null;
|
|
1593
|
+
const key = tok.slice(0, eq);
|
|
1594
|
+
if (key === "type") typeKeyCollision = true;
|
|
1595
|
+
else command[key] = scalar(tok.slice(eq + 1));
|
|
1596
|
+
}
|
|
1597
|
+
if (typeKeyCollision) {
|
|
1598
|
+
fail(lineNo, `'do' data key "type" collides with the command type (the leading token); rename it`);
|
|
1599
|
+
}
|
|
1600
|
+
return { kind: "command", commands: [command] };
|
|
1601
|
+
}
|
|
1602
|
+
__name(parseDo, "parseDo");
|
|
1603
|
+
function parseSay(line, lineNo, speakers) {
|
|
1604
|
+
let speaker;
|
|
1605
|
+
let expression;
|
|
1606
|
+
let body = line;
|
|
1607
|
+
const colon = line.indexOf(":");
|
|
1608
|
+
if (colon !== -1) {
|
|
1609
|
+
const header = line.slice(0, colon).trim();
|
|
1610
|
+
const tokens = header.length > 0 ? header.split(/\s+/) : [];
|
|
1611
|
+
const first = tokens[0];
|
|
1612
|
+
if (first !== void 0 && Object.hasOwn(speakers, first)) {
|
|
1613
|
+
if (tokens.length > 2) {
|
|
1614
|
+
fail(lineNo, `speaker header "${header}" has too many tokens (use "speaker [face]: text")`);
|
|
1615
|
+
}
|
|
1616
|
+
speaker = first;
|
|
1617
|
+
expression = tokens[1];
|
|
1618
|
+
body = line.slice(colon + 1).trimStart();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const { text, fields, meta } = peelSayHints(body, lineNo);
|
|
1622
|
+
return {
|
|
1623
|
+
kind: "say",
|
|
1624
|
+
...speaker !== void 0 ? { speaker } : {},
|
|
1625
|
+
...expression !== void 0 ? { expression } : {},
|
|
1626
|
+
text,
|
|
1627
|
+
...fields,
|
|
1628
|
+
...meta ? { meta } : {}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
__name(parseSay, "parseSay");
|
|
1632
|
+
function peelSayHints(body, lineNo) {
|
|
1633
|
+
let rest = body;
|
|
1634
|
+
const fields = {};
|
|
1635
|
+
const meta = {};
|
|
1636
|
+
let metaCount = 0;
|
|
1637
|
+
for (; ; ) {
|
|
1638
|
+
const hash = /(^|\s)#(\S+)\s*$/.exec(rest);
|
|
1639
|
+
if (hash) {
|
|
1640
|
+
const tag = hash[2];
|
|
1641
|
+
const lk = lineKey(tag);
|
|
1642
|
+
if (lk !== void 0) fields.key = lk;
|
|
1643
|
+
else metaCount += applyHashtag(meta, tag);
|
|
1644
|
+
rest = rest.slice(0, hash.index).replace(/\s+$/, "");
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
const hint = /(^|\s)(view|voice|speed|auto)=(\S+)\s*$/.exec(rest);
|
|
1648
|
+
if (hint) {
|
|
1649
|
+
applySayField(fields, hint[2], hint[3], lineNo);
|
|
1650
|
+
rest = rest.slice(0, hint.index).replace(/\s+$/, "");
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
return { text: rest, fields, meta: metaCount > 0 ? meta : void 0 };
|
|
1656
|
+
}
|
|
1657
|
+
__name(peelSayHints, "peelSayHints");
|
|
1658
|
+
function applySayField(fields, key, value, lineNo) {
|
|
1659
|
+
switch (key) {
|
|
1660
|
+
case "view":
|
|
1661
|
+
fields.view = unquote(value);
|
|
1662
|
+
return;
|
|
1663
|
+
case "voice":
|
|
1664
|
+
fields.voice = unquote(value);
|
|
1665
|
+
return;
|
|
1666
|
+
case "speed":
|
|
1667
|
+
fields.speed = numberHint(value, lineNo, "speed");
|
|
1668
|
+
return;
|
|
1669
|
+
case "auto":
|
|
1670
|
+
fields.autoAdvanceMs = numberHint(value, lineNo, "auto");
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
__name(applySayField, "applySayField");
|
|
1675
|
+
function numberHint(value, lineNo, key) {
|
|
1676
|
+
const n = Number(value);
|
|
1677
|
+
if (!Number.isFinite(n)) fail(lineNo, `'${key}=' expects a number, got "${value}"`);
|
|
1678
|
+
return n;
|
|
1679
|
+
}
|
|
1680
|
+
__name(numberHint, "numberHint");
|
|
1681
|
+
function parseChoice(body, lineNo) {
|
|
1682
|
+
let rest = body;
|
|
1683
|
+
const meta = {};
|
|
1684
|
+
let metaCount = 0;
|
|
1685
|
+
let once = false;
|
|
1686
|
+
let disabled = false;
|
|
1687
|
+
let target;
|
|
1688
|
+
let condition;
|
|
1689
|
+
let key;
|
|
1690
|
+
for (; ; ) {
|
|
1691
|
+
const hash = /(^|\s)#(\S+)\s*$/.exec(rest);
|
|
1692
|
+
if (!hash) break;
|
|
1693
|
+
const tag = hash[2];
|
|
1694
|
+
const lk = lineKey(tag);
|
|
1695
|
+
if (tag === "once") once = true;
|
|
1696
|
+
else if (tag === "disabled") disabled = true;
|
|
1697
|
+
else if (lk !== void 0) key = lk;
|
|
1698
|
+
else metaCount += applyHashtag(meta, tag);
|
|
1699
|
+
rest = rest.slice(0, hash.index).replace(/\s+$/, "");
|
|
1700
|
+
}
|
|
1701
|
+
const arrow = /(^|\s)->\s*(\S+)\s*$/.exec(rest);
|
|
1702
|
+
const named = arrow ? null : /(^|\s)target=(\S+)\s*$/.exec(rest);
|
|
1703
|
+
if (arrow) {
|
|
1704
|
+
target = arrow[2];
|
|
1705
|
+
rest = rest.slice(0, arrow.index).replace(/\s+$/, "");
|
|
1706
|
+
} else if (named) {
|
|
1707
|
+
target = named[2];
|
|
1708
|
+
rest = rest.slice(0, named.index).replace(/\s+$/, "");
|
|
1709
|
+
}
|
|
1710
|
+
const ifm = /(^|\s)if:\s*(.+)$/.exec(rest);
|
|
1711
|
+
if (ifm) {
|
|
1712
|
+
const condStr = ifm[2].trim();
|
|
1713
|
+
if (!condStr) fail(lineNo, "choice 'if:' has no condition");
|
|
1714
|
+
condition = parseExpr(condStr);
|
|
1715
|
+
rest = rest.slice(0, ifm.index).replace(/\s+$/, "");
|
|
1716
|
+
}
|
|
1717
|
+
const text = rest.trim();
|
|
1718
|
+
if (!text) fail(lineNo, "choice has no text");
|
|
1719
|
+
const unknown = firstUnknownTag(text);
|
|
1720
|
+
if (unknown !== null) {
|
|
1721
|
+
fail(
|
|
1722
|
+
lineNo,
|
|
1723
|
+
`unrecognized markup tag "[${unknown}]" in choice text \u2014 '[..]' is for inline markup only; write choice attributes as 'if: \u2026', '-> node', or '#flag'`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
text,
|
|
1728
|
+
...key !== void 0 ? { key } : {},
|
|
1729
|
+
...condition !== void 0 ? { condition } : {},
|
|
1730
|
+
...target !== void 0 ? { target } : {},
|
|
1731
|
+
...once ? { once: true } : {},
|
|
1732
|
+
...disabled ? { presentation: "disabled" } : {},
|
|
1733
|
+
...metaCount > 0 ? { meta } : {}
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
__name(parseChoice, "parseChoice");
|
|
1737
|
+
function applyHashtag(meta, tag) {
|
|
1738
|
+
const colon = tag.indexOf(":");
|
|
1739
|
+
if (colon === -1) meta[tag] = true;
|
|
1740
|
+
else meta[tag.slice(0, colon)] = scalar(tag.slice(colon + 1));
|
|
1741
|
+
return 1;
|
|
1742
|
+
}
|
|
1743
|
+
__name(applyHashtag, "applyHashtag");
|
|
1744
|
+
function lineKey(tag) {
|
|
1745
|
+
const colon = tag.indexOf(":");
|
|
1746
|
+
return colon > 0 && tag.slice(0, colon) === "line" ? tag.slice(colon + 1) : void 0;
|
|
1747
|
+
}
|
|
1748
|
+
__name(lineKey, "lineKey");
|
|
1749
|
+
var NOT_LITERAL = /* @__PURE__ */ Symbol("not-literal");
|
|
1750
|
+
function numberBoolNull(raw) {
|
|
1751
|
+
if (/^-?\d+(?:\.\d+)?$/.test(raw)) return Number(raw);
|
|
1752
|
+
if (raw === "true") return true;
|
|
1753
|
+
if (raw === "false") return false;
|
|
1754
|
+
if (raw === "null") return null;
|
|
1755
|
+
return NOT_LITERAL;
|
|
1756
|
+
}
|
|
1757
|
+
__name(numberBoolNull, "numberBoolNull");
|
|
1758
|
+
function scalar(raw) {
|
|
1759
|
+
const lit = numberBoolNull(raw);
|
|
1760
|
+
return lit === NOT_LITERAL ? unquote(raw) : lit;
|
|
1761
|
+
}
|
|
1762
|
+
__name(scalar, "scalar");
|
|
1763
|
+
function unquote(raw) {
|
|
1764
|
+
if (raw.length >= 2 && (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'"))) {
|
|
1765
|
+
return raw.slice(1, -1);
|
|
1766
|
+
}
|
|
1767
|
+
return raw;
|
|
1768
|
+
}
|
|
1769
|
+
__name(unquote, "unquote");
|
|
1770
|
+
function splitArgs(s) {
|
|
1771
|
+
const out = [];
|
|
1772
|
+
let i = 0;
|
|
1773
|
+
while (i < s.length) {
|
|
1774
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
1775
|
+
if (i >= s.length) break;
|
|
1776
|
+
let tok = "";
|
|
1777
|
+
while (i < s.length && !/\s/.test(s[i])) {
|
|
1778
|
+
const c = s[i];
|
|
1779
|
+
if (c === '"' || c === "'") {
|
|
1780
|
+
tok += c;
|
|
1781
|
+
i++;
|
|
1782
|
+
while (i < s.length && s[i] !== c) tok += s[i++];
|
|
1783
|
+
if (i < s.length) tok += s[i++];
|
|
1784
|
+
} else {
|
|
1785
|
+
tok += c;
|
|
1786
|
+
i++;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
out.push(tok);
|
|
1790
|
+
}
|
|
1791
|
+
return out;
|
|
1792
|
+
}
|
|
1793
|
+
__name(splitArgs, "splitArgs");
|
|
1794
|
+
function fail(lineNo, message) {
|
|
1795
|
+
throw new DialogueScriptError(`compact: line ${lineNo}: ${message}`);
|
|
1796
|
+
}
|
|
1797
|
+
__name(fail, "fail");
|
|
1798
|
+
|
|
1799
|
+
// src/core/defineScript.ts
|
|
1800
|
+
function defineScript(script) {
|
|
1801
|
+
return script;
|
|
1802
|
+
}
|
|
1803
|
+
__name(defineScript, "defineScript");
|
|
1804
|
+
|
|
1805
|
+
// src/core/runner.ts
|
|
1806
|
+
var DialogueRunner = class {
|
|
1807
|
+
constructor(script, env, handlers) {
|
|
1808
|
+
this.script = script;
|
|
1809
|
+
this.handlers = handlers;
|
|
1810
|
+
this.nodeId = script.start;
|
|
1811
|
+
this.storage = env.storage;
|
|
1812
|
+
this.scope = createScope(env.storage, env.functions);
|
|
1813
|
+
this.onError = env.onError;
|
|
1814
|
+
}
|
|
1815
|
+
script;
|
|
1816
|
+
handlers;
|
|
1817
|
+
static {
|
|
1818
|
+
__name(this, "DialogueRunner");
|
|
1819
|
+
}
|
|
1820
|
+
/** `option.once` keys already picked — per-conversation **cursor** state, NOT
|
|
1821
|
+
* the variable storage. Fresh per runner, so a new `play()` starts it empty
|
|
1822
|
+
* (a re-played conversation re-shows its `once` options; {@link getChosenOnce}
|
|
1823
|
+
* exposes the set so a save cursor could capture/restore it). */
|
|
1824
|
+
chosenOnce = /* @__PURE__ */ new Set();
|
|
1825
|
+
nodeId;
|
|
1826
|
+
stepIndex = 0;
|
|
1827
|
+
state = "idle";
|
|
1828
|
+
/** "play" normally; `skip()` flips it to "skip" to fast-forward the section. */
|
|
1829
|
+
runMode = "play";
|
|
1830
|
+
/** Storage (write through this so a read-only `cells` accessor throws) +
|
|
1831
|
+
* functions, wrapped once as the condition/`set`-value eval scope. */
|
|
1832
|
+
storage;
|
|
1833
|
+
scope;
|
|
1834
|
+
onError;
|
|
1835
|
+
/** Snapshot of the storage's variables — the `handle.getVars()` /
|
|
1836
|
+
* future save-cursor view. */
|
|
1837
|
+
getVars() {
|
|
1838
|
+
return materialize(this.storage);
|
|
1839
|
+
}
|
|
1840
|
+
// ── save seam (read-only cursor getters) ──────────────────────────────────
|
|
1841
|
+
// The runner's durable cursor is (nodeId, stepIndex, chosenOnce) + getVars().
|
|
1842
|
+
// These getters exist so a future `SnapshotContributor` can capture/restore a
|
|
1843
|
+
// conversation WITHOUT a breaking API change. Snapshot/restore itself is
|
|
1844
|
+
// deliberately NOT built yet — keep these read-only.
|
|
1845
|
+
/** Current node id (durable cursor; save seam). */
|
|
1846
|
+
getNodeId() {
|
|
1847
|
+
return this.nodeId;
|
|
1848
|
+
}
|
|
1849
|
+
/** Current step index within the node (durable cursor; save seam). */
|
|
1850
|
+
getStepIndex() {
|
|
1851
|
+
return this.stepIndex;
|
|
1852
|
+
}
|
|
1853
|
+
/** One-shot choice keys already picked (`option.once`); save seam. */
|
|
1854
|
+
getChosenOnce() {
|
|
1855
|
+
return this.chosenOnce;
|
|
1856
|
+
}
|
|
1857
|
+
isEnded() {
|
|
1858
|
+
return this.state === "ended";
|
|
1859
|
+
}
|
|
1860
|
+
/** Begin at the start node. Idempotent guard against double-start. The cursor
|
|
1861
|
+
* (`nodeId`/`stepIndex`) is already at the start from the ctor + field init. */
|
|
1862
|
+
start() {
|
|
1863
|
+
if (this.state !== "idle") return;
|
|
1864
|
+
void this.run();
|
|
1865
|
+
}
|
|
1866
|
+
/** Advance past the current `say` line. No-op unless we're awaiting it. */
|
|
1867
|
+
advance() {
|
|
1868
|
+
if (this.state !== "saying") return;
|
|
1869
|
+
this.stepIndex++;
|
|
1870
|
+
void this.run();
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Fast-forward from the current line: run intervening commands in `skip` mode
|
|
1874
|
+
* (so the game can reconstruct world state idempotently) without presenting
|
|
1875
|
+
* any lines, stopping at the next choice or the end. No-op unless on a line.
|
|
1876
|
+
*/
|
|
1877
|
+
async skip() {
|
|
1878
|
+
if (this.state !== "saying") return;
|
|
1879
|
+
this.runMode = "skip";
|
|
1880
|
+
this.stepIndex++;
|
|
1881
|
+
await this.run();
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Public, **wait-state-free** entry the Session uses to fire a `say` line's
|
|
1885
|
+
* commands at show / after-reveal / advance time. Handles built-in `set`,
|
|
1886
|
+
* surfaces the rest with the current mode (or `mode`, when the Session fires the
|
|
1887
|
+
* displayed line's batches as part of its own skip), and awaits `blocking` ones.
|
|
1888
|
+
* Delegates to {@link executeBatch}; the runner's wait-state is untouched (the
|
|
1889
|
+
* Session gates its own input).
|
|
1890
|
+
*/
|
|
1891
|
+
runCommands(commands, mode) {
|
|
1892
|
+
return this.executeBatch(commands, mode);
|
|
1893
|
+
}
|
|
1894
|
+
/** Pick choice `index` (the original option index). */
|
|
1895
|
+
async choose(index) {
|
|
1896
|
+
if (this.state !== "choosing") return;
|
|
1897
|
+
const step = this.currentStep();
|
|
1898
|
+
if (!step || step.kind !== "choice") return;
|
|
1899
|
+
const option = step.options[index];
|
|
1900
|
+
if (!option || !this.choiceEnabled(step, index, option)) return;
|
|
1901
|
+
if (option.once) this.chosenOnce.add(this.onceKey(step, index));
|
|
1902
|
+
this.state = "awaiting-command";
|
|
1903
|
+
await this.fireBatch(option.commands);
|
|
1904
|
+
if (this.isEnded()) return;
|
|
1905
|
+
if (option.target !== void 0) this.jump(option.target);
|
|
1906
|
+
else this.stepIndex++;
|
|
1907
|
+
void this.run();
|
|
1908
|
+
}
|
|
1909
|
+
// ── internal ────────────────────────────────────────────────────────────
|
|
1910
|
+
/** Run non-blocking steps until we hit one that needs input, or the end. */
|
|
1911
|
+
async run() {
|
|
1912
|
+
for (; ; ) {
|
|
1913
|
+
if (this.isEnded()) return;
|
|
1914
|
+
const step = this.currentStep();
|
|
1915
|
+
if (!step) {
|
|
1916
|
+
this.end();
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (await this.handleStep(step)) return;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
/** @returns true if the step blocks (waiting for advance/choose/command/end). */
|
|
1923
|
+
async handleStep(step) {
|
|
1924
|
+
switch (step.kind) {
|
|
1925
|
+
case "say": {
|
|
1926
|
+
if (this.runMode === "skip") {
|
|
1927
|
+
await this.fireBatch(step.commands);
|
|
1928
|
+
if (this.isEnded()) return true;
|
|
1929
|
+
this.stepIndex++;
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
this.state = "saying";
|
|
1933
|
+
this.handlers.onSay(step, this.speaker(step.speaker));
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
1936
|
+
case "choice": {
|
|
1937
|
+
const choices = this.resolveChoices(step);
|
|
1938
|
+
if (!choices.some((c) => !c.disabled)) {
|
|
1939
|
+
this.stepIndex++;
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
this.runMode = "play";
|
|
1943
|
+
this.state = "choosing";
|
|
1944
|
+
this.handlers.onChoice(step, choices, this.speaker(step.speaker));
|
|
1945
|
+
return true;
|
|
1946
|
+
}
|
|
1947
|
+
case "command": {
|
|
1948
|
+
await this.fireBatch(step.commands);
|
|
1949
|
+
if (this.state === "ended") return true;
|
|
1950
|
+
if (step.target !== void 0 && this.test(step.condition)) {
|
|
1951
|
+
this.jump(step.target);
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
this.stepIndex++;
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
case "goto":
|
|
1958
|
+
this.jump(step.target);
|
|
1959
|
+
return false;
|
|
1960
|
+
case "end":
|
|
1961
|
+
this.end();
|
|
1962
|
+
return true;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
jump(target) {
|
|
1966
|
+
this.nodeId = target;
|
|
1967
|
+
this.stepIndex = 0;
|
|
1968
|
+
}
|
|
1969
|
+
end() {
|
|
1970
|
+
if (this.state === "ended") return;
|
|
1971
|
+
this.state = "ended";
|
|
1972
|
+
this.handlers.onEnd();
|
|
1973
|
+
}
|
|
1974
|
+
currentStep() {
|
|
1975
|
+
return this.script.nodes[this.nodeId]?.steps[this.stepIndex];
|
|
1976
|
+
}
|
|
1977
|
+
speaker(id) {
|
|
1978
|
+
return id ? this.script.speakers?.[id] : void 0;
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Resolve a choice step to its visible rows. A spent `once` option is always
|
|
1982
|
+
* dropped (presentation governs condition failures only). A passing option is
|
|
1983
|
+
* enabled; a failing one is returned as a `disabled` row when its
|
|
1984
|
+
* `presentation` is `"disabled"`, else dropped (the default `"hidden"`).
|
|
1985
|
+
*/
|
|
1986
|
+
resolveChoices(step) {
|
|
1987
|
+
const out = [];
|
|
1988
|
+
step.options.forEach((option, index) => {
|
|
1989
|
+
if (this.isSpent(step, index)) return;
|
|
1990
|
+
if (this.test(option.condition)) out.push({ index, option });
|
|
1991
|
+
else if (option.presentation === "disabled") out.push({ index, option, disabled: true });
|
|
1992
|
+
});
|
|
1993
|
+
return out;
|
|
1994
|
+
}
|
|
1995
|
+
/** Whether option `index` can actually be picked — the gate `choose()` uses.
|
|
1996
|
+
* A spent `once` option or a failing condition refuses (a `"disabled"` row is
|
|
1997
|
+
* shown but still unpickable, so this stays the single selection authority). */
|
|
1998
|
+
choiceEnabled(step, index, option) {
|
|
1999
|
+
return !this.isSpent(step, index) && this.test(option.condition);
|
|
2000
|
+
}
|
|
2001
|
+
/** A `once` option already chosen this run — always dropped from the menu
|
|
2002
|
+
* regardless of `presentation`. Single source of truth for the once-gate,
|
|
2003
|
+
* shared by `resolveChoices` and `choiceEnabled`. Reads the option from
|
|
2004
|
+
* `step.options[index]`, so `(step, index)` is the only input. */
|
|
2005
|
+
isSpent(step, index) {
|
|
2006
|
+
const option = step.options[index];
|
|
2007
|
+
return option?.once === true && this.chosenOnce.has(this.onceKey(step, index));
|
|
2008
|
+
}
|
|
2009
|
+
onceKey(step, index) {
|
|
2010
|
+
return `${this.nodeId}#${this.stepIndex}#${index}#${step.options[index]?.text ?? ""}`;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Fire an inline command batch (a `command` step or a chosen option). Manages
|
|
2014
|
+
* wait-state: enters `awaiting-command` up front when the batch contains a
|
|
2015
|
+
* blocking command, so a stray advance/confirm during the await is ignored; the
|
|
2016
|
+
* caller transitions out of the state afterwards. The work itself goes through
|
|
2017
|
+
* the wait-state-free {@link executeBatch}.
|
|
2018
|
+
*/
|
|
2019
|
+
async fireBatch(commands) {
|
|
2020
|
+
if (!commands || commands.length === 0) return;
|
|
2021
|
+
if (commands.some((c) => c.blocking)) this.state = "awaiting-command";
|
|
2022
|
+
await this.executeBatch(commands);
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* The wait-state-free command executor, shared by {@link fireBatch} (inline
|
|
2026
|
+
* firing) and {@link runCommands} (the Session's line-timed firing). Applies
|
|
2027
|
+
* built-in `set`; surfaces the rest to the host with the current mode; awaits
|
|
2028
|
+
* `blocking` handlers and fire-and-forgets the others. Touches no wait-state.
|
|
2029
|
+
*/
|
|
2030
|
+
async executeBatch(commands, mode = this.runMode) {
|
|
2031
|
+
if (!commands) return;
|
|
2032
|
+
for (const cmd of commands) {
|
|
2033
|
+
if (cmd.type === "set" && typeof cmd.var === "string") {
|
|
2034
|
+
const value = cmd.value;
|
|
2035
|
+
const next = isExpr(value) ? evaluate(value, this.scope) : value;
|
|
2036
|
+
try {
|
|
2037
|
+
this.storage.set(cmd.var, next);
|
|
2038
|
+
} catch (e) {
|
|
2039
|
+
this.onError?.(
|
|
2040
|
+
`ignored "set ${cmd.var}": ${e instanceof Error ? e.message : String(e)}`,
|
|
2041
|
+
e
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
let result;
|
|
2047
|
+
try {
|
|
2048
|
+
result = this.handlers.onCommand(cmd, this.commandContext(mode));
|
|
2049
|
+
} catch {
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
if (!isPromise(result)) continue;
|
|
2053
|
+
if (cmd.blocking) {
|
|
2054
|
+
try {
|
|
2055
|
+
await result;
|
|
2056
|
+
} catch {
|
|
2057
|
+
}
|
|
2058
|
+
if (this.isEnded()) return;
|
|
2059
|
+
} else {
|
|
2060
|
+
void Promise.resolve(result).catch(() => {
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
/** The context handed to a command handler. `setVar` writes through the
|
|
2066
|
+
* conversation's storage (guarded by the session for staleness), the same
|
|
2067
|
+
* path as the `set` built-in — so the skill-check seam and `set` share one
|
|
2068
|
+
* guarded write. */
|
|
2069
|
+
commandContext(mode) {
|
|
2070
|
+
return { mode, setVar: /* @__PURE__ */ __name((name, value) => this.storage.set(name, value), "setVar") };
|
|
2071
|
+
}
|
|
2072
|
+
test(condition) {
|
|
2073
|
+
return holds(condition, this.scope);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
function isPromise(v) {
|
|
2077
|
+
return typeof v === "object" && v !== null && typeof v.then === "function";
|
|
2078
|
+
}
|
|
2079
|
+
__name(isPromise, "isPromise");
|
|
2080
|
+
|
|
2081
|
+
// src/core/session.ts
|
|
2082
|
+
var EMPTY_VARS = Object.freeze({});
|
|
2083
|
+
var DialogueSession = class {
|
|
2084
|
+
constructor(channels, opts = {}) {
|
|
2085
|
+
this.channels = channels;
|
|
2086
|
+
this.opts = opts;
|
|
2087
|
+
this.i18n = opts.i18n ?? new IdentityI18n();
|
|
2088
|
+
this.skipMul = opts.skipMultiplier ?? 4;
|
|
2089
|
+
this.defaultStorage = opts.storage ?? new MemoryVariableStorage();
|
|
2090
|
+
this.defaultFunctions = opts.functions ?? {};
|
|
2091
|
+
this.defaultCommands = opts.commands ?? {};
|
|
2092
|
+
this.defaultFallback = opts.fallbackCommand;
|
|
2093
|
+
this.channels.text.setRevealListener(() => this.handleRevealComplete());
|
|
2094
|
+
this.channels.text.setBeatListener((beat) => this.handleRevealBeat(beat));
|
|
2095
|
+
this.channels.choices.onChoiceChosen = (position) => this.confirmAt(position);
|
|
2096
|
+
}
|
|
2097
|
+
channels;
|
|
2098
|
+
opts;
|
|
2099
|
+
static {
|
|
2100
|
+
__name(this, "DialogueSession");
|
|
2101
|
+
}
|
|
2102
|
+
i18n;
|
|
2103
|
+
skipMul;
|
|
2104
|
+
/** Controller-installed environment (persists across plays); per-`play()`
|
|
2105
|
+
* overrides are layered on top into the resolved fields below. */
|
|
2106
|
+
defaultStorage;
|
|
2107
|
+
defaultFunctions;
|
|
2108
|
+
defaultCommands;
|
|
2109
|
+
defaultFallback;
|
|
2110
|
+
// Resolved per play() (controller install + call-site overrides).
|
|
2111
|
+
storage;
|
|
2112
|
+
functions = {};
|
|
2113
|
+
commands = {};
|
|
2114
|
+
fallbackCommand;
|
|
2115
|
+
// Fields use explicit `| undefined` (not `?`) so reassigning `undefined`
|
|
2116
|
+
// (e.g. `stop()` nulling the cursor) is legal under the repo's
|
|
2117
|
+
// `exactOptionalPropertyTypes`.
|
|
2118
|
+
runner;
|
|
2119
|
+
script;
|
|
2120
|
+
mode = "idle";
|
|
2121
|
+
scriptId = "";
|
|
2122
|
+
saying;
|
|
2123
|
+
autoTimer;
|
|
2124
|
+
/** Default auto-advance delay (ms) applied to lines without their own
|
|
2125
|
+
* `autoAdvanceMs`. `null` = off (manual advance). Set via {@link setAutoAdvance}. */
|
|
2126
|
+
autoAdvanceDefault = null;
|
|
2127
|
+
resolved = [];
|
|
2128
|
+
selected = 0;
|
|
2129
|
+
/** Count of in-flight blocking line-command batches (show/afterReveal/advance).
|
|
2130
|
+
* Input is gated while > 0. An ownership counter (not a shared boolean) so an
|
|
2131
|
+
* overlapping batch resolving — e.g. the afterReveal batch finishing while a
|
|
2132
|
+
* long blocking `show` command is still awaited — can't drop a gate it
|
|
2133
|
+
* doesn't own. */
|
|
2134
|
+
blockedCount = 0;
|
|
2135
|
+
/** True between an advance request and the runner stepping off the line —
|
|
2136
|
+
* guards against a second advance double-firing `advance`-timed commands. */
|
|
2137
|
+
advancing = false;
|
|
2138
|
+
/** True once the current line's `advance`-timed commands have fired, so a
|
|
2139
|
+
* second advance() while the runner is still stepping (e.g. awaiting a
|
|
2140
|
+
* blocking command step) can't re-fire them against the stale line. */
|
|
2141
|
+
advanceFired = false;
|
|
2142
|
+
/** True once the current line's `afterReveal`-timed commands have fired
|
|
2143
|
+
* (normally via handleRevealComplete; skip() fires them early when the line
|
|
2144
|
+
* hasn't finished revealing). */
|
|
2145
|
+
afterRevealFired = false;
|
|
2146
|
+
/** Latched by the first confirm() until the runner produces its next state
|
|
2147
|
+
* (handleSay/handleChoice/handleEnd), so mashing confirm while the runner
|
|
2148
|
+
* awaits the option's blocking commands can't emit duplicate onChoiceMade
|
|
2149
|
+
* events (`mode` stays "choosing" for that whole window). */
|
|
2150
|
+
confirming = false;
|
|
2151
|
+
/** Bumped by every stop()/play(). A suspended async continuation from a prior
|
|
2152
|
+
* conversation captures this and bails on resume if it changed, so it can't
|
|
2153
|
+
* drive (advance / show the caret on) the runner of a *new* conversation. */
|
|
2154
|
+
generation = 0;
|
|
2155
|
+
// ── lifecycle levers — host-level, NOT conversation state ──────────────
|
|
2156
|
+
/** `setHidden` — visual only; gates every channel's `setVisible`. Survives
|
|
2157
|
+
* `stop()`/`play()` (a host that hides for a cutscene and forgets to unhide
|
|
2158
|
+
* gets what it asked for) — so it is deliberately NOT reset by `stop()`. */
|
|
2159
|
+
hidden = false;
|
|
2160
|
+
/** `setPaused` — freezes the update loop AND the input-agnostic API. State is
|
|
2161
|
+
* left fully intact (no generation bump); also host-level, survives replays. */
|
|
2162
|
+
paused = false;
|
|
2163
|
+
/** Whether the chrome / body text are part of the CURRENT choice's layout
|
|
2164
|
+
* (false when a self-contained bubble panel owns the prompt) — remembered so
|
|
2165
|
+
* {@link applyVisibility} can recompute after a hide toggle mid-choice. */
|
|
2166
|
+
choiceShowsChrome = false;
|
|
2167
|
+
choiceShowsBody = false;
|
|
2168
|
+
/** Plain (speaker, text) of the line on screen — for the reveal-completed
|
|
2169
|
+
* event, which fires after `present` has discarded the resolved string. */
|
|
2170
|
+
currentLine;
|
|
2171
|
+
/** The full {@link PresentedLine} on screen — handed to an extra channel's
|
|
2172
|
+
* `revealComplete` (the session discards the local `line` after present). */
|
|
2173
|
+
currentPresented;
|
|
2174
|
+
/** Host-registered extra channels (Voice / Shop / CameraEffects / History).
|
|
2175
|
+
* The session fans its cross-cutting stream to these alongside the typed trio
|
|
2176
|
+
* and folds their `isRevealComplete()` into the auto-advance gate. */
|
|
2177
|
+
extras = [];
|
|
2178
|
+
/**
|
|
2179
|
+
* Begin a conversation. `play(script)` is **content-only** — the storage,
|
|
2180
|
+
* functions, and commands are installed on the session; `overrides` layers
|
|
2181
|
+
* per-conversation specifics on top (a scoped `storage`, extra `functions` /
|
|
2182
|
+
* `commands`). Declared defaults seed into the storage **only if absent**
|
|
2183
|
+
* (game-linked values win); variables persist across plays. Returns a
|
|
2184
|
+
* generation-stamped {@link DialogueHandle} for live `setVar` / `getVars`.
|
|
2185
|
+
*/
|
|
2186
|
+
play(rawScript, overrides) {
|
|
2187
|
+
const script = loadScript(rawScript);
|
|
2188
|
+
const analysis = analyzeScript(script);
|
|
2189
|
+
const storage = overrides?.storage ?? this.defaultStorage;
|
|
2190
|
+
const functions = { ...this.defaultFunctions, ...overrides?.functions };
|
|
2191
|
+
const commands = { ...this.defaultCommands, ...overrides?.commands };
|
|
2192
|
+
const fallbackCommand = overrides?.fallbackCommand ?? this.defaultFallback;
|
|
2193
|
+
validatePlay(analysis, { storage, functions, commands, fallbackCommand });
|
|
2194
|
+
for (const [name, value] of Object.entries(script.declare ?? {})) {
|
|
2195
|
+
if (storage.has(name)) continue;
|
|
2196
|
+
try {
|
|
2197
|
+
storage.set(name, value);
|
|
2198
|
+
} catch (cause) {
|
|
2199
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
2200
|
+
throw new DialoguePlayError(
|
|
2201
|
+
`cannot seed declared default "${name}": ${detail} (a read-only / pure cells() storage has no writable slot \u2014 compose it with a MemoryVariableStorage)`
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
this.stop();
|
|
2206
|
+
this.script = script;
|
|
2207
|
+
this.scriptId = script.id;
|
|
2208
|
+
this.storage = storage;
|
|
2209
|
+
this.functions = functions;
|
|
2210
|
+
this.commands = commands;
|
|
2211
|
+
this.fallbackCommand = fallbackCommand;
|
|
2212
|
+
const gen = this.generation;
|
|
2213
|
+
const live = /* @__PURE__ */ __name(() => gen === this.generation, "live");
|
|
2214
|
+
const guarded = {
|
|
2215
|
+
get: /* @__PURE__ */ __name((name) => storage.get(name), "get"),
|
|
2216
|
+
set: /* @__PURE__ */ __name((name, value) => {
|
|
2217
|
+
if (live()) storage.set(name, value);
|
|
2218
|
+
}, "set"),
|
|
2219
|
+
has: /* @__PURE__ */ __name((name) => storage.has(name), "has"),
|
|
2220
|
+
entries: /* @__PURE__ */ __name(() => storage.entries(), "entries")
|
|
2221
|
+
};
|
|
2222
|
+
const runner = new DialogueRunner(
|
|
2223
|
+
script,
|
|
2224
|
+
{ storage: guarded, functions, onError: this.opts.onError },
|
|
2225
|
+
{
|
|
2226
|
+
onSay: /* @__PURE__ */ __name((step, speaker) => {
|
|
2227
|
+
if (live()) this.handleSay(step, speaker);
|
|
2228
|
+
}, "onSay"),
|
|
2229
|
+
onChoice: /* @__PURE__ */ __name((step, choices, speaker) => {
|
|
2230
|
+
if (live()) this.handleChoice(step, choices, speaker);
|
|
2231
|
+
}, "onChoice"),
|
|
2232
|
+
onCommand: /* @__PURE__ */ __name((command, ctx) => live() ? this.handleCommand(command, ctx) : void 0, "onCommand"),
|
|
2233
|
+
onEnd: /* @__PURE__ */ __name(() => {
|
|
2234
|
+
if (live()) this.handleEnd();
|
|
2235
|
+
}, "onEnd")
|
|
2236
|
+
}
|
|
2237
|
+
);
|
|
2238
|
+
this.runner = runner;
|
|
2239
|
+
this.opts.onStarted?.({ scriptId: this.scriptId });
|
|
2240
|
+
runner.start();
|
|
2241
|
+
return {
|
|
2242
|
+
setVar: /* @__PURE__ */ __name((name, value) => guarded.set(name, value), "setVar"),
|
|
2243
|
+
getVars: /* @__PURE__ */ __name(() => gen === this.generation ? materialize(storage) : EMPTY_VARS, "getVars")
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
isActive() {
|
|
2247
|
+
return this.mode === "saying" || this.mode === "choosing";
|
|
2248
|
+
}
|
|
2249
|
+
isChoosing() {
|
|
2250
|
+
return this.mode === "choosing";
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Register an extra channel (Voice / Shop / CameraEffects / History) — the
|
|
2254
|
+
* open-ended companion to the built-in trio. It receives the cross-cutting
|
|
2255
|
+
* stream (`present` / `command` / `clear` / `setVisible` / `setPaused` /
|
|
2256
|
+
* `completeReveal` / `update`) and can gate auto-advance via
|
|
2257
|
+
* `isRevealComplete()`. Returns a disposer that unregisters **and** disposes
|
|
2258
|
+
* it. On register the channel catches up the current `setVisible` / `setPaused`
|
|
2259
|
+
* lever state ONLY — no content replay (replaying `present` would re-trigger a
|
|
2260
|
+
* voice clip). Safe to call mid-conversation.
|
|
2261
|
+
*/
|
|
2262
|
+
addChannel(ch) {
|
|
2263
|
+
this.extras.push(ch);
|
|
2264
|
+
try {
|
|
2265
|
+
ch.setVisible?.(!this.hidden);
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
this.opts.onError?.("dialogue: channel setVisible() failed", error);
|
|
2268
|
+
}
|
|
2269
|
+
try {
|
|
2270
|
+
ch.setPaused?.(this.paused);
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
this.opts.onError?.("dialogue: channel setPaused() failed", error);
|
|
2273
|
+
}
|
|
2274
|
+
let disposed = false;
|
|
2275
|
+
return () => {
|
|
2276
|
+
if (disposed) return;
|
|
2277
|
+
disposed = true;
|
|
2278
|
+
const i = this.extras.indexOf(ch);
|
|
2279
|
+
if (i >= 0) this.extras.splice(i, 1);
|
|
2280
|
+
try {
|
|
2281
|
+
ch.dispose?.();
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
this.opts.onError?.("dialogue: channel dispose() failed", error);
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
// ── lifecycle levers ──────────────────────────────────────────────────
|
|
2288
|
+
/**
|
|
2289
|
+
* Hide or show the whole dialogue UI — purely visual, state-preserving.
|
|
2290
|
+
* Drives every channel's `setVisible`; the conversation keeps running
|
|
2291
|
+
* underneath (reveal, timers, cursor intact). Host-level and **persistent**:
|
|
2292
|
+
* it survives `stop()` and the next `play()`, so a cutscene that hides and
|
|
2293
|
+
* forgets to unhide stays hidden — call `setHidden(false)` to restore.
|
|
2294
|
+
*/
|
|
2295
|
+
setHidden(hidden) {
|
|
2296
|
+
this.hidden = hidden;
|
|
2297
|
+
this.applyVisibility();
|
|
2298
|
+
}
|
|
2299
|
+
/** True while the UI is hidden via {@link setHidden}. */
|
|
2300
|
+
isHidden() {
|
|
2301
|
+
return this.hidden;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Freeze or resume the conversation — `update()` no-ops (reveal,
|
|
2305
|
+
* auto-advance clock, caret blink, avatar anim all freeze since they are
|
|
2306
|
+
* dt-driven) and the input-agnostic API (`advance`/`confirm`/`choose`/
|
|
2307
|
+
* `moveSelection`/`selectAt`/`skip`) no-ops. State is left fully intact: no
|
|
2308
|
+
* generation bump, and `lineBlocked`/`advancing`/`autoTimer` survive. It does
|
|
2309
|
+
* NOT block host-driven writes (`handle.setVar` / `ctx.setVar` / storage) —
|
|
2310
|
+
* only player-facing time + input freeze. Host-level and persistent like hide.
|
|
2311
|
+
*/
|
|
2312
|
+
setPaused(paused) {
|
|
2313
|
+
this.paused = paused;
|
|
2314
|
+
for (const ch of this.extras) {
|
|
2315
|
+
try {
|
|
2316
|
+
ch.setPaused?.(paused);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
this.opts.onError?.("dialogue: channel setPaused() failed", error);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
/** True while the conversation is frozen via {@link setPaused}. */
|
|
2323
|
+
isPaused() {
|
|
2324
|
+
return this.paused;
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Push the current desired visibility to every channel: a channel shows when
|
|
2328
|
+
* it has content for the current mode AND the host hasn't hidden the UI. The
|
|
2329
|
+
* single visibility authority — `setHidden` and every line/choice transition
|
|
2330
|
+
* route through here, so the host-hidden lever composes cleanly with per-line
|
|
2331
|
+
* content and a custom chrome (which may not implement the optional `present`)
|
|
2332
|
+
* is still reliably hidden by the explicit `setVisible(false)`.
|
|
2333
|
+
*/
|
|
2334
|
+
applyVisibility() {
|
|
2335
|
+
const shown = !this.hidden;
|
|
2336
|
+
const saying = this.mode === "saying";
|
|
2337
|
+
const choosing = this.mode === "choosing";
|
|
2338
|
+
this.channels.text.setVisible(shown && (saying || choosing && this.choiceShowsBody));
|
|
2339
|
+
this.channels.choices.setVisible(shown && choosing);
|
|
2340
|
+
this.channels.chrome?.setVisible(shown && (saying || choosing && this.choiceShowsChrome));
|
|
2341
|
+
this.channels.avatar?.setVisible?.(shown && (saying || choosing));
|
|
2342
|
+
for (const ch of this.extras) {
|
|
2343
|
+
try {
|
|
2344
|
+
ch.setVisible?.(shown);
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
this.opts.onError?.("dialogue: channel setVisible() failed", error);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
/** Clear every presentation channel's content — text, choices, chrome
|
|
2351
|
+
* (nameplate + caret + line), the avatar, and the registered extras. The
|
|
2352
|
+
* shared teardown for {@link stop} and the ended state; it touches no session
|
|
2353
|
+
* bookkeeping or visibility (the caller resets its own state, then
|
|
2354
|
+
* {@link goIdle} reasserts visibility). */
|
|
2355
|
+
clearAllChannels() {
|
|
2356
|
+
this.channels.text.clear();
|
|
2357
|
+
this.channels.choices.clear();
|
|
2358
|
+
this.channels.chrome?.setContinueVisible(false);
|
|
2359
|
+
this.channels.chrome?.setNameplate(void 0);
|
|
2360
|
+
this.channels.chrome?.present?.(void 0);
|
|
2361
|
+
this.channels.avatar?.setSpeaker(void 0);
|
|
2362
|
+
this.channels.avatar?.setSpeaking(false);
|
|
2363
|
+
this.channels.avatar?.present?.(void 0);
|
|
2364
|
+
for (const ch of this.extras) {
|
|
2365
|
+
try {
|
|
2366
|
+
ch.clear?.();
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
this.opts.onError?.("dialogue: channel clear() failed", error);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/** Drop to a quiescent presentation state (`mode` "idle" or "ended"): reset the
|
|
2373
|
+
* per-line/choice bookkeeping, clear every channel, and reassert visibility
|
|
2374
|
+
* (idle/ended → nothing shown, honestly via each channel's `setVisible`,
|
|
2375
|
+
* preserving the host-hidden lever). The caller owns any further reset —
|
|
2376
|
+
* {@link stop} also abandons the runner + timing latches. */
|
|
2377
|
+
goIdle(mode) {
|
|
2378
|
+
this.mode = mode;
|
|
2379
|
+
this.saying = void 0;
|
|
2380
|
+
this.resolved = [];
|
|
2381
|
+
this.confirming = false;
|
|
2382
|
+
this.currentLine = void 0;
|
|
2383
|
+
this.currentPresented = void 0;
|
|
2384
|
+
this.choiceShowsChrome = false;
|
|
2385
|
+
this.choiceShowsBody = false;
|
|
2386
|
+
this.clearAllChannels();
|
|
2387
|
+
this.applyVisibility();
|
|
2388
|
+
}
|
|
2389
|
+
/** Abandon the current conversation and reset to idle (clears all visuals).
|
|
2390
|
+
* Useful for ambient/eavesdrop dialogue that should stop when out of range. */
|
|
2391
|
+
stop() {
|
|
2392
|
+
this.generation++;
|
|
2393
|
+
this.runner = void 0;
|
|
2394
|
+
this.autoTimer = void 0;
|
|
2395
|
+
this.blockedCount = 0;
|
|
2396
|
+
this.advancing = false;
|
|
2397
|
+
this.advanceFired = false;
|
|
2398
|
+
this.afterRevealFired = false;
|
|
2399
|
+
this.goIdle("idle");
|
|
2400
|
+
}
|
|
2401
|
+
update(dt) {
|
|
2402
|
+
if (this.paused) return;
|
|
2403
|
+
this.channels.text.update(dt);
|
|
2404
|
+
this.channels.chrome?.update(dt);
|
|
2405
|
+
this.channels.avatar?.update(dt);
|
|
2406
|
+
for (const ch of this.extras) {
|
|
2407
|
+
try {
|
|
2408
|
+
ch.update?.(dt);
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
this.opts.onError?.("dialogue: channel update() failed", error);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
if (!this.runner || this.mode !== "saying") return;
|
|
2414
|
+
if (this.autoTimer !== void 0 && this.allRevealsComplete()) {
|
|
2415
|
+
this.autoTimer -= dt;
|
|
2416
|
+
if (this.autoTimer <= 0 && !this.lineBlocked && !this.advancing) {
|
|
2417
|
+
this.autoTimer = void 0;
|
|
2418
|
+
this.opts.onAutoAdvance?.({ scriptId: this.scriptId });
|
|
2419
|
+
this.advance();
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
/** True while any blocking line-command batch is awaited (input is gated). */
|
|
2424
|
+
get lineBlocked() {
|
|
2425
|
+
return this.blockedCount > 0;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* The auto-advance gate: the text reveal AND every registered extra channel
|
|
2429
|
+
* that *gates* (implements `isRevealComplete`) report complete. The clock is
|
|
2430
|
+
* armed on the text reveal alone (see `handleRevealComplete`/`setAutoAdvance`)
|
|
2431
|
+
* but only counts down once this is true — so a voice clip outlasting the
|
|
2432
|
+
* typewriter holds the line for `max(clipEnd, revealEnd)` with no duration
|
|
2433
|
+
* plumbing. A channel without the method never gates (a pure observer).
|
|
2434
|
+
*/
|
|
2435
|
+
allRevealsComplete() {
|
|
2436
|
+
if (!this.channels.text.isRevealComplete()) return false;
|
|
2437
|
+
for (const ch of this.extras) {
|
|
2438
|
+
if (ch.isRevealComplete && !ch.isRevealComplete()) return false;
|
|
2439
|
+
}
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* The storage read view for `{token}` interpolation at *this* present-time.
|
|
2444
|
+
* Materialized per evaluation so an earlier command's `set` shows up on a
|
|
2445
|
+
* later line; already-shown lines never re-render.
|
|
2446
|
+
*/
|
|
2447
|
+
readView() {
|
|
2448
|
+
return this.storage ? materialize(this.storage) : {};
|
|
2449
|
+
}
|
|
2450
|
+
// ── input-agnostic API ────────────────────────────────────────────────────
|
|
2451
|
+
/** Primary action. Saying → reveal-all if typing, else next line. Choosing → confirm. */
|
|
2452
|
+
advance() {
|
|
2453
|
+
if (this.paused) return;
|
|
2454
|
+
if (this.lineBlocked || this.advancing) return;
|
|
2455
|
+
if (this.mode === "saying") {
|
|
2456
|
+
if (this.channels.text.isRevealing()) {
|
|
2457
|
+
this.channels.text.completeReveal();
|
|
2458
|
+
for (const ch of this.extras) {
|
|
2459
|
+
try {
|
|
2460
|
+
ch.completeReveal?.();
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
this.opts.onError?.("dialogue: channel completeReveal() failed", error);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
} else void this.advanceLine();
|
|
2466
|
+
} else if (this.mode === "choosing") {
|
|
2467
|
+
this.confirm();
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
/** Fire any `advance`-timed line commands, then step the runner off the line.
|
|
2471
|
+
* `advancing` is held for the whole turn so a second advance can't re-fire
|
|
2472
|
+
* the (possibly non-blocking) `advance` commands before the runner steps. */
|
|
2473
|
+
async advanceLine() {
|
|
2474
|
+
const gen = this.generation;
|
|
2475
|
+
this.advancing = true;
|
|
2476
|
+
try {
|
|
2477
|
+
if (!this.advanceFired) {
|
|
2478
|
+
this.advanceFired = true;
|
|
2479
|
+
await this.fireLineCommands("advance");
|
|
2480
|
+
}
|
|
2481
|
+
if (gen !== this.generation || this.mode !== "saying") return;
|
|
2482
|
+
this.runner?.advance();
|
|
2483
|
+
} finally {
|
|
2484
|
+
if (gen === this.generation) this.advancing = false;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Fast-forward the current section: run intervening commands (in skip mode)
|
|
2489
|
+
* without presenting, stopping at the next choice or the end. No-op unless a
|
|
2490
|
+
* line is showing.
|
|
2491
|
+
*/
|
|
2492
|
+
skip() {
|
|
2493
|
+
if (this.paused) return;
|
|
2494
|
+
if (this.mode !== "saying" || this.lineBlocked || this.advancing) return;
|
|
2495
|
+
this.opts.onSkipUsed?.({ scriptId: this.scriptId });
|
|
2496
|
+
for (const ch of this.extras) {
|
|
2497
|
+
try {
|
|
2498
|
+
ch.completeReveal?.();
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
this.opts.onError?.("dialogue: channel completeReveal() failed", error);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
void this.skipLine();
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Fire the *displayed* line's not-yet-fired batches in skip mode — the runner
|
|
2507
|
+
* fires every skipped line's commands for world reconstruction, so dropping
|
|
2508
|
+
* the current line's `afterReveal`/`advance` batches would diverge from
|
|
2509
|
+
* normal play — then fast-forward the runner.
|
|
2510
|
+
*/
|
|
2511
|
+
async skipLine() {
|
|
2512
|
+
const gen = this.generation;
|
|
2513
|
+
this.advancing = true;
|
|
2514
|
+
try {
|
|
2515
|
+
if (!this.afterRevealFired) {
|
|
2516
|
+
this.afterRevealFired = true;
|
|
2517
|
+
await this.fireLineCommands("afterReveal", "skip");
|
|
2518
|
+
if (gen !== this.generation || this.mode !== "saying") return;
|
|
2519
|
+
}
|
|
2520
|
+
if (!this.advanceFired) {
|
|
2521
|
+
this.advanceFired = true;
|
|
2522
|
+
await this.fireLineCommands("advance", "skip");
|
|
2523
|
+
if (gen !== this.generation || this.mode !== "saying") return;
|
|
2524
|
+
}
|
|
2525
|
+
await this.runner?.skip();
|
|
2526
|
+
} finally {
|
|
2527
|
+
if (gen === this.generation) this.advancing = false;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Side-effect-free lookahead: the lines a node would show along its linear
|
|
2532
|
+
* path — following `goto` and conditional `command` jumps using the *current*
|
|
2533
|
+
* variable snapshot — stopping at the first choice or the end. Runs no
|
|
2534
|
+
* commands and mutates nothing. For a "skip with a summary" affordance.
|
|
2535
|
+
*/
|
|
2536
|
+
preview(nodeId, limit = 64) {
|
|
2537
|
+
const script = this.script;
|
|
2538
|
+
const storage = this.storage;
|
|
2539
|
+
if (!script || !storage) return [];
|
|
2540
|
+
const view = materialize(storage);
|
|
2541
|
+
const scope = createScope(storage, this.functions);
|
|
2542
|
+
const out = [];
|
|
2543
|
+
let node = nodeId;
|
|
2544
|
+
let i = 0;
|
|
2545
|
+
for (let guard = 0; guard < limit; guard++) {
|
|
2546
|
+
const step = script.nodes[node]?.steps[i];
|
|
2547
|
+
if (!step) break;
|
|
2548
|
+
if (step.kind === "say") {
|
|
2549
|
+
const speaker = step.speaker ? script.speakers?.[step.speaker] : void 0;
|
|
2550
|
+
const name = speaker ? this.i18n.t(speaker.nameKey, speaker.name, view) : void 0;
|
|
2551
|
+
out.push({
|
|
2552
|
+
speaker: name,
|
|
2553
|
+
text: stripMarkup(this.i18n.t(step.key, step.text, view))
|
|
2554
|
+
});
|
|
2555
|
+
i++;
|
|
2556
|
+
} else if (step.kind === "command") {
|
|
2557
|
+
if (step.target !== void 0 && holds(step.condition, scope)) {
|
|
2558
|
+
node = step.target;
|
|
2559
|
+
i = 0;
|
|
2560
|
+
} else {
|
|
2561
|
+
i++;
|
|
2562
|
+
}
|
|
2563
|
+
} else if (step.kind === "goto") {
|
|
2564
|
+
node = step.target;
|
|
2565
|
+
i = 0;
|
|
2566
|
+
} else {
|
|
2567
|
+
break;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
return out;
|
|
2571
|
+
}
|
|
2572
|
+
/** Move the choice cursor by `delta`, skipping disabled rows and wrapping.
|
|
2573
|
+
* No-op outside a choice, and a zero `delta` is a no-op (no cursor move, no
|
|
2574
|
+
* event). A move that steps over disabled rows fires exactly one
|
|
2575
|
+
* selection-changed event, for the row it lands on. */
|
|
2576
|
+
moveSelection(delta) {
|
|
2577
|
+
if (this.paused) return;
|
|
2578
|
+
if (this.mode !== "choosing" || this.confirming) return;
|
|
2579
|
+
if (this.resolved.length === 0 || delta === 0) return;
|
|
2580
|
+
const dir = delta < 0 ? -1 : 1;
|
|
2581
|
+
let pos = this.selected;
|
|
2582
|
+
for (let i = 0; i < Math.abs(delta); i++) {
|
|
2583
|
+
pos = this.nextEnabled(pos, dir);
|
|
2584
|
+
}
|
|
2585
|
+
if (pos === this.selected) return;
|
|
2586
|
+
this.selected = pos;
|
|
2587
|
+
this.channels.choices.highlight(this.selected);
|
|
2588
|
+
this.emitSelectionChanged();
|
|
2589
|
+
}
|
|
2590
|
+
/** Highlight a choice by absolute position (e.g. pointer hover). No wrap;
|
|
2591
|
+
* a disabled row is skipped (the cursor stays put). */
|
|
2592
|
+
selectAt(position) {
|
|
2593
|
+
if (this.paused) return;
|
|
2594
|
+
if (this.mode !== "choosing" || this.confirming) return;
|
|
2595
|
+
const n = this.resolved.length;
|
|
2596
|
+
if (n === 0 || position < 0 || position >= n || position === this.selected)
|
|
2597
|
+
return;
|
|
2598
|
+
if (this.resolved[position]?.disabled) return;
|
|
2599
|
+
this.selected = position;
|
|
2600
|
+
this.channels.choices.highlight(this.selected);
|
|
2601
|
+
this.emitSelectionChanged();
|
|
2602
|
+
}
|
|
2603
|
+
/** The next enabled choice position from `from` in direction `dir` (±1),
|
|
2604
|
+
* wrapping. Returns `from` when no other enabled row exists (so a single
|
|
2605
|
+
* enabled option among disabled ones never moves). */
|
|
2606
|
+
nextEnabled(from, dir) {
|
|
2607
|
+
const n = this.resolved.length;
|
|
2608
|
+
let pos = from;
|
|
2609
|
+
for (let i = 0; i < n; i++) {
|
|
2610
|
+
pos = (pos + dir + n) % n;
|
|
2611
|
+
if (pos === from) break;
|
|
2612
|
+
if (!this.resolved[pos]?.disabled) return pos;
|
|
2613
|
+
}
|
|
2614
|
+
return from;
|
|
2615
|
+
}
|
|
2616
|
+
/** Fire onSelectionChanged for the currently-highlighted choice (keyboard nav
|
|
2617
|
+
* and pointer hover both land here — one canonical selection event). */
|
|
2618
|
+
emitSelectionChanged() {
|
|
2619
|
+
const chosen = this.resolved[this.selected];
|
|
2620
|
+
if (!chosen) return;
|
|
2621
|
+
this.opts.onSelectionChanged?.({
|
|
2622
|
+
index: chosen.index,
|
|
2623
|
+
text: stripMarkup(
|
|
2624
|
+
this.i18n.t(chosen.option.key, chosen.option.text, this.readView())
|
|
2625
|
+
)
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
/** Commit the highlighted choice. */
|
|
2629
|
+
confirm() {
|
|
2630
|
+
this.commit(this.selected);
|
|
2631
|
+
}
|
|
2632
|
+
/** Commit by original option index (e.g. a direct pointer hit, or the
|
|
2633
|
+
* timed-choice recipe firing its default). Refuses an unknown or disabled
|
|
2634
|
+
* option. */
|
|
2635
|
+
choose(optionIndex) {
|
|
2636
|
+
this.commit(this.resolved.findIndex((c) => c.index === optionIndex));
|
|
2637
|
+
}
|
|
2638
|
+
/** Commit by display position (a pointer hit on a row). The position-keyed
|
|
2639
|
+
* counterpart to {@link choose}; refuses an out-of-range or disabled row.
|
|
2640
|
+
* Used by the pointer binding and the presenter pointer-commit seam. */
|
|
2641
|
+
confirmAt(position) {
|
|
2642
|
+
this.commit(position);
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* The single choice-commit authority: every commit path routes here after
|
|
2646
|
+
* translating its argument to a display position. It guards (paused / not
|
|
2647
|
+
* choosing / already confirming / a missing or disabled row), latches against a
|
|
2648
|
+
* double-commit, fires `onChoiceMade`, and steps the runner. The latch (not the
|
|
2649
|
+
* runner) is what guarantees a single commit: `mode` stays "choosing" while the
|
|
2650
|
+
* runner awaits the option's blocking commands, so without it a second confirm
|
|
2651
|
+
* would emit a duplicate `onChoiceMade`.
|
|
2652
|
+
*/
|
|
2653
|
+
commit(position) {
|
|
2654
|
+
if (this.paused) return;
|
|
2655
|
+
if (this.mode !== "choosing" || this.confirming) return;
|
|
2656
|
+
const chosen = this.resolved[position];
|
|
2657
|
+
if (!chosen || chosen.disabled) return;
|
|
2658
|
+
this.selected = position;
|
|
2659
|
+
this.confirming = true;
|
|
2660
|
+
const text = this.i18n.t(chosen.option.key, chosen.option.text, this.readView());
|
|
2661
|
+
this.opts.onChoiceMade?.({ index: chosen.index, text });
|
|
2662
|
+
this.runner?.choose(chosen.index);
|
|
2663
|
+
}
|
|
2664
|
+
/** Toggle hold-to-fast-forward; the text channel scales its reveal rate. */
|
|
2665
|
+
setFastForward(on) {
|
|
2666
|
+
this.channels.text.setSpeedMultiplier(on ? this.skipMul : 1);
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Default auto-advance: lines without their own `autoAdvanceMs` advance `ms`
|
|
2670
|
+
* after they finish revealing; `null` turns it off (manual advance). A
|
|
2671
|
+
* per-line `autoAdvanceMs` always overrides this. Toggling it while a line is
|
|
2672
|
+
* already sitting revealed arms/clears its timer immediately.
|
|
2673
|
+
*/
|
|
2674
|
+
setAutoAdvance(ms) {
|
|
2675
|
+
this.autoAdvanceDefault = ms;
|
|
2676
|
+
if (this.mode === "saying" && this.saying?.autoAdvanceMs === void 0 && this.channels.text.isRevealComplete()) {
|
|
2677
|
+
this.autoTimer = ms ?? void 0;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
// ── runner handlers ─────────────────────────────────────────────────────
|
|
2681
|
+
handleSay(step, speaker) {
|
|
2682
|
+
this.mode = "saying";
|
|
2683
|
+
this.saying = step;
|
|
2684
|
+
this.autoTimer = void 0;
|
|
2685
|
+
this.advanceFired = false;
|
|
2686
|
+
this.afterRevealFired = false;
|
|
2687
|
+
this.confirming = false;
|
|
2688
|
+
const view = this.readView();
|
|
2689
|
+
const resolved = this.i18n.t(step.key, step.text, view);
|
|
2690
|
+
const line = {
|
|
2691
|
+
speaker: this.speakerView(speaker, view),
|
|
2692
|
+
text: parseMarkup(resolved),
|
|
2693
|
+
speed: step.speed ?? 1,
|
|
2694
|
+
view: step.view,
|
|
2695
|
+
meta: step.meta,
|
|
2696
|
+
voice: step.voice
|
|
2697
|
+
};
|
|
2698
|
+
this.channels.choices.clear();
|
|
2699
|
+
this.channels.chrome?.setContinueVisible(false);
|
|
2700
|
+
this.channels.chrome?.setNameplate(
|
|
2701
|
+
this.speakerName(speaker, view),
|
|
2702
|
+
speaker?.color
|
|
2703
|
+
);
|
|
2704
|
+
this.channels.avatar?.setSpeaker(speaker);
|
|
2705
|
+
this.channels.avatar?.setExpression(step.expression);
|
|
2706
|
+
this.channels.avatar?.setSpeaking(true);
|
|
2707
|
+
this.channels.avatar?.present?.(line);
|
|
2708
|
+
this.channels.chrome?.present?.(line);
|
|
2709
|
+
this.channels.text.present(line);
|
|
2710
|
+
this.currentPresented = line;
|
|
2711
|
+
for (const ch of this.extras) {
|
|
2712
|
+
try {
|
|
2713
|
+
ch.present?.(line);
|
|
2714
|
+
} catch (error) {
|
|
2715
|
+
this.opts.onError?.("dialogue: channel present() failed", error);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
const plain = stripMarkup(resolved);
|
|
2719
|
+
this.currentLine = { speaker: this.speakerName(speaker, view), text: plain };
|
|
2720
|
+
this.opts.onLine?.({ speaker: this.currentLine.speaker, text: plain });
|
|
2721
|
+
this.applyVisibility();
|
|
2722
|
+
void this.fireLineCommands("show");
|
|
2723
|
+
}
|
|
2724
|
+
handleChoice(step, choices, speaker) {
|
|
2725
|
+
this.mode = "choosing";
|
|
2726
|
+
this.resolved = choices;
|
|
2727
|
+
const firstEnabled = choices.findIndex((c) => !c.disabled);
|
|
2728
|
+
if (firstEnabled < 0) {
|
|
2729
|
+
throw new Error("dialogue: choice step presented with no enabled option");
|
|
2730
|
+
}
|
|
2731
|
+
this.selected = firstEnabled;
|
|
2732
|
+
this.confirming = false;
|
|
2733
|
+
const view = this.readView();
|
|
2734
|
+
const line = {
|
|
2735
|
+
speaker: this.speakerView(speaker, view),
|
|
2736
|
+
text: step.text ? parseMarkup(this.i18n.t(step.key, step.text, view)) : EMPTY_PARSED,
|
|
2737
|
+
speed: 1,
|
|
2738
|
+
view: step.view,
|
|
2739
|
+
meta: step.meta
|
|
2740
|
+
};
|
|
2741
|
+
this.channels.avatar?.setSpeaker(speaker);
|
|
2742
|
+
this.channels.avatar?.setExpression(void 0);
|
|
2743
|
+
this.channels.avatar?.setSpeaking(false);
|
|
2744
|
+
this.channels.avatar?.present?.(line);
|
|
2745
|
+
const ctx = {
|
|
2746
|
+
view: step.view,
|
|
2747
|
+
speaker: line.speaker,
|
|
2748
|
+
prompt: line.text,
|
|
2749
|
+
meta: step.meta
|
|
2750
|
+
};
|
|
2751
|
+
const presenterOwnsPrompt = this.channels.choices.ownsPrompt?.(ctx) ?? false;
|
|
2752
|
+
this.choiceShowsChrome = !presenterOwnsPrompt;
|
|
2753
|
+
this.choiceShowsBody = !presenterOwnsPrompt && Boolean(step.text);
|
|
2754
|
+
this.channels.chrome?.setContinueVisible(false);
|
|
2755
|
+
if (presenterOwnsPrompt) {
|
|
2756
|
+
this.channels.chrome?.present?.(void 0);
|
|
2757
|
+
this.channels.text.clear();
|
|
2758
|
+
} else {
|
|
2759
|
+
this.channels.chrome?.setNameplate(
|
|
2760
|
+
this.speakerName(speaker, view),
|
|
2761
|
+
speaker?.color
|
|
2762
|
+
);
|
|
2763
|
+
this.channels.chrome?.present?.(line);
|
|
2764
|
+
if (step.text) this.channels.text.present(line);
|
|
2765
|
+
else this.channels.text.clear();
|
|
2766
|
+
}
|
|
2767
|
+
const labels = choices.map(
|
|
2768
|
+
(c) => stripMarkup(this.i18n.t(c.option.key, c.option.text, view))
|
|
2769
|
+
);
|
|
2770
|
+
const presented = choices.map((c, i) => ({
|
|
2771
|
+
label: labels[i],
|
|
2772
|
+
meta: c.option.meta,
|
|
2773
|
+
disabled: c.disabled,
|
|
2774
|
+
// i18n-resolve the reason (interpolating {tokens}) only for disabled rows
|
|
2775
|
+
// that carry one; there's no separate i18n key for it.
|
|
2776
|
+
disabledReason: c.disabled && c.option.disabledReason !== void 0 ? stripMarkup(this.i18n.t(void 0, c.option.disabledReason, view)) : void 0
|
|
2777
|
+
}));
|
|
2778
|
+
this.channels.choices.present(presented, ctx);
|
|
2779
|
+
this.channels.choices.highlight(this.selected);
|
|
2780
|
+
this.applyVisibility();
|
|
2781
|
+
this.opts.onChoiceShown?.({ options: labels });
|
|
2782
|
+
}
|
|
2783
|
+
handleCommand(command, ctx) {
|
|
2784
|
+
this.opts.onCommand?.(command, ctx);
|
|
2785
|
+
for (const ch of this.extras) {
|
|
2786
|
+
try {
|
|
2787
|
+
ch.command?.(command, ctx);
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
this.opts.onError?.("dialogue: channel command() failed", error);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
const handler = this.commands[command.type] ?? this.fallbackCommand;
|
|
2793
|
+
return handler?.(command, ctx);
|
|
2794
|
+
}
|
|
2795
|
+
handleEnd() {
|
|
2796
|
+
this.goIdle("ended");
|
|
2797
|
+
this.opts.onEnded?.({ scriptId: this.scriptId });
|
|
2798
|
+
}
|
|
2799
|
+
async handleRevealComplete() {
|
|
2800
|
+
if (this.mode !== "saying") return;
|
|
2801
|
+
const gen = this.generation;
|
|
2802
|
+
this.channels.avatar?.setSpeaking(false);
|
|
2803
|
+
if (!this.afterRevealFired) {
|
|
2804
|
+
this.afterRevealFired = true;
|
|
2805
|
+
await this.fireLineCommands("afterReveal");
|
|
2806
|
+
}
|
|
2807
|
+
if (gen !== this.generation || this.mode !== "saying") return;
|
|
2808
|
+
this.channels.chrome?.setContinueVisible(true);
|
|
2809
|
+
if (this.currentLine) this.opts.onRevealCompleted?.(this.currentLine);
|
|
2810
|
+
const presented = this.currentPresented;
|
|
2811
|
+
if (presented) {
|
|
2812
|
+
for (const ch of this.extras) {
|
|
2813
|
+
try {
|
|
2814
|
+
ch.revealComplete?.(presented);
|
|
2815
|
+
} catch (error) {
|
|
2816
|
+
this.opts.onError?.("dialogue: channel revealComplete() failed", error);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
const auto = this.saying?.autoAdvanceMs ?? this.autoAdvanceDefault;
|
|
2821
|
+
if (auto !== null) this.autoTimer = auto;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Fan one reveal beat out — the per-line typewriter stream. Extras (Voice /
|
|
2825
|
+
* CameraEffects / a typewriter-SFX channel) see the WHOLE stream via
|
|
2826
|
+
* `revealBeat?`. A `tick` then reaches the host's `onRevealTick` callback; a
|
|
2827
|
+
* `marker` reaches the avatar channel (which interprets `[expression=…/]`
|
|
2828
|
+
* itself — the Session name-matches nothing) and the host's `onRevealMarker`.
|
|
2829
|
+
*/
|
|
2830
|
+
handleRevealBeat(beat) {
|
|
2831
|
+
for (const ch of this.extras) {
|
|
2832
|
+
try {
|
|
2833
|
+
ch.revealBeat?.(beat);
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
this.opts.onError?.("dialogue: channel revealBeat() failed", error);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
if (beat.kind === "tick") {
|
|
2839
|
+
this.opts.onRevealTick?.(beat.index);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
this.channels.avatar?.marker?.(beat.marker);
|
|
2843
|
+
this.opts.onRevealMarker?.(beat.marker, beat.viaSkip);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Fire the current line's commands matching `at`, via the runner's command
|
|
2847
|
+
* pipeline (so `set`/blocking behave identically). While a blocking one is
|
|
2848
|
+
* awaited, `lineBlocked` gates input so the player can't advance through it.
|
|
2849
|
+
* `mode` overrides the runner's run mode (skip() fires the displayed line's
|
|
2850
|
+
* batches in skip mode).
|
|
2851
|
+
*/
|
|
2852
|
+
async fireLineCommands(at, mode) {
|
|
2853
|
+
const all = this.saying?.commands;
|
|
2854
|
+
if (!all || !this.runner) return;
|
|
2855
|
+
const batch = all.filter((c) => (c.at ?? "show") === at);
|
|
2856
|
+
if (batch.length === 0) return;
|
|
2857
|
+
const gen = this.generation;
|
|
2858
|
+
const blocking = batch.some((c) => c.blocking);
|
|
2859
|
+
if (blocking) this.blockedCount++;
|
|
2860
|
+
try {
|
|
2861
|
+
await this.runner.runCommands(batch, mode);
|
|
2862
|
+
} finally {
|
|
2863
|
+
if (blocking && gen === this.generation) this.blockedCount--;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
speakerName(speaker, view) {
|
|
2867
|
+
if (!speaker) return void 0;
|
|
2868
|
+
return this.i18n.t(speaker.nameKey, speaker.name, view);
|
|
2869
|
+
}
|
|
2870
|
+
speakerView(speaker, view) {
|
|
2871
|
+
if (!speaker) return void 0;
|
|
2872
|
+
return {
|
|
2873
|
+
id: speaker.id,
|
|
2874
|
+
name: this.speakerName(speaker, view),
|
|
2875
|
+
color: speaker.color
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
// src/core/channels/voice.ts
|
|
2881
|
+
function createVoiceChannel(opts) {
|
|
2882
|
+
const { play, onSkip = "cut", livenessMs, onError } = opts;
|
|
2883
|
+
const pauseClip = opts.pauseWithConversation ?? true;
|
|
2884
|
+
let active;
|
|
2885
|
+
let done = true;
|
|
2886
|
+
let startToken = 0;
|
|
2887
|
+
let elapsed = 0;
|
|
2888
|
+
const stop = /* @__PURE__ */ __name(() => {
|
|
2889
|
+
active?.stop();
|
|
2890
|
+
active = void 0;
|
|
2891
|
+
done = true;
|
|
2892
|
+
}, "stop");
|
|
2893
|
+
return {
|
|
2894
|
+
present(line) {
|
|
2895
|
+
active?.stop();
|
|
2896
|
+
active = void 0;
|
|
2897
|
+
startToken++;
|
|
2898
|
+
elapsed = 0;
|
|
2899
|
+
const id = line.voice;
|
|
2900
|
+
if (id === void 0) {
|
|
2901
|
+
done = true;
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
done = false;
|
|
2905
|
+
const token = startToken;
|
|
2906
|
+
try {
|
|
2907
|
+
active = play(id, () => {
|
|
2908
|
+
if (token !== startToken) return;
|
|
2909
|
+
done = true;
|
|
2910
|
+
active = void 0;
|
|
2911
|
+
});
|
|
2912
|
+
} catch (error) {
|
|
2913
|
+
done = true;
|
|
2914
|
+
throw error;
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
completeReveal() {
|
|
2918
|
+
if (onSkip === "ring") return;
|
|
2919
|
+
stop();
|
|
2920
|
+
},
|
|
2921
|
+
setPaused(paused) {
|
|
2922
|
+
if (!pauseClip) return;
|
|
2923
|
+
if (paused) active?.pause?.();
|
|
2924
|
+
else active?.resume?.();
|
|
2925
|
+
},
|
|
2926
|
+
update(dt) {
|
|
2927
|
+
if (done || !livenessMs) return;
|
|
2928
|
+
elapsed += dt;
|
|
2929
|
+
if (elapsed >= livenessMs) {
|
|
2930
|
+
done = true;
|
|
2931
|
+
onError?.(
|
|
2932
|
+
`voice clip exceeded the ${livenessMs}ms liveness budget without reporting its end; releasing the auto-advance gate`,
|
|
2933
|
+
new Error("voice clip liveness cap")
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
},
|
|
2937
|
+
clear() {
|
|
2938
|
+
stop();
|
|
2939
|
+
},
|
|
2940
|
+
dispose() {
|
|
2941
|
+
stop();
|
|
2942
|
+
},
|
|
2943
|
+
isRevealComplete() {
|
|
2944
|
+
return done;
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
__name(createVoiceChannel, "createVoiceChannel");
|
|
2949
|
+
|
|
2950
|
+
// src/DialogueController.ts
|
|
2951
|
+
var import_core2 = require("@yagejs/core");
|
|
2952
|
+
var import_input = require("@yagejs/input");
|
|
2953
|
+
|
|
2954
|
+
// src/input/InputBinding.ts
|
|
2955
|
+
var DEFAULT_ACTIONS = {
|
|
2956
|
+
advance: ["interact"],
|
|
2957
|
+
speed: ["attack"],
|
|
2958
|
+
up: ["move-up"],
|
|
2959
|
+
down: ["move-down"]
|
|
2960
|
+
};
|
|
2961
|
+
var FULL_ACTIONS = {
|
|
2962
|
+
...DEFAULT_ACTIONS,
|
|
2963
|
+
skip: ["skip"]
|
|
2964
|
+
};
|
|
2965
|
+
var CompositeInputBinding = class {
|
|
2966
|
+
constructor(bindings) {
|
|
2967
|
+
this.bindings = bindings;
|
|
2968
|
+
}
|
|
2969
|
+
bindings;
|
|
2970
|
+
static {
|
|
2971
|
+
__name(this, "CompositeInputBinding");
|
|
2972
|
+
}
|
|
2973
|
+
bind(input, session) {
|
|
2974
|
+
for (const b of this.bindings) b.bind(input, session);
|
|
2975
|
+
}
|
|
2976
|
+
poll() {
|
|
2977
|
+
for (const b of this.bindings) b.poll();
|
|
2978
|
+
}
|
|
2979
|
+
dispose() {
|
|
2980
|
+
for (const b of this.bindings) b.dispose?.();
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
var KeyboardInputBinding = class {
|
|
2984
|
+
/**
|
|
2985
|
+
* @param skipHoldMs Hold the `skip` action this long before it fires (the
|
|
2986
|
+
* classic "hold to skip" confirm). `0` (default) fires on press.
|
|
2987
|
+
*/
|
|
2988
|
+
constructor(actions = DEFAULT_ACTIONS, skipHoldMs = 0) {
|
|
2989
|
+
this.actions = actions;
|
|
2990
|
+
this.skipHoldMs = skipHoldMs;
|
|
2991
|
+
}
|
|
2992
|
+
actions;
|
|
2993
|
+
skipHoldMs;
|
|
2994
|
+
static {
|
|
2995
|
+
__name(this, "KeyboardInputBinding");
|
|
2996
|
+
}
|
|
2997
|
+
input;
|
|
2998
|
+
session;
|
|
2999
|
+
/** Latch so a held skip fires once per hold, not every frame past threshold. */
|
|
3000
|
+
skipFired = false;
|
|
3001
|
+
bind(input, session) {
|
|
3002
|
+
this.input = input;
|
|
3003
|
+
this.session = session;
|
|
3004
|
+
}
|
|
3005
|
+
poll() {
|
|
3006
|
+
const { input, session } = this;
|
|
3007
|
+
if (!input || !session) return;
|
|
3008
|
+
session.setFastForward(held(input, this.actions.speed));
|
|
3009
|
+
this.pollSkip(input, session);
|
|
3010
|
+
if (justPressed(input, this.actions.advance)) session.advance();
|
|
3011
|
+
if (justPressed(input, this.actions.up)) session.moveSelection(-1);
|
|
3012
|
+
else if (justPressed(input, this.actions.down)) session.moveSelection(1);
|
|
3013
|
+
}
|
|
3014
|
+
/** Fire skip once the action has been held `skipHoldMs` (hold-to-confirm),
|
|
3015
|
+
* re-arming only after it's released. */
|
|
3016
|
+
pollSkip(input, session) {
|
|
3017
|
+
const skip = this.actions.skip;
|
|
3018
|
+
if (!skip) return;
|
|
3019
|
+
const ready = skip.some(
|
|
3020
|
+
(a) => input.isPressed(a) && input.isHeldFor(a, this.skipHoldMs)
|
|
3021
|
+
);
|
|
3022
|
+
if (ready) {
|
|
3023
|
+
if (!this.skipFired) {
|
|
3024
|
+
session.skip();
|
|
3025
|
+
this.skipFired = true;
|
|
3026
|
+
}
|
|
3027
|
+
} else if (!held(input, skip)) {
|
|
3028
|
+
this.skipFired = false;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
function justPressed(input, actions) {
|
|
3033
|
+
return actions.some((a) => input.isJustPressed(a));
|
|
3034
|
+
}
|
|
3035
|
+
__name(justPressed, "justPressed");
|
|
3036
|
+
function held(input, actions) {
|
|
3037
|
+
return actions.some((a) => input.isPressed(a));
|
|
3038
|
+
}
|
|
3039
|
+
__name(held, "held");
|
|
3040
|
+
var PointerInputBinding = class {
|
|
3041
|
+
static {
|
|
3042
|
+
__name(this, "PointerInputBinding");
|
|
3043
|
+
}
|
|
3044
|
+
input;
|
|
3045
|
+
session;
|
|
3046
|
+
// Explicit `| undefined` so `dispose()` can null it (exactOptionalPropertyTypes).
|
|
3047
|
+
unsub;
|
|
3048
|
+
/** A primary-button press happened since the last poll (consumed in poll). */
|
|
3049
|
+
clicked = false;
|
|
3050
|
+
// Explicit `| undefined` (not `?:`) so the ctor can assign the possibly-
|
|
3051
|
+
// undefined argument under `exactOptionalPropertyTypes`.
|
|
3052
|
+
choices;
|
|
3053
|
+
/** Pointer position at the last hover hit-test, so an unmoved pointer
|
|
3054
|
+
* doesn't re-run the hit-test every frame. */
|
|
3055
|
+
lastX = Number.NaN;
|
|
3056
|
+
lastY = Number.NaN;
|
|
3057
|
+
/** Whether the previous poll saw a choice up (a fresh choice set must be
|
|
3058
|
+
* hit-tested even under a stationary pointer). */
|
|
3059
|
+
wasChoosing = false;
|
|
3060
|
+
constructor(choices) {
|
|
3061
|
+
this.choices = choices;
|
|
3062
|
+
}
|
|
3063
|
+
bind(input, session) {
|
|
3064
|
+
this.unsub?.();
|
|
3065
|
+
this.input = input;
|
|
3066
|
+
this.session = session;
|
|
3067
|
+
this.unsub = input.onPointerDown((info) => {
|
|
3068
|
+
if (info.button === 0) this.clicked = true;
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
/** Pointer position in the choice presenter's coordinate space. */
|
|
3072
|
+
pointer(input) {
|
|
3073
|
+
return this.choices?.pointerSpace === "world" ? input.getPointerPosition() : input.getPointerScreenPosition();
|
|
3074
|
+
}
|
|
3075
|
+
poll() {
|
|
3076
|
+
const { input, session } = this;
|
|
3077
|
+
if (!input || !session) return;
|
|
3078
|
+
const choosing = session.isChoosing();
|
|
3079
|
+
if (choosing && this.choices?.choiceAtPoint) {
|
|
3080
|
+
const p = this.pointer(input);
|
|
3081
|
+
if (!this.wasChoosing || p.x !== this.lastX || p.y !== this.lastY) {
|
|
3082
|
+
this.lastX = p.x;
|
|
3083
|
+
this.lastY = p.y;
|
|
3084
|
+
const hovered = this.choices.choiceAtPoint(p.x, p.y);
|
|
3085
|
+
if (hovered !== void 0) session.selectAt(hovered);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
this.wasChoosing = choosing;
|
|
3089
|
+
const clicked = this.clicked;
|
|
3090
|
+
this.clicked = false;
|
|
3091
|
+
if (!clicked) return;
|
|
3092
|
+
if (choosing) {
|
|
3093
|
+
const p = this.pointer(input);
|
|
3094
|
+
const hit = this.choices?.choiceAtPoint?.(p.x, p.y);
|
|
3095
|
+
if (hit !== void 0) session.confirmAt(hit);
|
|
3096
|
+
} else {
|
|
3097
|
+
session.advance();
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
dispose() {
|
|
3101
|
+
this.unsub?.();
|
|
3102
|
+
this.unsub = void 0;
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
function fullControls(choices, options = {}) {
|
|
3106
|
+
const { actions = FULL_ACTIONS, skipHoldMs = 0 } = options;
|
|
3107
|
+
return new CompositeInputBinding([
|
|
3108
|
+
new KeyboardInputBinding(actions, skipHoldMs),
|
|
3109
|
+
new PointerInputBinding(choices)
|
|
3110
|
+
]);
|
|
3111
|
+
}
|
|
3112
|
+
__name(fullControls, "fullControls");
|
|
3113
|
+
|
|
3114
|
+
// src/events.ts
|
|
3115
|
+
var import_core = require("@yagejs/core");
|
|
3116
|
+
var DialogueStartedEvent = (0, import_core.defineEvent)("dialogue:started");
|
|
3117
|
+
var DialogueLineEvent = (0, import_core.defineEvent)("dialogue:line");
|
|
3118
|
+
var DialogueChoiceShownEvent = (0, import_core.defineEvent)(
|
|
3119
|
+
"dialogue:choice-shown"
|
|
3120
|
+
);
|
|
3121
|
+
var DialogueChoiceMadeEvent = (0, import_core.defineEvent)(
|
|
3122
|
+
"dialogue:choice-made"
|
|
3123
|
+
);
|
|
3124
|
+
var DialogueCommandEvent = (0, import_core.defineEvent)(
|
|
3125
|
+
"dialogue:command"
|
|
3126
|
+
);
|
|
3127
|
+
var DialogueEndedEvent = (0, import_core.defineEvent)("dialogue:ended");
|
|
3128
|
+
var DialogueRevealCompletedEvent = (0, import_core.defineEvent)("dialogue:reveal-completed");
|
|
3129
|
+
var DialogueSelectionChangedEvent = (0, import_core.defineEvent)(
|
|
3130
|
+
"dialogue:selection-changed"
|
|
3131
|
+
);
|
|
3132
|
+
var DialogueSkipUsedEvent = (0, import_core.defineEvent)(
|
|
3133
|
+
"dialogue:skip-used"
|
|
3134
|
+
);
|
|
3135
|
+
var DialogueAutoAdvanceEvent = (0, import_core.defineEvent)(
|
|
3136
|
+
"dialogue:auto-advance"
|
|
3137
|
+
);
|
|
3138
|
+
var DialogueRevealMarkerEvent = (0, import_core.defineEvent)("dialogue:reveal-marker");
|
|
3139
|
+
|
|
3140
|
+
// src/DialogueController.ts
|
|
3141
|
+
var DialogueController = class extends import_core2.Component {
|
|
3142
|
+
constructor(opts) {
|
|
3143
|
+
super();
|
|
3144
|
+
this.opts = opts;
|
|
3145
|
+
this.binding = opts.input ?? new KeyboardInputBinding();
|
|
3146
|
+
}
|
|
3147
|
+
opts;
|
|
3148
|
+
static {
|
|
3149
|
+
__name(this, "DialogueController");
|
|
3150
|
+
}
|
|
3151
|
+
input = this.service(import_input.InputManagerKey);
|
|
3152
|
+
binding;
|
|
3153
|
+
session;
|
|
3154
|
+
/** Captured at onAdd (the scene is gone by the time a stale play() arrives). */
|
|
3155
|
+
logger;
|
|
3156
|
+
/** Set by onDestroy — the presenters are disposed, so play() must refuse. */
|
|
3157
|
+
destroyed = false;
|
|
3158
|
+
/** Input focus. When false, `update()` keeps pumping the session (an
|
|
3159
|
+
* ambient conversation stays alive) but the binding is NOT polled, so this
|
|
3160
|
+
* instance doesn't consume device input. NOT `Component.enabled` (which would
|
|
3161
|
+
* also freeze the session). */
|
|
3162
|
+
inputEnabled = true;
|
|
3163
|
+
/** Pause. Mirrors the session's pause so the binding poll is also gated
|
|
3164
|
+
* while frozen — a paused conversation neither updates nor consumes input.
|
|
3165
|
+
* Also the source of truth re-applied to the session in `onAdd` when a host
|
|
3166
|
+
* set it before the component was added (the session didn't exist yet). */
|
|
3167
|
+
paused = false;
|
|
3168
|
+
/** Hidden. Mirrors the session's hide so a `setHidden` issued before the
|
|
3169
|
+
* component was added isn't lost — it's re-applied once the session exists. */
|
|
3170
|
+
hidden = false;
|
|
3171
|
+
/** Disposers for every registered extra channel (ctor `channels` + live
|
|
3172
|
+
* `addChannel`). `onDestroy` runs them all — each idempotent — to unregister
|
|
3173
|
+
* and dispose (unmounting the Mountable ones). */
|
|
3174
|
+
channelDisposers = /* @__PURE__ */ new Set();
|
|
3175
|
+
onAdd() {
|
|
3176
|
+
this.logger = this.context.tryResolve(import_core2.LoggerKey);
|
|
3177
|
+
const warn = /* @__PURE__ */ __name((message) => this.logger?.warn("dialogue", message), "warn");
|
|
3178
|
+
this.opts.chrome.mount(this.scene);
|
|
3179
|
+
this.opts.choices.mount(this.scene);
|
|
3180
|
+
this.opts.avatar?.mount(this.scene);
|
|
3181
|
+
this.opts.text.mount(this.scene);
|
|
3182
|
+
this.opts.chrome.setDiagnostics?.(warn);
|
|
3183
|
+
this.opts.text.setDiagnostics?.(warn);
|
|
3184
|
+
this.opts.choices.setDiagnostics?.(warn);
|
|
3185
|
+
this.session = new DialogueSession(
|
|
3186
|
+
{
|
|
3187
|
+
text: this.opts.text,
|
|
3188
|
+
choices: this.opts.choices,
|
|
3189
|
+
avatar: this.opts.avatar,
|
|
3190
|
+
chrome: this.opts.chrome
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
i18n: this.opts.i18n,
|
|
3194
|
+
skipMultiplier: this.opts.skipMultiplier,
|
|
3195
|
+
// Controller-installed environment — persists across plays.
|
|
3196
|
+
storage: this.opts.storage,
|
|
3197
|
+
functions: this.opts.functions,
|
|
3198
|
+
commands: this.opts.commands,
|
|
3199
|
+
fallbackCommand: this.opts.fallbackCommand,
|
|
3200
|
+
onStarted: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueStartedEvent, e), "onStarted"),
|
|
3201
|
+
onLine: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueLineEvent, e), "onLine"),
|
|
3202
|
+
onChoiceShown: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueChoiceShownEvent, { options: e.options }), "onChoiceShown"),
|
|
3203
|
+
onChoiceMade: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueChoiceMadeEvent, e), "onChoiceMade"),
|
|
3204
|
+
// Observation only — the `commands` map does the work; this mirrors
|
|
3205
|
+
// every non-built-in command onto the scene event bus.
|
|
3206
|
+
onCommand: /* @__PURE__ */ __name((command, ctx) => this.entity.emit(DialogueCommandEvent, { command, mode: ctx.mode }), "onCommand"),
|
|
3207
|
+
// Route non-fatal runtime diagnostics (e.g. a `set` to a read-only cell)
|
|
3208
|
+
// through the engine logger rather than crashing or silently dropping.
|
|
3209
|
+
onError: /* @__PURE__ */ __name((message) => warn(message), "onError"),
|
|
3210
|
+
onEnded: /* @__PURE__ */ __name((e) => {
|
|
3211
|
+
this.entity.emit(DialogueEndedEvent, e);
|
|
3212
|
+
this.opts.onEnded?.();
|
|
3213
|
+
}, "onEnded"),
|
|
3214
|
+
// Observation events — the controller is the one canonical path that
|
|
3215
|
+
// turns the session's callbacks into entity→scene events (no matching
|
|
3216
|
+
// controller callback options).
|
|
3217
|
+
onRevealCompleted: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueRevealCompletedEvent, e), "onRevealCompleted"),
|
|
3218
|
+
onSelectionChanged: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueSelectionChangedEvent, e), "onSelectionChanged"),
|
|
3219
|
+
onSkipUsed: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueSkipUsedEvent, e), "onSkipUsed"),
|
|
3220
|
+
onAutoAdvance: /* @__PURE__ */ __name((e) => this.entity.emit(DialogueAutoAdvanceEvent, e), "onAutoAdvance"),
|
|
3221
|
+
// Inline markers fan to an entity event; per-grapheme ticks stay a direct
|
|
3222
|
+
// callback (forwarded verbatim — undefined when the host wires none).
|
|
3223
|
+
onRevealMarker: /* @__PURE__ */ __name((marker, viaSkip) => this.entity.emit(DialogueRevealMarkerEvent, { marker, viaSkip }), "onRevealMarker"),
|
|
3224
|
+
onRevealTick: this.opts.onRevealTick
|
|
3225
|
+
}
|
|
3226
|
+
);
|
|
3227
|
+
this.binding.bind(this.input, this.session);
|
|
3228
|
+
if (this.paused) this.session.setPaused(true);
|
|
3229
|
+
if (this.hidden) this.session.setHidden(true);
|
|
3230
|
+
for (const ch of this.opts.channels ?? []) this.addChannel(ch);
|
|
3231
|
+
}
|
|
3232
|
+
onDestroy() {
|
|
3233
|
+
this.destroyed = true;
|
|
3234
|
+
this.session?.stop();
|
|
3235
|
+
for (const dispose of [...this.channelDisposers]) dispose();
|
|
3236
|
+
this.binding.dispose?.();
|
|
3237
|
+
this.opts.text.dispose();
|
|
3238
|
+
this.opts.choices.dispose();
|
|
3239
|
+
this.opts.chrome.dispose();
|
|
3240
|
+
this.opts.avatar?.dispose();
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Begin a conversation. `play(script)` is **content-only** — storage,
|
|
3244
|
+
* functions, and commands are installed on the controller. `overrides` layers
|
|
3245
|
+
* per-conversation specifics on top (a scoped `storage`, extra
|
|
3246
|
+
* `functions`/`commands`). Returns a {@link DialogueHandle} for live `setVar` /
|
|
3247
|
+
* `getVars`, or `undefined` if the controller was removed.
|
|
3248
|
+
*/
|
|
3249
|
+
play(script, overrides) {
|
|
3250
|
+
if (this.destroyed) {
|
|
3251
|
+
this.logger?.warn(
|
|
3252
|
+
"dialogue",
|
|
3253
|
+
"DialogueController.play() ignored: the component has been removed/destroyed.",
|
|
3254
|
+
{ scriptId: script.id }
|
|
3255
|
+
);
|
|
3256
|
+
return void 0;
|
|
3257
|
+
}
|
|
3258
|
+
if (!this.session) {
|
|
3259
|
+
throw new Error(
|
|
3260
|
+
"DialogueController.play() called before the component was added to an entity (onAdd has not run yet)."
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
return this.session.play(script, overrides);
|
|
3264
|
+
}
|
|
3265
|
+
isActive() {
|
|
3266
|
+
return this.session?.isActive() ?? false;
|
|
3267
|
+
}
|
|
3268
|
+
/** Abandon the current conversation and reset to idle. */
|
|
3269
|
+
stop() {
|
|
3270
|
+
this.session?.stop();
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* Register an extra channel live — Voice / Shop / CameraEffects / History.
|
|
3274
|
+
* Mounts it if it needs the scene ({@link Mountable}), hands it to the session
|
|
3275
|
+
* (where it joins the cross-cutting stream and can gate auto-advance), and
|
|
3276
|
+
* returns a disposer that unregisters + disposes it. The `channels` ctor option
|
|
3277
|
+
* registers a bundle the same way at mount. Returns a no-op disposer if the
|
|
3278
|
+
* controller was destroyed; **throws** if called before the component is added
|
|
3279
|
+
* to an entity (use the `channels` ctor option to pre-wire a channel) — mirrors
|
|
3280
|
+
* {@link play}.
|
|
3281
|
+
*/
|
|
3282
|
+
addChannel(ch) {
|
|
3283
|
+
if (this.destroyed) {
|
|
3284
|
+
this.logger?.warn(
|
|
3285
|
+
"dialogue",
|
|
3286
|
+
"DialogueController.addChannel() ignored: the component has been removed/destroyed."
|
|
3287
|
+
);
|
|
3288
|
+
return () => {
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
if (!this.session) {
|
|
3292
|
+
throw new Error(
|
|
3293
|
+
"DialogueController.addChannel() called before the component was added to an entity (onAdd has not run yet). Use the `channels` constructor option to pre-wire a channel."
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
if (isMountable(ch)) {
|
|
3297
|
+
try {
|
|
3298
|
+
ch.mount(this.scene);
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
this.logger?.warn(
|
|
3301
|
+
"dialogue",
|
|
3302
|
+
`extra channel mount() failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
const unregister = this.session.addChannel(ch);
|
|
3307
|
+
const dispose = /* @__PURE__ */ __name(() => {
|
|
3308
|
+
if (!this.channelDisposers.delete(dispose)) return;
|
|
3309
|
+
unregister();
|
|
3310
|
+
}, "dispose");
|
|
3311
|
+
this.channelDisposers.add(dispose);
|
|
3312
|
+
return dispose;
|
|
3313
|
+
}
|
|
3314
|
+
/** Fast-forward the current section to the next choice or the end. */
|
|
3315
|
+
skip() {
|
|
3316
|
+
this.session?.skip();
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Auto-advance lines after they finish revealing (`ms`), or `null` to disable
|
|
3320
|
+
* (manual advance). A per-line `autoAdvanceMs` still overrides this. Toggle it
|
|
3321
|
+
* live for a VN-style "auto" control.
|
|
3322
|
+
*/
|
|
3323
|
+
setAutoAdvance(ms) {
|
|
3324
|
+
this.session?.setAutoAdvance(ms);
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Hide or show the whole dialogue UI without ending the conversation —
|
|
3328
|
+
* for a cutscene takeover (`setHidden(true)` while the camera pans, then
|
|
3329
|
+
* `setHidden(false)` to restore the exact line + caret). Purely visual; the
|
|
3330
|
+
* conversation keeps its state. **Persistent**: it survives `stop()`/`play()`,
|
|
3331
|
+
* so a host that hides and forgets to unhide stays hidden.
|
|
3332
|
+
*/
|
|
3333
|
+
setHidden(hidden) {
|
|
3334
|
+
this.hidden = hidden;
|
|
3335
|
+
this.session?.setHidden(hidden);
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Freeze or resume the conversation — a pause menu. While paused the
|
|
3339
|
+
* reveal, auto-advance, caret blink, and avatar anim all halt, input is inert,
|
|
3340
|
+
* and no state is lost (an in-flight blocking command keeps running). Also
|
|
3341
|
+
* gates this controller's input binding so a frozen conversation consumes no
|
|
3342
|
+
* device input. Does NOT block host-driven `handle.setVar` / storage writes.
|
|
3343
|
+
*/
|
|
3344
|
+
setPaused(paused) {
|
|
3345
|
+
this.paused = paused;
|
|
3346
|
+
this.session?.setPaused(paused);
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Set whether this controller consumes device input — the focus seam for
|
|
3350
|
+
* the multi-instance story. `setInputEnabled(false)` keeps the conversation
|
|
3351
|
+
* fully alive (it still updates, reveals, auto-advances) but stops polling its
|
|
3352
|
+
* binding, so an ambient conversation doesn't steal the advance key. Switch
|
|
3353
|
+
* focus between two conversations with `a.setInputEnabled(true);
|
|
3354
|
+
* b.setInputEnabled(false)`. (YAGE input is non-consuming, so two *enabled*
|
|
3355
|
+
* controllers both advance on one press — focus is the game's policy.)
|
|
3356
|
+
*/
|
|
3357
|
+
setInputEnabled(enabled) {
|
|
3358
|
+
this.inputEnabled = enabled;
|
|
3359
|
+
}
|
|
3360
|
+
/**
|
|
3361
|
+
* Primary action, host-driven (the input-agnostic seam): while saying,
|
|
3362
|
+
* reveal-all if still typing else advance to the next line; while choosing,
|
|
3363
|
+
* confirm the highlighted option. Lets a host (cutscene script, custom input,
|
|
3364
|
+
* or a test) drive the conversation without synthesising device input — the
|
|
3365
|
+
* same call the default {@link InputBinding} makes.
|
|
3366
|
+
*/
|
|
3367
|
+
advance() {
|
|
3368
|
+
this.session?.advance();
|
|
3369
|
+
}
|
|
3370
|
+
/** Move the choice cursor by `delta` (wraps). No-op outside a choice. */
|
|
3371
|
+
moveSelection(delta) {
|
|
3372
|
+
this.session?.moveSelection(delta);
|
|
3373
|
+
}
|
|
3374
|
+
/** Commit a choice by its original option index. No-op outside a choice. */
|
|
3375
|
+
choose(optionIndex) {
|
|
3376
|
+
this.session?.choose(optionIndex);
|
|
3377
|
+
}
|
|
3378
|
+
/** True while a choice is being presented. */
|
|
3379
|
+
isChoosing() {
|
|
3380
|
+
return this.session?.isChoosing() ?? false;
|
|
3381
|
+
}
|
|
3382
|
+
/** Side-effect-free lookahead of the lines a node would show. */
|
|
3383
|
+
preview(nodeId) {
|
|
3384
|
+
return this.session?.preview(nodeId) ?? [];
|
|
3385
|
+
}
|
|
3386
|
+
update(dt) {
|
|
3387
|
+
this.session?.update(dt);
|
|
3388
|
+
if (this.inputEnabled && !this.paused) this.binding.poll();
|
|
3389
|
+
}
|
|
3390
|
+
};
|
|
3391
|
+
function isMountable(ch) {
|
|
3392
|
+
return typeof ch.mount === "function";
|
|
3393
|
+
}
|
|
3394
|
+
__name(isMountable, "isMountable");
|
|
3395
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3396
|
+
0 && (module.exports = {
|
|
3397
|
+
CompositeInputBinding,
|
|
3398
|
+
DEFAULT_ACTIONS,
|
|
3399
|
+
DialogueAutoAdvanceEvent,
|
|
3400
|
+
DialogueChoiceMadeEvent,
|
|
3401
|
+
DialogueChoiceShownEvent,
|
|
3402
|
+
DialogueCommandEvent,
|
|
3403
|
+
DialogueController,
|
|
3404
|
+
DialogueEndedEvent,
|
|
3405
|
+
DialogueExprError,
|
|
3406
|
+
DialogueLineEvent,
|
|
3407
|
+
DialoguePlayError,
|
|
3408
|
+
DialogueRevealCompletedEvent,
|
|
3409
|
+
DialogueRevealMarkerEvent,
|
|
3410
|
+
DialogueRunner,
|
|
3411
|
+
DialogueScriptError,
|
|
3412
|
+
DialogueSelectionChangedEvent,
|
|
3413
|
+
DialogueSession,
|
|
3414
|
+
DialogueSkipUsedEvent,
|
|
3415
|
+
DialogueStartedEvent,
|
|
3416
|
+
EMPTY_PARSED,
|
|
3417
|
+
FULL_ACTIONS,
|
|
3418
|
+
IdentityI18n,
|
|
3419
|
+
KeyboardInputBinding,
|
|
3420
|
+
LineReveal,
|
|
3421
|
+
MemoryVariableStorage,
|
|
3422
|
+
PointerInputBinding,
|
|
3423
|
+
cells,
|
|
3424
|
+
compose,
|
|
3425
|
+
createVoiceChannel,
|
|
3426
|
+
defineScript,
|
|
3427
|
+
evalCondition,
|
|
3428
|
+
evaluate,
|
|
3429
|
+
fullControls,
|
|
3430
|
+
interpolate,
|
|
3431
|
+
isExpr,
|
|
3432
|
+
loadCompact,
|
|
3433
|
+
loadScript,
|
|
3434
|
+
materialize,
|
|
3435
|
+
parseCompact,
|
|
3436
|
+
parseExpr,
|
|
3437
|
+
parseMarkup,
|
|
3438
|
+
splitGraphemes,
|
|
3439
|
+
stripMarkup
|
|
3440
|
+
});
|
|
3441
|
+
//# sourceMappingURL=index.cjs.map
|