const PROGRESS_KEY = 'tang-detective-progress-v1' const SETTINGS_KEY = 'tang-detective-settings-v1' const AUDIO_KEY = 'tang-detective-audio-progress-v1' function defaultProgress() { return { completedHotspots: {}, completedChapters: [], lastChapter: 1, } } function isPlainRecord(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } function chapterId(number) { return `S01-C${String(number).padStart(2, '0')}` } function eventId(number) { return `S01-H${String(number).padStart(2, '0')}` } function normalizeCompletedHotspots(value) { const source = isPlainRecord(value) ? value : {} const normalized = {} for (let chapterNumber = 1; chapterNumber <= 15; chapterNumber += 1) { const id = chapterId(chapterNumber) const stored = Array.isArray(source[id]) ? source[id] : [] const firstEvent = (chapterNumber - 1) * 4 + 1 const allowed = new Set( Array.from({ length: 4 }, (_, index) => eventId(firstEvent + index)), ) const clean = [...new Set(stored.filter((item) => allowed.has(item)))] if (clean.length > 0 || Object.prototype.hasOwnProperty.call(source, id)) { normalized[id] = clean } } return normalized } function normalizeCompletedChapters(value) { if (!Array.isArray(value)) return [] const allowed = new Set( Array.from({ length: 15 }, (_, index) => chapterId(index + 1)), ) return [...new Set(value.filter((item) => allowed.has(item)))] } function normalizeProgress(value) { const source = isPlainRecord(value) ? value : {} const lastChapter = Number(source.lastChapter) return { ...source, completedHotspots: normalizeCompletedHotspots(source.completedHotspots), completedChapters: normalizeCompletedChapters(source.completedChapters), lastChapter: Number.isInteger(lastChapter) && lastChapter >= 1 && lastChapter <= 15 ? lastChapter : 1, } } function normalizeSettings(value) { const source = isPlainRecord(value) ? value : {} return { ...source, fontScale: source.fontScale === 'xlarge' ? 'xlarge' : 'large', sound: source.sound !== false, } } function read(key, fallback) { try { const value = wx.getStorageSync(key) return value || fallback } catch (error) { return fallback } } function write(key, value) { try { wx.setStorageSync(key, value) return true } catch (error) { // Storage failure must never block the text game. return false } } function getProgress() { return normalizeProgress(read(PROGRESS_KEY, null)) } function saveProgress(progress) { return write(PROGRESS_KEY, normalizeProgress(progress)) } /** * Start the story again without deleting the reader's collected memory cards * or accessibility preferences. Only story/level progress is reset. */ function resetStoryProgress() { const current = getProgress() const next = { ...current, ...defaultProgress(), } delete next.comicReaderByChapter delete next.lastPageId if (!saveProgress(next)) return false write(AUDIO_KEY, {}) return true } function getSettings() { return normalizeSettings(read(SETTINGS_KEY, null)) } function saveSettings(settings) { return write(SETTINGS_KEY, normalizeSettings(settings)) } function getAudioProgress() { const value = read(AUDIO_KEY, {}) return isPlainRecord(value) ? value : {} } function saveAudioProgress(progress) { write(AUDIO_KEY, progress) } module.exports = { getProgress, saveProgress, resetStoryProgress, getSettings, saveSettings, getAudioProgress, saveAudioProgress, }