- Fix LFO→Filter cutoff: add scaling Gain nodes so LFO (-1..1) maps to meaningful Hz modulation (±cutoff value). Same fix for LFO→Osc freq. Mod scale updates dynamically when user changes the base param value. - Visual modulation indicator: knobs receiving LFO/modulation show a pulsing dashed ring animation (spin + pulse) around the knob arc - Persist hint usage per level: using a hint permanently caps that level at 2 stars — survives reload/restart. No more cheating by restarting! - Hint state stored in separate localStorage key (synthquest-hints) - Admin reset also clears hint history Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
3.6 KiB
JavaScript
150 lines
3.6 KiB
JavaScript
/**
|
|
* gameState.js — Game progress persistence
|
|
* Tracks completed levels, stars earned, unlocks, and saved patches per level
|
|
*/
|
|
|
|
const STORAGE_KEY = 'synthquest-progress';
|
|
const PATCHES_KEY = 'synthquest-patches';
|
|
|
|
const defaultProgress = {
|
|
currentWorld: 'w1',
|
|
completedLevels: {}, // { levelId: { stars: 3 } }
|
|
unlockedWorlds: ['w1'],
|
|
totalStars: 0,
|
|
};
|
|
|
|
let _progress = null;
|
|
let _patches = null; // { levelId: { modules, connections } }
|
|
|
|
export function loadProgress() {
|
|
if (_progress) return _progress;
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
_progress = raw ? { ...defaultProgress, ...JSON.parse(raw) } : { ...defaultProgress };
|
|
} catch {
|
|
_progress = { ...defaultProgress };
|
|
}
|
|
return _progress;
|
|
}
|
|
|
|
export function saveProgress() {
|
|
if (!_progress) return;
|
|
_progress.totalStars = Object.values(_progress.completedLevels)
|
|
.reduce((sum, l) => sum + (l.stars || 0), 0);
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(_progress));
|
|
} catch {}
|
|
}
|
|
|
|
export function completeLevel(levelId, stars) {
|
|
const p = loadProgress();
|
|
const existing = p.completedLevels[levelId];
|
|
if (!existing || stars > existing.stars) {
|
|
p.completedLevels[levelId] = { stars, completedAt: Date.now() };
|
|
}
|
|
saveProgress();
|
|
}
|
|
|
|
export function getLevelProgress(levelId) {
|
|
const p = loadProgress();
|
|
return p.completedLevels[levelId] || null;
|
|
}
|
|
|
|
export function isLevelUnlocked(levelId, worldLevels) {
|
|
const p = loadProgress();
|
|
const idx = worldLevels.findIndex(l => l.id === levelId);
|
|
if (idx === 0) return true;
|
|
const prevId = worldLevels[idx - 1]?.id;
|
|
return prevId && p.completedLevels[prevId]?.stars >= 1;
|
|
}
|
|
|
|
export function resetProgress() {
|
|
_progress = { ...defaultProgress };
|
|
_patches = {};
|
|
_hints = {};
|
|
saveProgress();
|
|
savePatches();
|
|
saveHints();
|
|
}
|
|
|
|
// ==================== Level patch persistence ====================
|
|
|
|
function loadPatches() {
|
|
if (_patches) return _patches;
|
|
try {
|
|
const raw = localStorage.getItem(PATCHES_KEY);
|
|
_patches = raw ? JSON.parse(raw) : {};
|
|
} catch {
|
|
_patches = {};
|
|
}
|
|
return _patches;
|
|
}
|
|
|
|
function savePatches() {
|
|
if (!_patches) return;
|
|
try {
|
|
localStorage.setItem(PATCHES_KEY, JSON.stringify(_patches));
|
|
} catch {}
|
|
}
|
|
|
|
export function saveLevelPatch(levelId, modules, connections) {
|
|
const patches = loadPatches();
|
|
patches[levelId] = {
|
|
modules: modules.map(m => ({ id: m.id, type: m.type, x: m.x, y: m.y, params: { ...m.params } })),
|
|
connections: connections.map(c => ({ ...c })),
|
|
savedAt: Date.now(),
|
|
};
|
|
savePatches();
|
|
}
|
|
|
|
export function getLevelPatch(levelId) {
|
|
const patches = loadPatches();
|
|
return patches[levelId] || null;
|
|
}
|
|
|
|
export function clearLevelPatch(levelId) {
|
|
const patches = loadPatches();
|
|
delete patches[levelId];
|
|
savePatches();
|
|
}
|
|
|
|
// ==================== Hint tracking (persisted, no cheating!) ====================
|
|
|
|
const HINTS_KEY = 'synthquest-hints';
|
|
let _hints = null; // { levelId: true }
|
|
|
|
function loadHints() {
|
|
if (_hints) return _hints;
|
|
try {
|
|
const raw = localStorage.getItem(HINTS_KEY);
|
|
_hints = raw ? JSON.parse(raw) : {};
|
|
} catch {
|
|
_hints = {};
|
|
}
|
|
return _hints;
|
|
}
|
|
|
|
function saveHints() {
|
|
if (!_hints) return;
|
|
try {
|
|
localStorage.setItem(HINTS_KEY, JSON.stringify(_hints));
|
|
} catch {}
|
|
}
|
|
|
|
export function markHintUsed(levelId) {
|
|
const hints = loadHints();
|
|
hints[levelId] = true;
|
|
saveHints();
|
|
}
|
|
|
|
export function wasHintUsed(levelId) {
|
|
const hints = loadHints();
|
|
return !!hints[levelId];
|
|
}
|
|
|
|
export function clearHintForLevel(levelId) {
|
|
const hints = loadHints();
|
|
delete hints[levelId];
|
|
saveHints();
|
|
}
|