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>
This commit is contained in:
Jose Luis
2026-03-21 02:44:28 +01:00
parent c4a2cb3cef
commit f0100eb64f
7 changed files with 120 additions and 15 deletions

View File

@@ -6,7 +6,7 @@ import ModuleNode from '../components/ModuleNode.jsx';
import WireLayer from '../components/WireLayer.jsx';
import { playTarget, stopTarget, isTargetPlaying } from './targetAudio.js';
import LevelComplete from './LevelComplete.jsx';
import { completeLevel, saveLevelPatch, getLevelPatch } from './gameState.js';
import { completeLevel, saveLevelPatch, getLevelPatch, markHintUsed, wasHintUsed } from './gameState.js';
export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onNextLevel }) {
const [, forceUpdate] = useState(0);
@@ -76,8 +76,10 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
deserialize(data);
}
setResult(null);
setHintUsed(false);
setShowHint(false);
// Restore persisted hint state — no cheating by reloading!
const hintPersisted = wasHintUsed(level.id);
setHintUsed(hintPersisted);
setShowHint(hintPersisted); // If they used it before, show it again
if (state.isRunning) stopAudio();
}, [level]);
@@ -245,10 +247,11 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
}
};
// Reveal hint — permanently caps this attempt at 2 stars
// Reveal hint — PERMANENTLY caps this level at 2 stars (persisted, survives reload)
const handleRevealHint = () => {
setHintUsed(true);
setShowHint(true);
markHintUsed(level.id);
};
const handleCheck = () => {
@@ -357,7 +360,7 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
})}
{hintUsed && (
<div className="gm-hint-warning">
Pista usada maximo 2 estrellas. Reinicia para intentar sin pista.
Pista usada maximo 2 estrellas en este nivel (permanente).
</div>
)}
</div>