Files
reaktor/packages/client/src/game/levels/world10.js
Jose Luis b058997889 refactor: restructure to monorepo with npm workspaces (Phase 0)
Move frontend to packages/client/, server to packages/server/.
Root package.json uses npm workspaces to orchestrate both.

Structure:
  reaktor/
    packages/client/  (React + Vite + Tone.js frontend)
    packages/server/  (static file server, future API)
    dist/             (built output, shared)
    docker-compose.yml (app + PostgreSQL for future backend)

- npm run dev → runs Vite dev server from client workspace
- npm run build → builds client, outputs to root dist/
- npm run start → runs server.js serving dist/
- Dockerfile updated for multi-stage monorepo build
- docker-compose.yml added with PostgreSQL service (ready for Phase 1)
- All imports and paths preserved, zero functionality change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:52:57 +01:00

577 lines
27 KiB
JavaScript

/**
* World 10 — "Espacio y Stereo" (Space and Stereo)
*
* Teaches: Stereo imaging, spatial effects, delay for width, reverb placement
* 8 levels, boss challenges with complete stereo mix
*/
export const WORLD_10 = {
id: 'w10',
name: 'Espacio y Stereo',
subtitle: 'Profundidad y dimensión',
icon: '◉◉',
color: '#44ddaa',
unlockStars: 108,
levels: [
// ─────────────── LEVEL 10.1 ───────────────
{
id: 'w10-1',
title: 'Pan Left-Right',
subtitle: 'Los canales estéreo básicos',
description: 'La estéreo más simple: coloca una fuente en el canal izquierdo y otra en el derecho. El output tiene dos entradas: "left" y "right". Conecta diferentes osciladores a cada uno.',
concept: 'Osc 1 → Output (left). Osc 2 → Output (right). El output tiene dos canales separados. Juntos crean la ilusión de width — como si el sonido viniera de dos lugares diferentes.',
availableModules: ['oscillator', 'vca', 'envelope', 'keyboard', 'mixer'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 220, detune: 0 } },
{ type: 'oscillator', params: { waveform: 'sine', frequency: 330, detune: 0 } },
],
envelope: { attack: 0.05, decay: 0.3, sustain: 0.4, release: 0.2 },
duration: 3,
},
checks: [
{
star: 1,
name: 'Estéreo básica',
desc: 'Dos osciladores, uno al left, uno al right',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || !out) return false;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
return leftConn && rightConn;
},
},
{
star: 2,
name: 'Estéreo con VCA',
desc: 'Cada oscilador con su VCA antes de output',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const vcas = mods.filter(m => m.type === 'vca');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || vcas.length < 2 || !out) return false;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
return leftConn && rightConn &&
oscs.some(o => conns.some(c => c.from.moduleId === o.id && c.to.moduleId === vcas[0].id));
},
},
{
star: 3,
name: 'Estéreo Controlada',
desc: 'Oscs left/right con envelopes separados gateados por keyboard',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const vcas = mods.filter(m => m.type === 'vca');
const envs = mods.filter(m => m.type === 'envelope');
const kb = mods.find(m => m.type === 'keyboard');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || vcas.length < 2 || envs.length < 2 || !kb || !out) return false;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
const gated = envs.filter(e =>
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === e.id && c.to.port === 'gate')
);
return leftConn && rightConn && gated.length >= 2;
},
},
],
},
// ─────────────── LEVEL 10.2 ───────────────
{
id: 'w10-2',
title: 'Stereo Detune',
subtitle: 'Ancho con osciladores diferentes',
description: 'Coloca el mismo oscilador en ambos canales pero detuned: izquierda a la frecuencia exacta, derecha con un pequeño detune (+5 a +15 cents). Crea un "chorus" natural que te envuelve.',
concept: 'Osc 1 (detune 0) → Left. Osc 2 (detune +7) a misma nota → Right. Cuando están cerca pero no iguales, el beating crea width. Es como tener dos cantantes cantando casi al unísono.',
availableModules: ['oscillator', 'vca', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 220, detune: 0 } },
{ type: 'oscillator', params: { waveform: 'sine', frequency: 220, detune: 8 } },
],
envelope: { attack: 0.06, decay: 0.35, sustain: 0.35, release: 0.25 },
duration: 3,
},
checks: [
{
star: 1,
name: 'Dos osciladores detuned',
desc: 'Oscs a misma frecuencia pero con detune diferente',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || !out) return false;
const detunes = oscs.map(o => o.params.detune ?? 0);
const freqs = oscs.map(o => o.params.frequency ?? 440);
const sameFreq = Math.abs(freqs[0] - freqs[1]) < 10;
const differentDetune = Math.abs(detunes[0] - detunes[1]) > 3;
return sameFreq && differentDetune;
},
},
{
star: 2,
name: 'Stereo width audible',
desc: 'Detune entre oscs > 5 cents para efecto chorus',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
if (oscs.length < 2) return false;
const detunes = oscs.map(o => o.params.detune ?? 0);
return Math.abs(detunes[0] - detunes[1]) > 5;
},
},
{
star: 3,
name: 'Chorus Estéreo',
desc: 'Detuned oscs left/right con VCAs y envelopes',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const vcas = mods.filter(m => m.type === 'vca');
const envs = mods.filter(m => m.type === 'envelope');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || vcas.length < 2 || envs.length < 1 || !out) return false;
const detunes = oscs.map(o => o.params.detune ?? 0);
const freqs = oscs.map(o => o.params.frequency ?? 440);
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
return Math.abs(freqs[0] - freqs[1]) < 10 &&
Math.abs(detunes[0] - detunes[1]) > 5 &&
leftConn && rightConn;
},
},
],
},
// ─────────────── LEVEL 10.3 ───────────────
{
id: 'w10-3',
title: 'Delay para Ancho',
subtitle: 'La profundidad del eco',
description: 'El delay es uno de los mejores trucos para width: copia la señal, la envía al otro canal con un pequeño delay (20-80ms). El cerebro interpreta esto como "la misma fuente reflejada en espacio".',
concept: 'Osc → Left (seco). Osc → Delay (15-50ms) → Right. El delay crea la ilusión de distancia. Cuanto más delay, más separación. Mantén el feedback bajo para evitar caos.',
availableModules: ['oscillator', 'vca', 'delay', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 165, detune: 0 } },
],
effects: [
{ type: 'delay', delayTime: 0.035, feedback: 0.15, wet: 0.8 },
],
envelope: { attack: 0.08, decay: 0.4, sustain: 0.3, release: 0.3 },
duration: 3,
},
checks: [
{
star: 1,
name: 'Delay en señal',
desc: 'Oscilador → Delay → Output',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const del = mods.find(m => m.type === 'delay');
const out = mods.find(m => m.type === 'output');
if (!osc || !del || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === del.id) &&
conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'Delay corto',
desc: 'Delay con tiempo entre 20-80ms',
test: (mods) => {
const del = mods.find(m => m.type === 'delay');
if (!del) return false;
const time = del.params.delayTime ?? 0.3;
return time >= 0.02 && time <= 0.08;
},
},
{
star: 3,
name: 'Delay Estéreo',
desc: 'Osc left + Osc/Delay right con envelopes',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const del = mods.find(m => m.type === 'delay');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 1 || !del || !out) return false;
const time = del.params.delayTime ?? 0.3;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id && c.to.port === 'right');
return time >= 0.015 && time <= 0.1 &&
(del.params.feedback ?? 0.4) < 0.5 &&
leftConn && rightConn;
},
},
],
},
// ─────────────── LEVEL 10.4 ───────────────
{
id: 'w10-4',
title: 'Reverb Corta',
subtitle: 'La sala pequeña',
description: 'Una reverb corta (decay 1-2s) simula una habitación pequeña. No es mucha cola, solo lo suficiente para darle "espacio" al sonido sin que desaparezca en la distancia. Perfecto para síntesis.',
concept: 'Osc → VCA → Reverb (decay 1-2s, wet 0.3-0.5) → Output. La reverb enturbia ligeramente el sonido y lo coloca "en una sala". Mantén wet bajo para que no sea un sonido amortiguado.',
availableModules: ['oscillator', 'vca', 'reverb', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 262, detune: 0 } },
],
effects: [
{ type: 'reverb', decay: 1.5, wet: 0.4 },
],
envelope: { attack: 0.07, decay: 0.4, sustain: 0.35, release: 0.25 },
duration: 3,
},
checks: [
{
star: 1,
name: 'Reverb en la cadena',
desc: 'Osc → Reverb → Output',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const rev = mods.find(m => m.type === 'reverb');
const out = mods.find(m => m.type === 'output');
if (!osc || !rev || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === rev.id) &&
conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'Decay corta',
desc: 'Reverb con decay entre 1-2 segundos',
test: (mods) => {
const rev = mods.find(m => m.type === 'reverb');
if (!rev) return false;
const decay = rev.params.decay ?? 3;
return decay >= 1 && decay <= 2;
},
},
{
star: 3,
name: 'Sala Perfecta',
desc: 'Reverb (decay 1-2s, wet 0.3-0.5) + envelope al VCA',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
if (!osc || !vca || !rev || !env) return false;
const decay = rev.params.decay ?? 3;
const wet = rev.params.wet ?? 0.4;
return decay >= 1 && decay <= 2 &&
wet >= 0.25 && wet <= 0.6 &&
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv');
},
},
],
},
// ─────────────── LEVEL 10.5 ───────────────
{
id: 'w10-5',
title: 'Catedral Reverb',
subtitle: 'Los espacios enormes',
description: 'Una catedral reverb es lo opuesto: decay largo (3+ segundos), wet alto. El sonido se desvanece lentamente, como si estuvieras en una basílica gigante. Crea atmósfera épica.',
concept: 'Osc → VCA → Reverb (decay > 3s, wet > 0.5) → Output. El sonido se desmorona lentamente en el aire. Usa notas largas para aprovechar la cola reverb. ¡Es mágico!',
availableModules: ['oscillator', 'vca', 'reverb', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 196, detune: 0 } },
],
effects: [
{ type: 'reverb', decay: 4.2, wet: 0.65 },
],
envelope: { attack: 0.06, decay: 0.8, sustain: 0.4, release: 0.5 },
duration: 4,
},
checks: [
{
star: 1,
name: 'Reverb larga',
desc: 'Reverb con decay > 3 segundos',
test: (mods) => {
const rev = mods.find(m => m.type === 'reverb');
if (!rev) return false;
return (rev.params.decay ?? 3) > 3;
},
},
{
star: 2,
name: 'Reverb mojada',
desc: 'Reverb con wet > 0.5 para efecto dramático',
test: (mods) => {
const rev = mods.find(m => m.type === 'reverb');
if (!rev) return false;
return (rev.params.decay ?? 3) > 3 &&
(rev.params.wet ?? 0.4) > 0.5;
},
},
{
star: 3,
name: 'Catedral Épica',
desc: 'Reverb (decay > 4s, wet > 0.6) con envelope lento al VCA',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
const kb = mods.find(m => m.type === 'keyboard');
if (!osc || !vca || !rev || !env || !kb) return false;
return (rev.params.decay ?? 3) > 4 &&
(rev.params.wet ?? 0.4) > 0.6 &&
(env.params.attack ?? 0.01) < 0.1 &&
(env.params.decay ?? 0.2) > 0.5 &&
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id && c.to.port === 'gate');
},
},
],
},
// ─────────────── LEVEL 10.6 ───────────────
{
id: 'w10-6',
title: 'Slapback Echo',
subtitle: 'Doblado rítmico',
description: 'El slapback echo es un delay muy corto (100-200ms) sin feedback, que crea un efecto de "doblado" — como si hubiera una copia del sonido muy cerca. Popular en rockabilly y sintetizadores.',
concept: 'Osc → Left (seco). Osc → Delay (100-200ms, feedback bajo) → Right. El delay corto mantiene la segunda "voz" identificable pero cercana. Es como tener un doblante.',
availableModules: ['oscillator', 'vca', 'delay', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'square', frequency: 220, detune: 0 } },
],
effects: [
{ type: 'delay', delayTime: 0.15, feedback: 0.08, wet: 0.75 },
],
envelope: { attack: 0.05, decay: 0.35, sustain: 0.4, release: 0.2 },
duration: 3,
},
checks: [
{
star: 1,
name: 'Delay rítmico',
desc: 'Delay entre 80-250ms',
test: (mods) => {
const del = mods.find(m => m.type === 'delay');
if (!del) return false;
const time = del.params.delayTime ?? 0.3;
return time >= 0.08 && time <= 0.25;
},
},
{
star: 2,
name: 'Sin feedback',
desc: 'Delay con feedback < 0.2 para no crear repeticiones caóticas',
test: (mods) => {
const del = mods.find(m => m.type === 'delay');
if (!del) return false;
const time = del.params.delayTime ?? 0.3;
const fb = del.params.feedback ?? 0.4;
return time >= 0.08 && time <= 0.25 && fb < 0.2;
},
},
{
star: 3,
name: 'Doblante Perfecto',
desc: 'Delay (100-200ms, feedback < 0.1) en stereo left/right',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const del = mods.find(m => m.type === 'delay');
const out = mods.find(m => m.type === 'output');
if (!osc || !del || !out) return false;
const time = del.params.delayTime ?? 0.3;
const fb = del.params.feedback ?? 0.4;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id && c.to.port === 'right');
return time >= 0.1 && time <= 0.2 &&
fb < 0.1 &&
leftConn && rightConn;
},
},
],
},
// ─────────────── LEVEL 10.7 ───────────────
{
id: 'w10-7',
title: 'Orden de Efectos',
subtitle: 'La cadena de procesamiento',
description: 'El orden de los efectos es crítico: ¿delay antes o después de reverb? ¿Filtro antes que distortion? Aquí aprendes a construir cadenas de efectos que suenen coherentes y profesionales.',
concept: 'Construye: Osc → Filter → Distortion → Delay → Reverb → Output. Cada efecto transforma el anterior. El filtro quita brillo, distortion añade armónicos, delay añade movimiento, reverb añade espacio.',
availableModules: ['oscillator', 'filter', 'distortion', 'delay', 'reverb', 'vca', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 900, y: 140, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 280, detune: 0 } },
],
filter: { type: 'lowpass', frequency: 3500, Q: 1.5 },
effects: [
{ type: 'distortion', distortion: 0.45, wet: 0.5 },
{ type: 'delay', delayTime: 0.3, feedback: 0.35, wet: 0.55 },
{ type: 'reverb', decay: 2.2, wet: 0.45 },
],
envelope: { attack: 0.08, decay: 0.45, sustain: 0.25, release: 0.3 },
duration: 4,
},
checks: [
{
star: 1,
name: 'Cadena básica',
desc: 'Osc → Filter → Delay → Reverb → Output',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const del = mods.find(m => m.type === 'delay');
const rev = mods.find(m => m.type === 'reverb');
const out = mods.find(m => m.type === 'output');
if (!osc || !flt || !del || !rev || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id) &&
conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === del.id) &&
conns.some(c => c.from.moduleId === del.id && c.to.moduleId === rev.id) &&
conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'Con distortion',
desc: 'Cadena con filtro + distortion + delay + reverb',
test: (mods, conns) => {
const flt = mods.find(m => m.type === 'filter');
const dist = mods.find(m => m.type === 'distortion');
const del = mods.find(m => m.type === 'delay');
const rev = mods.find(m => m.type === 'reverb');
if (!flt || !dist || !del || !rev) return false;
return conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === del.id) ||
conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === rev.id);
},
},
{
star: 3,
name: 'Cadena Profesional',
desc: 'Osc → Filter → Distortion → Delay → Reverb con envelope',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const dist = mods.find(m => m.type === 'distortion');
const del = mods.find(m => m.type === 'delay');
const rev = mods.find(m => m.type === 'reverb');
const env = mods.find(m => m.type === 'envelope');
const out = mods.find(m => m.type === 'output');
if (!osc || !flt || !dist || !del || !rev || !env || !out) return false;
const fltOsc = conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id);
const distFlt = conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === dist.id);
const delDist = conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === del.id);
const revDel = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === rev.id);
const outRev = conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
return fltOsc && distFlt && delDist && revDel && outRev;
},
},
],
},
// ─────────────── LEVEL 10.8: BOSS ───────────────
{
id: 'w10-8',
title: 'Mix Espacial',
subtitle: 'BOSS FINAL: Orquesta Estéreo',
description: 'Construye una mezcla estéreo completa con múltiples fuentes, cada una con su propia posición en el espacio. Usa delay, reverb, y pan para colocar cada instrumento. Crea una orquesta de sintetizadores.',
concept: 'Múltiples osciladores/fuentes, algunos en left/right, algunos con delay, algunos con reverb, todos controlados por keyboard/sequencer. La mezcla final debe sonar amplia, profunda, y multidimensional.',
availableModules: ['oscillator', 'filter', 'vca', 'mixer', 'lfo', 'envelope', 'keyboard', 'sequencer', 'delay', 'reverb', 'distortion'],
preplacedModules: [
{ id: 1, type: 'output', x: 950, y: 160, params: { volume: -8 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 110, detune: 0 } },
{ type: 'oscillator', params: { waveform: 'sine', frequency: 110, detune: 10 } },
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220, detune: 0 } },
],
filter: { type: 'lowpass', frequency: 4000, Q: 1.3 },
lfo: { frequency: 0.6, type: 'sine', min: 2000, max: 5000, target: 'frequency' },
effects: [
{ type: 'delay', delayTime: 0.25, feedback: 0.4, wet: 0.6 },
{ type: 'reverb', decay: 3, wet: 0.55 },
],
envelope: { attack: 0.1, decay: 0.5, sustain: 0.3, release: 0.4 },
duration: 5,
},
checks: [
{
star: 1,
name: 'Mezcla funcional',
desc: 'Múltiples fuentes en left y right del output',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || !out) return false;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
return leftConn && rightConn && conns.length >= 8;
},
},
{
star: 2,
name: 'Con efectos espaciales',
desc: 'Delay y Reverb en la mezcla creando profundidad',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const del = mods.find(m => m.type === 'delay');
const rev = mods.find(m => m.type === 'reverb');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 2 || !del || !rev || !out) return false;
const delToOut = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id);
const revToOut = conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
return delToOut && revToOut;
},
},
{
star: 3,
name: 'Orquesta Completa',
desc: '3+ oscs, stereo pan, delay + reverb, filter, envelope, keyboard/sequencer',
test: (mods, conns) => {
const oscs = mods.filter(m => m.type === 'oscillator');
const flts = mods.filter(m => m.type === 'filter');
const envs = mods.filter(m => m.type === 'envelope');
const del = mods.find(m => m.type === 'delay');
const rev = mods.find(m => m.type === 'reverb');
const kb = mods.find(m => m.type === 'keyboard');
const seq = mods.find(m => m.type === 'sequencer');
const out = mods.find(m => m.type === 'output');
if (oscs.length < 3 || flts.length < 1 || envs.length < 1 || !del || !rev || !out) return false;
if (!kb && !seq) return false;
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
const delToOut = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id);
const revToOut = conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
return leftConn && rightConn && delToOut && revToOut && conns.length >= 15;
},
},
],
},
],
};