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 /**
30 * Whether the player asked for the whole screen. Fullscreen itself never
31 * survives a reload, so this is what re-arms it on the next tap — see
32 * ui/fullscreen.ts. Not an audio setting: it is a choice, and it carries.
33 */
34 fullscreen: boolean;
35 names: string[];
36 rules: Rules;
37}
38
39export const DEFAULT_PREFS: Prefs = {
40 audioGeneration: AUDIO_GENERATION,
41 sound: true,
42 volume: 0.7,
43 voice: true,
44 fullscreen: false,
45 names: DEFAULT_NAMES,
46 rules: DEFAULT_RULES,
47};
48
49export function loadPrefs(): Prefs {
50 try {
51 const raw = localStorage.getItem(KEY);
52 if (!raw) return DEFAULT_PREFS;
53 const p = JSON.parse(raw) as Partial<Prefs>;
54 const stale = p.audioGeneration !== AUDIO_GENERATION;
55 return {
56 audioGeneration: AUDIO_GENERATION,
57 sound: !stale && typeof p.sound === 'boolean' ? p.sound : DEFAULT_PREFS.sound,
58 volume: clamp(typeof p.volume === 'number' ? p.volume : DEFAULT_PREFS.volume, 0, 1),
59 voice: !stale && typeof p.voice === 'boolean' ? p.voice : DEFAULT_PREFS.voice,
60 fullscreen: typeof p.fullscreen === 'boolean' ? p.fullscreen : DEFAULT_PREFS.fullscreen,
61 names:
62 Array.isArray(p.names) && p.names.length === 4 && p.names.every((n) => typeof n === 'string')
63 ? p.names
64 : DEFAULT_NAMES,
65 rules: { ...DEFAULT_RULES, ...(p.rules ?? {}) },
66 };
67 } catch {
68 return DEFAULT_PREFS;
69 }
70}
71
72export function savePrefs(p: Prefs) {
73 try {
74 localStorage.setItem(KEY, JSON.stringify(p));
75 } catch {
76 // Storage unavailable — the settings just don't outlive the tab.
77 }
78}
79
80export const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));