narraleaf-react 0.22.0 → 0.23.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.
@@ -3,9 +3,11 @@ import { GameState } from "../../player/gameState";
3
3
  import { Storable, Namespace } from "../elements/persistent/storable";
4
4
  import { LiveGame } from "../game/liveGame";
5
5
  import { Preference } from "../game/preference";
6
+ import { AudioBusError, AudioBusMixer, AudioBusTree, DefaultAudioBusIds, MaxAudioBusDepth, acceptsAudioBus, getActiveAudioBusTree } from "../game/audioBus";
7
+ import type { AudioBusDeclaration, AudioBusNode, AudioBusState } from "../game/audioBus";
6
8
  import type { StorableChange, StorableRestore } from "../elements/persistent/storable";
7
9
  import type { SavedGame } from "../gameTypes";
8
10
  import type { StackSnapshot, StackFrameSnapshot } from "../action/stackModel";
9
11
  import { KeyMap } from "../game/keyMap";
10
- export { LiveGame, GameState, Game, Storable, Namespace, Preference, KeyMap, };
11
- export type { SavedGame, StackSnapshot, StackFrameSnapshot, StorableChange, StorableRestore, };
12
+ export { LiveGame, GameState, Game, Storable, Namespace, Preference, KeyMap, AudioBusError, AudioBusMixer, AudioBusTree, DefaultAudioBusIds, MaxAudioBusDepth, acceptsAudioBus, getActiveAudioBusTree, };
13
+ export type { AudioBusDeclaration, AudioBusNode, AudioBusState, SavedGame, StackSnapshot, StackFrameSnapshot, StorableChange, StorableRestore, };
@@ -9,9 +9,10 @@ import type { LayoutRouter } from "../../../game/player/lib/PageRouter/router";
9
9
  import { KeyBindingType, WebKeyboardKey } from "../game/types";
10
10
  import { KeyBindingValue } from "../game/keyMap";
11
11
  import { SoundType } from "../elements/sound";
12
+ import type { SoundBusId } from "../elements/sound";
12
13
  import { StorableType } from "../elements/persistent/type";
13
14
  import { ScriptCtx } from "../elements/script";
14
15
  import { IStoryConfig } from "../elements/story";
15
16
  export * from "../elements/type";
16
- export type { GameHistory, IGamePluginRegistry, LiveGameEventToken, Origins, ServiceHandlerCtx, TransformDefinitions, GameConfig, SavedGame, NotificationToken, SavedGameMetaData, LayoutRouter, KeyBindingValue, WebKeyboardKey, StorableType, ScriptCtx, IStoryConfig };
17
+ export type { GameHistory, IGamePluginRegistry, LiveGameEventToken, Origins, ServiceHandlerCtx, TransformDefinitions, GameConfig, SavedGame, NotificationToken, SavedGameMetaData, LayoutRouter, KeyBindingValue, WebKeyboardKey, StorableType, ScriptCtx, IStoryConfig, SoundBusId };
17
18
  export { KeyBindingType, SoundType, };
@@ -7,6 +7,18 @@ export declare enum SoundType {
7
7
  Bgm = "bgm",
8
8
  Sound = "sound"
9
9
  }
10
+ /**
11
+ * The audio bus a clip plays on.
12
+ *
13
+ * The three {@link SoundType} values are buses the engine always seeds and they mean exactly what
14
+ * they have always meant. Any other string is a bus the host declared in
15
+ * {@link import("../gameTypes").GameConfig.audioBuses} — `"alice"` under `"voice"`, `"ambience"`
16
+ * under `"bgm"`, as deep as makes sense.
17
+ *
18
+ * The `(string & {})` half is what widens this without giving up the completions: an editor still
19
+ * offers `"bgm" | "sound" | "voice"` first, and still narrows a `SoundType` where one is expected.
20
+ */
21
+ export type SoundBusId = SoundType | (string & {});
10
22
  export type SoundDataRaw = {
11
23
  state: Record<string, any>;
12
24
  };
@@ -84,23 +96,35 @@ export interface ISoundUserConfig {
84
96
  */
85
97
  loopStart?: number;
86
98
  /**
87
- * The type of the sound
99
+ * The audio bus this clip plays on.
100
+ *
101
+ * One of the three seeded buses, or the id of any bus the host declared in
102
+ * {@link import("../gameTypes").GameConfig.audioBuses}. The bus decides which volume the
103
+ * player controls governs this clip, and nothing else — a clip on any bus can be played,
104
+ * stopped, faded and seeked the same way.
88
105
  * @default SoundType.Sound
89
106
  */
90
- type: SoundType;
107
+ type: SoundBusId;
91
108
  }
92
109
  export declare class Sound extends Actionable<SoundDataRaw, Sound> {
93
110
  /**
94
111
  * Create a voice sound for dialog lines.
112
+ *
113
+ * `type` picks the bus and defaults to `voice`; pass one to put the line on a bus beneath it,
114
+ * which is how a game gives each member of its cast a volume of its own. It used to be
115
+ * overwritten and silently ignored here.
95
116
  * @param arg0 - Source or config for the voice clip.
96
117
  * @example
97
118
  * ```ts
98
119
  * Sound.voice({ src: "voice.mp3" });
120
+ * Sound.voice({ src: "alice-01.mp3", type: "alice" }); // a bus declared under `voice`
99
121
  * ```
100
122
  */
101
123
  static voice(arg0: Partial<ISoundUserConfig> | string): Sound;
102
124
  /**
103
125
  * Create background music that cannot be played via `play()`.
126
+ *
127
+ * `type` defaults to `bgm` and may name any bus beneath it.
104
128
  * @param arg0 - Source or config for the bgm clip.
105
129
  * @example
106
130
  * ```ts
@@ -110,6 +134,8 @@ export declare class Sound extends Actionable<SoundDataRaw, Sound> {
110
134
  static bgm(arg0: Partial<ISoundUserConfig> | string): Sound;
111
135
  /**
112
136
  * Create a one-off sound effect.
137
+ *
138
+ * `type` defaults to `sound` and may name any bus beneath it.
113
139
  * @param arg0 - Source or config for the sound effect.
114
140
  */
115
141
  static sound(arg0: Partial<ISoundUserConfig> | string): Sound;
@@ -0,0 +1,268 @@
1
+ import { EventDispatcher } from "../../../util/data";
2
+ /**
3
+ * The buses the engine seeds for every game, whatever the host declares.
4
+ *
5
+ * Their ids are the values of {@link import("../elements/sound").SoundType} — deliberately, and
6
+ * a test asserts the two never drift apart. Content written before buses existed, and every save
7
+ * ever written, references these three strings; they have to keep meaning what they meant.
8
+ */
9
+ export declare const DefaultAudioBusIds: {
10
+ readonly bgm: "bgm";
11
+ readonly sound: "sound";
12
+ readonly voice: "voice";
13
+ };
14
+ /**
15
+ * How deep a declared tree may nest, counting the bus itself.
16
+ *
17
+ * Generous on purpose: `voice → cast → alice → alice-shouting` is four, and nothing sane needs
18
+ * eight. The cap exists so a malformed declaration fails at boot with a legible message instead
19
+ * of walking a chain nobody meant to build.
20
+ */
21
+ export declare const MaxAudioBusDepth = 8;
22
+ /**
23
+ * One bus, as a host declares it.
24
+ *
25
+ * Only `id` is required. `parentId` omitted (or `null`) hangs the bus directly off the master
26
+ * output; `volume` omitted is full.
27
+ */
28
+ export type AudioBusDeclaration = {
29
+ /**
30
+ * Stable, unique across the whole tree, and the string a {@link import("../elements/sound").Sound}'s
31
+ * `type` names. Two buses may not share an id even under different parents — the engine
32
+ * addresses buses by id alone.
33
+ */
34
+ id: string;
35
+ /**
36
+ * The bus this one feeds into, or `null`/omitted for the master output.
37
+ */
38
+ parentId?: string | null;
39
+ /**
40
+ * **The author's mix position for this bus**, 0..1 — where it sits relative to the others
41
+ * before any player has touched anything. "SFX sits 40% down in my mix" is `volume: 0.6`.
42
+ *
43
+ * It is not the volume a clip plays at and it is not the player's slider. A player's control
44
+ * is a *separate* number that multiplies on top of this one and defaults to 1, so a player who
45
+ * has changed nothing hears the mix the author built, and a player who pushes a slider to
46
+ * maximum gets the author's intent rather than a bus at full gain. See
47
+ * {@link AudioBusMixer.getVolume}.
48
+ * @default 1
49
+ */
50
+ volume?: number;
51
+ };
52
+ /**
53
+ * A bus after the tree has been resolved: parent settled, depth known.
54
+ */
55
+ export type AudioBusNode = {
56
+ id: string;
57
+ parentId: string | null;
58
+ /**
59
+ * The author's mix position, straight off the declaration. Fixed for the life of the game —
60
+ * what a player moves is a separate number on the mixer.
61
+ */
62
+ volume: number;
63
+ /**
64
+ * 1 for a bus directly under the master output.
65
+ */
66
+ depth: number;
67
+ };
68
+ /**
69
+ * A bus and both of its numbers.
70
+ *
71
+ * The two are kept apart on purpose. One gain node was being asked to be two things at once — the
72
+ * author's mix position and the player's slider — and whichever was written last silently erased
73
+ * the other. Splitting them makes the layering total and unambiguous:
74
+ * **declared × player = what is on the node**, and neither can overwrite the other.
75
+ */
76
+ export type AudioBusState = {
77
+ id: string;
78
+ parentId: string | null;
79
+ /**
80
+ * The player's control, 0..1, default 1. **This is the one to persist** — it is the only half
81
+ * a player owns, and restoring it lets an author re-mix a shipped game without a returning
82
+ * player's saved settings pinning the old mix forever.
83
+ */
84
+ volume: number;
85
+ /**
86
+ * The author's mix position, from {@link AudioBusDeclaration.volume}. Comes from the game, not
87
+ * from storage; persisting it would be persisting the game's own content.
88
+ */
89
+ declaredVolume: number;
90
+ /**
91
+ * `declaredVolume * volume` — what is actually on the gain node.
92
+ */
93
+ effectiveVolume: number;
94
+ };
95
+ /**
96
+ * A declared bus tree that cannot be realized.
97
+ *
98
+ * Always the host's declaration at fault — an unknown parent, a cycle, a duplicate id, a chain
99
+ * past {@link MaxAudioBusDepth}. The audio backend cannot produce any of these by construction:
100
+ * it builds a child *from* its parent, so its graph is a tree or it does not exist. Everything
101
+ * that can go wrong goes wrong one layer up, here, which is why the validation is here too.
102
+ */
103
+ export declare class AudioBusError extends Error {
104
+ constructor(message: string);
105
+ }
106
+ /**
107
+ * The resolved bus graph: every declared bus, in an order a consumer can walk front-to-back
108
+ * knowing each bus's parent has already been seen.
109
+ *
110
+ * Immutable. A tree is realized into audio channels once, at boot, and never re-shaped — removing
111
+ * a channel stops every sound in its subtree, so live re-parenting would cut the music off mid-bar.
112
+ */
113
+ export declare class AudioBusTree {
114
+ private readonly nodes;
115
+ private readonly index;
116
+ /**
117
+ * Resolve a host declaration into a tree, seeding `bgm`/`sound`/`voice` first.
118
+ *
119
+ * A declaration may re-state a seeded id to move it or change its volume; it may not remove
120
+ * one. Forward references are fine — the whole declaration is collected before anything is
121
+ * validated, so `{id: "alice", parentId: "cast"}` may precede `{id: "cast", parentId: "voice"}`.
122
+ *
123
+ * @throws {AudioBusError} on a duplicate id, an unknown parent, a cycle, or a chain deeper
124
+ * than {@link MaxAudioBusDepth}.
125
+ */
126
+ static resolve(declarations?: readonly AudioBusDeclaration[]): AudioBusTree;
127
+ /**
128
+ * Whether an id is one the engine seeds itself.
129
+ */
130
+ static isSeeded(id: string): boolean;
131
+ /**
132
+ * Everything that can be wrong with a declaration, checked before a single node is built.
133
+ *
134
+ * Walking up from each bus with a visited set catches every shape of cycle at once — a bus
135
+ * parented to itself, a mutual pair, and a longer ring — because a tree walk that revisits a
136
+ * bus it has already stood on cannot terminate.
137
+ */
138
+ private static assertResolvable;
139
+ private constructor();
140
+ /**
141
+ * Every bus, parents before their children. Realize channels by walking this front to back.
142
+ */
143
+ getNodes(): readonly AudioBusNode[];
144
+ get(id: string): AudioBusNode | null;
145
+ has(id: string): boolean;
146
+ /**
147
+ * Whether `id` is `ancestorId` or feeds into it, however many buses down.
148
+ *
149
+ * Inclusive at the top on purpose: a clip on `voice` itself has always been a voice, and this
150
+ * replaces an equality test that said exactly that.
151
+ */
152
+ isUnder(id: string, ancestorId: string): boolean;
153
+ }
154
+ /**
155
+ * The bus tree the last {@link AudioBusMixer} to resolve produced.
156
+ */
157
+ export declare function getActiveAudioBusTree(): AudioBusTree;
158
+ /**
159
+ * Whether a clip on bus `busId` is allowed in a slot that accepts `ancestorIds` and their
160
+ * descendants.
161
+ *
162
+ * **An id the registry has never heard of is accepted.** The alternative is worse than the bug it
163
+ * would catch: a story module usually constructs its scenes before the host constructs its `Game`,
164
+ * so at check time a perfectly valid custom bus is routinely not yet declared. Rejecting unknown
165
+ * ids would fail story compile for correct games depending on module evaluation order, which is
166
+ * not something an author can see or control. What the check is actually for — catching
167
+ * `Sound.bgm()` dropped into a voice slot — still works in every ordering, because the three
168
+ * seeded ids are known from the moment this module loads.
169
+ *
170
+ * A misspelled bus is therefore caught later and more cheaply: at play time, where the manager
171
+ * warns once and routes the clip somewhere audible.
172
+ */
173
+ export declare function acceptsAudioBus(busId: string, ancestorIds: readonly string[]): boolean;
174
+ type AudioBusEvents = {
175
+ "event:audioBus.volumeChange": [string, number, number];
176
+ };
177
+ /**
178
+ * The per-game mixer: the declared tree, plus what the player has done to it.
179
+ *
180
+ * **Every bus carries two numbers, and they never overwrite each other.** The declaration holds
181
+ * the author's mix — where a bus sits relative to the others in the game as shipped. The mixer
182
+ * holds the player's control, which starts at 1 and means "leave the author's mix alone". What
183
+ * reaches the gain node is the product. There is exactly one gain node per bus, because two gain
184
+ * stages in series compute the same product a multiplication does.
185
+ *
186
+ * That split is what makes the layering total: declared value → persisted player override → live
187
+ * changes, each a strictly later writer of a *different* number, so none of them can silently
188
+ * erase the one before. It also means an author can re-mix a shipped game and the new mix reaches
189
+ * players who already have settings saved, which a single conflated number cannot do.
190
+ *
191
+ * It lives on {@link import("../game").Game} rather than on the audio manager because a player's
192
+ * bus volumes are a setting, not game state — they exist before the audio context unlocks, they
193
+ * survive an unmount, and a host restores them out of its own storage at whatever point it likes.
194
+ * Setting a volume before the tree has been realized is normal and is not an error: the value is
195
+ * recorded and applied the moment the channels exist.
196
+ */
197
+ export declare class AudioBusMixer {
198
+ private readonly declarations;
199
+ static EventTypes: {
200
+ readonly "event:audioBus.volumeChange": "event:audioBus.volumeChange";
201
+ };
202
+ readonly events: EventDispatcher<AudioBusEvents>;
203
+ /** The player's half. Absent means "untouched", which is 1 — not 0, and not the declaration. */
204
+ private readonly overrides;
205
+ private tree;
206
+ /**
207
+ * @param declarations - Read lazily, so a host that calls `configure()` between constructing
208
+ * the `Game` and mounting the player still gets the tree it declared.
209
+ */
210
+ constructor(declarations: () => readonly AudioBusDeclaration[]);
211
+ /**
212
+ * The resolved tree, resolving it on first use and caching it afterwards.
213
+ *
214
+ * @throws {AudioBusError} if the declaration cannot be resolved.
215
+ */
216
+ getTree(): AudioBusTree;
217
+ /**
218
+ * Whether the tree has been resolved yet. Reading it is what resolves it, so this exists for
219
+ * callers that must not trigger validation as a side effect.
220
+ */
221
+ isResolved(): boolean;
222
+ /**
223
+ * Set **the player's** volume for a bus, 0..1. This is what a slider writes.
224
+ *
225
+ * 1 means "leave the author's mix alone" and is the value every bus starts at, so a game whose
226
+ * author put SFX at 0.6 plays SFX at 0.6 until a player says otherwise, and a player who drags
227
+ * the slider to maximum gets 0.6 back rather than a bus at full gain. The author's half is
228
+ * {@link getDeclaredVolume} and this cannot overwrite it.
229
+ *
230
+ * Applies to sounds that are **already playing**: a bus is a gain node every clip beneath it
231
+ * is routed through, so the change reaches them without touching a single token.
232
+ */
233
+ setVolume(id: string, volume: number): this;
234
+ /**
235
+ * Set many player volumes at once — what a host calls when restoring its saved mixer state.
236
+ * Ids the tree does not contain are recorded anyway, so restoring before the tree is resolved
237
+ * is safe.
238
+ */
239
+ setVolumes(volumes: Readonly<Record<string, number>>): this;
240
+ /**
241
+ * The player's volume for a bus — what was last set, else 1.
242
+ *
243
+ * Deliberately **not** the declared volume and deliberately **not** what is on the gain node.
244
+ * A bus the player has never touched reads 1 whatever the author declared, which is what makes
245
+ * a slider bound to this sit at maximum on a fresh install and what makes the persisted value
246
+ * mean "what the player did" rather than "what the game shipped with".
247
+ */
248
+ getVolume(id: string): number;
249
+ /**
250
+ * The author's mix position for a bus, from the declaration. Never written at runtime.
251
+ */
252
+ getDeclaredVolume(id: string): number;
253
+ /**
254
+ * What is actually on the bus's gain node: the author's mix times the player's control.
255
+ */
256
+ getEffectiveVolume(id: string): number;
257
+ /**
258
+ * Every bus with both of its numbers, parents first — the whole mixer.
259
+ */
260
+ list(): AudioBusState[];
261
+ /**
262
+ * Just the player's volumes, keyed by bus id — **the half a host persists**, and the shape
263
+ * {@link setVolumes} takes back. The author's mix is game content and comes back with the game.
264
+ */
265
+ getVolumes(): Record<string, number>;
266
+ onVolumeChange(listener: (id: string, volume: number, effectiveVolume: number) => void): import("../../../util/data").EventToken<import("../../../util/data").EventTypes>;
267
+ }
268
+ export {};
@@ -4,13 +4,22 @@ type PreferenceEventToken = {
4
4
  };
5
5
  type StringKeyof<T> = Extract<keyof T, string>;
6
6
  export declare class Preference<T extends Record<string, string | boolean | number | null | undefined>> {
7
- private readonly settings;
8
7
  static EventTypes: {
9
8
  readonly "event:game.preference.change": "event:game.preference.change";
10
9
  };
11
10
  readonly events: EventDispatcher<{
12
11
  "event:game.preference.change": [StringKeyof<T>, any];
13
12
  }>;
13
+ private readonly settings;
14
+ /**
15
+ * @param settings - The initial values. **Copied, not adopted.**
16
+ *
17
+ * This used to keep the caller's object and write straight into it, and the only caller in the
18
+ * engine passes the module-level `Game.DefaultPreference`. So every `Game` ever constructed
19
+ * shared one settings object: a second game started with the first player's volume, and a
20
+ * player moving a slider permanently rewrote the framework's own defaults for the rest of the
21
+ * process. A shallow copy is enough - every preference value is a primitive.
22
+ */
14
23
  constructor(settings: T);
15
24
  setPreference<K extends StringKeyof<T>>(key: K, value: T[K]): void;
16
25
  getPreference<K extends StringKeyof<T>>(key: K): T[K];
@@ -4,6 +4,7 @@ import { LogicAction } from "./action/logicAction";
4
4
  import { LiveGame } from "./game/liveGame";
5
5
  import { Preference } from "./game/preference";
6
6
  import { GameState } from "../player/gameState";
7
+ import { AudioBusMixer } from "./game/audioBus";
7
8
  import { Plugins, IGamePluginRegistry } from "./game/plugin/plugin";
8
9
  import { PuppetBackend } from "./game/puppet/puppetBackend";
9
10
  import { LayoutRouter } from "../player/lib/PageRouter/router";
@@ -57,6 +58,37 @@ export declare class Game {
57
58
  * Game settings
58
59
  */
59
60
  preference: Preference<GamePreference>;
61
+ /**
62
+ * The audio bus mixer: the tree declared in {@link GameConfig.audioBuses}, and what the player
63
+ * has done to it.
64
+ *
65
+ * Every bus carries **two** numbers. The declaration holds the author's mix — where a bus sits
66
+ * relative to the others in the game as shipped. This mixer holds the player's control, which
67
+ * starts at 1 and means "leave the author's mix alone". The product is what reaches the gain
68
+ * node, so neither half can silently erase the other and the layering is total: declared →
69
+ * persisted player override → live change.
70
+ *
71
+ * It is on `Game` rather than on the audio manager because a bus volume is a player setting,
72
+ * not game state — it exists before the audio context unlocks, it survives the player
73
+ * unmounting, and a host restores it from its own storage whenever it likes. Setting a volume
74
+ * at any point after `new Game(...)` is safe; if the channels do not exist yet the value is
75
+ * applied the moment they do.
76
+ *
77
+ * The four volume preferences (`bgmVolume`, `soundVolume`, `voiceVolume`, `globalVolume`) are
78
+ * unchanged and keep working: the first three are aliases onto the seeded buses of the same
79
+ * name and write the *player's* half, so their default of 1 no longer overwrites a declared
80
+ * mix. Drive the seeded three through the preferences, and use this for buses the host
81
+ * declared.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // persist the player's half only - the author's mix comes back with the game
86
+ * localStorage.setItem("mixer", JSON.stringify(game.audioBuses.getVolumes()));
87
+ * // restore, any time after `new Game(...)`
88
+ * game.audioBuses.setVolumes(JSON.parse(localStorage.getItem("mixer") ?? "{}"));
89
+ * ```
90
+ */
91
+ readonly audioBuses: AudioBusMixer;
60
92
  /**
61
93
  * Game key bindings
62
94
  */
@@ -10,6 +10,7 @@ import { StackModel, StackModelRawData } from "./action/stackModel";
10
10
  import type { GameElementHistory } from "./action/gameHistory";
11
11
  import { MenuComponent, NotificationComponent, NvlDialogComponent, SayComponent } from "./common/player";
12
12
  import { LiveGameEventToken } from "./types";
13
+ import type { AudioBusDeclaration } from "./game/audioBus";
13
14
  /**
14
15
  * Current save format version.
15
16
  *
@@ -187,6 +188,46 @@ export type GameConfig = {
187
188
  * @default 10
188
189
  */
189
190
  maxPreloadActions: number;
191
+ /**
192
+ * The audio bus tree, declared by the host at boot.
193
+ *
194
+ * A bus is a gain node every sound routed to it passes through, and buses nest: a clip on
195
+ * `alice` under `voice` is attenuated by `alice`, then by `voice`, then by the master volume.
196
+ * That is what lets a player turn one character down without touching the rest of the cast.
197
+ *
198
+ * A bus's `volume` here is **the author's mix position**, not the player's slider. The player's
199
+ * control is a separate number that starts at 1 and multiplies on top of this one — see
200
+ * {@link import("./game").Game.audioBuses} — so a mix declared here survives boot and
201
+ * survives a player who has never touched a slider.
202
+ *
203
+ * `bgm`, `sound` and `voice` are always present whether or not they appear here, so leaving
204
+ * this empty is exactly the behaviour every game had before buses existed. Naming one of them
205
+ * here moves it or changes its volume; nothing can remove it.
206
+ *
207
+ * Declaration order does not matter — a bus may name a parent declared after it. What is
208
+ * rejected, loudly and at boot, is an unknown parent, a duplicate id, a cycle of any length,
209
+ * or a chain nested deeper than
210
+ * {@link import("./game/audioBus").MaxAudioBusDepth}.
211
+ *
212
+ * **Read once, when the audio subsystem starts.** Re-parenting a live bus would mean removing
213
+ * a channel, which stops every sound in its subtree, so a `configure()` after the player has
214
+ * mounted does not re-shape the graph. Volumes, on the other hand, are live at all times —
215
+ * see {@link import("./game").Game.audioBuses}.
216
+ *
217
+ * @default []
218
+ * @example
219
+ * ```ts
220
+ * new Game({
221
+ * audioBuses: [
222
+ * {id: "ambience", parentId: "bgm", volume: 0.6},
223
+ * {id: "cast", parentId: "voice"},
224
+ * {id: "alice", parentId: "cast"},
225
+ * ],
226
+ * });
227
+ * // Sound.voice({src: "alice-01.mp3", type: "alice"})
228
+ * ```
229
+ */
230
+ audioBuses: AudioBusDeclaration[];
190
231
  /**
191
232
  * Src of the cursor image, if null, the game will show the default cursor
192
233
  * @default null
@@ -1,4 +1,12 @@
1
1
  import { GameState } from "../../../../game/nlcore/common/game";
2
+ /**
3
+ * The three volume preferences drive the three seeded buses.
4
+ *
5
+ * This is the whole of the alias: a preference is what a player's slider writes to, a bus is what
6
+ * the audio graph reads, and one pushes into the other. Buses the host declared are not
7
+ * preferences and are driven through `game.audioBuses` instead - `GamePreference` is a closed
8
+ * object type and cannot grow a key per character.
9
+ */
2
10
  export default function PreferenceUpdateAnnouncer({ gameState }: Readonly<{
3
11
  gameState: GameState;
4
12
  }>): null;