managarten/games/whopixels/js/managers/StorageManager.js
Till JS c0c11c325a feat(whopixels): major refactor with 20 improvements across architecture, gameplay, UX, security, and i18n
Split monolithic RPGScene.js (1210 lines) into modular manager classes:
- WorldManager, PlayerManager, NPCManager, ChatUI, StorageManager,
  SoundManager, TouchControls

Key improvements:
- Constants config (GAME_CONFIG) replacing all magic numbers
- JSDoc types + jsconfig.json for IDE type-safety
- LocalStorage persistence for progress, stats, and custom avatars
- Synthesized sound effects via Web Audio API
- 26 NPCs (up from 10) in 3 categories
- Stats/leaderboard in main menu
- Pixel editor avatar integration with RPG game
- Mobile touch controls (virtual joystick + interact button)
- Chat UI with typing indicator and conversation history
- Interactive tutorial overlay for first-time players
- Floating question mark over NPCs in range
- Server hardened: rate limiting, input sanitization, CORS restrictions,
  API timeouts, conversation history cap
- Particle effect object pooling
- i18n framework with DE/EN and language switcher

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:26:40 +01:00

92 lines
2.3 KiB
JavaScript

/**
* @typedef {Object} GameSaveData
* @property {number[]} discoveredNPCs - IDs der entdeckten NPCs
* @property {number} totalGuesses - Gesamtanzahl der Rateversuche
* @property {number} totalRevealed - Gesamtanzahl aufgedeckter NPCs
* @property {number} bestStreak - Beste Serie korrekt erratener NPCs
* @property {number} currentStreak - Aktuelle Serie
* @property {Record<number, number>} guessesPerNPC - Anzahl Versuche pro NPC (ID -> Anzahl)
* @property {number} lastPlayed - Timestamp des letzten Spiels
*/
class StorageManager {
constructor() {
this.STORAGE_KEY = 'whopixels_save';
}
/** @returns {GameSaveData} */
load() {
try {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
}
} catch (error) {
console.error('Fehler beim Laden des Spielstands:', error);
}
return this._createDefault();
}
/** @param {GameSaveData} data */
save(data) {
try {
data.lastPlayed = Date.now();
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
} catch (error) {
console.error('Fehler beim Speichern:', error);
}
}
/**
* NPC als entdeckt markieren und Statistiken aktualisieren
* @param {number} npcId
* @param {number} guessCount - Anzahl der Fragen bis zur Enthüllung
*/
recordDiscovery(npcId, guessCount) {
const data = this.load();
if (!data.discoveredNPCs.includes(npcId)) {
data.discoveredNPCs.push(npcId);
}
data.totalRevealed++;
data.totalGuesses += guessCount;
data.currentStreak++;
data.guessesPerNPC[npcId] = guessCount;
if (data.currentStreak > data.bestStreak) {
data.bestStreak = data.currentStreak;
}
this.save(data);
return data;
}
/** @returns {GameSaveData} */
_createDefault() {
return {
discoveredNPCs: [],
totalGuesses: 0,
totalRevealed: 0,
bestStreak: 0,
currentStreak: 0,
guessesPerNPC: {},
lastPlayed: 0,
};
}
reset() {
localStorage.removeItem(this.STORAGE_KEY);
}
/** @returns {{averageGuesses: number, totalRevealed: number, bestStreak: number}} */
getStats() {
const data = this.load();
return {
averageGuesses:
data.totalRevealed > 0 ? Math.round((data.totalGuesses / data.totalRevealed) * 10) / 10 : 0,
totalRevealed: data.totalRevealed,
bestStreak: data.bestStreak,
};
}
}