feat: admin mode, worlds 4-6, and stereo output fix
- Admin panel: add/remove stars, unlock worlds, reset progress (🛠 button) - World 4 "Modulación" (8 levels): vibrato, sirena, wah-wah, auto-pan, FM, wobble bass - World 5 "Efectos" (8 levels): delay, slapback, reverb, distortion, dub echo, shoegaze, ambient - World 6 "Diseño Sonoro" (8 levels): kick, hi-hat, snare, pad, reese bass, laser, trance arp, final boss - Star unlock progression: W4=36★, W5=48★, W6=60★ (total 48 levels, 144 stars) - Fix stereo output: left/right channels now route through Tone.Merge for true stereo separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
448
src/game/levels/world5.js
Normal file
448
src/game/levels/world5.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* World 5 — "Efectos" (Effects)
|
||||
*
|
||||
* Teaches: delay, reverb, distortion, effect chains, wet/dry mixing
|
||||
* 8 levels, progressive difficulty
|
||||
*/
|
||||
|
||||
export const WORLD_5 = {
|
||||
id: 'w5',
|
||||
name: 'Efectos',
|
||||
subtitle: 'Transforma el sonido con efectos',
|
||||
icon: '◈',
|
||||
color: '#44ff88',
|
||||
unlockStars: 48,
|
||||
levels: [
|
||||
// ─────────────── LEVEL 5.1 ───────────────
|
||||
{
|
||||
id: 'w5-1',
|
||||
title: 'El Eco',
|
||||
subtitle: 'Delay básico',
|
||||
description: 'El delay repite el sonido después de un tiempo. Es como gritar en un cañón y escuchar tu voz rebotando. El delay más simple tiene un tiempo de repetición y un feedback que controla cuántas veces se repite.',
|
||||
concept: 'Conecta: Oscilador → Delay → Output. El knob "Time" controla el tiempo entre repeticiones. El "Feedback" controla cuántas repeticiones. Empieza con un feedback bajo (~0.3).',
|
||||
availableModules: [],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sine', frequency: 440, detune: 0 }, locked: true },
|
||||
{ id: 2, type: 'delay', x: 340, y: 80, params: { time: 0.3, feedback: 0.3, mix: 0.5 }, locked: false },
|
||||
{ id: 3, type: 'output', x: 600, y: 100, params: { volume: -6 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 3 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Delay conectado',
|
||||
desc: 'Oscilador → Delay → Salida',
|
||||
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: 'Repeticiones',
|
||||
desc: 'Feedback por encima de 0.2',
|
||||
test: (mods) => {
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
return del && (del.params.feedback ?? 0) > 0.2;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Eco rítmico',
|
||||
desc: 'Delay time 0.2-0.5s, feedback 0.3-0.6',
|
||||
test: (mods) => {
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
if (!del) return false;
|
||||
const t = del.params.time ?? 0.3;
|
||||
const fb = del.params.feedback ?? 0;
|
||||
return t >= 0.2 && t <= 0.5 && fb >= 0.3 && fb <= 0.6;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.2 ───────────────
|
||||
{
|
||||
id: 'w5-2',
|
||||
title: 'Slapback',
|
||||
subtitle: 'El delay rockabilly',
|
||||
description: 'El slapback es un delay muy corto (50-120ms) con una sola repetición. Es el efecto clásico de las voces de Elvis y el rockabilly — da presencia sin crear un eco largo.',
|
||||
concept: 'Delay con tiempo corto (~0.05-0.12s) y feedback muy bajo (~0.1 o menos). Una sola repetición rápida. El mix controla cuánto delay se mezcla con la señal original.',
|
||||
availableModules: ['delay'],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'square', frequency: 330, detune: 0 }, locked: true },
|
||||
{ id: 2, type: 'output', x: 600, y: 100, params: { volume: -6 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 2 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Delay en la cadena',
|
||||
desc: 'Osc → Delay → Salida',
|
||||
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: 'Tiempo corto',
|
||||
desc: 'Delay time menor de 0.15s',
|
||||
test: (mods) => {
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
return del && (del.params.time ?? 0.3) < 0.15;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Slapback perfecto',
|
||||
desc: 'Time 0.05-0.12s, feedback < 0.15',
|
||||
test: (mods) => {
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
if (!del) return false;
|
||||
const t = del.params.time ?? 0.3;
|
||||
const fb = del.params.feedback ?? 0.3;
|
||||
return t >= 0.05 && t <= 0.12 && fb < 0.15;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.3 ───────────────
|
||||
{
|
||||
id: 'w5-3',
|
||||
title: 'Reverb Espacial',
|
||||
subtitle: 'El sonido del espacio',
|
||||
description: 'La reverb simula el sonido de un espacio acústico — desde una habitación pequeña hasta una catedral enorme. Es quizás el efecto más usado en toda la producción musical.',
|
||||
concept: 'Conecta: Oscilador → Reverb → Output. El knob de "decay" (o room size) controla el tamaño del espacio. Más largo = catedral. Más corto = habitación pequeña.',
|
||||
availableModules: [],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'triangle', frequency: 440, detune: 0 }, locked: true },
|
||||
{ id: 2, type: 'reverb', x: 340, y: 80, params: { decay: 2, mix: 0.4 }, locked: false },
|
||||
{ id: 3, type: 'output', x: 600, y: 100, params: { volume: -6 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 3 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Reverb conectada',
|
||||
desc: 'Oscilador → Reverb → Salida',
|
||||
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: 'Espacio grande',
|
||||
desc: 'Decay mayor de 3 segundos',
|
||||
test: (mods) => {
|
||||
const rev = mods.find(m => m.type === 'reverb');
|
||||
return rev && (rev.params.decay ?? 2) > 3;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Catedral',
|
||||
desc: 'Decay > 5s, mix 0.3-0.6 (no demasiado)',
|
||||
test: (mods) => {
|
||||
const rev = mods.find(m => m.type === 'reverb');
|
||||
if (!rev) return false;
|
||||
return (rev.params.decay ?? 2) > 5 &&
|
||||
(rev.params.mix ?? 0.5) >= 0.3 && (rev.params.mix ?? 0.5) <= 0.6;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.4 ───────────────
|
||||
{
|
||||
id: 'w5-4',
|
||||
title: 'Distorsión',
|
||||
subtitle: 'Rompe la señal',
|
||||
description: 'La distorsión amplifica la señal hasta que se "rompe", creando armónicos nuevos. Desde el overdrive suave de un amplificador de guitarra hasta el fuzz salvaje — la distorsión añade agresividad y presencia.',
|
||||
concept: 'Conecta: Oscilador → Distortion → Output. Sube el "Drive" para más distorsión. Con una onda sine pura, escucharás cómo aparecen armónicos que no estaban antes.',
|
||||
availableModules: ['distortion'],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sine', frequency: 220, detune: 0 }, locked: true },
|
||||
{ id: 2, type: 'output', x: 600, y: 100, params: { volume: -10 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 2.5 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Distorsión conectada',
|
||||
desc: 'Osc → Distortion → Salida',
|
||||
test: (mods, conns) => {
|
||||
const osc = mods.find(m => m.type === 'oscillator');
|
||||
const dist = mods.find(m => m.type === 'distortion');
|
||||
const out = mods.find(m => m.type === 'output');
|
||||
if (!osc || !dist || !out) return false;
|
||||
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === dist.id) &&
|
||||
conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === out.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 2,
|
||||
name: 'Drive alto',
|
||||
desc: 'Distorsión con drive moderado-alto',
|
||||
test: (mods) => {
|
||||
const dist = mods.find(m => m.type === 'distortion');
|
||||
return dist && (dist.params.drive ?? 1) > 3;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Fuzz sine',
|
||||
desc: 'Onda sine con drive > 5 (máxima transformación)',
|
||||
test: (mods) => {
|
||||
const osc = mods.find(m => m.type === 'oscillator');
|
||||
const dist = mods.find(m => m.type === 'distortion');
|
||||
if (!osc || !dist) return false;
|
||||
return osc.params.waveform === 'sine' && (dist.params.drive ?? 1) > 5;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.5 ───────────────
|
||||
{
|
||||
id: 'w5-5',
|
||||
title: 'Cadena de Efectos',
|
||||
subtitle: 'Orden importa',
|
||||
description: 'El orden de los efectos cambia radicalmente el resultado. Distorsión antes de delay suena diferente a delay antes de distorsión. Experimenta encadenando efectos en diferente orden.',
|
||||
concept: 'Prueba: Osc → Distortion → Delay → Output (la distorsión se repite limpia). El orden crea caracteres distintos. Encadena al menos 2 efectos diferentes.',
|
||||
availableModules: ['delay', 'distortion'],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 60, y: 80, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true },
|
||||
{ id: 2, type: 'output', x: 740, y: 100, params: { volume: -10 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 3 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Dos efectos',
|
||||
desc: 'Al menos 2 módulos de efecto en la cadena',
|
||||
test: (mods, conns) => {
|
||||
const effects = mods.filter(m => ['delay', 'reverb', 'distortion'].includes(m.type));
|
||||
const out = mods.find(m => m.type === 'output');
|
||||
return effects.length >= 2 && out && conns.some(c => c.to.moduleId === out.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 2,
|
||||
name: 'Efectos encadenados',
|
||||
desc: 'Los efectos están conectados en serie (uno al otro)',
|
||||
test: (mods, conns) => {
|
||||
const effects = mods.filter(m => ['delay', 'reverb', 'distortion'].includes(m.type));
|
||||
if (effects.length < 2) return false;
|
||||
// Check if any effect connects to another effect
|
||||
return effects.some(e1 => effects.some(e2 =>
|
||||
e1.id !== e2.id && conns.some(c => c.from.moduleId === e1.id && c.to.moduleId === e2.id)
|
||||
));
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Cadena completa',
|
||||
desc: 'Osc → efecto1 → efecto2 → Output (cadena lineal)',
|
||||
test: (mods, conns) => {
|
||||
const osc = mods.find(m => m.type === 'oscillator');
|
||||
const effects = mods.filter(m => ['delay', 'reverb', 'distortion'].includes(m.type));
|
||||
const out = mods.find(m => m.type === 'output');
|
||||
if (!osc || effects.length < 2 || !out) return false;
|
||||
// Osc → some effect
|
||||
const oscToFx = effects.find(e => conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === e.id));
|
||||
if (!oscToFx) return false;
|
||||
// That effect → another effect
|
||||
const fxToFx = effects.find(e => e.id !== oscToFx.id &&
|
||||
conns.some(c => c.from.moduleId === oscToFx.id && c.to.moduleId === e.id));
|
||||
if (!fxToFx) return false;
|
||||
// Second effect → output
|
||||
return conns.some(c => c.from.moduleId === fxToFx.id && c.to.moduleId === out.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.6 ───────────────
|
||||
{
|
||||
id: 'w5-6',
|
||||
title: 'Delay + Filtro',
|
||||
subtitle: 'Dub echo',
|
||||
description: 'El sonido dub es delay con feedback alto pasado por un filtro que va quitando agudos. Cada repetición suena más oscura y lejana — es el efecto que definió el reggae dub en los 70.',
|
||||
concept: 'Osc → Delay (feedback alto ~0.5-0.7) → Filter (lowpass, cutoff bajo ~800 Hz) → Output. El filtro después del delay oscurece las repeticiones, creando profundidad.',
|
||||
availableModules: ['delay', 'filter'],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 60, y: 80, params: { waveform: 'square', frequency: 330, detune: 0 }, locked: true },
|
||||
{ id: 2, type: 'output', x: 720, y: 100, params: { volume: -8 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 3 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Delay + Filter',
|
||||
desc: 'Osc → Delay → Filter → Output',
|
||||
test: (mods, conns) => {
|
||||
const osc = mods.find(m => m.type === 'oscillator');
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
const flt = mods.find(m => m.type === 'filter');
|
||||
const out = mods.find(m => m.type === 'output');
|
||||
if (!osc || !del || !flt || !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 === flt.id) &&
|
||||
conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === out.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 2,
|
||||
name: 'Echo largo',
|
||||
desc: 'Delay feedback > 0.4, time > 0.2s',
|
||||
test: (mods) => {
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
return del && (del.params.feedback ?? 0) > 0.4 && (del.params.time ?? 0.3) > 0.2;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Dub echo',
|
||||
desc: 'Feedback 0.5-0.7, filtro lowpass cutoff < 1000 Hz',
|
||||
test: (mods) => {
|
||||
const del = mods.find(m => m.type === 'delay');
|
||||
const flt = mods.find(m => m.type === 'filter');
|
||||
if (!del || !flt) return false;
|
||||
const fb = del.params.feedback ?? 0;
|
||||
return fb >= 0.5 && fb <= 0.7 &&
|
||||
flt.params.type === 'lowpass' && (flt.params.frequency ?? 2000) < 1000;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.7 ───────────────
|
||||
{
|
||||
id: 'w5-7',
|
||||
title: 'Shoegaze Wall',
|
||||
subtitle: 'Reverb + Distorsión',
|
||||
description: 'El sonido shoegaze (My Bloody Valentine, Slowdive) es una pared de sonido creada con distorsión y reverb masiva. La distorsión aplasta la señal y la reverb la convierte en una nube etérea.',
|
||||
concept: 'Osc → Distortion (drive medio) → Reverb (decay largo, mix alto) → Output. La combinación de distorsión y reverb crea una textura densa y atmosférica.',
|
||||
availableModules: ['distortion', 'reverb'],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'oscillator', x: 60, y: 80, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: false },
|
||||
{ id: 2, type: 'output', x: 720, y: 100, params: { volume: -10 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 4 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Dist + Reverb',
|
||||
desc: 'Osc → Distortion → Reverb → Output',
|
||||
test: (mods, conns) => {
|
||||
const osc = mods.find(m => m.type === 'oscillator');
|
||||
const dist = mods.find(m => m.type === 'distortion');
|
||||
const rev = mods.find(m => m.type === 'reverb');
|
||||
const out = mods.find(m => m.type === 'output');
|
||||
if (!osc || !dist || !rev || !out) return false;
|
||||
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === dist.id) &&
|
||||
conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === rev.id) &&
|
||||
conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 2,
|
||||
name: 'Pared de sonido',
|
||||
desc: 'Drive > 3, reverb decay > 4s',
|
||||
test: (mods) => {
|
||||
const dist = mods.find(m => m.type === 'distortion');
|
||||
const rev = mods.find(m => m.type === 'reverb');
|
||||
if (!dist || !rev) return false;
|
||||
return (dist.params.drive ?? 1) > 3 && (rev.params.decay ?? 2) > 4;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Shoegaze perfecto',
|
||||
desc: 'Drive 4-8, decay > 6s, reverb mix > 0.5',
|
||||
test: (mods) => {
|
||||
const dist = mods.find(m => m.type === 'distortion');
|
||||
const rev = mods.find(m => m.type === 'reverb');
|
||||
if (!dist || !rev) return false;
|
||||
const drive = dist.params.drive ?? 1;
|
||||
return drive >= 4 && drive <= 8 &&
|
||||
(rev.params.decay ?? 2) > 6 && (rev.params.mix ?? 0.5) > 0.5;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─────────────── LEVEL 5.8: BOSS ───────────────
|
||||
{
|
||||
id: 'w5-8',
|
||||
title: 'Ambient Scape',
|
||||
subtitle: 'BOSS: Paisaje sonoro',
|
||||
description: 'Crea un paisaje sonoro ambient completo: un sonido que evoluciona lentamente, envuelto en efectos. Combina osciladores, filtros, modulación y efectos para crear una textura atmosférica.',
|
||||
concept: 'Osc → Filter (LFO al cutoff) → Delay → Reverb → Output. Envelope al VCA para control. Experimenta con tiempos largos, feedback alto, y modulación lenta para un sonido que "flota".',
|
||||
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'delay', 'reverb', 'keyboard'],
|
||||
preplacedModules: [
|
||||
{ id: 1, type: 'output', x: 900, y: 120, params: { volume: -8 }, locked: true },
|
||||
],
|
||||
target: { build: [], duration: 5 },
|
||||
checks: [
|
||||
{
|
||||
star: 1,
|
||||
name: 'Cadena con efectos',
|
||||
desc: 'Al menos un efecto (delay/reverb) conectado a la salida',
|
||||
test: (mods, conns) => {
|
||||
const effects = mods.filter(m => ['delay', 'reverb'].includes(m.type));
|
||||
const out = mods.find(m => m.type === 'output');
|
||||
if (effects.length === 0 || !out) return false;
|
||||
return effects.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === out.id)) ||
|
||||
conns.some(c => c.to.moduleId === out.id && effects.some(e => c.from.moduleId === e.id));
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 2,
|
||||
name: 'Modulación + Efectos',
|
||||
desc: 'Tiene oscilador, filtro, y al menos 2 efectos conectados',
|
||||
test: (mods, conns) => {
|
||||
const osc = mods.find(m => m.type === 'oscillator');
|
||||
const flt = mods.find(m => m.type === 'filter');
|
||||
const effects = mods.filter(m => ['delay', 'reverb', 'distortion'].includes(m.type));
|
||||
if (!osc || !flt || effects.length < 2) return false;
|
||||
// Check osc is connected to something
|
||||
return conns.some(c => c.from.moduleId === osc.id) && effects.length >= 2;
|
||||
},
|
||||
},
|
||||
{
|
||||
star: 3,
|
||||
name: 'Paisaje completo',
|
||||
desc: 'Osc+Filter+LFO(cutoff)+Delay+Reverb, todo conectado',
|
||||
test: (mods, conns) => {
|
||||
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 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 || !lfo || !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 === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff') &&
|
||||
conns.some(c => c.to.moduleId === out.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user