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:
@@ -14,7 +14,7 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
||||
const messages = [
|
||||
'',
|
||||
'Has dado el primer paso...',
|
||||
hintPenalty ? 'Reinicia sin pista para conseguir 3 estrellas.' : 'Buen trabajo — casi perfecto.',
|
||||
hintPenalty ? 'Pista usada — tercera estrella bloqueada permanentemente.' : 'Buen trabajo — casi perfecto.',
|
||||
'Ejecucion impecable.',
|
||||
];
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
||||
<div className="gm-complete-actions">
|
||||
<button className="gm-btn secondary" onClick={onMap}>Mapa</button>
|
||||
<button className="gm-btn secondary" onClick={onRetry}>
|
||||
{hintPenalty ? '↺ Sin pista' : 'Reintentar'}
|
||||
Reintentar
|
||||
</button>
|
||||
{stars >= 1 && !isLastLevel && (
|
||||
<button className="gm-btn primary" onClick={onNext}>Siguiente →</button>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -61,8 +61,10 @@ export function isLevelUnlocked(levelId, worldLevels) {
|
||||
export function resetProgress() {
|
||||
_progress = { ...defaultProgress };
|
||||
_patches = {};
|
||||
_hints = {};
|
||||
saveProgress();
|
||||
savePatches();
|
||||
saveHints();
|
||||
}
|
||||
|
||||
// ==================== Level patch persistence ====================
|
||||
@@ -105,3 +107,43 @@ export function clearLevelPatch(levelId) {
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user