Files
reaktor/packages/client/src/game/levels/world3.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

512 lines
23 KiB
JavaScript

/**
* World 3 — "Envelopes" (ADSR)
*
* Teaches: attack, decay, sustain, release, VCA, amplitude shaping, sound design
* 8 levels, progressive difficulty
*/
export const WORLD_3 = {
id: 'w3',
name: 'Envelopes',
subtitle: 'Dale forma al sonido en el tiempo',
icon: '⏤╲',
color: '#aa55ff',
unlockStars: 24, // Need 24 stars from World 1+2 to unlock
levels: [
// ─────────────── LEVEL 3.1 ───────────────
{
id: 'w3-1',
title: 'El VCA',
subtitle: 'Control de volumen',
description: 'Un VCA (Voltage Controlled Amplifier) es un amplificador cuyo volumen se puede controlar con una señal externa. Pasa el oscilador por un VCA para poder controlar su volumen.',
concept: 'Conecta: Oscilador → VCA (input "In") → Output. El knob "Gain" del VCA controla cuánto deja pasar. Es como un grifo para el sonido.',
availableModules: [],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true },
{ id: 2, type: 'vca', x: 340, y: 80, params: { gain: 0.5 }, locked: false },
{ id: 3, type: 'output', x: 580, y: 100, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } },
],
duration: 2,
},
checks: [
{
star: 1,
name: 'VCA conectado',
desc: 'Conecta oscilador → VCA → salida',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const out = mods.find(m => m.type === 'output');
if (!osc || !vca || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id && c.to.port === 'in') &&
conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'Volumen moderado',
desc: 'Gain del VCA por debajo de 0.7',
test: (mods) => {
const vca = mods.find(m => m.type === 'vca');
return vca && (vca.params.gain ?? 0.8) < 0.7;
},
},
{
star: 3,
name: 'Medio volumen',
desc: 'Gain cercano a 0.5 (±0.1)',
test: (mods) => {
const vca = mods.find(m => m.type === 'vca');
return vca && Math.abs((vca.params.gain ?? 0.8) - 0.5) <= 0.1;
},
},
],
},
// ─────────────── LEVEL 3.2 ───────────────
{
id: 'w3-2',
title: 'ADSR',
subtitle: 'Las 4 fases del sonido',
description: 'Todo sonido tiene una forma en el tiempo: el Attack (subida), Decay (bajada), Sustain (mantenimiento) y Release (apagado). Un Envelope genera esa curva ADSR que puedes usar para controlar el VCA.',
concept: 'Conecta el Envelope al VCA: la salida del Envelope → entrada CV del VCA. Conecta el Keyboard al Gate del Envelope para que se dispare al tocar. Toca notas y escucha cómo el Envelope da forma al volumen.',
availableModules: ['envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'square', frequency: 440, detune: 0 }, locked: true },
{ id: 2, type: 'vca', x: 400, y: 60, params: { gain: 0 }, locked: false },
{ id: 3, type: 'output', x: 620, y: 80, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'square', frequency: 440 } },
],
envelope: { attack: 0.2, decay: 0.15, sustain: 0.6, release: 0.5 },
duration: 2,
},
checks: [
{
star: 1,
name: 'Cadena con VCA',
desc: 'Oscilador → VCA → Salida',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const out = mods.find(m => m.type === 'output');
if (!osc || !vca || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id) &&
conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'Envelope al VCA',
desc: 'Conecta Envelope → VCA (CV) y Keyboard → Envelope (Gate)',
test: (mods, conns) => {
const env = mods.find(m => m.type === 'envelope');
const vca = mods.find(m => m.type === 'vca');
const kb = mods.find(m => m.type === 'keyboard');
if (!env || !vca || !kb) return false;
return conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv') &&
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id && c.to.port === 'gate');
},
},
{
star: 3,
name: 'Keyboard controla frecuencia',
desc: 'Keyboard → Osc (Freq) para tocar melodías',
test: (mods, conns) => {
const kb = mods.find(m => m.type === 'keyboard');
const osc = mods.find(m => m.type === 'oscillator');
if (!kb || !osc) return false;
return conns.some(c => c.from.moduleId === kb.id && c.from.port === 'freq' && c.to.moduleId === osc.id && c.to.port === 'freq');
},
},
],
},
// ─────────────── LEVEL 3.3 ───────────────
{
id: 'w3-3',
title: 'Percusión',
subtitle: 'Attack rápido, decay corto',
description: 'Los sonidos percusivos tienen un attack instantáneo y un decay corto sin sustain. Piensa en un tambor, un clic, un bleep — el sonido aparece de golpe y muere rápido. Configura un envelope percusivo.',
concept: 'Attack muy bajo (~0.001s), Decay corto (~0.1-0.2s), Sustain a 0, Release corto. Esto crea un "blip" percusivo. Perfecto para hi-hats, kicks sintéticos, y bleeps 8-bit.',
availableModules: ['envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sine', frequency: 440, detune: 0 }, locked: true },
{ id: 2, type: 'vca', x: 400, y: 60, params: { gain: 0 }, locked: false },
{ id: 3, type: 'output', x: 620, y: 80, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 440 } },
],
envelope: { attack: 0.005, decay: 0.15, sustain: 0, release: 0.1 },
duration: 2,
},
checks: [
{
star: 1,
name: 'Señal con envelope',
desc: 'Osc → VCA → Out, con Envelope al CV y Keyboard al Gate',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const env = mods.find(m => m.type === 'envelope');
const kb = mods.find(m => m.type === 'keyboard');
const out = mods.find(m => m.type === 'output');
if (!osc || !vca || !env || !kb || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id) &&
conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id) &&
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv') &&
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id);
},
},
{
star: 2,
name: 'Sin sustain',
desc: 'Sustain a 0 (o casi)',
test: (mods) => {
const env = mods.find(m => m.type === 'envelope');
return env && (env.params.sustain ?? 0.5) < 0.05;
},
},
{
star: 3,
name: 'Blip perfecto',
desc: 'Attack <0.01s, Decay 0.05-0.3s, Sustain ~0',
test: (mods) => {
const env = mods.find(m => m.type === 'envelope');
if (!env) return false;
return (env.params.attack ?? 0.01) < 0.015 &&
(env.params.decay ?? 0.2) >= 0.05 && (env.params.decay ?? 0.2) <= 0.3 &&
(env.params.sustain ?? 0.5) < 0.05;
},
},
],
},
// ─────────────── LEVEL 3.4 ───────────────
{
id: 'w3-4',
title: 'Pad Atmosférico',
subtitle: 'Suave y envolvente',
description: 'Los pads son sonidos largos y suaves que rellenan el fondo de una mezcla. Se consiguen con un attack lento (el sonido entra gradualmente), sustain alto, y release largo (se desvanece lentamente).',
concept: 'Attack lento (~1-2s), Decay corto (~0.3s), Sustain alto (~0.7-0.9), Release largo (~2-4s). El sonido "respira" — entra suave y se queda flotando.',
availableModules: ['envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true },
{ id: 2, type: 'vca', x: 400, y: 60, params: { gain: 0 }, locked: false },
{ id: 3, type: 'output', x: 620, y: 80, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } },
],
envelope: { attack: 1.2, decay: 0.3, sustain: 0.75, release: 2.5 },
duration: 2,
},
checks: [
{
star: 1,
name: 'Señal con envelope',
desc: 'Osc → VCA → Out, Envelope al CV, Keyboard al Gate',
test: (mods, conns) => {
const env = mods.find(m => m.type === 'envelope');
const vca = mods.find(m => m.type === 'vca');
const kb = mods.find(m => m.type === 'keyboard');
if (!env || !vca || !kb) return false;
return conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv') &&
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id);
},
},
{
star: 2,
name: 'Attack lento',
desc: 'Attack mayor de 0.5 segundos',
test: (mods) => {
const env = mods.find(m => m.type === 'envelope');
return env && (env.params.attack ?? 0.01) > 0.5;
},
},
{
star: 3,
name: 'Pad perfecto',
desc: 'Attack >0.8s, Sustain >0.6, Release >1.5s',
test: (mods) => {
const env = mods.find(m => m.type === 'envelope');
if (!env) return false;
return (env.params.attack ?? 0.01) > 0.8 &&
(env.params.sustain ?? 0.5) > 0.6 &&
(env.params.release ?? 0.5) > 1.5;
},
},
],
},
// ─────────────── LEVEL 3.5 ───────────────
{
id: 'w3-5',
title: 'Pluck',
subtitle: 'Cuerdas pulsadas',
description: 'El sonido de una cuerda pulsada (guitarra, arpa) tiene un attack rápido y un decay medio. No tiene sustain real — el sonido decrece naturalmente. El filtro ayuda a que suene más natural.',
concept: 'Envelope con Attack rápido (~0.001s), Decay medio (~0.4-0.8s), Sustain bajo (~0.1), Release ~0.3s. Usa una onda triangle o saw con un filtro lowpass para suavizar.',
availableModules: ['envelope', 'keyboard', 'filter'],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'triangle', frequency: 440, detune: 0 }, locked: false },
{ id: 2, type: 'vca', x: 500, y: 60, params: { gain: 0 }, locked: false },
{ id: 3, type: 'output', x: 760, y: 80, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'triangle', frequency: 440 } },
],
filter: { type: 'lowpass', frequency: 3000, Q: 2 },
envelope: { attack: 0.008, decay: 0.5, sustain: 0.05, release: 0.2 },
duration: 2,
},
checks: [
{
star: 1,
name: 'Cadena completa',
desc: 'Osc → (Filter →) VCA → Out con Envelope y Keyboard',
test: (mods, conns) => {
const env = mods.find(m => m.type === 'envelope');
const vca = mods.find(m => m.type === 'vca');
const kb = mods.find(m => m.type === 'keyboard');
const out = mods.find(m => m.type === 'output');
if (!env || !vca || !kb || !out) return false;
return conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id) &&
conns.some(c => c.from.moduleId === env.id && c.to.port === 'cv') &&
conns.some(c => c.from.moduleId === kb.id && c.to.port === 'gate');
},
},
{
star: 2,
name: 'Forma pluck',
desc: 'Attack rápido (<0.02s), Sustain bajo (<0.2)',
test: (mods) => {
const env = mods.find(m => m.type === 'envelope');
if (!env) return false;
return (env.params.attack ?? 0.01) < 0.02 && (env.params.sustain ?? 0.5) < 0.2;
},
},
{
star: 3,
name: 'Pluck natural',
desc: 'Pluck shape + filtro lowpass en la cadena',
test: (mods, conns) => {
const env = mods.find(m => m.type === 'envelope');
const flt = mods.find(m => m.type === 'filter');
if (!env || !flt) return false;
return (env.params.attack ?? 0.01) < 0.02 &&
(env.params.sustain ?? 0.5) < 0.2 &&
(env.params.decay ?? 0.2) >= 0.3 &&
flt.params.type === 'lowpass';
},
},
],
},
// ─────────────── LEVEL 3.6 ───────────────
{
id: 'w3-6',
title: 'Filtro Dinámico',
subtitle: 'Envelope → Cutoff',
description: 'Los envelopes no solo controlan volumen — ¡también pueden controlar el filtro! Conectar un envelope al cutoff crea sonidos que se "abren" y "cierran" con cada nota. Es la técnica más importante de síntesis sustractiva.',
concept: 'Conecta un segundo Envelope a la entrada Cutoff del filtro. Keyboard → Gate de ambos envelopes. Un envelope controla volumen (VCA), otro controla brillo (filtro cutoff).',
availableModules: ['envelope', 'keyboard', 'filter'],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 60, y: 40, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true },
{ id: 2, type: 'vca', x: 520, y: 40, params: { gain: 0 }, locked: false },
{ id: 3, type: 'output', x: 760, y: 80, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } },
],
filter: { type: 'lowpass', frequency: 800, Q: 4 },
envelope: { attack: 0.01, decay: 0.3, sustain: 0.4, release: 0.2 },
duration: 2,
},
checks: [
{
star: 1,
name: 'Doble envelope',
desc: 'Dos envelopes: uno al VCA, otro al filtro cutoff',
test: (mods, conns) => {
const envs = mods.filter(m => m.type === 'envelope');
const vca = mods.find(m => m.type === 'vca');
const flt = mods.find(m => m.type === 'filter');
if (envs.length < 2 || !vca || !flt) return false;
const envToVca = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === vca.id && c.to.port === 'cv'));
const envToFlt = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === flt.id && c.to.port === 'cutoff'));
return envToVca && envToFlt;
},
},
{
star: 2,
name: 'Gates conectados',
desc: 'Keyboard → Gate de ambos envelopes',
test: (mods, conns) => {
const kb = mods.find(m => m.type === 'keyboard');
const envs = mods.filter(m => m.type === 'envelope');
if (!kb || envs.length < 2) return false;
const gatedEnvs = envs.filter(e =>
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === e.id && c.to.port === 'gate')
);
return gatedEnvs.length >= 2;
},
},
{
star: 3,
name: 'Envelopes distintos',
desc: 'Los dos envelopes tienen decays diferentes (>0.1s diferencia)',
test: (mods) => {
const envs = mods.filter(m => m.type === 'envelope');
if (envs.length < 2) return false;
const decays = envs.map(e => e.params.decay ?? 0.2);
return Math.abs(decays[0] - decays[1]) > 0.1;
},
},
],
},
// ─────────────── LEVEL 3.7 ───────────────
{
id: 'w3-7',
title: 'Tremolo',
subtitle: 'LFO → Volumen',
description: 'El tremolo es una variación rítmica del volumen. Se consigue conectando un LFO a la ganancia del VCA. Es un efecto clásico de guitarras, órganos y sintetizadores vintage.',
concept: 'Conecta un LFO a la entrada CV del VCA (no del filtro). Un LFO a ~4-8 Hz con amplitud moderada crea un tremolo clásico. Más lento (~1-2 Hz) suena como un "pulso".',
availableModules: ['lfo'],
preplacedModules: [
{ id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sine', frequency: 440, detune: 0 }, locked: true },
{ id: 2, type: 'vca', x: 340, y: 60, params: { gain: 0.7 }, locked: false },
{ id: 3, type: 'output', x: 580, y: 80, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sine', frequency: 440 } },
],
lfo: { frequency: 6, type: 'sine', min: 0.2, max: 1.0, target: 'amplitude' },
duration: 3,
},
checks: [
{
star: 1,
name: 'Cadena básica',
desc: 'Oscilador → VCA → Salida',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const vca = mods.find(m => m.type === 'vca');
const out = mods.find(m => m.type === 'output');
if (!osc || !vca || !out) return false;
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id) &&
conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'LFO al VCA',
desc: 'Conecta LFO → VCA (CV)',
test: (mods, conns) => {
const lfo = mods.find(m => m.type === 'lfo');
const vca = mods.find(m => m.type === 'vca');
if (!lfo || !vca) return false;
return conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === vca.id && c.to.port === 'cv');
},
},
{
star: 3,
name: 'Tremolo rítmico',
desc: 'LFO entre 3-10 Hz (tremolo audible)',
test: (mods) => {
const lfo = mods.find(m => m.type === 'lfo');
if (!lfo) return false;
const rate = lfo.params.frequency ?? 2;
return rate >= 3 && rate <= 10;
},
},
],
},
// ─────────────── LEVEL 3.8: BOSS ───────────────
{
id: 'w3-8',
title: 'Synth Lead Completo',
subtitle: 'BOSS: Ponlo todo junto',
description: 'Es hora de construir un sonido de lead completo desde cero. Combina todo lo que has aprendido: oscilador, filtro con envelope, VCA con envelope, y keyboard para tocar. Es el patch clásico de síntesis sustractiva.',
concept: 'Keyboard → Osc (freq) + Env1 (gate) + Env2 (gate). Osc → Filter → VCA → Output. Env1 → Filter cutoff (decay medio para "apertura"). Env2 → VCA cv (sustain para mantener). Ajusta para un lead expresivo.',
availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'keyboard'],
preplacedModules: [
{ id: 1, type: 'output', x: 800, y: 120, params: { volume: -6 }, locked: true },
],
target: {
build: [
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } },
],
filter: { type: 'lowpass', frequency: 2000, Q: 6 },
envelope: { attack: 0.05, decay: 0.3, sustain: 0.5, release: 0.6 },
duration: 3,
},
checks: [
{
star: 1,
name: 'Cadena sustractiva',
desc: 'Osc → Filter → VCA → Output',
test: (mods, conns) => {
const osc = mods.find(m => m.type === 'oscillator');
const flt = mods.find(m => m.type === 'filter');
const vca = mods.find(m => m.type === 'vca');
const out = mods.find(m => m.type === 'output');
if (!osc || !flt || !vca || !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 === vca.id) &&
conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id);
},
},
{
star: 2,
name: 'Doble modulación',
desc: 'Envelope al filtro cutoff Y envelope al VCA cv',
test: (mods, conns) => {
const envs = mods.filter(m => m.type === 'envelope');
const flt = mods.find(m => m.type === 'filter');
const vca = mods.find(m => m.type === 'vca');
if (envs.length < 2 || !flt || !vca) return false;
const envToFlt = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === flt.id && c.to.port === 'cutoff'));
const envToVca = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === vca.id && c.to.port === 'cv'));
return envToFlt && envToVca;
},
},
{
star: 3,
name: 'Lead expresivo',
desc: 'Keyboard controla freq + gates, envelopes distintos',
test: (mods, conns) => {
const kb = mods.find(m => m.type === 'keyboard');
const osc = mods.find(m => m.type === 'oscillator');
const envs = mods.filter(m => m.type === 'envelope');
if (!kb || !osc || envs.length < 2) return false;
// KB → osc freq
const kbFreq = conns.some(c => c.from.moduleId === kb.id && c.from.port === 'freq' && c.to.moduleId === osc.id);
// KB → both env gates
const gated = envs.filter(e =>
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === e.id && c.to.port === 'gate')
);
// Envelopes have different settings
const decays = envs.map(e => e.params.decay ?? 0.2);
const diffDecay = Math.abs(decays[0] - decays[1]) > 0.05;
return kbFreq && gated.length >= 2 && diffDecay;
},
},
],
},
],
};