{stars >= 1 && !isLastLevel && (
diff --git a/src/game/PuzzleView.jsx b/src/game/PuzzleView.jsx
index a062807..a69f323 100644
--- a/src/game/PuzzleView.jsx
+++ b/src/game/PuzzleView.jsx
@@ -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 && (
- Pista usada — maximo 2 estrellas. Reinicia para intentar sin pista.
+ Pista usada — maximo 2 estrellas en este nivel (permanente).
)}
diff --git a/src/game/gameState.js b/src/game/gameState.js
index 8c09dba..0ac5fbe 100644
--- a/src/game/gameState.js
+++ b/src/game/gameState.js
@@ -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();
+}
diff --git a/src/index.css b/src/index.css
index 2de9818..c5d6197 100644
--- a/src/index.css
+++ b/src/index.css
@@ -168,6 +168,27 @@ html, body, #root {
.knob-fill { fill: none; stroke-width: 3; stroke-linecap: round; }
.knob-dot { fill: var(--text); }
+/* Modulation indicator: pulsing ring around modulated knobs */
+.knob-mod-ring {
+ fill: none;
+ stroke-width: 1.5;
+ stroke-dasharray: 3 2;
+ opacity: 0.7;
+ animation: knob-mod-pulse 1.2s ease-in-out infinite alternate, knob-mod-spin 4s linear infinite;
+}
+@keyframes knob-mod-pulse {
+ from { opacity: 0.3; stroke-width: 1; }
+ to { opacity: 0.9; stroke-width: 2; }
+}
+@keyframes knob-mod-spin {
+ from { stroke-dashoffset: 0; }
+ to { stroke-dashoffset: 30; }
+}
+.knob-modulated .param-label,
+.knob-container.knob-modulated + .param-value {
+ color: var(--accent2);
+}
+
.knob-editing { display: flex; align-items: center; justify-content: center; }
.knob-input {
width: 48px; height: 22px; padding: 0 4px;