@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
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
import { Scene, Component } from '@yagejs/core';
|
|
2
|
+
import { M as MarkerToken, P as ParsedText, b as VarMap, D as DialogueScript, N as NodeId, q as DialogueNode, w as SpeakerId, d as SpeakerDef, V as VarValue, g as Command, h as CommandContext, a as VariableStorage, c as DialogueFunction, k as CommandHandler, r as DialoguePlayOptions, p as DialogueHandle } from './types-DSbBSlh7.js';
|
|
3
|
+
import { InputManager } from '@yagejs/input';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LineReveal — the headless typewriter clock. Given a {@link ParsedText} (markup
|
|
7
|
+
* already parsed into runs + an ordered {@link RevealToken} stream), a base
|
|
8
|
+
* `charsPerSec`, a per-line speed multiplier, and `update(dt)` ticks, it advances
|
|
9
|
+
* a reveal cursor in **graphemes** (the unit `markup.ts` counts and the renderer
|
|
10
|
+
* splits glyphs by), drains the tokens IN SOURCE ORDER (a `pause` holds, a
|
|
11
|
+
* `marker` fires a {@link RevealBeat}), applies per-run `[speed]`, and fires
|
|
12
|
+
* completion **exactly once** per line.
|
|
13
|
+
*
|
|
14
|
+
* It is renderer-free on purpose: a DOM-overlay or per-word presenter can drive
|
|
15
|
+
* the same reveal logic and map the grapheme cursor onto its own rendering,
|
|
16
|
+
* without pulling the renderer in. The default `DialogueTextView` consumes it
|
|
17
|
+
* and keeps only the SplitText concerns — the code-unit→glyph prefix mapping and
|
|
18
|
+
* per-glyph style fan-out — which LineReveal deliberately does NOT own.
|
|
19
|
+
*
|
|
20
|
+
* What it owns: the reveal cursor, the `ParsedText.tokens` drain (one ordered
|
|
21
|
+
* index over pauses + markers), the hold-to-fast-forward multiplier, the per-line
|
|
22
|
+
* and per-run (`RunStyle.speed`) speeds, and the fired-once completion. What it
|
|
23
|
+
* does NOT own: anything that touches a glyph, a texture, or a layout. Counts are
|
|
24
|
+
* graphemes throughout — it reads the pre-computed grapheme counts off
|
|
25
|
+
* `ParsedText` (`length`, `TextRun.graphemeCount`, a token's `atChar`) and never
|
|
26
|
+
* re-segments.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A reveal-time beat the clock emits as the cursor advances: a per-grapheme
|
|
31
|
+
* `tick` (one per revealed grapheme — raw, including whitespace; the host
|
|
32
|
+
* filters) and a `marker` when the cursor reaches a {@link MarkerToken}'s
|
|
33
|
+
* offset. `viaSkip` is true when the marker was drained by {@link
|
|
34
|
+
* LineReveal.complete} (a skip / fast-forward) rather than reached during normal
|
|
35
|
+
* typing, so a host can suppress a loud one-shot that only fired because of a
|
|
36
|
+
* skip click. Ticks are NOT emitted on a skip (replaying dozens at once would
|
|
37
|
+
* machine-gun).
|
|
38
|
+
*/
|
|
39
|
+
type RevealBeat = {
|
|
40
|
+
readonly kind: "tick";
|
|
41
|
+
readonly index: number;
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: "marker";
|
|
44
|
+
readonly marker: MarkerToken;
|
|
45
|
+
readonly viaSkip: boolean;
|
|
46
|
+
};
|
|
47
|
+
declare class LineReveal {
|
|
48
|
+
private readonly charsPerSec;
|
|
49
|
+
private parsed;
|
|
50
|
+
/** Reveal cursor, in graphemes (fractional while typing). */
|
|
51
|
+
private cursor;
|
|
52
|
+
private pauseTimer;
|
|
53
|
+
/** Next un-drained token in `parsed.tokens` (one ordered cursor over pauses +
|
|
54
|
+
* markers — source order is drain order). */
|
|
55
|
+
private tokenIdx;
|
|
56
|
+
/** Graphemes already ticked (so each grapheme ticks exactly once). */
|
|
57
|
+
private tickCount;
|
|
58
|
+
/** Hold-to-fast-forward rate (1 = normal). */
|
|
59
|
+
private speedMul;
|
|
60
|
+
/** Per-line `say.speed` multiplier (1 = base). */
|
|
61
|
+
private lineSpeed;
|
|
62
|
+
private done;
|
|
63
|
+
private completed;
|
|
64
|
+
/** Fired exactly once when the line finishes revealing. The consuming view
|
|
65
|
+
* wires this to the session-owned reveal listener (NOT a public mutable
|
|
66
|
+
* field a game could clobber). */
|
|
67
|
+
private onComplete;
|
|
68
|
+
/** Per-grapheme ticks + inline markers, wired by the consuming view to the
|
|
69
|
+
* session-owned beat listener (like {@link onComplete}, never a public field). */
|
|
70
|
+
private onBeat;
|
|
71
|
+
/** @param charsPerSec base reveal rate (graphemes/second), scaled by the
|
|
72
|
+
* hold, per-line, and per-run multipliers. */
|
|
73
|
+
constructor(charsPerSec: number);
|
|
74
|
+
/**
|
|
75
|
+
* Register the completion listener — fires once per line, the moment the
|
|
76
|
+
* cursor reaches the end (or synchronously from {@link begin} for an empty
|
|
77
|
+
* line, or from {@link complete}). Pass `undefined` to clear.
|
|
78
|
+
*/
|
|
79
|
+
setCompletionListener(listener: (() => void) | undefined): void;
|
|
80
|
+
/**
|
|
81
|
+
* Register the reveal-beat listener — per-grapheme ticks and inline markers,
|
|
82
|
+
* in char order, the moment the cursor reaches each. Session-owned (set once,
|
|
83
|
+
* like {@link setCompletionListener}); pass `undefined` to clear.
|
|
84
|
+
*/
|
|
85
|
+
setBeatListener(listener: ((beat: RevealBeat) => void) | undefined): void;
|
|
86
|
+
/**
|
|
87
|
+
* Start revealing a new line. Resets the cursor, pauses, and the hold
|
|
88
|
+
* multiplier (a stale fast-forward must not leak into the next line — an
|
|
89
|
+
* active binding re-asserts it on its next poll). An **empty** line
|
|
90
|
+
* (`parsed.length === 0`) is complete immediately and fires the completion
|
|
91
|
+
* listener synchronously, matching the no-typewriter contract.
|
|
92
|
+
*/
|
|
93
|
+
begin(parsed: ParsedText, lineSpeed?: number): void;
|
|
94
|
+
/** Hold-to-fast-forward multiplier (1 = normal, e.g. 4 while skip is held). */
|
|
95
|
+
setSpeedMultiplier(m: number): void;
|
|
96
|
+
/** Advance the reveal cursor by `dt` (ms). Honours armed pauses and per-run
|
|
97
|
+
* speed; fires completion once the cursor reaches the end. No-op after the
|
|
98
|
+
* line is done or before the first {@link begin}. */
|
|
99
|
+
update(dt: number): void;
|
|
100
|
+
/** Reveal everything now (skip-to-end on a click/tap). Drains any not-yet-fired
|
|
101
|
+
* markers in order so their consequences still happen (`viaSkip=true` lets a
|
|
102
|
+
* host suppress a loud one-shot) and blows straight through pending pauses (a
|
|
103
|
+
* skip ignores holds), but DISCARDS pending ticks — replaying dozens of
|
|
104
|
+
* typewriter blips at once would machine-gun. Fires completion. */
|
|
105
|
+
complete(): void;
|
|
106
|
+
/** Revealed grapheme count (fractional while typing). The view floors this to
|
|
107
|
+
* map onto its glyph prefix table. */
|
|
108
|
+
get revealed(): number;
|
|
109
|
+
/** True once the line is fully revealed (also true for an empty line). */
|
|
110
|
+
isComplete(): boolean;
|
|
111
|
+
/** True while glyphs are still appearing. */
|
|
112
|
+
isRevealing(): boolean;
|
|
113
|
+
private finish;
|
|
114
|
+
/**
|
|
115
|
+
* Drain tokens whose offset the cursor has reached, IN SOURCE ORDER. A `marker`
|
|
116
|
+
* emits a beat; a `pause` arms the hold, clamps the cursor to its offset, and
|
|
117
|
+
* STOPS the drain for this frame (a one-frame advance can overshoot the offset,
|
|
118
|
+
* so the clamp keeps glyphs past the beat from popping in early, and a later
|
|
119
|
+
* token waits until the hold resumes). `viaSkip` (from {@link complete}) tags
|
|
120
|
+
* drained markers and blows straight through pauses without holding. Monotonic
|
|
121
|
+
* `tokenIdx` → each token is handled exactly once.
|
|
122
|
+
*/
|
|
123
|
+
private drainTokens;
|
|
124
|
+
/** Emit a `tick` for each grapheme newly revealed since the last call (raw —
|
|
125
|
+
* no whitespace test; the host filters). Multiple in order on a large-dt
|
|
126
|
+
* frame; `tickCount` is monotonic so none repeat. */
|
|
127
|
+
private emitTicks;
|
|
128
|
+
/** Reveal speed multiplier for whichever run the cursor currently sits in. */
|
|
129
|
+
private runSpeedAt;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `defineScript` — the TS-first authoring path. An identity function at
|
|
134
|
+
* runtime; at compile time it captures the script's declared variable value
|
|
135
|
+
* types (from their {@link DialogueScript.declare} defaults), branding the
|
|
136
|
+
* returned script so `play()` can hand back a typed {@link DialogueHandle}.
|
|
137
|
+
*
|
|
138
|
+
* JSON scripts (a plain {@link DialogueScript} literal) get the exact same
|
|
139
|
+
* runtime rules via load-/play-time validation — the brand is a pure
|
|
140
|
+
* compile-time convenience and never exists at runtime. Inference stays shallow:
|
|
141
|
+
* it lives here and at the `play()` boundary, never threaded through the
|
|
142
|
+
* session/controller internals.
|
|
143
|
+
*
|
|
144
|
+
* const script = defineScript({
|
|
145
|
+
* id: "shop", start: "n",
|
|
146
|
+
* declare: { greeted: false, gold: 0 }, // greeted: boolean, gold: number
|
|
147
|
+
* nodes: { n: { id: "n", steps: [{ kind: "end" }] } },
|
|
148
|
+
* });
|
|
149
|
+
* const handle = controller.play(script); // content-only
|
|
150
|
+
* handle.setVar("greeted", true); // ^ key is typed to keyof declare
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
declare const VARS_BRAND: unique symbol;
|
|
154
|
+
/** Phantom carrier of a script's captured variable types. Never present at
|
|
155
|
+
* runtime — only `defineScript`'s cast asserts it. */
|
|
156
|
+
interface ScriptTypes<V extends VarMap> {
|
|
157
|
+
readonly [VARS_BRAND]: V;
|
|
158
|
+
}
|
|
159
|
+
/** A {@link DialogueScript} branded with its declared variable types. */
|
|
160
|
+
type TypedScript<V extends VarMap> = DialogueScript & ScriptTypes<V>;
|
|
161
|
+
/** Widen a declared default's literal type to its base — a variable declared
|
|
162
|
+
* `false` is a boolean (accepts `true`), `0` is a number, `"x"` is a string. A
|
|
163
|
+
* `null` default is untyped, so it widens to the full {@link VarValue} union. */
|
|
164
|
+
type Widen<T extends VarValue> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : VarValue;
|
|
165
|
+
type WidenVars<V extends VarMap> = {
|
|
166
|
+
[K in keyof V]: Widen<V[K]>;
|
|
167
|
+
};
|
|
168
|
+
declare function defineScript<V extends VarMap = Record<never, never>>(script: Omit<DialogueScript, "declare"> & {
|
|
169
|
+
readonly id: string;
|
|
170
|
+
readonly start?: NodeId;
|
|
171
|
+
readonly nodes: Record<NodeId, DialogueNode>;
|
|
172
|
+
readonly speakers?: Record<SpeakerId, SpeakerDef>;
|
|
173
|
+
readonly declare?: V;
|
|
174
|
+
}): TypedScript<WidenVars<V>>;
|
|
175
|
+
/** Declared variable types captured for a script (the loose {@link VarMap} for a
|
|
176
|
+
* plain JSON script). Drives the typed {@link DialogueHandle} `play()` returns. */
|
|
177
|
+
type VarsOf<S> = S extends ScriptTypes<infer V> ? V : VarMap;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* i18n seam. The runtime never reaches for a translation library directly —
|
|
181
|
+
* it asks an {@link I18nAdapter}. Ship the identity adapter (literal text +
|
|
182
|
+
* `{param}` interpolation) by default; wrap i18next / FormatJS / your own
|
|
183
|
+
* string table in a ~10-line adapter to localise without touching the engine.
|
|
184
|
+
*/
|
|
185
|
+
interface I18nAdapter {
|
|
186
|
+
/** Current locale tag, e.g. "en", "fr-CA". Informational. */
|
|
187
|
+
readonly locale: string;
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a string. `key` is the translation key when the script provides
|
|
190
|
+
* one; `fallback` is the authored literal text. `params` feed interpolation.
|
|
191
|
+
* Implementations should return localised markup-bearing text.
|
|
192
|
+
*/
|
|
193
|
+
t(key: string | undefined, fallback: string, params?: Readonly<Record<string, unknown>>): string;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* No-op adapter: returns the authored literal, interpolating `{name}` tokens
|
|
197
|
+
* from `params`. This is what runs until a real i18n backend is plugged in.
|
|
198
|
+
*/
|
|
199
|
+
declare class IdentityI18n implements I18nAdapter {
|
|
200
|
+
readonly locale: string;
|
|
201
|
+
constructor(locale?: string);
|
|
202
|
+
t(_key: string | undefined, fallback: string, params?: Readonly<Record<string, unknown>>): string;
|
|
203
|
+
}
|
|
204
|
+
/** Replace `{token}` with `params.token`; leaves unknown tokens untouched.
|
|
205
|
+
* Own-property check only — `{constructor}`/`{toString}` must not stringify
|
|
206
|
+
* inherited Object.prototype members. */
|
|
207
|
+
declare function interpolate(text: string, params: Readonly<Record<string, unknown>>): string;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Extensible presentation channels — the open-ended companion to the built-in
|
|
211
|
+
* typed trio (text / choices / avatar / chrome). A host *registers* one of these
|
|
212
|
+
* on a running conversation to add behaviour the addon doesn't own (voice-over,
|
|
213
|
+
* a shop reacting to a `buy` command, a camera shake, a history recorder).
|
|
214
|
+
*
|
|
215
|
+
* Pixi-free, like the rest of `core/`: a channel that needs the scene also
|
|
216
|
+
* implements {@link Mountable} (re-exported from the root barrel), but this
|
|
217
|
+
* interface itself imposes no engine dependency.
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* An optional-hook channel the host registers via
|
|
222
|
+
* {@link DialogueController.addChannel} / {@link DialogueSession.addChannel} (or
|
|
223
|
+
* the controller's `channels` ctor array). **Every method is optional** — a
|
|
224
|
+
* one-method observer (a shop reacting to `command`, a history recorder reacting
|
|
225
|
+
* to `revealComplete`) implements just what it needs and ignores the rest.
|
|
226
|
+
*
|
|
227
|
+
* The {@link DialogueSession} fans its cross-cutting stream out to every
|
|
228
|
+
* registered channel at the same sites it drives the trio — `present`,
|
|
229
|
+
* `command`, `clear`, `setVisible`, `setPaused`, `completeReveal`, `update` —
|
|
230
|
+
* each call wrapped so a throwing channel is routed to the session's `onError`
|
|
231
|
+
* and never breaks the conversation. The trio stays trusted/unwrapped.
|
|
232
|
+
*
|
|
233
|
+
* Coupling is deliberately one-directional: the ONLY value a channel hands back
|
|
234
|
+
* is {@link isRevealComplete}, a boolean the session folds into the auto-advance
|
|
235
|
+
* gate. Everything else is **consequences-out** — a channel changes game state
|
|
236
|
+
* through {@link CommandContext.setVar} (write-only) and reads it back through
|
|
237
|
+
* the host-held `DialogueHandle.getVars()`, never through the session.
|
|
238
|
+
*/
|
|
239
|
+
interface DialogueExtraChannel {
|
|
240
|
+
/**
|
|
241
|
+
* A say line was presented — called right after the text channel's `present`
|
|
242
|
+
* and before the line's `show` commands. Read `line.voice` / `line.meta`
|
|
243
|
+
* here. NOT called for choice prompts (only say lines carry a voice/reveal).
|
|
244
|
+
*/
|
|
245
|
+
present?(line: PresentedLine): void;
|
|
246
|
+
/**
|
|
247
|
+
* The current say line finished its typewriter reveal (right after the host's
|
|
248
|
+
* `onRevealCompleted`). For a history / analytics recorder that commits a line
|
|
249
|
+
* once it's fully shown. Carries the same {@link PresentedLine} as `present`.
|
|
250
|
+
*/
|
|
251
|
+
revealComplete?(line: PresentedLine): void;
|
|
252
|
+
/**
|
|
253
|
+
* A non-built-in command fired — mirrors the host `onCommand`, with the same
|
|
254
|
+
* exclusion (never `set`, which the runner owns). A shop channel reacts to a
|
|
255
|
+
* `buy` command here. (A mid-line face change is an `[expression=…/]` reveal
|
|
256
|
+
* marker on {@link revealBeat}, not a command.)
|
|
257
|
+
*/
|
|
258
|
+
command?(command: Command, ctx: CommandContext): void;
|
|
259
|
+
/** The conversation cleared (a `stop()` or its natural end) — reset any
|
|
260
|
+
* per-conversation state. Distinct from {@link dispose} (final teardown). */
|
|
261
|
+
clear?(): void;
|
|
262
|
+
/** The whole dialogue UI was shown / hidden (the host `setHidden` lever). */
|
|
263
|
+
setVisible?(visible: boolean): void;
|
|
264
|
+
/** The conversation was paused / resumed — freezes player-facing time. A voice
|
|
265
|
+
* channel pauses its clip here. */
|
|
266
|
+
setPaused?(paused: boolean): void;
|
|
267
|
+
/** The player skipped the typewriter (advance-while-revealing) or fast-forwarded
|
|
268
|
+
* the section (skip) — cut a clip, drain a queued effect. */
|
|
269
|
+
completeReveal?(): void;
|
|
270
|
+
/**
|
|
271
|
+
* A reveal beat fired during the current say line's typewriter — a
|
|
272
|
+
* per-grapheme `tick` or an inline `[name k=v/]` `marker`. A typewriter-SFX
|
|
273
|
+
* channel blips on ticks; a CameraEffects channel reacts to a `[shake/]`
|
|
274
|
+
* marker. Fires once per grapheme (hundreds per line), so keep it cheap.
|
|
275
|
+
* Markers also reach the host via `DialogueRevealMarkerEvent` and the avatar
|
|
276
|
+
* channel; this is the registered-channel path to the same stream.
|
|
277
|
+
*/
|
|
278
|
+
revealBeat?(beat: RevealBeat): void;
|
|
279
|
+
/** Per-frame tick. Already gated by the session pause (not called while
|
|
280
|
+
* paused), so a dt-driven timer freezes for free. */
|
|
281
|
+
update?(dt: number): void;
|
|
282
|
+
/** Final teardown — called by the {@link DialogueSession.addChannel} disposer
|
|
283
|
+
* and the controller's `onDestroy`. Release anything {@link clear} doesn't. */
|
|
284
|
+
dispose?(): void;
|
|
285
|
+
/**
|
|
286
|
+
* Auto-advance gate. While this returns `false` the session's auto-advance
|
|
287
|
+
* clock is frozen (a manual advance is **always** allowed). Omit it to never
|
|
288
|
+
* gate — a pure observer. The session **arms** its clock on the TEXT reveal
|
|
289
|
+
* but **counts down** only once text AND every registered gater report
|
|
290
|
+
* complete, so a line auto-advances at `max(clipEnd, revealEnd)` with no
|
|
291
|
+
* duration plumbing. Must be a cheap, total boolean read (never throws — it is
|
|
292
|
+
* polled every frame and, unlike the fanned-out hooks, is not wrapped).
|
|
293
|
+
*/
|
|
294
|
+
isRevealComplete?(): boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* DialogueSession — the headless orchestrator. It binds a {@link DialogueRunner}
|
|
299
|
+
* to a set of presentation *channels* (text / choices / avatar / chrome) and
|
|
300
|
+
* sequences a conversation: resolve i18n + markup, drive the typewriter, gate on
|
|
301
|
+
* reveal-completion, run the auto-advance clock, and book-keep choice selection.
|
|
302
|
+
*
|
|
303
|
+
* It is engine-agnostic (zero `@yagejs` imports): the channels are semantic
|
|
304
|
+
* interfaces — `present(line)`, `highlight(i)`, `setSpeaking(bool)` — with no
|
|
305
|
+
* pixels, no input, and no world entities. A YAGE host (`DialogueController`)
|
|
306
|
+
* supplies concrete channels, an `InputBinding`, and pumps `update(dt)`; a
|
|
307
|
+
* headless test can supply stubs. The public API is input-agnostic
|
|
308
|
+
* (`advance / moveSelection / confirm / choose / setFastForward`) so any device
|
|
309
|
+
* binding maps onto it.
|
|
310
|
+
*
|
|
311
|
+
* Observation is via callbacks (`onLine`, `onChoiceMade`, `onCommand`, …) so a
|
|
312
|
+
* host can forward to engine events or a history recorder without the Session
|
|
313
|
+
* knowing about either.
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
/** One previewed line: speaker name + plain (markup-stripped) text. */
|
|
317
|
+
interface PreviewedLine {
|
|
318
|
+
readonly speaker?: string | undefined;
|
|
319
|
+
readonly text: string;
|
|
320
|
+
}
|
|
321
|
+
/** Resolved speaker descriptor on a presented line (for nameplates / anchoring). */
|
|
322
|
+
interface SpeakerView {
|
|
323
|
+
readonly id: string;
|
|
324
|
+
readonly name?: string | undefined;
|
|
325
|
+
readonly color?: number | undefined;
|
|
326
|
+
}
|
|
327
|
+
/** A fully-resolved line (i18n + markup already applied) handed to presenters. */
|
|
328
|
+
interface PresentedLine {
|
|
329
|
+
/** Who's speaking (if anyone) — lets world presenters anchor to their actor. */
|
|
330
|
+
readonly speaker?: SpeakerView | undefined;
|
|
331
|
+
readonly text: ParsedText;
|
|
332
|
+
/** Per-line reveal-speed multiplier (`say.speed`, default 1). */
|
|
333
|
+
readonly speed: number;
|
|
334
|
+
/** Opaque preset name for per-line layout/variant (presenter interprets). */
|
|
335
|
+
readonly view?: string | undefined;
|
|
336
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
337
|
+
/** Voice-clip id (audio handler interprets; reveal may sync to it). */
|
|
338
|
+
readonly voice?: string | undefined;
|
|
339
|
+
}
|
|
340
|
+
/** One choice row, resolved to a display label. Position = array index. */
|
|
341
|
+
interface PresentedChoice {
|
|
342
|
+
readonly label: string;
|
|
343
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
344
|
+
/** True for a visible-but-disabled row (its condition failed and its
|
|
345
|
+
* `presentation` is `"disabled"`). Presenters render it greyed and
|
|
346
|
+
* non-selectable; the Session's nav/confirm skip it. */
|
|
347
|
+
readonly disabled?: boolean | undefined;
|
|
348
|
+
/** i18n-resolved reason for a {@link disabled} row, shown beside it where the
|
|
349
|
+
* layout allows (e.g. "Requires the rusty key"). */
|
|
350
|
+
readonly disabledReason?: string | undefined;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Body-text channel. Owns reveal timing (the Session only learns *that* a line
|
|
354
|
+
* finished, via the reveal listener, never *when* a glyph appears). An
|
|
355
|
+
* accessibility / no-typewriter presenter is
|
|
356
|
+
* `present(){ draw(); this.revealListener?.() }`.
|
|
357
|
+
*/
|
|
358
|
+
interface TextChannel {
|
|
359
|
+
present(line: PresentedLine): void;
|
|
360
|
+
/** Reveal everything immediately (skip-to-end). */
|
|
361
|
+
completeReveal(): void;
|
|
362
|
+
isRevealComplete(): boolean;
|
|
363
|
+
isRevealing(): boolean;
|
|
364
|
+
/** Hold-to-fast-forward rate flag (1 = normal). */
|
|
365
|
+
setSpeedMultiplier(multiplier: number): void;
|
|
366
|
+
/**
|
|
367
|
+
* Show or hide the body text **without** disturbing reveal progress — a
|
|
368
|
+
* cutscene can hide mid-typewriter and show again to resume mid-line. Purely
|
|
369
|
+
* visual: the reveal cursor, timers, and the laid-out line are untouched.
|
|
370
|
+
*/
|
|
371
|
+
setVisible(visible: boolean): void;
|
|
372
|
+
update(dt: number): void;
|
|
373
|
+
clear(): void;
|
|
374
|
+
/**
|
|
375
|
+
* Register the reveal-completed listener. The Session owns this seam — it
|
|
376
|
+
* registers its handler once in the ctor — so a game can't clobber the wiring
|
|
377
|
+
* by assigning a public field. Pass `undefined` to clear.
|
|
378
|
+
*/
|
|
379
|
+
setRevealListener(listener: (() => void) | undefined): void;
|
|
380
|
+
/**
|
|
381
|
+
* Register the reveal-beat listener — per-grapheme typewriter ticks and inline
|
|
382
|
+
* `[name k=v/]` markers, in char order, as the reveal cursor reaches each.
|
|
383
|
+
* Session-owned like {@link setRevealListener} (registered once in the ctor);
|
|
384
|
+
* pass `undefined` to clear. A no-typewriter presenter that reveals instantly
|
|
385
|
+
* may omit beats; the bundled view forwards the headless {@link LineReveal}
|
|
386
|
+
* clock's beats.
|
|
387
|
+
*/
|
|
388
|
+
setBeatListener(listener: ((beat: RevealBeat) => void) | undefined): void;
|
|
389
|
+
}
|
|
390
|
+
/** Per-choice presentation context, so a choice list can route/anchor the same
|
|
391
|
+
* way lines do (box list vs a bubble over `speaker`'s actor) and optionally
|
|
392
|
+
* render the prompt itself. */
|
|
393
|
+
interface ChoiceContext {
|
|
394
|
+
readonly view?: string | undefined;
|
|
395
|
+
readonly speaker?: SpeakerView | undefined;
|
|
396
|
+
/** The (resolved + parsed) prompt, made available so a presenter can render
|
|
397
|
+
* it itself (see {@link ChoiceChannel.ownsPrompt}). Empty when no prompt. */
|
|
398
|
+
readonly prompt?: ParsedText | undefined;
|
|
399
|
+
/** The choice step's opaque `meta` bag, passed straight through. A custom
|
|
400
|
+
* presenter reads it to render extras the model doesn't own — e.g. the
|
|
401
|
+
* timed-choice recipe's `{ timeoutMs }` for a countdown. */
|
|
402
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
403
|
+
}
|
|
404
|
+
/** Choice channel. Selection nav lives in the Session; pointer/touch commits
|
|
405
|
+
* come back through `onChoiceChosen(position)`. `context` carries the choice's
|
|
406
|
+
* view/speaker/prompt so a composite presenter can route (box vs bubble). */
|
|
407
|
+
interface ChoiceChannel {
|
|
408
|
+
present(choices: readonly PresentedChoice[], context?: ChoiceContext): void;
|
|
409
|
+
highlight(position: number): void;
|
|
410
|
+
/** Show or hide the choice list without clearing it — state-preserving,
|
|
411
|
+
* so the selection and laid-out rows survive a hide/show round-trip. */
|
|
412
|
+
setVisible(visible: boolean): void;
|
|
413
|
+
clear(): void;
|
|
414
|
+
onChoiceChosen?: (position: number) => void;
|
|
415
|
+
/**
|
|
416
|
+
* If true for this `context`, the presenter draws the prompt itself (e.g. a
|
|
417
|
+
* self-contained bubble panel), so the Session suppresses the chrome + body-
|
|
418
|
+
* text prompt. Default false → the chrome/text show the prompt (box body).
|
|
419
|
+
*/
|
|
420
|
+
ownsPrompt?(context?: ChoiceContext): boolean;
|
|
421
|
+
}
|
|
422
|
+
/** Avatar channel — who's talking + their expression + talk state. */
|
|
423
|
+
interface AvatarChannel {
|
|
424
|
+
setSpeaker(speaker: SpeakerDef | undefined): void;
|
|
425
|
+
setExpression(expression: string | undefined): void;
|
|
426
|
+
setSpeaking(speaking: boolean): void;
|
|
427
|
+
/**
|
|
428
|
+
* Optional per-line hook (mirrors {@link ChromeChannel.present}) so an avatar
|
|
429
|
+
* can be **line-driven**: read the line's `meta` (e.g. `portrait` / `side` /
|
|
430
|
+
* `presence`) to pick an image/side/presence, beyond what `setSpeaker` carries.
|
|
431
|
+
* The Session calls it on each say/choice line alongside `setSpeaker`, and
|
|
432
|
+
* with `undefined` when the conversation clears (stop/end). A reflowing in-box
|
|
433
|
+
* avatar registers a text inset here so the body text reflows around it. Most
|
|
434
|
+
* avatars (portrait, scene-figure) omit it.
|
|
435
|
+
*/
|
|
436
|
+
present?(line: PresentedLine | undefined): void;
|
|
437
|
+
/**
|
|
438
|
+
* Optional inline-marker hook (sibling to {@link present} / {@link setVisible}).
|
|
439
|
+
* The Session fans every `[name k=v/]` reveal marker here so an avatar can
|
|
440
|
+
* interpret the ones it owns — the bundled portrait/scene presenters read
|
|
441
|
+
* `[expression=…/]` and call their own `setExpression`. The Session name-matches
|
|
442
|
+
* NOTHING; an avatar ignores markers it doesn't recognize. `viaSkip` markers
|
|
443
|
+
* (drained by a skip) arrive here too — the avatar collapses to the last one.
|
|
444
|
+
*/
|
|
445
|
+
marker?(marker: MarkerToken): void;
|
|
446
|
+
/** Optional visibility gate — a portrait hides during a cutscene; a
|
|
447
|
+
* scene-figure avatar (a world NPC the game owns) omits it and stays put. */
|
|
448
|
+
setVisible?(visible: boolean): void;
|
|
449
|
+
update(dt: number): void;
|
|
450
|
+
}
|
|
451
|
+
/** Chrome channel — frame / nameplate / continue caret (everything but body
|
|
452
|
+
* text and choices). `present?` lets a chrome react to per-line variants;
|
|
453
|
+
* `present(undefined)` means "no line — clear the chrome's content". */
|
|
454
|
+
interface ChromeChannel {
|
|
455
|
+
/** Set the speaker name, or `undefined` for **no name** — NOT a covert
|
|
456
|
+
* hide-all; visibility is governed solely by {@link setVisible}. */
|
|
457
|
+
setNameplate(name: string | undefined, color?: number): void;
|
|
458
|
+
setContinueVisible(visible: boolean): void;
|
|
459
|
+
/**
|
|
460
|
+
* Show or hide the whole chrome — the honest visibility verb the Session
|
|
461
|
+
* drives (a composite restores its active variant on show). State-preserving.
|
|
462
|
+
*/
|
|
463
|
+
setVisible(visible: boolean): void;
|
|
464
|
+
/**
|
|
465
|
+
* React to a per-line variant (`meta.chrome`, `meta.position`). Called
|
|
466
|
+
* **before** {@link TextChannel.present} for the same line, so a composite
|
|
467
|
+
* selects its active variant and a layout owner commits the frame the text
|
|
468
|
+
* then reads. `present(undefined)` means "no line — clear the chrome's content".
|
|
469
|
+
*/
|
|
470
|
+
present?(line: PresentedLine | undefined): void;
|
|
471
|
+
update(dt: number): void;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The presentation channels a {@link DialogueSession} drives. Writing a custom
|
|
475
|
+
* presenter (a DOM overlay, a ui-react chrome) means implementing these — so the
|
|
476
|
+
* **call-order contract** the Session guarantees is documented here, on the
|
|
477
|
+
* interfaces themselves:
|
|
478
|
+
*
|
|
479
|
+
* Per say line, the Session calls (in this order):
|
|
480
|
+
* 1. `chrome.setNameplate(name)` / `chrome.setContinueVisible(false)`
|
|
481
|
+
* 2. `avatar.setSpeaker` / `setExpression` / `setSpeaking`, then `avatar.present?(line)`
|
|
482
|
+
* 3. `chrome.present?(line)` — **before** the text, so a composite picks the
|
|
483
|
+
* active variant first and a layout owner commits the frame the text reads
|
|
484
|
+
* 4. `text.present(line)` — render + start revealing
|
|
485
|
+
* 5. (each channel's `setVisible(shown)` reflects the host-hidden lever)
|
|
486
|
+
*
|
|
487
|
+
* Then, exactly once, when the line finishes revealing, the text channel fires
|
|
488
|
+
* the listener registered via {@link TextChannel.setRevealListener} (the
|
|
489
|
+
* Session owns that seam — never a public field). For an **empty** line the
|
|
490
|
+
* completion fires synchronously inside `text.present`.
|
|
491
|
+
*
|
|
492
|
+
* Per choice the order mirrors a line (chrome/avatar/text for the optional
|
|
493
|
+
* prompt) and then `choices.present(options, context)`. If a presenter
|
|
494
|
+
* {@link ChoiceChannel.ownsPrompt | owns the prompt}, the Session clears the
|
|
495
|
+
* chrome + body instead of step 3/4.
|
|
496
|
+
*
|
|
497
|
+
* `setBox`/geometry is always applied **before** `present` for that line (a
|
|
498
|
+
* presenter that lays out off a region reads the committed region in `present`).
|
|
499
|
+
*/
|
|
500
|
+
interface DialogueChannels {
|
|
501
|
+
readonly text: TextChannel;
|
|
502
|
+
readonly choices: ChoiceChannel;
|
|
503
|
+
readonly avatar?: AvatarChannel | undefined;
|
|
504
|
+
readonly chrome?: ChromeChannel | undefined;
|
|
505
|
+
}
|
|
506
|
+
interface DialogueSessionOptions {
|
|
507
|
+
readonly i18n?: I18nAdapter | undefined;
|
|
508
|
+
/** Hold-to-fast-forward multiplier. Default 4. */
|
|
509
|
+
readonly skipMultiplier?: number | undefined;
|
|
510
|
+
/**
|
|
511
|
+
* The variable storage installed for every `play()`. Persists across
|
|
512
|
+
* plays. Omit for a zero-config {@link MemoryVariableStorage}; supply your own
|
|
513
|
+
* or {@link compose} several to bridge game state. A per-`play()`
|
|
514
|
+
* `overrides.storage` replaces it for that conversation.
|
|
515
|
+
*/
|
|
516
|
+
readonly storage?: VariableStorage | undefined;
|
|
517
|
+
/** Argument-capable read functions (`has_item("key")`) for conditions/`set`
|
|
518
|
+
* expressions. Per-`play()` `overrides.functions` merge on top. */
|
|
519
|
+
readonly functions?: Readonly<Record<string, DialogueFunction>> | undefined;
|
|
520
|
+
/** Command handlers (`type` → handler). Per-`play()` `overrides.commands`
|
|
521
|
+
* merge on top (call site wins). */
|
|
522
|
+
readonly commands?: Readonly<Record<string, CommandHandler>> | undefined;
|
|
523
|
+
/** Catch-all for command types with no explicit handler. */
|
|
524
|
+
readonly fallbackCommand?: CommandHandler | undefined;
|
|
525
|
+
/** Non-fatal runtime diagnostics (e.g. a `set` to a read-only `cells`
|
|
526
|
+
* accessor that was ignored). The controller routes these to the engine
|
|
527
|
+
* logger; a bare session may handle or drop them. */
|
|
528
|
+
readonly onError?: ((message: string, error: unknown) => void) | undefined;
|
|
529
|
+
readonly onStarted?: (e: {
|
|
530
|
+
scriptId: string;
|
|
531
|
+
}) => void;
|
|
532
|
+
/** Plain (markup-stripped) line text — for logs / a11y / history. */
|
|
533
|
+
readonly onLine?: (e: {
|
|
534
|
+
speaker?: string | undefined;
|
|
535
|
+
text: string;
|
|
536
|
+
}) => void;
|
|
537
|
+
readonly onChoiceShown?: (e: {
|
|
538
|
+
options: readonly string[];
|
|
539
|
+
}) => void;
|
|
540
|
+
readonly onChoiceMade?: (e: {
|
|
541
|
+
index: number;
|
|
542
|
+
text: string;
|
|
543
|
+
}) => void;
|
|
544
|
+
/**
|
|
545
|
+
* Observation hook fired for every non-built-in command (`set` is runner-owned
|
|
546
|
+
* and never reaches it) — the host forwards it to {@link DialogueCommandEvent}.
|
|
547
|
+
* The actual *handling* (and any `blocking` await) is the binding's `commands`
|
|
548
|
+
* map / `fallbackCommand`, not this; this returns nothing.
|
|
549
|
+
*/
|
|
550
|
+
readonly onCommand?: (command: Command, ctx: CommandContext) => void;
|
|
551
|
+
readonly onEnded?: (e: {
|
|
552
|
+
scriptId: string;
|
|
553
|
+
}) => void;
|
|
554
|
+
/** A line finished its typewriter reveal — the "typing finished" hook
|
|
555
|
+
* (audio blip, etc.). Plain (markup-stripped) text, like {@link onLine}. */
|
|
556
|
+
readonly onRevealCompleted?: (e: {
|
|
557
|
+
speaker?: string | undefined;
|
|
558
|
+
text: string;
|
|
559
|
+
}) => void;
|
|
560
|
+
/**
|
|
561
|
+
* Per-grapheme typewriter tick — a direct CALLBACK only (NOT forwarded to an
|
|
562
|
+
* entity event; it fires hundreds of times per line). `index` is the 0-based
|
|
563
|
+
* grapheme index just revealed, raw (whitespace included — the host filters if
|
|
564
|
+
* it only wants a blip on visible glyphs). Wire a typewriter SFX here. Not
|
|
565
|
+
* fired on a skip / fast-forward (pending ticks are discarded).
|
|
566
|
+
*/
|
|
567
|
+
readonly onRevealTick?: ((index: number) => void) | undefined;
|
|
568
|
+
/**
|
|
569
|
+
* An inline `[name k=v/]` marker reached its char offset during reveal — the
|
|
570
|
+
* host forwards it to {@link DialogueRevealMarkerEvent}. `viaSkip` is true when
|
|
571
|
+
* a skip/complete drained it (a host can suppress a loud one-shot that only
|
|
572
|
+
* fired because the player skipped). The Session name-matches NO marker: the
|
|
573
|
+
* avatar channel interprets `[expression=…/]` itself; every other name flows
|
|
574
|
+
* opaquely to the host / registered channels.
|
|
575
|
+
*/
|
|
576
|
+
readonly onRevealMarker?: (marker: MarkerToken, viaSkip: boolean) => void;
|
|
577
|
+
/** The choice cursor moved — keyboard nav AND pointer hover both funnel here
|
|
578
|
+
* `index` is the resolved option index, `text` its plain label. */
|
|
579
|
+
readonly onSelectionChanged?: (e: {
|
|
580
|
+
index: number;
|
|
581
|
+
text: string;
|
|
582
|
+
}) => void;
|
|
583
|
+
/** The player skipped the current section — for skip-used analytics. */
|
|
584
|
+
readonly onSkipUsed?: (e: {
|
|
585
|
+
scriptId: string;
|
|
586
|
+
}) => void;
|
|
587
|
+
/** A line auto-advanced via the auto-advance clock — distinct from a
|
|
588
|
+
* manual advance so a game can tell them apart. */
|
|
589
|
+
readonly onAutoAdvance?: (e: {
|
|
590
|
+
scriptId: string;
|
|
591
|
+
}) => void;
|
|
592
|
+
}
|
|
593
|
+
declare class DialogueSession {
|
|
594
|
+
private readonly channels;
|
|
595
|
+
private readonly opts;
|
|
596
|
+
private readonly i18n;
|
|
597
|
+
private readonly skipMul;
|
|
598
|
+
/** Controller-installed environment (persists across plays); per-`play()`
|
|
599
|
+
* overrides are layered on top into the resolved fields below. */
|
|
600
|
+
private readonly defaultStorage;
|
|
601
|
+
private readonly defaultFunctions;
|
|
602
|
+
private readonly defaultCommands;
|
|
603
|
+
private readonly defaultFallback;
|
|
604
|
+
private storage;
|
|
605
|
+
private functions;
|
|
606
|
+
private commands;
|
|
607
|
+
private fallbackCommand;
|
|
608
|
+
private runner;
|
|
609
|
+
private script;
|
|
610
|
+
private mode;
|
|
611
|
+
private scriptId;
|
|
612
|
+
private saying;
|
|
613
|
+
private autoTimer;
|
|
614
|
+
/** Default auto-advance delay (ms) applied to lines without their own
|
|
615
|
+
* `autoAdvanceMs`. `null` = off (manual advance). Set via {@link setAutoAdvance}. */
|
|
616
|
+
private autoAdvanceDefault;
|
|
617
|
+
private resolved;
|
|
618
|
+
private selected;
|
|
619
|
+
/** Count of in-flight blocking line-command batches (show/afterReveal/advance).
|
|
620
|
+
* Input is gated while > 0. An ownership counter (not a shared boolean) so an
|
|
621
|
+
* overlapping batch resolving — e.g. the afterReveal batch finishing while a
|
|
622
|
+
* long blocking `show` command is still awaited — can't drop a gate it
|
|
623
|
+
* doesn't own. */
|
|
624
|
+
private blockedCount;
|
|
625
|
+
/** True between an advance request and the runner stepping off the line —
|
|
626
|
+
* guards against a second advance double-firing `advance`-timed commands. */
|
|
627
|
+
private advancing;
|
|
628
|
+
/** True once the current line's `advance`-timed commands have fired, so a
|
|
629
|
+
* second advance() while the runner is still stepping (e.g. awaiting a
|
|
630
|
+
* blocking command step) can't re-fire them against the stale line. */
|
|
631
|
+
private advanceFired;
|
|
632
|
+
/** True once the current line's `afterReveal`-timed commands have fired
|
|
633
|
+
* (normally via handleRevealComplete; skip() fires them early when the line
|
|
634
|
+
* hasn't finished revealing). */
|
|
635
|
+
private afterRevealFired;
|
|
636
|
+
/** Latched by the first confirm() until the runner produces its next state
|
|
637
|
+
* (handleSay/handleChoice/handleEnd), so mashing confirm while the runner
|
|
638
|
+
* awaits the option's blocking commands can't emit duplicate onChoiceMade
|
|
639
|
+
* events (`mode` stays "choosing" for that whole window). */
|
|
640
|
+
private confirming;
|
|
641
|
+
/** Bumped by every stop()/play(). A suspended async continuation from a prior
|
|
642
|
+
* conversation captures this and bails on resume if it changed, so it can't
|
|
643
|
+
* drive (advance / show the caret on) the runner of a *new* conversation. */
|
|
644
|
+
private generation;
|
|
645
|
+
/** `setHidden` — visual only; gates every channel's `setVisible`. Survives
|
|
646
|
+
* `stop()`/`play()` (a host that hides for a cutscene and forgets to unhide
|
|
647
|
+
* gets what it asked for) — so it is deliberately NOT reset by `stop()`. */
|
|
648
|
+
private hidden;
|
|
649
|
+
/** `setPaused` — freezes the update loop AND the input-agnostic API. State is
|
|
650
|
+
* left fully intact (no generation bump); also host-level, survives replays. */
|
|
651
|
+
private paused;
|
|
652
|
+
/** Whether the chrome / body text are part of the CURRENT choice's layout
|
|
653
|
+
* (false when a self-contained bubble panel owns the prompt) — remembered so
|
|
654
|
+
* {@link applyVisibility} can recompute after a hide toggle mid-choice. */
|
|
655
|
+
private choiceShowsChrome;
|
|
656
|
+
private choiceShowsBody;
|
|
657
|
+
/** Plain (speaker, text) of the line on screen — for the reveal-completed
|
|
658
|
+
* event, which fires after `present` has discarded the resolved string. */
|
|
659
|
+
private currentLine;
|
|
660
|
+
/** The full {@link PresentedLine} on screen — handed to an extra channel's
|
|
661
|
+
* `revealComplete` (the session discards the local `line` after present). */
|
|
662
|
+
private currentPresented;
|
|
663
|
+
/** Host-registered extra channels (Voice / Shop / CameraEffects / History).
|
|
664
|
+
* The session fans its cross-cutting stream to these alongside the typed trio
|
|
665
|
+
* and folds their `isRevealComplete()` into the auto-advance gate. */
|
|
666
|
+
private readonly extras;
|
|
667
|
+
constructor(channels: DialogueChannels, opts?: DialogueSessionOptions);
|
|
668
|
+
/**
|
|
669
|
+
* Begin a conversation. `play(script)` is **content-only** — the storage,
|
|
670
|
+
* functions, and commands are installed on the session; `overrides` layers
|
|
671
|
+
* per-conversation specifics on top (a scoped `storage`, extra `functions` /
|
|
672
|
+
* `commands`). Declared defaults seed into the storage **only if absent**
|
|
673
|
+
* (game-linked values win); variables persist across plays. Returns a
|
|
674
|
+
* generation-stamped {@link DialogueHandle} for live `setVar` / `getVars`.
|
|
675
|
+
*/
|
|
676
|
+
play<S extends DialogueScript>(rawScript: S, overrides?: DialoguePlayOptions): DialogueHandle<VarsOf<S>>;
|
|
677
|
+
isActive(): boolean;
|
|
678
|
+
isChoosing(): boolean;
|
|
679
|
+
/**
|
|
680
|
+
* Register an extra channel (Voice / Shop / CameraEffects / History) — the
|
|
681
|
+
* open-ended companion to the built-in trio. It receives the cross-cutting
|
|
682
|
+
* stream (`present` / `command` / `clear` / `setVisible` / `setPaused` /
|
|
683
|
+
* `completeReveal` / `update`) and can gate auto-advance via
|
|
684
|
+
* `isRevealComplete()`. Returns a disposer that unregisters **and** disposes
|
|
685
|
+
* it. On register the channel catches up the current `setVisible` / `setPaused`
|
|
686
|
+
* lever state ONLY — no content replay (replaying `present` would re-trigger a
|
|
687
|
+
* voice clip). Safe to call mid-conversation.
|
|
688
|
+
*/
|
|
689
|
+
addChannel(ch: DialogueExtraChannel): () => void;
|
|
690
|
+
/**
|
|
691
|
+
* Hide or show the whole dialogue UI — purely visual, state-preserving.
|
|
692
|
+
* Drives every channel's `setVisible`; the conversation keeps running
|
|
693
|
+
* underneath (reveal, timers, cursor intact). Host-level and **persistent**:
|
|
694
|
+
* it survives `stop()` and the next `play()`, so a cutscene that hides and
|
|
695
|
+
* forgets to unhide stays hidden — call `setHidden(false)` to restore.
|
|
696
|
+
*/
|
|
697
|
+
setHidden(hidden: boolean): void;
|
|
698
|
+
/** True while the UI is hidden via {@link setHidden}. */
|
|
699
|
+
isHidden(): boolean;
|
|
700
|
+
/**
|
|
701
|
+
* Freeze or resume the conversation — `update()` no-ops (reveal,
|
|
702
|
+
* auto-advance clock, caret blink, avatar anim all freeze since they are
|
|
703
|
+
* dt-driven) and the input-agnostic API (`advance`/`confirm`/`choose`/
|
|
704
|
+
* `moveSelection`/`selectAt`/`skip`) no-ops. State is left fully intact: no
|
|
705
|
+
* generation bump, and `lineBlocked`/`advancing`/`autoTimer` survive. It does
|
|
706
|
+
* NOT block host-driven writes (`handle.setVar` / `ctx.setVar` / storage) —
|
|
707
|
+
* only player-facing time + input freeze. Host-level and persistent like hide.
|
|
708
|
+
*/
|
|
709
|
+
setPaused(paused: boolean): void;
|
|
710
|
+
/** True while the conversation is frozen via {@link setPaused}. */
|
|
711
|
+
isPaused(): boolean;
|
|
712
|
+
/**
|
|
713
|
+
* Push the current desired visibility to every channel: a channel shows when
|
|
714
|
+
* it has content for the current mode AND the host hasn't hidden the UI. The
|
|
715
|
+
* single visibility authority — `setHidden` and every line/choice transition
|
|
716
|
+
* route through here, so the host-hidden lever composes cleanly with per-line
|
|
717
|
+
* content and a custom chrome (which may not implement the optional `present`)
|
|
718
|
+
* is still reliably hidden by the explicit `setVisible(false)`.
|
|
719
|
+
*/
|
|
720
|
+
private applyVisibility;
|
|
721
|
+
/** Clear every presentation channel's content — text, choices, chrome
|
|
722
|
+
* (nameplate + caret + line), the avatar, and the registered extras. The
|
|
723
|
+
* shared teardown for {@link stop} and the ended state; it touches no session
|
|
724
|
+
* bookkeeping or visibility (the caller resets its own state, then
|
|
725
|
+
* {@link goIdle} reasserts visibility). */
|
|
726
|
+
private clearAllChannels;
|
|
727
|
+
/** Drop to a quiescent presentation state (`mode` "idle" or "ended"): reset the
|
|
728
|
+
* per-line/choice bookkeeping, clear every channel, and reassert visibility
|
|
729
|
+
* (idle/ended → nothing shown, honestly via each channel's `setVisible`,
|
|
730
|
+
* preserving the host-hidden lever). The caller owns any further reset —
|
|
731
|
+
* {@link stop} also abandons the runner + timing latches. */
|
|
732
|
+
private goIdle;
|
|
733
|
+
/** Abandon the current conversation and reset to idle (clears all visuals).
|
|
734
|
+
* Useful for ambient/eavesdrop dialogue that should stop when out of range. */
|
|
735
|
+
stop(): void;
|
|
736
|
+
update(dt: number): void;
|
|
737
|
+
/** True while any blocking line-command batch is awaited (input is gated). */
|
|
738
|
+
private get lineBlocked();
|
|
739
|
+
/**
|
|
740
|
+
* The auto-advance gate: the text reveal AND every registered extra channel
|
|
741
|
+
* that *gates* (implements `isRevealComplete`) report complete. The clock is
|
|
742
|
+
* armed on the text reveal alone (see `handleRevealComplete`/`setAutoAdvance`)
|
|
743
|
+
* but only counts down once this is true — so a voice clip outlasting the
|
|
744
|
+
* typewriter holds the line for `max(clipEnd, revealEnd)` with no duration
|
|
745
|
+
* plumbing. A channel without the method never gates (a pure observer).
|
|
746
|
+
*/
|
|
747
|
+
private allRevealsComplete;
|
|
748
|
+
/**
|
|
749
|
+
* The storage read view for `{token}` interpolation at *this* present-time.
|
|
750
|
+
* Materialized per evaluation so an earlier command's `set` shows up on a
|
|
751
|
+
* later line; already-shown lines never re-render.
|
|
752
|
+
*/
|
|
753
|
+
private readView;
|
|
754
|
+
/** Primary action. Saying → reveal-all if typing, else next line. Choosing → confirm. */
|
|
755
|
+
advance(): void;
|
|
756
|
+
/** Fire any `advance`-timed line commands, then step the runner off the line.
|
|
757
|
+
* `advancing` is held for the whole turn so a second advance can't re-fire
|
|
758
|
+
* the (possibly non-blocking) `advance` commands before the runner steps. */
|
|
759
|
+
private advanceLine;
|
|
760
|
+
/**
|
|
761
|
+
* Fast-forward the current section: run intervening commands (in skip mode)
|
|
762
|
+
* without presenting, stopping at the next choice or the end. No-op unless a
|
|
763
|
+
* line is showing.
|
|
764
|
+
*/
|
|
765
|
+
skip(): void;
|
|
766
|
+
/**
|
|
767
|
+
* Fire the *displayed* line's not-yet-fired batches in skip mode — the runner
|
|
768
|
+
* fires every skipped line's commands for world reconstruction, so dropping
|
|
769
|
+
* the current line's `afterReveal`/`advance` batches would diverge from
|
|
770
|
+
* normal play — then fast-forward the runner.
|
|
771
|
+
*/
|
|
772
|
+
private skipLine;
|
|
773
|
+
/**
|
|
774
|
+
* Side-effect-free lookahead: the lines a node would show along its linear
|
|
775
|
+
* path — following `goto` and conditional `command` jumps using the *current*
|
|
776
|
+
* variable snapshot — stopping at the first choice or the end. Runs no
|
|
777
|
+
* commands and mutates nothing. For a "skip with a summary" affordance.
|
|
778
|
+
*/
|
|
779
|
+
preview(nodeId: string, limit?: number): PreviewedLine[];
|
|
780
|
+
/** Move the choice cursor by `delta`, skipping disabled rows and wrapping.
|
|
781
|
+
* No-op outside a choice, and a zero `delta` is a no-op (no cursor move, no
|
|
782
|
+
* event). A move that steps over disabled rows fires exactly one
|
|
783
|
+
* selection-changed event, for the row it lands on. */
|
|
784
|
+
moveSelection(delta: number): void;
|
|
785
|
+
/** Highlight a choice by absolute position (e.g. pointer hover). No wrap;
|
|
786
|
+
* a disabled row is skipped (the cursor stays put). */
|
|
787
|
+
selectAt(position: number): void;
|
|
788
|
+
/** The next enabled choice position from `from` in direction `dir` (±1),
|
|
789
|
+
* wrapping. Returns `from` when no other enabled row exists (so a single
|
|
790
|
+
* enabled option among disabled ones never moves). */
|
|
791
|
+
private nextEnabled;
|
|
792
|
+
/** Fire onSelectionChanged for the currently-highlighted choice (keyboard nav
|
|
793
|
+
* and pointer hover both land here — one canonical selection event). */
|
|
794
|
+
private emitSelectionChanged;
|
|
795
|
+
/** Commit the highlighted choice. */
|
|
796
|
+
confirm(): void;
|
|
797
|
+
/** Commit by original option index (e.g. a direct pointer hit, or the
|
|
798
|
+
* timed-choice recipe firing its default). Refuses an unknown or disabled
|
|
799
|
+
* option. */
|
|
800
|
+
choose(optionIndex: number): void;
|
|
801
|
+
/** Commit by display position (a pointer hit on a row). The position-keyed
|
|
802
|
+
* counterpart to {@link choose}; refuses an out-of-range or disabled row.
|
|
803
|
+
* Used by the pointer binding and the presenter pointer-commit seam. */
|
|
804
|
+
confirmAt(position: number): void;
|
|
805
|
+
/**
|
|
806
|
+
* The single choice-commit authority: every commit path routes here after
|
|
807
|
+
* translating its argument to a display position. It guards (paused / not
|
|
808
|
+
* choosing / already confirming / a missing or disabled row), latches against a
|
|
809
|
+
* double-commit, fires `onChoiceMade`, and steps the runner. The latch (not the
|
|
810
|
+
* runner) is what guarantees a single commit: `mode` stays "choosing" while the
|
|
811
|
+
* runner awaits the option's blocking commands, so without it a second confirm
|
|
812
|
+
* would emit a duplicate `onChoiceMade`.
|
|
813
|
+
*/
|
|
814
|
+
private commit;
|
|
815
|
+
/** Toggle hold-to-fast-forward; the text channel scales its reveal rate. */
|
|
816
|
+
setFastForward(on: boolean): void;
|
|
817
|
+
/**
|
|
818
|
+
* Default auto-advance: lines without their own `autoAdvanceMs` advance `ms`
|
|
819
|
+
* after they finish revealing; `null` turns it off (manual advance). A
|
|
820
|
+
* per-line `autoAdvanceMs` always overrides this. Toggling it while a line is
|
|
821
|
+
* already sitting revealed arms/clears its timer immediately.
|
|
822
|
+
*/
|
|
823
|
+
setAutoAdvance(ms: number | null): void;
|
|
824
|
+
private handleSay;
|
|
825
|
+
private handleChoice;
|
|
826
|
+
private handleCommand;
|
|
827
|
+
private handleEnd;
|
|
828
|
+
private handleRevealComplete;
|
|
829
|
+
/**
|
|
830
|
+
* Fan one reveal beat out — the per-line typewriter stream. Extras (Voice /
|
|
831
|
+
* CameraEffects / a typewriter-SFX channel) see the WHOLE stream via
|
|
832
|
+
* `revealBeat?`. A `tick` then reaches the host's `onRevealTick` callback; a
|
|
833
|
+
* `marker` reaches the avatar channel (which interprets `[expression=…/]`
|
|
834
|
+
* itself — the Session name-matches nothing) and the host's `onRevealMarker`.
|
|
835
|
+
*/
|
|
836
|
+
private handleRevealBeat;
|
|
837
|
+
/**
|
|
838
|
+
* Fire the current line's commands matching `at`, via the runner's command
|
|
839
|
+
* pipeline (so `set`/blocking behave identically). While a blocking one is
|
|
840
|
+
* awaited, `lineBlocked` gates input so the player can't advance through it.
|
|
841
|
+
* `mode` overrides the runner's run mode (skip() fires the displayed line's
|
|
842
|
+
* batches in skip mode).
|
|
843
|
+
*/
|
|
844
|
+
private fireLineCommands;
|
|
845
|
+
private speakerName;
|
|
846
|
+
private speakerView;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Adapter-level presenter contracts. The headless {@link DialogueChannels}
|
|
851
|
+
* (core) describe *what* a chrome / choice presenter does; these add the YAGE
|
|
852
|
+
* lifecycle (`mount(scene)` / `dispose()`) the host drives. Concrete renderer
|
|
853
|
+
* presenters ({@link DialogueChrome}, {@link ChoiceListPresenter}) implement
|
|
854
|
+
* these; a ui-react / DOM chrome you write later implements the same shape.
|
|
855
|
+
*/
|
|
856
|
+
|
|
857
|
+
/** A dev-facing diagnostics sink — the controller wires this to the engine
|
|
858
|
+
* Logger (the same seam as the session's `onError`), so a presenter-level
|
|
859
|
+
* warning (e.g. a missing actor) routes there instead of `console.warn`. */
|
|
860
|
+
type DiagnosticSink = (message: string) => void;
|
|
861
|
+
/** YAGE lifecycle shared by the renderer-based presenters. */
|
|
862
|
+
interface Mountable {
|
|
863
|
+
mount(scene: Scene): void;
|
|
864
|
+
dispose(): void;
|
|
865
|
+
/** Optional: receive a diagnostics sink. The controller injects one at
|
|
866
|
+
* mount so a presenter can report dev-facing issues (a missing actor) through
|
|
867
|
+
* the engine Logger. Presenters with nothing to report omit it. */
|
|
868
|
+
setDiagnostics?(warn: DiagnosticSink): void;
|
|
869
|
+
}
|
|
870
|
+
/** Frame / nameplate / continue caret. `setVisible` (now part of
|
|
871
|
+
* {@link ChromeChannel}) lets a composite chrome show/hide a whole variant
|
|
872
|
+
* (e.g. hide the box while a bubble line plays). */
|
|
873
|
+
interface ChromePresenter extends ChromeChannel, Mountable {
|
|
874
|
+
}
|
|
875
|
+
/** The choice list / wheel / panel. Optionally hit-tests pointer coords so a
|
|
876
|
+
* pointer binding can hover/click rows ({@link PointerChoiceTarget}). The
|
|
877
|
+
* hit-test (and `pointerSpace`) are read in screen space by default; a
|
|
878
|
+
* world-anchored presenter (e.g. a bubble) sets `pointerSpace: "world"`. */
|
|
879
|
+
interface ChoicePresenter extends ChoiceChannel, Mountable {
|
|
880
|
+
choiceAtPoint?(x: number, y: number): number | undefined;
|
|
881
|
+
/** Coordinate space `choiceAtPoint` expects. Default "screen". */
|
|
882
|
+
readonly pointerSpace?: "screen" | "world";
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Body-text presenter: the headless {@link TextChannel} (reveal timing) plus the
|
|
886
|
+
* YAGE lifecycle the host drives.
|
|
887
|
+
*
|
|
888
|
+
* This contract lives in this pixi-free adapter module — NOT on the concrete,
|
|
889
|
+
* renderer-backed `DialogueTextView` — so the headless root entry (`"."`) can
|
|
890
|
+
* reach it through the `DialogueController` without transitively importing
|
|
891
|
+
* `@yagejs/renderer`. The renderer-backed views only *implement* this shape.
|
|
892
|
+
*/
|
|
893
|
+
interface TextPresenter extends TextChannel, Mountable {
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* The avatar layer is deliberately decoupled from the box: the controller only
|
|
898
|
+
* ever talks to this interface, so a speaker can be shown as a portrait beside
|
|
899
|
+
* the box, as a full figure already standing in the scene, or not at all —
|
|
900
|
+
* without the dialogue runtime knowing which. Implementations live alongside
|
|
901
|
+
* (PortraitPresenter, SceneFigurePresenter); swap freely or compose your own.
|
|
902
|
+
*/
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* The adapter-level avatar presenter: the headless {@link AvatarChannel}
|
|
906
|
+
* (setSpeaker / setExpression / setSpeaking / marker / update) plus the YAGE
|
|
907
|
+
* lifecycle the host drives (mount / dispose).
|
|
908
|
+
*/
|
|
909
|
+
interface AvatarPresenter extends AvatarChannel {
|
|
910
|
+
/** Called once when the controller mounts. */
|
|
911
|
+
mount(scene: Scene): void;
|
|
912
|
+
dispose(): void;
|
|
913
|
+
}
|
|
914
|
+
/** No-op presenter — the default when a script has no avatars. */
|
|
915
|
+
declare class NullAvatarPresenter implements AvatarPresenter {
|
|
916
|
+
mount(): void;
|
|
917
|
+
setSpeaker(): void;
|
|
918
|
+
setExpression(): void;
|
|
919
|
+
setSpeaking(): void;
|
|
920
|
+
update(): void;
|
|
921
|
+
dispose(): void;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Input is externalised so it's obviously *optional* and obviously *swappable*.
|
|
926
|
+
* A `DialogueSession` exposes an input-agnostic API (`advance / moveSelection /
|
|
927
|
+
* setFastForward`); an {@link InputBinding} is whatever maps a device onto it.
|
|
928
|
+
* The default {@link KeyboardInputBinding} polls the YAGE `InputManager` action
|
|
929
|
+
* map. An ambient (auto-advancing) conversation simply attaches no binding; a
|
|
930
|
+
* touch/gamepad/pointer binding is a parallel implementation of this interface.
|
|
931
|
+
*/
|
|
932
|
+
|
|
933
|
+
interface DialogueActions {
|
|
934
|
+
/** Tap → reveal-all if typing, else next line / confirm choice. */
|
|
935
|
+
readonly advance: readonly string[];
|
|
936
|
+
/** Hold → fast-forward the typewriter. */
|
|
937
|
+
readonly speed: readonly string[];
|
|
938
|
+
readonly up: readonly string[];
|
|
939
|
+
readonly down: readonly string[];
|
|
940
|
+
/** Tap → skip the current section to the next choice/end. Unbound by default. */
|
|
941
|
+
readonly skip?: readonly string[];
|
|
942
|
+
}
|
|
943
|
+
declare const DEFAULT_ACTIONS: DialogueActions;
|
|
944
|
+
/** Keyboard actions with skip bound (the game maps `skip` → KeyX in main.ts). */
|
|
945
|
+
declare const FULL_ACTIONS: DialogueActions;
|
|
946
|
+
interface InputBinding {
|
|
947
|
+
/**
|
|
948
|
+
* Wire a device to a session. Called by the host once both exist. A binding
|
|
949
|
+
* has a single owner: re-binding re-targets it (implementations must release
|
|
950
|
+
* any resources held for the previous bind) — don't share one instance
|
|
951
|
+
* between two live controllers.
|
|
952
|
+
*/
|
|
953
|
+
bind(input: InputManager, session: DialogueSession): void;
|
|
954
|
+
/** Poll the device and drive the session. Called once per frame by the host. */
|
|
955
|
+
poll(): void;
|
|
956
|
+
/** Optional teardown (e.g. unsubscribe pointer listeners). */
|
|
957
|
+
dispose?(): void;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* A presenter that can resolve a pointer point to a choice position — lets a
|
|
961
|
+
* pointer binding pick/hover choices without owning their geometry. Coords are
|
|
962
|
+
* in `pointerSpace` ("screen" default; a bubble/world list uses "world").
|
|
963
|
+
*/
|
|
964
|
+
interface PointerChoiceTarget {
|
|
965
|
+
/** Position of the choice row under this point, or undefined. Omit for no
|
|
966
|
+
* pointer hit-testing. */
|
|
967
|
+
choiceAtPoint?(x: number, y: number): number | undefined;
|
|
968
|
+
readonly pointerSpace?: "screen" | "world";
|
|
969
|
+
}
|
|
970
|
+
/** Fan a single session out to several device bindings (keyboard + pointer …). */
|
|
971
|
+
declare class CompositeInputBinding implements InputBinding {
|
|
972
|
+
private readonly bindings;
|
|
973
|
+
constructor(bindings: readonly InputBinding[]);
|
|
974
|
+
bind(input: InputManager, session: DialogueSession): void;
|
|
975
|
+
poll(): void;
|
|
976
|
+
dispose(): void;
|
|
977
|
+
}
|
|
978
|
+
/** Keyboard/gamepad action-map binding (the default). */
|
|
979
|
+
declare class KeyboardInputBinding implements InputBinding {
|
|
980
|
+
private readonly actions;
|
|
981
|
+
private readonly skipHoldMs;
|
|
982
|
+
private input?;
|
|
983
|
+
private session?;
|
|
984
|
+
/** Latch so a held skip fires once per hold, not every frame past threshold. */
|
|
985
|
+
private skipFired;
|
|
986
|
+
/**
|
|
987
|
+
* @param skipHoldMs Hold the `skip` action this long before it fires (the
|
|
988
|
+
* classic "hold to skip" confirm). `0` (default) fires on press.
|
|
989
|
+
*/
|
|
990
|
+
constructor(actions?: DialogueActions, skipHoldMs?: number);
|
|
991
|
+
bind(input: InputManager, session: DialogueSession): void;
|
|
992
|
+
poll(): void;
|
|
993
|
+
/** Fire skip once the action has been held `skipHoldMs` (hold-to-confirm),
|
|
994
|
+
* re-arming only after it's released. */
|
|
995
|
+
private pollSkip;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Mouse/touch binding. A tap during a line advances (reveal-all, then next);
|
|
999
|
+
* a tap on a choice row picks it, and hover highlights it — provided a
|
|
1000
|
+
* {@link PointerChoiceTarget} is supplied so the binding can hit-test rows.
|
|
1001
|
+
* Works for both mouse and touch since it rides the unified pointer stream.
|
|
1002
|
+
*/
|
|
1003
|
+
declare class PointerInputBinding implements InputBinding {
|
|
1004
|
+
private input?;
|
|
1005
|
+
private session?;
|
|
1006
|
+
private unsub;
|
|
1007
|
+
/** A primary-button press happened since the last poll (consumed in poll). */
|
|
1008
|
+
private clicked;
|
|
1009
|
+
private readonly choices;
|
|
1010
|
+
/** Pointer position at the last hover hit-test, so an unmoved pointer
|
|
1011
|
+
* doesn't re-run the hit-test every frame. */
|
|
1012
|
+
private lastX;
|
|
1013
|
+
private lastY;
|
|
1014
|
+
/** Whether the previous poll saw a choice up (a fresh choice set must be
|
|
1015
|
+
* hit-tested even under a stationary pointer). */
|
|
1016
|
+
private wasChoosing;
|
|
1017
|
+
constructor(choices?: PointerChoiceTarget);
|
|
1018
|
+
bind(input: InputManager, session: DialogueSession): void;
|
|
1019
|
+
/** Pointer position in the choice presenter's coordinate space. */
|
|
1020
|
+
private pointer;
|
|
1021
|
+
poll(): void;
|
|
1022
|
+
dispose(): void;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* The full control set in one binding: keyboard/gamepad (advance / fast-forward
|
|
1026
|
+
* hold / choice nav / skip) **and** mouse/touch (tap to advance, tap/hover
|
|
1027
|
+
* choices). Pass a scene's choice presenter so the pointer can hit-test rows;
|
|
1028
|
+
* `skipHoldMs` adds the classic "hold to skip" confirm.
|
|
1029
|
+
*/
|
|
1030
|
+
declare function fullControls(choices?: PointerChoiceTarget, options?: {
|
|
1031
|
+
actions?: DialogueActions;
|
|
1032
|
+
skipHoldMs?: number;
|
|
1033
|
+
}): InputBinding;
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* DialogueController — the thin YAGE host. It owns no dialogue logic; it just:
|
|
1037
|
+
*
|
|
1038
|
+
* • mounts the presenters onto the scene,
|
|
1039
|
+
* • builds a headless {@link DialogueSession} over them,
|
|
1040
|
+
* • forwards the session's observation callbacks to engine events,
|
|
1041
|
+
* • attaches an {@link InputBinding} (keyboard by default) and pumps it,
|
|
1042
|
+
* • pumps `session.update(dt)` each frame.
|
|
1043
|
+
*
|
|
1044
|
+
* All the sequencing, reveal-gating, choice book-keeping, and i18n live in the
|
|
1045
|
+
* session (engine-agnostic). The presenter bundle usually comes from a factory
|
|
1046
|
+
* (`createBoxDialogue(theme)`), spread-and-overridden as needed:
|
|
1047
|
+
*
|
|
1048
|
+
* host.add(new DialogueController({ ...createBoxDialogue(theme), avatar, storage }));
|
|
1049
|
+
*/
|
|
1050
|
+
|
|
1051
|
+
/** The presenter trio a factory assembles (see `createBoxDialogue`).
|
|
1052
|
+
* Optional fields are `T | undefined` so factories and games can assign
|
|
1053
|
+
* possibly-undefined theme values directly (exactOptionalPropertyTypes). */
|
|
1054
|
+
interface DialogueBundle {
|
|
1055
|
+
readonly chrome: ChromePresenter;
|
|
1056
|
+
readonly text: TextPresenter;
|
|
1057
|
+
readonly choices: ChoicePresenter;
|
|
1058
|
+
readonly avatar?: AvatarPresenter | undefined;
|
|
1059
|
+
/** Hold-to-fast-forward multiplier. Default 4. */
|
|
1060
|
+
readonly skipMultiplier?: number | undefined;
|
|
1061
|
+
}
|
|
1062
|
+
interface DialogueControllerOptions<TStorage extends VariableStorage = VariableStorage> extends DialogueBundle {
|
|
1063
|
+
readonly i18n?: I18nAdapter | undefined;
|
|
1064
|
+
/**
|
|
1065
|
+
* The variable storage installed for every `play()`. Persists across
|
|
1066
|
+
* plays. Omit for a zero-config `MemoryVariableStorage`; supply your own (or
|
|
1067
|
+
* `compose(cells(...), new MemoryVariableStorage())`) to bridge game state. A
|
|
1068
|
+
* per-`play()` `overrides.storage` replaces it for that conversation.
|
|
1069
|
+
*/
|
|
1070
|
+
readonly storage?: TStorage | undefined;
|
|
1071
|
+
/** Argument-capable read functions (`has_item("key")`) shared across plays. */
|
|
1072
|
+
readonly functions?: Readonly<Record<string, DialogueFunction>> | undefined;
|
|
1073
|
+
/** Command handlers (`type` → handler) shared across plays; per-`play()`
|
|
1074
|
+
* `overrides.commands` merge on top (call site wins). */
|
|
1075
|
+
readonly commands?: Readonly<Record<string, CommandHandler>> | undefined;
|
|
1076
|
+
/** Catch-all for command types with no explicit handler. */
|
|
1077
|
+
readonly fallbackCommand?: CommandHandler | undefined;
|
|
1078
|
+
/** Device → session binding. Omit for the default keyboard binding. */
|
|
1079
|
+
readonly input?: InputBinding;
|
|
1080
|
+
/**
|
|
1081
|
+
* Extra channels registered on the session at mount (Voice / Shop /
|
|
1082
|
+
* CameraEffects / History) — the open-ended companion to the presenter trio.
|
|
1083
|
+
* Each is wired via {@link DialogueController.addChannel}; one that also
|
|
1084
|
+
* implements {@link Mountable} (it needs the scene) is mounted in `onAdd` and
|
|
1085
|
+
* disposed in `onDestroy`. A factory bundle can pre-wire e.g. a voice channel
|
|
1086
|
+
* here; a game can also add one live with {@link DialogueController.addChannel}.
|
|
1087
|
+
*/
|
|
1088
|
+
readonly channels?: readonly DialogueExtraChannel[] | undefined;
|
|
1089
|
+
/**
|
|
1090
|
+
* Per-grapheme typewriter tick — a direct callback (NOT an entity event; it
|
|
1091
|
+
* fires hundreds of times per line). `index` is the raw grapheme index revealed
|
|
1092
|
+
* (whitespace included — filter if you only want a blip on visible glyphs).
|
|
1093
|
+
* Wire a typewriter SFX here. Inline `[name k=v/]` markers, by contrast, come
|
|
1094
|
+
* through {@link DialogueRevealMarkerEvent} on the entity bus.
|
|
1095
|
+
*/
|
|
1096
|
+
readonly onRevealTick?: ((index: number) => void) | undefined;
|
|
1097
|
+
/** Called once when a conversation ends (in addition to the scene event). */
|
|
1098
|
+
readonly onEnded?: () => void;
|
|
1099
|
+
}
|
|
1100
|
+
declare class DialogueController<TStorage extends VariableStorage = VariableStorage> extends Component {
|
|
1101
|
+
private readonly opts;
|
|
1102
|
+
private readonly input;
|
|
1103
|
+
private readonly binding;
|
|
1104
|
+
private session;
|
|
1105
|
+
/** Captured at onAdd (the scene is gone by the time a stale play() arrives). */
|
|
1106
|
+
private logger;
|
|
1107
|
+
/** Set by onDestroy — the presenters are disposed, so play() must refuse. */
|
|
1108
|
+
private destroyed;
|
|
1109
|
+
/** Input focus. When false, `update()` keeps pumping the session (an
|
|
1110
|
+
* ambient conversation stays alive) but the binding is NOT polled, so this
|
|
1111
|
+
* instance doesn't consume device input. NOT `Component.enabled` (which would
|
|
1112
|
+
* also freeze the session). */
|
|
1113
|
+
private inputEnabled;
|
|
1114
|
+
/** Pause. Mirrors the session's pause so the binding poll is also gated
|
|
1115
|
+
* while frozen — a paused conversation neither updates nor consumes input.
|
|
1116
|
+
* Also the source of truth re-applied to the session in `onAdd` when a host
|
|
1117
|
+
* set it before the component was added (the session didn't exist yet). */
|
|
1118
|
+
private paused;
|
|
1119
|
+
/** Hidden. Mirrors the session's hide so a `setHidden` issued before the
|
|
1120
|
+
* component was added isn't lost — it's re-applied once the session exists. */
|
|
1121
|
+
private hidden;
|
|
1122
|
+
/** Disposers for every registered extra channel (ctor `channels` + live
|
|
1123
|
+
* `addChannel`). `onDestroy` runs them all — each idempotent — to unregister
|
|
1124
|
+
* and dispose (unmounting the Mountable ones). */
|
|
1125
|
+
private readonly channelDisposers;
|
|
1126
|
+
constructor(opts: DialogueControllerOptions<TStorage>);
|
|
1127
|
+
onAdd(): void;
|
|
1128
|
+
onDestroy(): void;
|
|
1129
|
+
/**
|
|
1130
|
+
* Begin a conversation. `play(script)` is **content-only** — storage,
|
|
1131
|
+
* functions, and commands are installed on the controller. `overrides` layers
|
|
1132
|
+
* per-conversation specifics on top (a scoped `storage`, extra
|
|
1133
|
+
* `functions`/`commands`). Returns a {@link DialogueHandle} for live `setVar` /
|
|
1134
|
+
* `getVars`, or `undefined` if the controller was removed.
|
|
1135
|
+
*/
|
|
1136
|
+
play<S extends DialogueScript>(script: S, overrides?: DialoguePlayOptions): DialogueHandle<VarsOf<S>> | undefined;
|
|
1137
|
+
isActive(): boolean;
|
|
1138
|
+
/** Abandon the current conversation and reset to idle. */
|
|
1139
|
+
stop(): void;
|
|
1140
|
+
/**
|
|
1141
|
+
* Register an extra channel live — Voice / Shop / CameraEffects / History.
|
|
1142
|
+
* Mounts it if it needs the scene ({@link Mountable}), hands it to the session
|
|
1143
|
+
* (where it joins the cross-cutting stream and can gate auto-advance), and
|
|
1144
|
+
* returns a disposer that unregisters + disposes it. The `channels` ctor option
|
|
1145
|
+
* registers a bundle the same way at mount. Returns a no-op disposer if the
|
|
1146
|
+
* controller was destroyed; **throws** if called before the component is added
|
|
1147
|
+
* to an entity (use the `channels` ctor option to pre-wire a channel) — mirrors
|
|
1148
|
+
* {@link play}.
|
|
1149
|
+
*/
|
|
1150
|
+
addChannel(ch: DialogueExtraChannel): () => void;
|
|
1151
|
+
/** Fast-forward the current section to the next choice or the end. */
|
|
1152
|
+
skip(): void;
|
|
1153
|
+
/**
|
|
1154
|
+
* Auto-advance lines after they finish revealing (`ms`), or `null` to disable
|
|
1155
|
+
* (manual advance). A per-line `autoAdvanceMs` still overrides this. Toggle it
|
|
1156
|
+
* live for a VN-style "auto" control.
|
|
1157
|
+
*/
|
|
1158
|
+
setAutoAdvance(ms: number | null): void;
|
|
1159
|
+
/**
|
|
1160
|
+
* Hide or show the whole dialogue UI without ending the conversation —
|
|
1161
|
+
* for a cutscene takeover (`setHidden(true)` while the camera pans, then
|
|
1162
|
+
* `setHidden(false)` to restore the exact line + caret). Purely visual; the
|
|
1163
|
+
* conversation keeps its state. **Persistent**: it survives `stop()`/`play()`,
|
|
1164
|
+
* so a host that hides and forgets to unhide stays hidden.
|
|
1165
|
+
*/
|
|
1166
|
+
setHidden(hidden: boolean): void;
|
|
1167
|
+
/**
|
|
1168
|
+
* Freeze or resume the conversation — a pause menu. While paused the
|
|
1169
|
+
* reveal, auto-advance, caret blink, and avatar anim all halt, input is inert,
|
|
1170
|
+
* and no state is lost (an in-flight blocking command keeps running). Also
|
|
1171
|
+
* gates this controller's input binding so a frozen conversation consumes no
|
|
1172
|
+
* device input. Does NOT block host-driven `handle.setVar` / storage writes.
|
|
1173
|
+
*/
|
|
1174
|
+
setPaused(paused: boolean): void;
|
|
1175
|
+
/**
|
|
1176
|
+
* Set whether this controller consumes device input — the focus seam for
|
|
1177
|
+
* the multi-instance story. `setInputEnabled(false)` keeps the conversation
|
|
1178
|
+
* fully alive (it still updates, reveals, auto-advances) but stops polling its
|
|
1179
|
+
* binding, so an ambient conversation doesn't steal the advance key. Switch
|
|
1180
|
+
* focus between two conversations with `a.setInputEnabled(true);
|
|
1181
|
+
* b.setInputEnabled(false)`. (YAGE input is non-consuming, so two *enabled*
|
|
1182
|
+
* controllers both advance on one press — focus is the game's policy.)
|
|
1183
|
+
*/
|
|
1184
|
+
setInputEnabled(enabled: boolean): void;
|
|
1185
|
+
/**
|
|
1186
|
+
* Primary action, host-driven (the input-agnostic seam): while saying,
|
|
1187
|
+
* reveal-all if still typing else advance to the next line; while choosing,
|
|
1188
|
+
* confirm the highlighted option. Lets a host (cutscene script, custom input,
|
|
1189
|
+
* or a test) drive the conversation without synthesising device input — the
|
|
1190
|
+
* same call the default {@link InputBinding} makes.
|
|
1191
|
+
*/
|
|
1192
|
+
advance(): void;
|
|
1193
|
+
/** Move the choice cursor by `delta` (wraps). No-op outside a choice. */
|
|
1194
|
+
moveSelection(delta: number): void;
|
|
1195
|
+
/** Commit a choice by its original option index. No-op outside a choice. */
|
|
1196
|
+
choose(optionIndex: number): void;
|
|
1197
|
+
/** True while a choice is being presented. */
|
|
1198
|
+
isChoosing(): boolean;
|
|
1199
|
+
/** Side-effect-free lookahead of the lines a node would show. */
|
|
1200
|
+
preview(nodeId: string): PreviewedLine[];
|
|
1201
|
+
update(dt: number): void;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export { type AvatarChannel as A, type ChoiceChannel as C, type DialogueExtraChannel as D, FULL_ACTIONS as F, type I18nAdapter as I, KeyboardInputBinding as K, LineReveal as L, type Mountable as M, NullAvatarPresenter as N, type PointerChoiceTarget as P, type RevealBeat as R, type SpeakerView as S, type TextChannel as T, type VarsOf as V, type ChoiceContext as a, type ChromeChannel as b, CompositeInputBinding as c, DEFAULT_ACTIONS as d, type DialogueActions as e, type DialogueBundle as f, type DialogueChannels as g, DialogueController as h, type DialogueControllerOptions as i, DialogueSession as j, type DialogueSessionOptions as k, IdentityI18n as l, type InputBinding as m, PointerInputBinding as n, type PresentedChoice as o, type PresentedLine as p, type PreviewedLine as q, type TypedScript as r, defineScript as s, fullControls as t, interpolate as u, type TextPresenter as v, type DiagnosticSink as w, type ChromePresenter as x, type ChoicePresenter as y, type AvatarPresenter as z };
|