Files
reaktor/src/game/levels/world12.js
Jose Luis a1be6df355 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>
2026-03-21 03:03:29 +01:00

501 lines
26 KiB
JavaScript

/**
* World 12 — "Gran Final" (Grand Finale)
*
* Teaches: building a complete track from start to finish
* 8 levels creating a full production: intro, drop, lead, breakdown, build-up, mix, outro
* boss challenge: create a complete musical piece with scope visualization
*/
export const WORLD_12 = {
id: 'w12',
name: 'Gran Final',
subtitle: 'Tu obra maestra',
icon: '♛',
color: '#ffd700',
unlockStars: 132,
levels: [
// ─────────────── LEVEL 12.1 ───────────────
{
id: 'w12-1',
title: 'Intro Ambiental',
subtitle: 'Comenzando suavemente',
description: 'Toda gran pista comienza con una introducción ambiental. Crea una atmósfera con pads, sonidos largos y efectos de reverb/delay. Sin ritmo fuerte, solo texturas flotantes.',
concept: 'Dos oscs sine graves detuned + Mixer → Filter LP → VCA con envelope muy largo → Reverb → Output. LFO lento al cutoff. Sin percusión, puro ambiente. Cero attack, máximo sustain.',
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'mixer', 'reverb'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 120, params: { volume: -8 }, locked: true },
],
target: { build: [], duration: 5 },
checks: [
{
star: 1,
name: 'Pad ambiental',
desc: '2 oscs sine grave + reverb largo',
test: (mods) => {
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'sine');
const rev = mods.find(m => m.type === 'reverb');
if (oscs.length < 2 || !rev) return false;
return oscs.filter(o => (o.params.frequency ?? 440) < 150).length >= 2 &&
(rev.params.decay ?? 2) > 3;
},
},
{
star: 2,
name: 'Evolución lenta',
desc: 'LFO < 1 Hz modulando cutoff, envelope muy largo (decay > 1s)',
test: (mods) => {
const lfo = mods.find(m => m.type === 'lfo');
const env = mods.find(m => m.type === 'envelope');
if (!lfo || !env) return false;
return (lfo.params.frequency ?? 2) < 1 &&
(env.params.decay ?? 0.2) > 1 &&
(env.params.sustain ?? 0.5) > 0.4;
},
},
{
star: 3,
name: 'Intro hipnótica',
desc: '2+ oscs detuned, filter LP, LFO lento al cutoff, reverb > 4s, envelope attack 0',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'sine');
const flt = mods.find(m => m.type === 'filter');
const lfo = mods.find(m => m.type === 'lfo');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
if (oscs.length < 2 || !flt || !lfo || !rev || !env) return false;
const graveLong = oscs.filter(o => (o.params.frequency ?? 440) < 150).length >= 2;
const hasDetune = oscs.some(o => Math.abs(o.params.detune ?? 0) > 2);
const slowLfo = (lfo.params.frequency ?? 2) < 1;
const longRev = (rev.params.decay ?? 2) > 4;
const niceEnv = (env.params.attack ?? 0.01) < 0.05 && (env.params.decay ?? 0.2) > 1;
const lfoToFilter = conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
return graveLong && hasDetune && slowLfo && longRev && niceEnv && lfoToFilter;
},
},
],
},
// ─────────────── LEVEL 12.2 ───────────────
{
id: 'w12-2',
title: 'El Drop',
subtitle: 'Entra el beat con fuerza',
description: 'Después de la intro, llega el drop: un cambio dramático donde entra el kick, snare y bass graves. Es el momento de tensión y energía. Combina un bass grave con un beat de síntesis.',
concept: 'Dos elementos: 1) Drum: Osc sine grave (~55 Hz) con envelope rápido (attack 0, decay 0.2). 2) Bass: Oscs sawtooth detuned, filtro LP abierto, sonido gordo y agresivo. Sequencer para el ritmo.',
availableModules: ['oscillator', 'vca', 'envelope', 'mixer', 'filter', 'sequencer'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -6 }, locked: true },
],
target: { build: [], duration: 4 },
checks: [
{
star: 1,
name: 'Kick + Bass',
desc: 'Osc grave con envelope corto (kick) + osc grave para bass',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const env = mods.find(m => m.type === 'envelope');
if (oscs.length < 2 || !vca || !env) return false;
const graveOscs = oscs.filter(o => (o.params.frequency ?? 440) < 100);
return graveOscs.length >= 2 && (env.params.decay ?? 0.2) < 0.3;
},
},
{
star: 2,
name: 'Ritmo percibible',
desc: 'Sequencer conectado, beat claro con kick percusivo',
test: (mods, conns) => {
const seq = mods.find(m => m.type === 'sequencer');
const osc = mods.find(m => m.type === 'oscillator');
const env = mods.find(m => m.type === 'envelope');
if (!seq || !osc || !env) return false;
return conns.some(c => c.from.moduleId === seq.id && c.to.moduleId === osc.id) &&
(env.params.decay ?? 0.2) < 0.25;
},
},
{
star: 3,
name: 'Drop potente',
desc: 'Kick < 80 Hz decay < 0.2s, bass sawtooth detuned, sequencer, sonido gordo y fuerte',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const seq = mods.find(m => m.type === 'sequencer');
const env = mods.find(m => m.type === 'envelope');
const flt = mods.find(m => m.type === 'filter');
if (oscs.length < 2 || !seq || !env) return false;
const kickOsc = oscs.find(o => (o.params.frequency ?? 440) < 80);
const sawOscs = oscs.filter(o => o.params.waveform === 'sawtooth');
const hasDetune = sawOscs.some(o => Math.abs(o.params.detune ?? 0) > 3);
const fastKick = (env.params.decay ?? 0.2) < 0.2;
const seqConnected = conns.some(c => c.from.moduleId === seq.id);
return kickOsc && sawOscs.length > 0 && hasDetune && fastKick && seqConnected;
},
},
],
},
// ─────────────── LEVEL 12.3 ───────────────
{
id: 'w12-3',
title: 'Lead Melódico',
subtitle: 'Melodía protagonista',
description: 'Usa el piano roll para crear una melodía líder que brille sobre el bass. El lead es típicamente un solo sintetizado con oscilador brillante, filtro modulado y reverb para espaciosidad.',
concept: 'Piano roll → Osc square/bright → Filter LP con resonancia → VCA → Reverb → Mixer. Envelope para notas definidas (attack corto, decay/sustain para "peso"). LFO lento al cutoff para movimiento.',
availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'lfo', 'pianoroll', 'reverb', 'mixer'],
preplacedModules: [
{ id: 1, type: 'output', x: 850, y: 140, params: { volume: -8 }, locked: true },
],
target: { build: [], duration: 5 },
checks: [
{
star: 1,
name: 'Melodía activa',
desc: 'Piano roll conectado a osc, notas reproducidas',
test: (mods, conns) => {
const pr = mods.find(m => m.type === 'pianoroll');
const osc = mods.find(m => m.type === 'oscillator');
if (!pr || !osc) return false;
return conns.some(c => c.from.moduleId === pr.id && c.to.moduleId === osc.id && c.to.port === 'freq');
},
},
{
star: 2,
name: 'Lead con carácter',
desc: 'Osc square/bright, filter resonante, envelope con ataque corto',
test: (mods) => {
const osc = mods.find(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const env = mods.find(m => m.type === 'envelope');
if (!osc || !flt || !env) return false;
const isBright = osc.params.waveform === 'square' || osc.params.waveform === 'sawtooth';
const hasResonance = (flt.params.Q ?? 1) > 2;
const quickAttack = (env.params.attack ?? 0.01) < 0.05;
return isBright && hasResonance && quickAttack;
},
},
{
star: 3,
name: 'Lead melódico',
desc: 'Piano roll + osc square con filter resonante + LFO al cutoff + reverb, notas claramente escuchables',
test: (mods, conns) => {
const pr = mods.find(m => m.type === 'pianoroll');
const osc = mods.find(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const lfo = mods.find(m => m.type === 'lfo');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
if (!pr || !osc || !flt || !lfo || !rev || !env) return false;
const prConnected = conns.some(c => c.from.moduleId === pr.id && c.to.moduleId === osc.id && c.to.port === 'freq');
const gateConnected = conns.some(c => c.from.moduleId === pr.id && c.to.moduleId === env.id && c.to.port === 'gate');
const lfoToFilter = conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
return prConnected && gateConnected && lfoToFilter && (rev.params.decay ?? 2) > 2;
},
},
],
},
// ─────────────── LEVEL 12.4 ───────────────
{
id: 'w12-4',
title: 'Breakdown',
subtitle: 'Menos es más',
description: 'El breakdown es una sección donde quitas elementos clave para crear contraste. Quitas el kick, quitas el bass pesado, dejas solo los pads suaves o un synth secundario. Construye anticipación para el regreso.',
concept: 'Calla el kick y bass de secciones previas. Deja solo pads suaves, lead melódico suave, y efectos. Opcional: introduce un elemento nuevo y suave (strings sintéticos, pad etéreo). Todo con reverb abundante.',
availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'lfo', 'mixer', 'reverb', 'pianoroll'],
preplacedModules: [
{ id: 1, type: 'output', x: 850, y: 140, params: { volume: -8 }, locked: true },
],
target: { build: [], duration: 5 },
checks: [
{
star: 1,
name: 'Sonido suave',
desc: 'Oscs sine/pads, sin percusión aguda, reverb presente',
test: (mods) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const rev = mods.find(m => m.type === 'reverb');
if (!rev || oscs.length < 1) return false;
const sines = oscs.filter(o => o.params.waveform === 'sine');
return sines.length >= 1 && (rev.params.decay ?? 2) > 2;
},
},
{
star: 2,
name: 'Atmósfera ambiental',
desc: 'Múltiples layers suaves, LFO modulando filtro, no hay kicks agudos',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const lfo = mods.find(m => m.type === 'lfo');
if (oscs.length < 2 || !flt || !lfo) return false;
const softOscs = oscs.filter(o => (o.params.frequency ?? 440) < 200);
return softOscs.length >= 1 &&
conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
},
},
{
star: 3,
name: 'Breakdown perfecto',
desc: '2+ oscs suaves, filtro con LFO, envelope largo, reverb > 3s, sonido flotante y aéreo',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const lfo = mods.find(m => m.type === 'lfo');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
if (oscs.length < 2 || !flt || !lfo || !rev || !env) return false;
const softOscs = oscs.filter(o => (o.params.frequency ?? 440) < 200);
const longEnv = (env.params.decay ?? 0.2) > 1 && (env.params.sustain ?? 0.5) > 0.3;
const lfoToFilter = conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
return softOscs.length >= 2 && longEnv && lfoToFilter && (rev.params.decay ?? 2) > 3;
},
},
],
},
// ─────────────── LEVEL 12.5 ───────────────
{
id: 'w12-5',
title: 'Build-Up',
subtitle: 'La tensión sube',
description: 'El build-up es donde añades elementos gradualmente para construir tensión. Comienzas minimal, y lentamente añades más capas: pads, bass, efectos, filtros abriendo. La audiencia siente que algo grande viene.',
concept: 'Empieza con un LFO lento abriendo un filtro sobre un oscilador suave. Gradualmente: añade un segundo osc, un tercer osc, baja el cutoff, suena más agresivo. El sequencer acelera. La reverb se vuelve más agresiva (menos decay).',
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'mixer', 'reverb', 'sequencer'],
preplacedModules: [
{ id: 1, type: 'output', x: 850, y: 140, params: { volume: -6 }, locked: true },
],
target: { build: [], duration: 5 },
checks: [
{
star: 1,
name: 'Tensión creciente',
desc: 'LFO modulando filter cutoff, sonido evoluciona',
test: (mods, conns) => {
const lfo = mods.find(m => m.type === 'lfo');
const flt = mods.find(m => m.type === 'filter');
if (!lfo || !flt) return false;
return conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
},
},
{
star: 2,
name: 'Múltiples layers',
desc: '3+ oscs, filtro con LFO, sonido más agresivo que intro',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const seq = mods.find(m => m.type === 'sequencer');
if (oscs.length < 3 || !flt) return false;
const hasSeq = seq && conns.some(c => c.from.moduleId === seq.id);
return hasSeq;
},
},
{
star: 3,
name: 'Build-Up intenso',
desc: '3+ oscs, LFO lento al cutoff, sequencer activo, reverb < 2s (más seco), sonido cresce',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const lfo = mods.find(m => m.type === 'lfo');
const seq = mods.find(m => m.type === 'sequencer');
const rev = mods.find(m => m.type === 'reverb');
if (oscs.length < 3 || !flt || !lfo || !seq) return false;
const slowLfo = (lfo.params.frequency ?? 2) < 1;
const dryReverb = rev && (rev.params.decay ?? 2) < 2.5;
const seqConnected = conns.some(c => c.from.moduleId === seq.id);
const lfoToFilter = conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
return slowLfo && seqConnected && lfoToFilter && conns.length >= 10;
},
},
],
},
// ─────────────── LEVEL 12.6 ───────────────
{
id: 'w12-6',
title: 'Mix Completo',
subtitle: 'Todos los elementos unidos',
description: 'Ahora mezcla todo: intro, drop, lead, breakdown, build-up. Todos los elementos están presentes. El desafío es balancear los volúmenes para que nada se ahogue. Usa un mixer y output con gain correcto.',
concept: 'Enruta todos los elementos de secciones anteriores a un único mixer. Todos los canales del mixer contribuyen al sonido final. Ajusta los gains del mixer y output para balance: nada clipeado, nada muy suave. Sonido cohesivo.',
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'mixer', 'reverb', 'sequencer', 'pianoroll'],
preplacedModules: [
{ id: 1, type: 'output', x: 950, y: 160, params: { volume: -10 }, locked: true },
],
target: { build: [], duration: 6 },
checks: [
{
star: 1,
name: 'Mixer activo',
desc: 'Mixer con múltiples entradas, output rellenado',
test: (mods, conns) => {
const mixer = mods.find(m => m.type === 'mixer');
const out = mods.find(m => m.type === 'output');
if (!mixer || !out) return false;
const inputsToMixer = conns.filter(c => c.to.moduleId === mixer.id).length;
const mixerToOut = conns.some(c => c.from.moduleId === mixer.id && c.to.moduleId === out.id);
return inputsToMixer >= 2 && mixerToOut;
},
},
{
star: 2,
name: 'Balance de sonido',
desc: 'Múltiples elementos (oscs, reverb, seq, pianoroll) todos en mixer',
test: (mods, conns) => {
const mixer = mods.find(m => m.type === 'mixer');
const oscs = mods.filter(m => m.type === 'oscillator');
const seq = mods.find(m => m.type === 'sequencer');
const pr = mods.find(m => m.type === 'pianoroll');
if (!mixer) return false;
const inputCount = conns.filter(c => c.to.moduleId === mixer.id).length;
return oscs.length >= 3 && inputCount >= 4;
},
},
{
star: 3,
name: 'Mix profesional',
desc: '8+ elementos en mixer, sonido balanceado, output -10 a -6dB, 15+ conexiones totales',
test: (mods, conns) => {
const nonOut = mods.filter(m => m.type !== 'output');
const mixer = mods.find(m => m.type === 'mixer');
const out = mods.find(m => m.type === 'output');
if (nonOut.length < 8 || !mixer || !out) return false;
const inputsToMixer = conns.filter(c => c.to.moduleId === mixer.id).length;
const outVolume = out.params.volume ?? -6;
return inputsToMixer >= 5 && outVolume >= -12 && outVolume <= -4 && conns.length >= 15;
},
},
],
},
// ─────────────── LEVEL 12.7 ───────────────
{
id: 'w12-7',
title: 'Outro Etéreo',
subtitle: 'Despedida musical',
description: 'El outro es donde se desvanece todo. Quitas elementos poco a poco, quizás repites la intro ambiental, y añades mucha reverb para crear una sensación de distancia y cierre. El sonido debe desvanecer suavemente.',
concept: 'Repite elementos de la intro: oscs sine graves detuned, filtro suave, LFO muy lento al cutoff, reverb LARGO (5+ segundos). Envelope con sustain muy bajo para fade suave. Opcional: distorsión suave o delay con feedback para movimiento final.',
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'mixer', 'reverb', 'delay'],
preplacedModules: [
{ id: 1, type: 'output', x: 850, y: 140, params: { volume: -10 }, locked: true },
],
target: { build: [], duration: 5 },
checks: [
{
star: 1,
name: 'Reverb largo',
desc: 'Reverb con decay > 4s para fade etéreo',
test: (mods) => {
const rev = mods.find(m => m.type === 'reverb');
return rev && (rev.params.decay ?? 2) > 4;
},
},
{
star: 2,
name: 'Sonido desvanecido',
desc: 'Oscs graves, LFO lento, reverb largo, envelope largo sin gates',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const lfo = mods.find(m => m.type === 'lfo');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
if (oscs.length < 1 || !lfo || !rev || !env) return false;
const softOscs = oscs.filter(o => (o.params.frequency ?? 440) < 150);
const slowLfo = (lfo.params.frequency ?? 2) < 0.5;
const veryLongRev = (rev.params.decay ?? 2) > 4;
return softOscs.length >= 1 && slowLfo && veryLongRev;
},
},
{
star: 3,
name: 'Outro perfecto',
subtitle: '2+ oscs graves detuned, LFO < 0.5 Hz, reverb > 5s, delay con feedback, sonido flota al silencio',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const lfo = mods.find(m => m.type === 'lfo');
const rev = mods.find(m => m.type === 'reverb');
const del = mods.find(m => m.type === 'delay');
const env = mods.find(m => m.type === 'envelope');
if (oscs.length < 2 || !flt || !lfo || !rev || !env) return false;
const graveDetuned = oscs.filter(o => (o.params.frequency ?? 440) < 150).length >= 2 &&
oscs.some(o => Math.abs(o.params.detune ?? 0) > 2);
const verySlowLfo = (lfo.params.frequency ?? 2) < 0.5;
const veryLongRev = (rev.params.decay ?? 2) > 5;
const lfoToFilter = conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
return graveDetuned && verySlowLfo && veryLongRev && lfoToFilter;
},
},
],
},
// ─────────────── LEVEL 12.8: BOSS FINAL ───────────────
{
id: 'w12-8',
title: 'Tu Obra Maestra',
subtitle: 'BOSS FINAL: Tu track completa',
description: 'Eres un sintetista maestro. Construye una obra musical completa: una pista de principio a fin. Intro, drop, lead, breakdown, build-up, mezcla y outro. Usa el módulo scope para visualizar tu sonido. Sin límites. Solo tu visión.',
concept: 'Crea un track de 10+ módulos y 12+ conexiones. Debe tener: keyboard O sequencer, pianoroll para lead, múltiples osciladores, filtros modulados, reverb/delay, y OBLIGATORIO: scope module para visualización. Mixer para balance. Sonido profesional, único y musical.',
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'mixer', 'reverb', 'delay', 'sequencer', 'pianoroll', 'keyboard', 'scope'],
preplacedModules: [
{ id: 1, type: 'output', x: 1000, y: 160, params: { volume: -8 }, locked: true },
],
target: { build: [], duration: 8 },
checks: [
{
star: 1,
name: 'Track básica',
desc: '10+ módulos, 12+ conexiones, scope presente, sonido a través de output',
test: (mods, conns) => {
const nonOut = mods.filter(m => m.type !== 'output');
const scope = mods.find(m => m.type === 'scope');
const out = mods.find(m => m.type === 'output');
if (nonOut.length < 10 || !scope || !out) return false;
const hasOutput = conns.some(c => c.to.moduleId === out.id);
return conns.length >= 12 && hasOutput;
},
},
{
star: 2,
name: 'Estructura musical',
desc: '4+ secciones reconocibles: lead, bass, pads, efectos. Scope visualiza.',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const seq = mods.find(m => m.type === 'sequencer');
const pr = mods.find(m => m.type === 'pianoroll');
const flt = mods.filter(m => m.type === 'filter');
const rev = mods.find(m => m.type === 'reverb');
const scope = mods.find(m => m.type === 'scope');
if (oscs.length < 4 || !scope) return false;
const hasSequencing = seq || pr;
const hasMelody = (pr && conns.some(c => c.from.moduleId === pr.id)) ||
(seq && conns.some(c => c.from.moduleId === seq.id));
return hasSequencing && flt.length >= 2 && rev && hasMelody;
},
},
{
star: 3,
name: 'Masterpiece',
desc: '10+ módulos, keyboard/sequencer/pianoroll, 4+ oscs, mixer, 3+ efectos, scope, 15+ conexiones, música profesional',
test: (mods, conns) => {
const nonOut = mods.filter(m => m.type !== 'output');
const oscs = mods.filter(m => m.type === 'oscillator');
const seq = mods.find(m => m.type === 'sequencer');
const pr = mods.find(m => m.type === 'pianoroll');
const kb = mods.find(m => m.type === 'keyboard');
const mixer = mods.find(m => m.type === 'mixer');
const effects = mods.filter(m => ['delay', 'reverb', 'distortion'].includes(m.type));
const scope = mods.find(m => m.type === 'scope');
if (nonOut.length < 10 || oscs.length < 4 || !mixer || !scope || conns.length < 15) return false;
const hasControl = (seq && conns.some(c => c.from.moduleId === seq.id)) ||
(pr && conns.some(c => c.from.moduleId === pr.id)) ||
(kb && conns.some(c => c.from.moduleId === kb.id));
return hasControl && effects.length >= 3 && conns.length >= 15;
},
},
],
},
],
};