Newer
Older
rpg / packages / engine / src / core / Settings.ts
/**
 * Настройки игрока: громкости, язык, прочие предпочтения.
 * Отдельный стор поверх 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);
    }
}