anvilsign in

collin/mahjong

1import { DEFAULT_NAMES, DEFAULT_RULES, type Rules } from './types';
2
3const KEY = 'taiwanese-mahjong/prefs';
4
5/**
6 * Preferences belong to the table, not to a hand: they outlive a save, survive
7 * 開新局, and are what a fresh `Game` is built from.
8 */
9export interface Prefs {
10 /** Master mute. Off by default — a shared screen should not surprise a room. */
11 sound: boolean;
12 /** 0..1 */
13 volume: number;
14 /** 報牌 — say the calls and the discarded tiles out loud. Needs `sound`. */
15 voice: boolean;
16 names: string[];
17 rules: Rules;
18}
19
20export const DEFAULT_PREFS: Prefs = {
21 sound: false,
22 volume: 0.7,
23 voice: true,
24 names: DEFAULT_NAMES,
25 rules: DEFAULT_RULES,
26};
27
28export function loadPrefs(): Prefs {
29 try {
30 const raw = localStorage.getItem(KEY);
31 if (!raw) return DEFAULT_PREFS;
32 const p = JSON.parse(raw) as Partial<Prefs>;
33 return {
34 sound: typeof p.sound === 'boolean' ? p.sound : DEFAULT_PREFS.sound,
35 volume: clamp(typeof p.volume === 'number' ? p.volume : DEFAULT_PREFS.volume, 0, 1),
36 voice: typeof p.voice === 'boolean' ? p.voice : DEFAULT_PREFS.voice,
37 names:
38 Array.isArray(p.names) && p.names.length === 4 && p.names.every((n) => typeof n === 'string')
39 ? p.names
40 : DEFAULT_NAMES,
41 rules: { ...DEFAULT_RULES, ...(p.rules ?? {}) },
42 };
43 } catch {
44 return DEFAULT_PREFS;
45 }
46}
47
48export function savePrefs(p: Prefs) {
49 try {
50 localStorage.setItem(KEY, JSON.stringify(p));
51 } catch {
52 // Storage unavailable — the settings just don't outlive the tab.
53 }
54}
55
56export const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));