/**
* Настройки игрока: громкости, язык, прочие предпочтения.
* Отдельный стор поверх SaveManager (слот «settings»), не попадает в сейвы игры.
*/
import type { StorageLike } from '../save/SaveManager';
export interface SettingsData {
/** Громкости 0..1. */
master: number;
music: number;
sfx: number;
/** Язык локализации (BCP-47, например 'ru'). */
lang: string;
/** Прочие настройки игры (произвольные пары). */
extra: Record<string, number | string | boolean>;
}
export const DEFAULT_SETTINGS: SettingsData = {
master: 1,
music: 1,
sfx: 1,
lang: 'ru',
extra: {}
};
/** Интерфейс подписки на изменения (например, AudioManager применяет громкости). */
export type SettingsListener = (s: SettingsData) => void;
export class Settings {
private current: SettingsData = { ...DEFAULT_SETTINGS, extra: {} };
private listeners = new Set<SettingsListener>();
constructor(private storage: StorageLike, private slot = 'settings') {}
get data(): Readonly<SettingsData> {
return this.current;
}
/** Загрузить из хранилища; отсутствующий/битый слот — настройки по умолчанию. */
load(): void {
try {
const raw = this.storage.getItem(this.slot);
if (raw) {
const parsed = JSON.parse(raw) as Partial<SettingsData>;
this.current = {
...DEFAULT_SETTINGS,
...parsed,
extra: { ...(parsed.extra ?? {}) }
};
}
} catch {
this.current = { ...DEFAULT_SETTINGS, extra: {} };
}
}
/** Изменить часть настроек, сохранить и уведомить слушателей. */
update(patch: Partial<Omit<SettingsData, 'extra'>> & { extra?: Record<string, number | string | boolean> }): void {
this.current = {
...this.current,
...patch,
extra: { ...this.current.extra, ...(patch.extra ?? {}) }
};
this.save();
this.emit();
}
/** Вернуть настройку из extra (или undefined). */
getExtra(key: string): number | string | boolean | undefined {
return this.data.extra[key];
}
onChange(listener: SettingsListener): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private save(): void {
try {
this.storage.setItem(this.slot, JSON.stringify(this.data));
} catch {
// хранилище может быть недоступно (приватный режим) — работаем в памяти
}
}
private emit(): void {
for (const l of this.listeners) l(this.data);
}
}