anvilsign in

collin/mahjong

1import { DEFAULT_NAMES, DEFAULT_RULES, type Rules } from './types';
2
3const KEY = 'taiwanese-mahjong/prefs';
4
5/**
6 * Bump when a new switch should start from its default rather than from
7 * whatever an older build happened to write. Preferences are saved on load, so
8 * a switch that shipped off and was then turned on by default would otherwise
9 * stay off forever for anyone who had already opened the game once.
10 *
11 * Only the audio settings are reset; names and house rules are things somebody
12 * actually chose, and they carry across.
13 */
14const AUDIO_GENERATION = 2;
15
16/**
17 * Preferences belong to the table, not to a hand: they outlive a save, survive
18 * 開新局, and are what a fresh `Game` is built from.
19 */
20export interface Prefs {
21 /** Which set of audio defaults this was last saved against. */
22 audioGeneration: number;
23 /** Master mute. */
24 sound: boolean;
25 /** 0..1 */
26 volume: number;
27 /** 報牌 — say the calls and the discarded tiles out loud. Needs `sound`. */
28 voice: boolean;
29 names: string[];
30 rules: Rules;
31}
32
33export const DEFAULT_PREFS: Prefs = {
34 audioGeneration: AUDIO_GENERATION,
35 sound: true,
36 volume: 0.7,
37 voice: true,
38 names: DEFAULT_NAMES,
39 rules: DEFAULT_RULES,
40};
41
42export function loadPrefs(): Prefs {
43 try {
44 const raw = localStorage.getItem(KEY);
45 if (!raw) return DEFAULT_PREFS;
46 const p = JSON.parse(raw) as Partial<Prefs>;
47 const stale = p.audioGeneration !== AUDIO_GENERATION;
48 return {
49 audioGeneration: AUDIO_GENERATION,
50 sound: !stale && typeof p.sound === 'boolean' ? p.sound : DEFAULT_PREFS.sound,
51 volume: clamp(typeof p.volume === 'number' ? p.volume : DEFAULT_PREFS.volume, 0, 1),
52 voice: !stale && typeof p.voice === 'boolean' ? p.voice : DEFAULT_PREFS.voice,
53 names:
54 Array.isArray(p.names) && p.names.length === 4 && p.names.every((n) => typeof n === 'string')
55 ? p.names
56 : DEFAULT_NAMES,
57 rules: { ...DEFAULT_RULES, ...(p.rules ?? {}) },
58 };
59 } catch {
60 return DEFAULT_PREFS;
61 }
62}
63
64export function savePrefs(p: Prefs) {
65 try {
66 localStorage.setItem(KEY, JSON.stringify(p));
67 } catch {
68 // Storage unavailable — the settings just don't outlive the tab.
69 }
70}
71
72export const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));