Files
reaktor/src/game/gameState.js
Jose Luis f0100eb64f fix: LFO→cutoff modulation, visual knob feedback, persistent hints
- 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>
2026-03-21 02:44:28 +01:00

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();
}