narraleaf-react 0.23.0 → 0.23.1

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,11 +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
+ import { AudioBusError, AudioBusMixer, AudioBusTree, DefaultAudioBusIds, MaxAudioBusDepth, SeededBusPreferenceKeys, acceptsAudioBus, getActiveAudioBusTree } from "../game/audioBus";
7
+ import type { AudioBusAlias, AudioBusDeclaration, AudioBusNode, AudioBusState } from "../game/audioBus";
8
8
  import type { StorableChange, StorableRestore } from "../elements/persistent/storable";
9
9
  import type { SavedGame } from "../gameTypes";
10
10
  import type { StackSnapshot, StackFrameSnapshot } from "../action/stackModel";
11
11
  import { KeyMap } from "../game/keyMap";
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, };
12
+ export { LiveGame, GameState, Game, Storable, Namespace, Preference, KeyMap, AudioBusError, AudioBusMixer, AudioBusTree, DefaultAudioBusIds, MaxAudioBusDepth, SeededBusPreferenceKeys, acceptsAudioBus, getActiveAudioBusTree, };
13
+ export type { AudioBusAlias, AudioBusDeclaration, AudioBusNode, AudioBusState, SavedGame, StackSnapshot, StackFrameSnapshot, StorableChange, StorableRestore, };
@@ -174,6 +174,33 @@ export declare function acceptsAudioBus(busId: string, ancestorIds: readonly str
174
174
  type AudioBusEvents = {
175
175
  "event:audioBus.volumeChange": [string, number, number];
176
176
  };
177
+ /**
178
+ * Somewhere other than the mixer that already stores a bus's player volume.
179
+ *
180
+ * There is exactly one of these in the engine: the three volume preferences. `voiceVolume` is not
181
+ * a number that gets *copied* onto the voice bus, it **is** the voice bus's player volume, reached
182
+ * through this. A bus with an alias has no entry in the mixer's own map at all, so there is one
183
+ * store and one writer and nothing can overwrite anything.
184
+ *
185
+ * Structural on purpose - `audioBus.ts` stays free of imports, and a test can hand it a real
186
+ * `Preference` and exercise exactly what `Game` wires up.
187
+ */
188
+ export type AudioBusAlias = {
189
+ get(): number;
190
+ set(volume: number): void;
191
+ /** Fires when the backing store changes by any route, including one that bypasses the mixer. */
192
+ subscribe(listener: (volume: number) => void): {
193
+ cancel: () => void;
194
+ };
195
+ };
196
+ /**
197
+ * The preference key that *is* each seeded bus's player volume.
198
+ */
199
+ export declare const SeededBusPreferenceKeys: {
200
+ readonly bgm: "bgmVolume";
201
+ readonly sound: "soundVolume";
202
+ readonly voice: "voiceVolume";
203
+ };
177
204
  /**
178
205
  * The per-game mixer: the declared tree, plus what the player has done to it.
179
206
  *
@@ -200,14 +227,22 @@ export declare class AudioBusMixer {
200
227
  readonly "event:audioBus.volumeChange": "event:audioBus.volumeChange";
201
228
  };
202
229
  readonly events: EventDispatcher<AudioBusEvents>;
203
- /** The player's half. Absent means "untouched", which is 1 — not 0, and not the declaration. */
230
+ /**
231
+ * The player's half, for buses that do not have an alias. Absent means "untouched", which is
232
+ * 1 — not 0, and not the declaration.
233
+ */
204
234
  private readonly overrides;
235
+ private readonly aliases;
236
+ private readonly aliasTokens;
205
237
  private tree;
206
238
  /**
207
239
  * @param declarations - Read lazily, so a host that calls `configure()` between constructing
208
240
  * the `Game` and mounting the player still gets the tree it declared.
241
+ * @param aliases - Buses whose player volume is stored somewhere else, by bus id. See
242
+ * {@link AudioBusAlias}; in the engine this is the three volume preferences and nothing else.
209
243
  */
210
- constructor(declarations: () => readonly AudioBusDeclaration[]);
244
+ constructor(declarations: () => readonly AudioBusDeclaration[], aliases?: Record<string, AudioBusAlias>);
245
+ private announce;
211
246
  /**
212
247
  * The resolved tree, resolving it on first use and caching it afterwards.
213
248
  *
@@ -244,6 +279,10 @@ export declare class AudioBusMixer {
244
279
  * A bus the player has never touched reads 1 whatever the author declared, which is what makes
245
280
  * a slider bound to this sit at maximum on a fresh install and what makes the persisted value
246
281
  * mean "what the player did" rather than "what the game shipped with".
282
+ *
283
+ * For the three seeded buses this reads the corresponding volume preference, because that
284
+ * preference *is* this number. `mixer.getVolume("voice")` and `getPreference("voiceVolume")`
285
+ * cannot disagree; there is only one of them.
247
286
  */
248
287
  getVolume(id: string): number;
249
288
  /**
@@ -74,17 +74,18 @@ export declare class Game {
74
74
  * at any point after `new Game(...)` is safe; if the channels do not exist yet the value is
75
75
  * applied the moment they do.
76
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.
77
+ * `bgmVolume`, `soundVolume` and `voiceVolume` **are** the player's half of the three seeded
78
+ * buses not numbers copied onto them. `game.audioBuses.getVolume("voice")` and
79
+ * `game.preference.getPreference("voiceVolume")` read the same storage and cannot disagree,
80
+ * and writing either drives the audio graph immediately, mounted or not. Both surfaces stay
81
+ * supported; use whichever suits, and use this one for buses the host declared.
82
+ * (`globalVolume` is the master output, not a bus, and is unchanged.)
82
83
  *
83
84
  * @example
84
85
  * ```ts
85
86
  * // persist the player's half only - the author's mix comes back with the game
86
87
  * localStorage.setItem("mixer", JSON.stringify(game.audioBuses.getVolumes()));
87
- * // restore, any time after `new Game(...)`
88
+ * // restore, any time after `new Game(...)` - no ordering requirement, seeded or not
88
89
  * game.audioBuses.setVolumes(JSON.parse(localStorage.getItem("mixer") ?? "{}"));
89
90
  * ```
90
91
  */
@@ -1,11 +1,17 @@
1
1
  import { GameState } from "../../../../game/nlcore/common/game";
2
2
  /**
3
- * The three volume preferences drive the three seeded buses.
3
+ * Carries `globalVolume` and only `globalVolume` — from the preferences to the audio subsystem.
4
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.
5
+ * The three *bus* volumes used to be pushed from here too, one `useEffect` each. They are gone, and
6
+ * their absence is the point: `bgmVolume`/`soundVolume`/`voiceVolume` **are** the player's half of
7
+ * the three seeded buses now, read straight through by `game.audioBuses`, so there is nothing left
8
+ * to copy. Copying them from a mounted component is what broke the feature twice — the effects fire
9
+ * on mount with whatever the preference happens to hold, which erased first the author's declared
10
+ * mix and then a player override the host had restored a moment earlier. It also meant a preference
11
+ * written while no player was mounted reached the graph only on the next mount.
12
+ *
13
+ * `globalVolume` stays because it is the master output rather than a bus — it has no declared half
14
+ * and no gain node in the tree.
9
15
  */
10
16
  export default function PreferenceUpdateAnnouncer({ gameState }: Readonly<{
11
17
  gameState: GameState;
@@ -263,19 +263,4 @@ export declare class AudioManager {
263
263
  setGlobalVolume(volume: number): void;
264
264
  getGlobalVolume(): number;
265
265
  destroy(): void;
266
- /**
267
- * The three volume preferences are aliases onto the three seeded buses, and this is the alias.
268
- *
269
- * They stay the way a player's music/sfx/voice sliders are driven - widening the preference key
270
- * union to cover arbitrary bus ids is not possible without reshaping `GamePreference`, and is
271
- * not needed: a host bus is driven through {@link AudioManager.setBusVolume} instead.
272
- *
273
- * They write the **player's** half only. This used to write the bus's whole gain, and because
274
- * these preferences default to 1 it meant that a host declaring `{id: "sound", volume: 0.6}`
275
- * had its mix silently overwritten with 1 the moment the audio subsystem started - the author
276
- * set SFX to 60% and every player heard 100%. Custom buses were unaffected because nothing
277
- * aliases them, so the three buses every existing project uses were the only ones that ignored
278
- * the declaration. Now `soundVolume: 1` means "do not attenuate further" and 0.6 survives.
279
- */
280
- private setupGroupVolume;
281
266
  }