Files
mrleo1nidandClaude Sonnet 5.5 a93f33c93d
CI / frontend (push) Successful in 41s
CI / backend (push) Successful in 1m41s
feat: shared volume settings with a per-game override
- 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>
2026-10-25 18:27:39 +03:00

59 lines
2.6 KiB
JavaScript

import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { build } from "esbuild";
const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/volume-store.ts", import.meta.url))],
bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
const { VolumeStore } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`);
function storage(initial = {}) {
const data = new Map(Object.entries(initial));
return {
data,
getItem: (k) => data.get(k) ?? null,
setItem: (k, v) => void data.set(k, v),
removeItem: (k) => void data.delete(k),
};
}
test("a game that was never launched has no volumes of its own, so it plays with the shared ones", () => {
const store = new VolumeStore("pgvn:audio:game:7", storage());
assert.equal(store.read(), null);
});
test("own volumes are kept per game and do not touch other games", () => {
const backing = storage();
new VolumeStore("pgvn:audio:game:7", backing).write({ music: 0.2, sound: 0.3, voice: 1 });
assert.deepEqual(new VolumeStore("pgvn:audio:game:7", backing).read(), { music: 0.2, sound: 0.3, voice: 1 });
assert.equal(new VolumeStore("pgvn:audio:game:8", backing).read(), null);
});
test("clearing returns the game to the shared volumes", () => {
const backing = storage();
const store = new VolumeStore("k", backing);
store.write({ music: 0, sound: 0, voice: 0 });
store.clear();
assert.equal(store.read(), null);
assert.equal(backing.data.size, 0);
});
test("damaged or out-of-range data is ignored or limited, and a missing key means shared volumes only", () => {
assert.equal(new VolumeStore("k", storage({ k: "{not json" })).read(), null);
assert.equal(new VolumeStore("k", storage({ k: JSON.stringify({ music: "loud", sound: 1, voice: 1 }) })).read(), null);
assert.deepEqual(new VolumeStore("k", storage({ k: JSON.stringify({ music: 5, sound: -1, voice: 0.5 }) })).read(),
{ music: 1, sound: 0, voice: 0.5 });
const none = new VolumeStore(null, storage());
assert.equal(none.available, false);
none.write({ music: 1, sound: 1, voice: 1 });
assert.equal(none.read(), null);
});
test("an unavailable storage does not break playing", () => {
const broken = { getItem: () => { throw new Error("blocked"); }, setItem: () => { throw new Error("blocked"); },
removeItem: () => { throw new Error("blocked"); } };
const store = new VolumeStore("k", broken);
assert.equal(store.read(), null);
assert.doesNotThrow(() => { store.write({ music: 1, sound: 1, voice: 1 }); store.clear(); });
});