Files
slop-game-opus5.5/app/static/js/systems/Settings.js
T
2026-09-24 17:57:54 +02:00

158 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Dauerhafte Einstellungen im Browser (localStorage) – ohne Phaser.
*
* localStorage kann fehlen oder Ausnahmen werfen (privater Modus, blockierte
* Website-Daten, volle Quote). Das Spiel muss dann trotzdem laufen: Werte bleiben
* in diesem Fall nur für die aktuelle Sitzung im Speicher.
* @module systems/Settings
*/
import { AUDIO, HIGHSCORE, STORAGE_KEYS } from '../config.js';
import { clamp } from '../logic/math.js';
/** @typedef {'master'|'music'|'sfx'} VolumeChannel */
/** @type {readonly VolumeChannel[]} */
export const VOLUME_CHANNELS = Object.freeze(['master', 'music', 'sfx']);
/**
* Schlüssel-Wert-Speicher mit JSON und Fallback auf den Arbeitsspeicher.
*/
export class SafeStorage {
/** @param {Storage|null|undefined} backend */
constructor(backend) {
this.backend = backend ?? null;
/** @type {Map<string, string>} */
this.memory = new Map();
}
/**
* Erzeugt einen Speicher für den Browser; der Zugriff auf `localStorage` selbst
* kann bereits eine Ausnahme werfen (z. B. bei gesperrten Cookies).
* @returns {SafeStorage}
*/
static forBrowser() {
try {
return new SafeStorage(globalThis.localStorage);
} catch {
return new SafeStorage(null);
}
}
/**
* @template T
* @param {string} key
* @param {T} fallback
* @returns {T}
*/
get(key, fallback) {
let raw = this.memory.get(key) ?? null;
try {
raw = this.backend?.getItem(key) ?? raw;
} catch {
// Browser verweigert den Zugriff – Speicherwert verwenden.
}
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return fallback;
}
}
/**
* @param {string} key
* @param {unknown} value
* @returns {boolean} true, wenn dauerhaft gespeichert wurde.
*/
set(key, value) {
const raw = JSON.stringify(value);
this.memory.set(key, raw);
try {
this.backend?.setItem(key, raw);
return this.backend !== null;
} catch {
return false;
}
}
}
/**
* Spielereinstellungen: Disclaimer-Bestätigung, Lautstärken, letzter Name.
*/
export class Settings {
/** @param {SafeStorage} storage */
constructor(storage) {
this.storage = storage;
/** @type {Set<(channel: VolumeChannel, value: number) => void>} */
this.volumeListeners = new Set();
const stored = storage.get(STORAGE_KEYS.volumes, {});
/** @type {Record<VolumeChannel, number>} */
this.volumes = { ...AUDIO.defaultVolumes };
for (const channel of VOLUME_CHANNELS) {
const value = stored?.[channel];
if (typeof value === 'number' && Number.isFinite(value)) {
this.volumes[channel] = clamp(value, 0, 1);
}
}
}
/** @returns {boolean} */
isDisclaimerAccepted() {
return this.storage.get(STORAGE_KEYS.disclaimerAccepted, false) === true;
}
acceptDisclaimer() {
this.storage.set(STORAGE_KEYS.disclaimerAccepted, true);
}
/**
* @param {VolumeChannel} channel
* @returns {number} 0..1
*/
getVolume(channel) {
return this.volumes[channel];
}
/**
* Effektive Lautstärke eines Kanals (inkl. Gesamtlautstärke).
* @param {'music'|'sfx'} channel
* @returns {number}
*/
effectiveVolume(channel) {
return this.volumes.master * this.volumes[channel];
}
/**
* @param {VolumeChannel} channel
* @param {number} value 0..1, wird begrenzt.
*/
setVolume(channel, value) {
if (!VOLUME_CHANNELS.includes(channel)) return;
const safe = Number.isFinite(value) ? clamp(value, 0, 1) : this.volumes[channel];
this.volumes[channel] = safe;
this.storage.set(STORAGE_KEYS.volumes, this.volumes);
for (const listener of this.volumeListeners) listener(channel, safe);
}
/**
* @param {(channel: VolumeChannel, value: number) => void} listener
* @returns {() => void} Funktion zum Abmelden.
*/
onVolumeChange(listener) {
this.volumeListeners.add(listener);
return () => this.volumeListeners.delete(listener);
}
/** @returns {string} */
getLastName() {
const name = this.storage.get(STORAGE_KEYS.lastName, '');
return typeof name === 'string' ? name.slice(0, HIGHSCORE.nameMaxLength) : '';
}
/** @param {string} name */
setLastName(name) {
this.storage.set(STORAGE_KEYS.lastName, String(name).slice(0, HIGHSCORE.nameMaxLength));
}
}