slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+157
View File
@@ -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));
}
}