Files
2026-09-09 14:47:29 +08:00

143 lines
3.5 KiB
JavaScript

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,
}