slop game oder so
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user