@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.
@@ -0,0 +1,591 @@
1
+ import { P as ParsedText, D as DialogueScript, V as VarValue, a as VariableStorage, b as VarMap, C as Condition, E as Expr, c as DialogueFunction, S as SayStep, d as SpeakerDef, e as ChoiceStep, f as ChoiceOption, g as Command, h as CommandContext, R as RunMode, M as MarkerToken } from './types-DSbBSlh7.js';
2
+ export { A as ArithmeticOp, i as AvatarRef, B as BinaryOp, j as BuiltinEffectId, k as CommandHandler, l as CommandStep, m as CommandTiming, n as CompareOp, o as ComparisonOp, p as DialogueHandle, q as DialogueNode, r as DialoguePlayOptions, s as EndStep, G as GotoStep, L as LogicalOp, N as NodeId, t as PauseToken, u as RevealToken, v as RunStyle, w as SpeakerId, x as Step, T as TextRun, U as UnaryOp } from './types-DSbBSlh7.js';
3
+ import { D as DialogueExtraChannel } from './DialogueController-Cs5IUc-u.js';
4
+ export { A as AvatarChannel, C as ChoiceChannel, a as ChoiceContext, b as ChromeChannel, c as CompositeInputBinding, d as DEFAULT_ACTIONS, e as DialogueActions, f as DialogueBundle, g as DialogueChannels, h as DialogueController, i as DialogueControllerOptions, j as DialogueSession, k as DialogueSessionOptions, F as FULL_ACTIONS, I as I18nAdapter, l as IdentityI18n, m as InputBinding, K as KeyboardInputBinding, L as LineReveal, M as Mountable, P as PointerChoiceTarget, n as PointerInputBinding, o as PresentedChoice, p as PresentedLine, q as PreviewedLine, R as RevealBeat, S as SpeakerView, T as TextChannel, r as TypedScript, V as VarsOf, s as defineScript, t as fullControls, u as interpolate } from './DialogueController-Cs5IUc-u.js';
5
+ import * as _yagejs_core from '@yagejs/core';
6
+ import '@yagejs/input';
7
+
8
+ /**
9
+ * Inline-markup parser. Turns an authored string into styled {@link TextRun}s
10
+ * plus {@link RevealToken}s, using a small BBCode-ish tag syntax that survives
11
+ * translation (translators keep the tags, reorder the words):
12
+ *
13
+ * plain text
14
+ * [b]bold[/b] [i]italic[/i]
15
+ * [color=#ffcc00]hex[/color] [color=gold]named[/color]
16
+ * [wave]animated[/wave] (effect span — OPEN vocabulary: any [name]..[/name];
17
+ * the bundled view animates wave/shake/pulse/rainbow)
18
+ * [speed=2]faster[/speed] [speed=0.5]slower[/speed]
19
+ * [pause=400/] (self-closing reveal PAUSE — holds at its offset, in ms)
20
+ * [sfx=ding/] (self-closing reveal MARKER — fires at its offset)
21
+ * [expression=happy/] (self-named shortcut → props { expression: happy })
22
+ * [shake amount=3/] (marker with explicit key=value props)
23
+ * [shake=500 amount=3/] (shortcut + props compose → { shake: 500, amount: 3 })
24
+ * \[literal bracket]
25
+ *
26
+ * Tags nest; styles inherit down the stack (so [b][color=red]X[/color][/b]
27
+ * is bold+red). A trailing `/` makes a tag **self-closing** — a zero-width
28
+ * {@link RevealToken} (a `[pause=600/]` hold or a `[name k=v/]` marker) that the
29
+ * reveal drains at its char offset, distinct from the styling tags (which never
30
+ * end in `/`). Pause + markers share one ordered stream, so **source order is
31
+ * drain order**: `[pause=600/][shake/]` holds then fires; `[shake/][pause=600/]`
32
+ * fires then holds. A non-self-closing tag that isn't a built-in text attribute
33
+ * opens an EFFECT span named after the tag (an open vocabulary the presenter
34
+ * interprets); translators MUST keep a marker/pause's self-closing `/` so the
35
+ * token survives a re-order.
36
+ */
37
+
38
+ /** The empty parse result (no runs / tokens, length 0). Shared so a presenter or
39
+ * the session can present a contentless line (an empty choice prompt) without
40
+ * re-constructing the shape — and without forgetting the required `tokens`
41
+ * field. */
42
+ declare const EMPTY_PARSED: ParsedText;
43
+ /**
44
+ * Split a string into graphemes (user-perceived characters) — the unit the
45
+ * renderer creates one glyph node per, and the unit every reveal-side count
46
+ * (`ParsedText.length`, `TextRun.graphemeCount`, `PauseToken.atChar`) uses.
47
+ */
48
+ declare function splitGraphemes(text: string): string[];
49
+ declare function parseMarkup(input: string): ParsedText;
50
+ /** Strip every tag, returning plain text (useful for measuring / a11y / logs). */
51
+ declare function stripMarkup(input: string): string;
52
+
53
+ /**
54
+ * Two-stage validation for the storage model.
55
+ *
56
+ * • **Load-time** ({@link analyzeScript}, environment-free): walk the script
57
+ * once, collecting the names it **reads** (conditions, `{token}`s, `set`
58
+ * values), the names it **writes** (`set` targets), the **functions** it
59
+ * calls, and the **command types** it fires. Type-check what's statically
60
+ * knowable — an atomic numeric comparison against a declared non-number, a
61
+ * literal `set` value whose type conflicts with the target's declared
62
+ * default. Undeclared *references* are NOT rejected here: the installed
63
+ * storage / functions may provide them, which is only known at play-time.
64
+ * • **Play-time** ({@link validatePlay}): given the installed storage,
65
+ * functions, and commands, throw on a *significant* mismatch — a read name
66
+ * nothing provides, a called function with no implementation, a `set` target
67
+ * that's a function (read-only), a command type with no handler/fallback, a
68
+ * declared default whose type conflicts with the value the storage already
69
+ * holds.
70
+ *
71
+ * Both throw hard — a dangling reference or an environment that can't satisfy the
72
+ * script is a programming error, not a recoverable runtime condition.
73
+ */
74
+
75
+ /** A script reference is broken (load-time). */
76
+ declare class DialogueScriptError extends Error {
77
+ }
78
+ /** The installed storage/functions/commands don't satisfy the script (play-time). */
79
+ declare class DialoguePlayError extends Error {
80
+ }
81
+
82
+ /**
83
+ * Canonical loader: validates + normalises a hand-authored / JSON
84
+ * {@link DialogueScript} into a frozen, structurally-checked script the runner
85
+ * can trust. Other "common formats" (a Yarn/ink-style screenplay parser) are
86
+ * additional modules in this folder that emit the same canonical shape — the
87
+ * runner only ever sees the canonical model.
88
+ */
89
+
90
+ declare function loadScript(raw: DialogueScript): DialogueScript;
91
+
92
+ /**
93
+ * Compact authoring front-end — a small, line-oriented DSL for RPG-style
94
+ * dialogue that compiles to the same {@link DialogueScript} IR as JSON / YAML.
95
+ * `parseCompact(text)` produces the IR; `loadCompact(text)` runs it through
96
+ * {@link loadScript}, so validation and the frozen model are identical to every
97
+ * other loader. Pixi-free: it imports only the headless core (`parseExpr`,
98
+ * `loadScript`, the markup tag guard).
99
+ *
100
+ * One statement per line. Leading whitespace is insignificant (indent nodes for
101
+ * readability). Blank lines and `// comment` lines are ignored. Each non-blank
102
+ * line is one of:
103
+ *
104
+ * # id script id (required, once) — the start node is the
105
+ * first `::` node defined.
106
+ * @ id Name [#hex] a speaker: an opaque id, a display name (may have
107
+ * spaces), an optional nameplate colour (`#ffcc00` /
108
+ * `#fc0`). `@` lines may appear before or after their use.
109
+ * :: nodeId opens a node; following step lines belong to it.
110
+ * speaker[ face]: text a spoken line — ONLY when the first token is a declared
111
+ * `@`-speaker. `face` (a 2nd header token) becomes the
112
+ * line's avatar `expression`. Otherwise the WHOLE line,
113
+ * colons and all, is a narrator line.
114
+ * text a narrator line (no declared speaker prefix).
115
+ * ? text … a choice option; consecutive `?` lines coalesce into
116
+ * one choice step (see below).
117
+ * -> nodeId [if: cond] a jump — unconditional, or conditional (taken only if
118
+ * `cond` holds, else fall through to the next step).
119
+ * declare v = value a script-level variable default (a literal value).
120
+ * set v = rhs write a variable. A bare number / `true` / `false` /
121
+ * `null` stays a literal; anything else is parsed as an
122
+ * expression (`set hp = hp - 1`), so the host reads it
123
+ * back through the same evaluator JSON uses.
124
+ * do type k=v … #flag a host command: `type` then `key=value` data and
125
+ * `#flag` booleans (`do give-item id=key count=1 #blocking`).
126
+ * end ends the conversation.
127
+ *
128
+ * **Per-line hints** ride the end of a `say` line: `view=` / `voice=` / `speed=`
129
+ * / `auto=` set the first-class {@link SayStep} fields, and trailing `#key:value`
130
+ * / bare `#flag` hashtags become {@link SayStep.meta} (Yarn-aligned — metadata is
131
+ * trailing). A `say` line's text is otherwise passed to the markup parser
132
+ * **verbatim**, so inline `[..]` markup (and any markup tokens a later release
133
+ * adds) survives untouched.
134
+ *
135
+ * **Choices** carry their attributes as non-bracket sigils, in this order after
136
+ * the text: `if: cond`, then `-> target` (or `target=node`), then `#once` /
137
+ * `#disabled` / `#key:value` hashtags. They are lexed off and stripped before
138
+ * the remaining choice text reaches markup — `[..]` is reserved for inline
139
+ * markup there, so a bracketed token that markup doesn't recognize is reported
140
+ * as an error (it is almost always a mistyped attribute that would otherwise be
141
+ * dropped silently).
142
+ *
143
+ * Conditions and non-literal `set` values are parsed with the shared
144
+ * {@link parseExpr}, so a malformed expression throws {@link DialogueExprError}
145
+ * (a {@link DialogueScriptError} subtype) with its position.
146
+ */
147
+
148
+ /**
149
+ * Parse compact-DSL source into a (mutable) {@link DialogueScript}. Throws
150
+ * {@link DialogueScriptError} on a structural problem (with the 1-based line) and
151
+ * {@link DialogueExprError} on a malformed condition / `set` expression.
152
+ */
153
+ declare function parseCompact(text: string): DialogueScript;
154
+ /** Parse compact-DSL source and run it through {@link loadScript} — same
155
+ * validated, frozen IR as the JSON and YAML loaders. */
156
+ declare function loadCompact(text: string): DialogueScript;
157
+
158
+ /**
159
+ * {@link VariableStorage} implementations — the read/write bridge between a
160
+ * conversation and game state. One **opaque** name namespace; scoping is
161
+ * the host's policy. Three building blocks:
162
+ *
163
+ * • {@link MemoryVariableStorage} — the zero-config default. A plain Map; holds
164
+ * dialogue-locals and seeded defaults, persists across plays.
165
+ * • {@link cells} — first-class **two-way binding**: `{ gold: { get, set } }`
166
+ * drives a value the *script* owns the arithmetic of (a read-only getter
167
+ * throws on `set`). A bare `() => value` is the read-only shorthand.
168
+ * • {@link compose} — layer several storages into one (reads/writes route to
169
+ * the first that `has` the name; a brand-new name lands in the last —
170
+ * so put a writable store last to catch seeds + locals).
171
+ *
172
+ * The interface lives in `types.ts`; this file is the concrete kit. Seed-if-
173
+ * absent + persistence are policy of the *caller* (`session.play`), not the
174
+ * storage — these just hold values.
175
+ */
176
+
177
+ /** Materialize a storage's enumerable variables into a plain map — backs
178
+ * `{token}` interpolation params and `handle.getVars()`. */
179
+ declare function materialize(storage: VariableStorage): VarMap;
180
+ /** The zero-config default storage: a Map-backed, fully-enumerable store. */
181
+ declare class MemoryVariableStorage implements VariableStorage {
182
+ private readonly map;
183
+ constructor(initial?: Readonly<VarMap>);
184
+ get(name: string): VarValue | undefined;
185
+ set(name: string, value: VarValue): void;
186
+ has(name: string): boolean;
187
+ entries(): Iterable<readonly [string, VarValue]>;
188
+ /** Drop everything — host-controlled reset (variables persist across plays by default). */
189
+ clear(): void;
190
+ }
191
+ /** A two-way (or read-only) binding for one game-owned value. A bare function is
192
+ * the read-only shorthand for `{ get }`. */
193
+ type Cell = {
194
+ get(): VarValue;
195
+ set?(value: VarValue): void;
196
+ } | (() => VarValue);
197
+ /**
198
+ * A {@link VariableStorage} over named accessors into game state. `has` is true
199
+ * for exactly the declared names; `get` invokes the getter live; `set` writes
200
+ * through the setter, or throws if the cell is read-only (a getter with no
201
+ * setter). This is the seam for a value whose arithmetic the *script* owns
202
+ * (`set gold = gold - 50`).
203
+ */
204
+ declare function cells(defs: Readonly<Record<string, Cell>>): VariableStorage;
205
+ /**
206
+ * Layer storages into one. `get`/`has` consult them in order (first that `has`
207
+ * the name wins); `set` writes through the first that `has` it, else the **last**
208
+ * storage — so a brand-new name (a dialogue-local or a seeded default) lands in
209
+ * whatever writable store you put last. Typical: `compose(cells(...game), new
210
+ * MemoryVariableStorage())`.
211
+ */
212
+ declare function compose(...storages: readonly VariableStorage[]): VariableStorage;
213
+
214
+ /**
215
+ * Expression evaluator. `Condition`s and `set` values are expression
216
+ * *trees* — `literal | varRef | call | unary | binary | group` — evaluated
217
+ * against an {@link EvalScope} (variable reads + installed functions). The
218
+ * operator set mirrors Yarn Spinner so a future Yarn parser maps onto this IR
219
+ * 1:1; the atomic `{ var, op, value }` comparison evaluates as the degenerate
220
+ * one-level tree (lowered into a `binary` node in {@link evalCondition}).
221
+ */
222
+
223
+ /** What an expression evaluates against: per-name reads + function calls, plus
224
+ * a materialized snapshot for the `(vars) => boolean` predicate escape hatch. */
225
+ interface EvalScope {
226
+ /** Read a variable (absent → `null`). */
227
+ get(name: string): VarValue;
228
+ /** Invoke an installed function with already-evaluated args. */
229
+ call(fn: string, args: readonly VarValue[]): VarValue;
230
+ /** Materialize the readable variables (for a predicate condition). */
231
+ vars(): VarMap;
232
+ }
233
+ /** A condition holds when its value is truthy. */
234
+ declare function evalCondition(condition: Condition, scope: EvalScope): boolean;
235
+ /** Evaluate an expression tree to a single value. */
236
+ declare function evaluate(expr: Expr, scope: EvalScope): VarValue;
237
+ /** True for an {@link Expr} node (discriminated by `kind`), so `Condition` can
238
+ * tell a tree apart from the atomic `{ var, op, value }` shape. */
239
+ declare function isExpr(value: unknown): value is Expr;
240
+
241
+ /**
242
+ * String → expression front-end. `parseExpr("str >= 8 and has_item('key')")`
243
+ * produces the same {@link Expr} tree a hand-authored JSON condition / `set`
244
+ * value would — `literal | varRef | call | unary | binary | group`, no new node
245
+ * kinds — so the evaluator (`expr.ts`) and the load-time walk (`validate.ts`)
246
+ * are reused unchanged. This is purely a parser: it does no type-checking and
247
+ * no name resolution (that stays in `validate.ts`), which keeps it reusable 1:1
248
+ * for a future Yarn front-end.
249
+ *
250
+ * The operator set mirrors Yarn Spinner. v1 wires what the authoring examples
251
+ * exercise: `or`/`||`, `and`/`&&`, `not`/`!`, the comparisons (`== != > < >= <=`
252
+ * plus the word forms `eq neq gt lt gte lte is`), unary `-`, binary `+ -`, calls
253
+ * `f(a, b)`, and parentheses. `xor`/`^` and `* / %` are reserved but not yet
254
+ * wired (the IR + evaluator already accept them, so adding them later is purely
255
+ * additive). Word-form operators normalise to their symbol equivalents in the IR
256
+ * (`and` → `&&`, `eq` → `==`, `gt` → `>`, …), so `a and b` and `a && b` parse to
257
+ * the identical tree.
258
+ *
259
+ * An identifier is `[A-Za-z_$]` followed by `[A-Za-z0-9_.$]` repeats — `.` and
260
+ * `$` are included (so `$gold` and `quest.stage` each read as ONE name,
261
+ * Yarn-forward) but `-` is excluded, so `hp-1` is `hp` minus `1` and an item id
262
+ * like `'rusty-key'` must live in a quoted string literal.
263
+ */
264
+
265
+ /**
266
+ * A string expression failed to parse. Carries the 1-based source position.
267
+ * Extends {@link DialogueScriptError} so the loaders' contract holds: a malformed
268
+ * string condition / `set` value surfaced by `loadScript` / `loadYaml` is caught
269
+ * by a single `catch (e instanceof DialogueScriptError)`, while `instanceof
270
+ * DialogueExprError` (and `line` / `col`) still distinguish a parse error.
271
+ */
272
+ declare class DialogueExprError extends DialogueScriptError {
273
+ readonly line: number;
274
+ readonly col: number;
275
+ constructor(message: string, line: number, col: number);
276
+ }
277
+ /**
278
+ * Parse a string into an {@link Expr} tree. Throws {@link DialogueExprError}
279
+ * (with line/col) on an empty/blank source, a leftover trailing token, or a
280
+ * dangling operator.
281
+ */
282
+ declare function parseExpr(src: string): Expr;
283
+
284
+ /**
285
+ * DialogueRunner — the engine-agnostic state machine. It walks a normalised
286
+ * {@link DialogueScript}, pausing on `say`/`choice` steps (which need player
287
+ * input) and running `command`/`goto`/`end` steps straight through. All
288
+ * presentation is delegated via callbacks, so the same runner drives a
289
+ * renderer-based box, a ui-react box, or a headless test.
290
+ *
291
+ * Branching reads one {@link VariableStorage} namespace through an
292
+ * {@link EvalScope} (per-name reads + installed functions): `set` / `ctx.setVar`
293
+ * write storage, conditions and `set` values are evaluated as expression trees.
294
+ * The runner resolves built-in commands (`set`) itself and surfaces every other
295
+ * command to the host through `onCommand` — that's the seam where the game turns
296
+ * `{ type: "give-item", id: "key" }` into an actual effect.
297
+ */
298
+
299
+ /** The runtime environment the session installs behind a running conversation:
300
+ * the variable storage (read + guarded write) + the callable functions. */
301
+ interface RunnerEnv {
302
+ readonly storage: VariableStorage;
303
+ readonly functions: Readonly<Record<string, DialogueFunction>>;
304
+ /**
305
+ * Surfaces a non-fatal runtime diagnostic — currently a `set` whose write the
306
+ * storage rejected (a getter-only `cells` accessor). The runner ignores the
307
+ * write and keeps the conversation running; the host (via the session →
308
+ * controller) routes the message to the engine logger. Engine-agnostic: the
309
+ * core never reaches for `console`/a logger directly.
310
+ */
311
+ readonly onError?: ((message: string, error: unknown) => void) | undefined;
312
+ }
313
+ interface ResolvedChoice {
314
+ readonly index: number;
315
+ readonly option: ChoiceOption;
316
+ /**
317
+ * A visible-but-disabled row: the option's condition currently fails AND its
318
+ * `presentation` is `"disabled"`, so it's shown greyed-out and non-selectable
319
+ * instead of filtered. Omitted (falsy) for a normal, selectable option.
320
+ * Default-`"hidden"` condition failures and spent `once` options aren't
321
+ * returned at all.
322
+ */
323
+ readonly disabled?: boolean;
324
+ }
325
+ interface RunnerHandlers {
326
+ /** A line is ready to display. Runner waits for `advance()`. */
327
+ onSay(step: SayStep, speaker: SpeakerDef | undefined): void;
328
+ /** Choices are ready. Runner waits for `choose(index)`. `prompt` pre-resolved by host. */
329
+ onChoice(step: ChoiceStep, choices: readonly ResolvedChoice[], speaker: SpeakerDef | undefined): void;
330
+ /**
331
+ * A non-built-in command fired (give-item, play-sfx, …). May return a promise;
332
+ * if the command is `blocking`, the runner waits for it.
333
+ */
334
+ onCommand(command: Command, ctx: CommandContext): void | Promise<void>;
335
+ /** Conversation finished (ran off the end or hit an `end` step). */
336
+ onEnd(): void;
337
+ }
338
+ declare class DialogueRunner {
339
+ private readonly script;
340
+ private readonly handlers;
341
+ /** `option.once` keys already picked — per-conversation **cursor** state, NOT
342
+ * the variable storage. Fresh per runner, so a new `play()` starts it empty
343
+ * (a re-played conversation re-shows its `once` options; {@link getChosenOnce}
344
+ * exposes the set so a save cursor could capture/restore it). */
345
+ private readonly chosenOnce;
346
+ private nodeId;
347
+ private stepIndex;
348
+ private state;
349
+ /** "play" normally; `skip()` flips it to "skip" to fast-forward the section. */
350
+ private runMode;
351
+ /** Storage (write through this so a read-only `cells` accessor throws) +
352
+ * functions, wrapped once as the condition/`set`-value eval scope. */
353
+ private readonly storage;
354
+ private readonly scope;
355
+ private readonly onError;
356
+ constructor(script: DialogueScript,
357
+ /** The variable storage + functions (built by the session per play()). */
358
+ env: RunnerEnv, handlers: RunnerHandlers);
359
+ /** Snapshot of the storage's variables — the `handle.getVars()` /
360
+ * future save-cursor view. */
361
+ getVars(): Readonly<VarMap>;
362
+ /** Current node id (durable cursor; save seam). */
363
+ getNodeId(): string;
364
+ /** Current step index within the node (durable cursor; save seam). */
365
+ getStepIndex(): number;
366
+ /** One-shot choice keys already picked (`option.once`); save seam. */
367
+ getChosenOnce(): ReadonlySet<string>;
368
+ isEnded(): boolean;
369
+ /** Begin at the start node. Idempotent guard against double-start. The cursor
370
+ * (`nodeId`/`stepIndex`) is already at the start from the ctor + field init. */
371
+ start(): void;
372
+ /** Advance past the current `say` line. No-op unless we're awaiting it. */
373
+ advance(): void;
374
+ /**
375
+ * Fast-forward from the current line: run intervening commands in `skip` mode
376
+ * (so the game can reconstruct world state idempotently) without presenting
377
+ * any lines, stopping at the next choice or the end. No-op unless on a line.
378
+ */
379
+ skip(): Promise<void>;
380
+ /**
381
+ * Public, **wait-state-free** entry the Session uses to fire a `say` line's
382
+ * commands at show / after-reveal / advance time. Handles built-in `set`,
383
+ * surfaces the rest with the current mode (or `mode`, when the Session fires the
384
+ * displayed line's batches as part of its own skip), and awaits `blocking` ones.
385
+ * Delegates to {@link executeBatch}; the runner's wait-state is untouched (the
386
+ * Session gates its own input).
387
+ */
388
+ runCommands(commands: readonly Command[] | undefined, mode?: RunMode): Promise<void>;
389
+ /** Pick choice `index` (the original option index). */
390
+ choose(index: number): Promise<void>;
391
+ /** Run non-blocking steps until we hit one that needs input, or the end. */
392
+ private run;
393
+ /** @returns true if the step blocks (waiting for advance/choose/command/end). */
394
+ private handleStep;
395
+ private jump;
396
+ private end;
397
+ private currentStep;
398
+ private speaker;
399
+ /**
400
+ * Resolve a choice step to its visible rows. A spent `once` option is always
401
+ * dropped (presentation governs condition failures only). A passing option is
402
+ * enabled; a failing one is returned as a `disabled` row when its
403
+ * `presentation` is `"disabled"`, else dropped (the default `"hidden"`).
404
+ */
405
+ private resolveChoices;
406
+ /** Whether option `index` can actually be picked — the gate `choose()` uses.
407
+ * A spent `once` option or a failing condition refuses (a `"disabled"` row is
408
+ * shown but still unpickable, so this stays the single selection authority). */
409
+ private choiceEnabled;
410
+ /** A `once` option already chosen this run — always dropped from the menu
411
+ * regardless of `presentation`. Single source of truth for the once-gate,
412
+ * shared by `resolveChoices` and `choiceEnabled`. Reads the option from
413
+ * `step.options[index]`, so `(step, index)` is the only input. */
414
+ private isSpent;
415
+ private onceKey;
416
+ /**
417
+ * Fire an inline command batch (a `command` step or a chosen option). Manages
418
+ * wait-state: enters `awaiting-command` up front when the batch contains a
419
+ * blocking command, so a stray advance/confirm during the await is ignored; the
420
+ * caller transitions out of the state afterwards. The work itself goes through
421
+ * the wait-state-free {@link executeBatch}.
422
+ */
423
+ private fireBatch;
424
+ /**
425
+ * The wait-state-free command executor, shared by {@link fireBatch} (inline
426
+ * firing) and {@link runCommands} (the Session's line-timed firing). Applies
427
+ * built-in `set`; surfaces the rest to the host with the current mode; awaits
428
+ * `blocking` handlers and fire-and-forgets the others. Touches no wait-state.
429
+ */
430
+ private executeBatch;
431
+ /** The context handed to a command handler. `setVar` writes through the
432
+ * conversation's storage (guarded by the session for staleness), the same
433
+ * path as the `set` built-in — so the skill-check seam and `set` share one
434
+ * guarded write. */
435
+ private commandContext;
436
+ private test;
437
+ }
438
+
439
+ /**
440
+ * Voice-over as a registered {@link DialogueExtraChannel}. It plays a line's
441
+ * `voice` clip and gates auto-advance until the clip ends — so a line
442
+ * auto-advances at `max(clipEnd, revealEnd)` with no duration plumbing, and a
443
+ * short line never moves on while its long clip is still talking.
444
+ *
445
+ * The addon owns **no audio**: the host supplies `play` (wired over
446
+ * `@yagejs/audio` in the game), which starts a clip and returns a handle. This
447
+ * module imports neither audio nor a renderer, so it stays on the pixi-free root
448
+ * entry alongside the rest of the headless model.
449
+ */
450
+
451
+ /**
452
+ * The host-owned playback handle returned by {@link VoiceChannelOptions.play}.
453
+ * `pause` / `resume` are optional — a host that can't pause a clip degrades to a
454
+ * no-op (the conversation still pauses; the clip just keeps playing).
455
+ */
456
+ interface VoiceHandle {
457
+ /** Stop the clip immediately and release it. */
458
+ stop(): void;
459
+ /** Pause playback (optional). */
460
+ pause?(): void;
461
+ /** Resume playback (optional). */
462
+ resume?(): void;
463
+ }
464
+ interface VoiceChannelOptions {
465
+ /**
466
+ * Start the clip for `id` (the line's `voice`) and return a {@link VoiceHandle}.
467
+ * Call `onEnded` when the clip finishes **naturally** — that releases the
468
+ * auto-advance gate. The addon imports no audio; a YAGE host wires this over
469
+ * `@yagejs/audio`, e.g.
470
+ *
471
+ * play: (id, onEnded) => {
472
+ * const sound = audio.play(id, { onEnd: onEnded });
473
+ * return { stop: () => sound.stop(), pause: () => sound.pause(), resume: () => sound.resume() };
474
+ * }
475
+ */
476
+ play(id: string, onEnded: () => void): VoiceHandle;
477
+ /**
478
+ * What a skip does to a still-playing clip. `"cut"` (default) stops it and
479
+ * releases the gate the moment the player completes the typewriter or
480
+ * fast-forwards the section; `"ring"` lets the clip play out — auto-advance
481
+ * keeps waiting for `onEnded`, a manual advance still works (it is never gated).
482
+ */
483
+ onSkip?: "cut" | "ring";
484
+ /**
485
+ * Pause the clip when the conversation pauses ({@link DialogueSession.setPaused}).
486
+ * Default `true` — a paused conversation stops talking, the least-surprising
487
+ * default now that the channel knows a clip is mid-flight. Set `false` to let a
488
+ * clip play through a pause. `pause` / `resume` on the handle are optional, so
489
+ * this is a no-op when the host omits them.
490
+ */
491
+ pauseWithConversation?: boolean;
492
+ /**
493
+ * Safety budget (ms). If a clip's `onEnded` never arrives within this many ms
494
+ * of starting, the gate is force-released and {@link onError} is called — so a
495
+ * wedged host (a clip that silently fails to report its end) can't soft-lock
496
+ * auto-advance. Omit (or `0`) to disable the cap.
497
+ */
498
+ livenessMs?: number;
499
+ /** Diagnostics sink for the liveness cap — route it to the engine logger, the
500
+ * same seam as the session's `onError`. */
501
+ onError?: (message: string, error: unknown) => void;
502
+ }
503
+ /**
504
+ * Build a voice-over {@link DialogueExtraChannel}. Register it on a controller:
505
+ *
506
+ * const voice = createVoiceChannel({ play: (id, onEnded) => host.playClip(id, onEnded) });
507
+ * controller.addChannel(voice);
508
+ *
509
+ * Hardened against two real failure modes:
510
+ * - **generation guard** — a late `onEnded` from a clip that has since been
511
+ * superseded (the next line started) can't ungate the new line.
512
+ * - **liveness cap** — an optional budget force-releases the gate if a clip
513
+ * never reports its end, so the conversation can't soft-lock.
514
+ *
515
+ * On a mid-line save/restore the host re-presents the current line, so `present`
516
+ * fires again here — it stops any active clip first, so a restore restarts the
517
+ * line's clip cleanly (the restore-safety property).
518
+ */
519
+ declare function createVoiceChannel(opts: VoiceChannelOptions): DialogueExtraChannel;
520
+
521
+ /**
522
+ * Lifecycle + command events the {@link DialogueController} emits from its host
523
+ * entity. A scene listens with `this.on(DialogueEndedEvent, …)` (events bubble
524
+ * entity → scene). `DialogueCommandEvent` is the main game hook: every script
525
+ * command that isn't a built-in (`set`) arrives here for the game to interpret.
526
+ */
527
+ declare const DialogueStartedEvent: _yagejs_core.EventToken<{
528
+ scriptId: string;
529
+ }>;
530
+ declare const DialogueLineEvent: _yagejs_core.EventToken<{
531
+ speaker?: string | undefined;
532
+ /** Plain (markup-stripped) text — handy for logs, a11y, history. */
533
+ text: string;
534
+ }>;
535
+ declare const DialogueChoiceShownEvent: _yagejs_core.EventToken<{
536
+ options: readonly string[];
537
+ }>;
538
+ declare const DialogueChoiceMadeEvent: _yagejs_core.EventToken<{
539
+ index: number;
540
+ text: string;
541
+ }>;
542
+ declare const DialogueCommandEvent: _yagejs_core.EventToken<{
543
+ command: Command;
544
+ mode: RunMode;
545
+ }>;
546
+ declare const DialogueEndedEvent: _yagejs_core.EventToken<{
547
+ scriptId: string;
548
+ }>;
549
+ /**
550
+ * Lifecycle observation events. These are the moments games
551
+ * actually hook — a "typing finished" blip, a choice-hover tick, skip-used
552
+ * analytics, an auto-advance beat — emitted by the controller from the session's
553
+ * observation callbacks (the one canonical observation path; there are no
554
+ * matching controller callback options).
555
+ */
556
+ /** A line finished its typewriter reveal — the "typing finished" hook. Plain
557
+ * (markup-stripped) text, mirroring {@link DialogueLineEvent}. */
558
+ declare const DialogueRevealCompletedEvent: _yagejs_core.EventToken<{
559
+ speaker?: string | undefined;
560
+ text: string;
561
+ }>;
562
+ /** The choice cursor moved (keyboard nav OR pointer hover) — `index` is the
563
+ * original option index, `text` its plain label. */
564
+ declare const DialogueSelectionChangedEvent: _yagejs_core.EventToken<{
565
+ index: number;
566
+ text: string;
567
+ }>;
568
+ /** The player skipped the current section (skip-used analytics). */
569
+ declare const DialogueSkipUsedEvent: _yagejs_core.EventToken<{
570
+ scriptId: string;
571
+ }>;
572
+ /** A line advanced on its own via the auto-advance clock (vs a manual advance). */
573
+ declare const DialogueAutoAdvanceEvent: _yagejs_core.EventToken<{
574
+ scriptId: string;
575
+ }>;
576
+ /**
577
+ * An inline `[name k=v/]` reveal marker reached its char offset during the
578
+ * current line's typewriter — the game hook for positional effects
579
+ * (`[sfx=ding/]` → play a sound). `viaSkip` is true when a skip / complete
580
+ * drained it, so a loud one-shot can be suppressed. The avatar channel handles
581
+ * `[expression=…/]` itself; the addon name-matches no marker, so every other
582
+ * name flows here opaquely. Per-grapheme typewriter *ticks* are deliberately NOT
583
+ * an event (they fire hundreds of times per line) — wire `onRevealTick` on the
584
+ * controller instead.
585
+ */
586
+ declare const DialogueRevealMarkerEvent: _yagejs_core.EventToken<{
587
+ marker: MarkerToken;
588
+ viaSkip: boolean;
589
+ }>;
590
+
591
+ export { type Cell, ChoiceOption, ChoiceStep, Command, CommandContext, Condition, DialogueAutoAdvanceEvent, DialogueChoiceMadeEvent, DialogueChoiceShownEvent, DialogueCommandEvent, DialogueEndedEvent, DialogueExprError, DialogueExtraChannel, DialogueFunction, DialogueLineEvent, DialoguePlayError, DialogueRevealCompletedEvent, DialogueRevealMarkerEvent, DialogueRunner, DialogueScript, DialogueScriptError, DialogueSelectionChangedEvent, DialogueSkipUsedEvent, DialogueStartedEvent, EMPTY_PARSED, type EvalScope, Expr, MarkerToken, MemoryVariableStorage, ParsedText, type ResolvedChoice, RunMode, type RunnerEnv, type RunnerHandlers, SayStep, SpeakerDef, VarMap, VarValue, VariableStorage, type VoiceChannelOptions, type VoiceHandle, cells, compose, createVoiceChannel, evalCondition, evaluate, isExpr, loadCompact, loadScript, materialize, parseCompact, parseExpr, parseMarkup, splitGraphemes, stripMarkup };