- Music, sound and voice volumes are shared by all games by default; a game that
has never been launched with its own volumes plays with the shared ones
- The sound section of the player settings has a "own volume for this game" switch:
turning it on starts from the current values and then the sliders change only this
game, turning it off removes the override and returns to the shared volumes
- The override belongs to the game (game_id from /api/versions/{id}), so forks and mods
share it; it falls back to the version id when the id is unavailable
- Reading speed and auto delay stay shared
- volume-store.ts keeps the storage logic (damaged data ignored, values clamped,
unavailable storage tolerated) and has its own tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
52 lines
1.4 KiB
TypeScript
52 lines
1.4 KiB
TypeScript
// Volumes of a game: shared by all games unless the game has been given its own.
|
|
|
|
import type { Volumes } from "./audio";
|
|
|
|
type Store = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
|
|
function usable(v: any): v is number {
|
|
return typeof v === "number" && Number.isFinite(v);
|
|
}
|
|
|
|
const clamp = (v: number) => Math.max(0, Math.min(1, v));
|
|
|
|
export class VolumeStore {
|
|
constructor(private key: string | null, private storage: Store = localStorage) {}
|
|
|
|
get available() {
|
|
return this.key !== null;
|
|
}
|
|
|
|
/** This game's own volumes, or null when it has none (it then plays with the shared ones). */
|
|
read(): Volumes | null {
|
|
if (!this.key) return null;
|
|
try {
|
|
const data = JSON.parse(this.storage.getItem(this.key) ?? "null");
|
|
if (data && usable(data.music) && usable(data.sound) && usable(data.voice)) {
|
|
return { music: clamp(data.music), sound: clamp(data.sound), voice: clamp(data.voice) };
|
|
}
|
|
} catch {
|
|
/* storage unavailable or damaged: fall back to the shared volumes */
|
|
}
|
|
return null;
|
|
}
|
|
|
|
write({ music, sound, voice }: Volumes) {
|
|
if (!this.key) return;
|
|
try {
|
|
this.storage.setItem(this.key, JSON.stringify({ music, sound, voice }));
|
|
} catch {
|
|
/* the change still applies for this session */
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
if (!this.key) return;
|
|
try {
|
|
this.storage.removeItem(this.key);
|
|
} catch {
|
|
/* nothing to remove */
|
|
}
|
|
}
|
|
}
|