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.
- package/dist/game/nlcore/common/game.d.ts +4 -2
- package/dist/game/nlcore/common/types.d.ts +2 -1
- package/dist/game/nlcore/elements/sound.d.ts +28 -2
- package/dist/game/nlcore/game/audioBus.d.ts +268 -0
- package/dist/game/nlcore/game/preference.d.ts +10 -1
- package/dist/game/nlcore/game.d.ts +32 -0
- package/dist/game/nlcore/gameTypes.d.ts +41 -0
- package/dist/game/player/elements/player/PreferenceUpdateAnnouncer.d.ts +8 -0
- package/dist/game/player/lib/AudioManager.d.ts +147 -5
- package/dist/main.js +42 -42
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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;
|