feat: UI sounds, live LFO visualization, wire fix, worlds 7-12, bug fixes

- Add procedural UI sound effects (connect/disconnect, engine start/stop,
  level complete/fail, star earned, hint, navigation) via Tone.js
- Live LFO modulation visualization: knobs animate in real-time showing
  modulated value, ghost dot shows base value, number glows cyan
- Fix wire recalculation on zoom/pan/level re-entry (post-layout refresh)
- Fix retry button to keep current patch instead of reloading level
- Fix default param detection: newly added modules now populate all
  default params so level checkers work without manual param changes
- Add worlds 7-12: Secuencias y Ritmos, Texturas de Ruido, Síntesis
  Sustractiva, Espacio y Stereo, Técnicas Avanzadas, Gran Final
  (48 new levels, 144 new possible stars, 288 total stars)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 03:03:29 +01:00
parent f0100eb64f
commit a1be6df355
17 changed files with 3317 additions and 24 deletions

View File

@@ -2,6 +2,8 @@
* state.js — Centralized reactive state for the modular synth
* Uses a simple pub/sub pattern for React integration
*/
import { playConnect, playDisconnect, playModuleAdd, playModuleDelete } from './uiSounds.js';
import { getModuleDef } from './moduleRegistry.js';
let _listeners = new Set();
let _nextModuleId = 1;
@@ -40,9 +42,15 @@ export function emit() {
export function addModule(type, x, y) {
const id = _nextModuleId++;
state.modules.push({ id, type, x, y, params: {}, collapsed: false });
// Populate ALL default params so level checkers can read them immediately
const def = getModuleDef(type);
const defaults = def
? Object.fromEntries(Object.entries(def.params).map(([k, v]) => [k, v.default]))
: {};
state.modules.push({ id, type, x, y, params: defaults, collapsed: false });
state.selectedModuleId = id;
emit();
playModuleAdd();
return id;
}
@@ -53,6 +61,7 @@ export function removeModule(id) {
);
if (state.selectedModuleId === id) state.selectedModuleId = null;
emit();
playModuleDelete();
}
export function updateModulePosition(id, x, y) {
@@ -78,19 +87,21 @@ export function addConnection(fromModuleId, fromPort, toModuleId, toPort) {
c.to.moduleId === toModuleId && c.to.port === toPort
);
if (inputTaken) {
// Remove old connection to this input
removeConnection(inputTaken.id);
// Remove old connection to this input (silent — connect sound will play)
removeConnection(inputTaken.id, true);
}
const id = _nextConnectionId++;
state.connections.push({ id, from: { moduleId: fromModuleId, port: fromPort }, to: { moduleId: toModuleId, port: toPort } });
emit();
playConnect();
return id;
}
export function removeConnection(id) {
export function removeConnection(id, _silent = false) {
state.connections = state.connections.filter(c => c.id !== id);
emit();
if (!_silent) playDisconnect();
}
export function getModule(id) {