# Cartuchos de Pixelbook · API v1

Un **cartucho** es un nivel de Pixelbook hecho fuera del equipo. Pixelbook funciona como una consola: el cartucho trae la historia, el dibujo, los retos y, si quiere, la receta de su sonido; la consola pone todo lo demás (voces, el sonido ya producido, puntos, logros, partida guardada y cuenta). El código del cartucho corre en una caja cerrada: sin red, sin almacenamiento y sin acceso a la partida ni a quien juega. Solo habla con la consola a través de esta API.

Todos los niveles de Pixelbook son cartuchos (`carts/`): los nuestros y los hechos fuera, con las mismas reglas. El jefe final de la unidad 1 de Ciencias (`carts/u01-b-jefe`) sirve de ejemplo completo; los de Geografía e Historia (`carts/t02-v…`), de EN DIRECTO.

## Qué lleva un cartucho

```
carts/<id>/
├── cart.json        manifiesto: qué es, qué puntos puede dar, qué logros y qué sonidos usa
├── script.json      guion en datos: escenas, voces, pausas, marcas, música, efectos y retos
├── nivel.json       la ficha del nivel en el mapa, sus cromos, sus logros y sus preguntas para el reto del día
├── soluciones.json  cómo se supera cada reto: la partida automática lo juega así
├── src/*.js         código: cómo se dibuja cada escena y cómo funciona cada reto
└── receta/*.js      receta de sonido (si la hay): cómo suenan su música y sus sonidos propios
```

El manifiesto, el guion, `nivel.json` y `soluciones.json` son **datos**: nunca se ejecutan. Las voces las produce Pixelbook a partir del guion. La música y los efectos salen del catálogo o de la **receta** del cartucho, que Pixelbook cocina en una caja cerrada y masteriza (ver «La receta»). El cartucho nunca trae archivos de audio.

## cart.json

| Campo | Qué es |
| --- | --- |
| `api` | Siempre `1` |
| `id` | Igual que la carpeta: minúsculas, cifras y guiones (40 como mucho). Con un encargo, su `slug` (ver «Encargos») |
| `version` | `1.0.0`. Cada cambio es una versión nueva y vuelve a revisión |
| `title`, `series`, `book`, `course`, `unit`, `unitTitle`, `part`, `pages` | Qué nivel es y de qué libro sale |
| `fps` | Siempre `30` |
| `cast` | Reparto de la serie: `modo-ciencia`, `en-directo` o `remix` |
| `boss` | `true` si es un jefe final |
| `xpBudget` | Máximo de XP que pueden dar los retos del nivel. La consola no da más aunque el código lo pida. Los combos los da la consola y no cuentan (ver `X.right`) |
| `stars` | `{ "three": 1, "two": 4 }`: fallos como mucho para 3 y para 2 estrellas |
| `flags` | Logros que el nivel puede activar (`X.flag`): solo letras, 30 como mucho, y nunca un nombre reservado (ver «Nombres reservados»). Con un encargo, empiezan por su prefijo. Qué medalla o cromo da cada uno lo dice `nivel.json` (y lo confirma Pixelbook al publicar) |
| `look` | `true` si el nivel dibuja a Bit con el color y el accesorio de quien juega |
| `audio` | `music`, `sfx` y `amb`: los nombres que usa el nivel, del catálogo o de su receta (las canciones de la receta van en `music` y sus sonidos, en `sfx`) |
| `directo` | Solo EN DIRECTO (ver «EN DIRECTO»): cómo suena su informativo |

## script.json

Cada escena es `{ "id", "sec", "hud"?, "phase"?, "steps": [ ... ] }`. Cada paso es **una sola** de estas cosas:

| Paso | Qué hace |
| --- | --- |
| `{ "say": "v001", "who": "vega", "text": "Lo que se dice", "show"?: "Lo que se lee", "gap"?: 0.3 }` | Una voz. La historia avanza hasta que termina, más `gap` segundos (0,22 por defecto). `show` es el subtítulo si difiere de lo hablado (siglas, números). El texto puede llevar etiquetas de voz entre corchetes, como `[laughs]` |
| `{ "pause": 1.5 }` | Tiempo sin voz |
| `{ "mark": "nombre" }` | Marca de tiempo para animar (`M('escena', 'nombre')` en el código) |
| `{ "sfx": "hit", "at"?: 0.2, "gain"?: 0.8 }` | Efecto de sonido (los que empiezan por `amb-` son ambientes) |
| `{ "music": "boss" }` | Cambia la música (`"stop"` la para): del catálogo o de la receta |
| `{ "overlap": 0.3 }` | La siguiente voz pisa a la anterior |
| `{ "gate": "id" }` | Reto: la historia se para aquí hasta que quien juega lo resuelve |

`clips` son las **líneas sueltas**: lo que se dice como respuesta a un toque (explicaciones de cada fallo, pistas, reacciones). No ocupan tiempo en la historia y el código las pide con `X.say(id)`. El kit de retos (`choiceIX`, `orderIX`, `binIX`) dice al acertar `r-first` (a la primera), `r-ok1` o `r-ok2`: si lo usas, decláralas en `clips` con una voz de tu serie (la plantilla ya las trae); si no, la consola las rechaza y la partida automática falla.

**El marcador** (`hud`) y **las fases** (`phase`): `hud` es el número del marcador que se ve arriba durante la escena. El código lo define en `HUD_LEVELS[n] = { name: 'NIVEL 3', title: 'EL LABORATORIO', ramp: C.teal }`: a la izquierda, el nombre y el título; a la derecha, la XP del nivel (con `noXp: true`, sin XP). Una escena sin `hud`, o con un número que no está en `HUD_LEVELS`, no lleva marcador (la portada, las transiciones). Con `phases: true`, el marcador enseña además las cinco fases del método científico, y `phase` (de 1 a 5) es la de la escena: las anteriores salen hechas y la actual parpadea.

Reglas del texto (las mismas en `nivel.json` y, como aviso, en las cadenas del código): español de España; sin raya larga (usa coma, punto, dos puntos o paréntesis); sin datos personales; sin enlaces ni dominios (`www.…`, `algo.com`), sin correos, sin teléfonos y sin @usuarios. La letra de la consola dibuja las del español y las de los nombres que se escriben en el juego (À, Â, Ä, Ã, Ç…): lo que no dibuja sale como «?», y el validador avisa.

## nivel.json

La ficha del nivel en el mapa, sus cromos, sus logros y sus preguntas para el reto del día (formato 1). La consola los junta con los suyos (rangos, logros generales, cofres) al arrancar, y el validador comprueba que cada texto quepa donde se dibuja, con los anchos de la letra de la consola.

```json
{
  "format": 1,
  "level": {
    "title": "Escucha la Edad Media",
    "sub": "REMIX · págs. 8 a 12",
    "info": ["Reconoce los instrumentos", "y las formas de la Edad Media"],
    "icon": "note",
    "reward": { "card": "mbe-zanfona" }
  },
  "cards": [
    { "id": "mbe-zanfona", "name": "Zanfona", "sub": "Instrumento medieval", "who": "Instrumento de cuerda frotada",
      "fact": "Una rueda roza las cuerdas mientras se toca un pequeño teclado.", "art": { "kind": "icon", "name": "note", "bg": "teal" } },
    { "id": "mbe-arpa", "name": "Arpa", "sub": "Instrumento medieval", "who": "Instrumento de cuerda pulsada",
      "fact": "Se toca con los dedos y acompañaba a los trovadores.", "art": { "kind": "icon", "name": "note", "ramp": "yellow" } }
  ],
  "badges": [
    { "id": "mbe-oido", "name": "Buen oído", "desc": "Reconoce los cinco instrumentos sin fallar.", "icon": "note", "goal": 1,
      "rule": { "flag": "mbeOido" }, "reward": { "card": "mbe-arpa" } }
  ],
  "questions": [
    { "id": "mbe1", "type": "choice", "q": "¿Qué instrumento tiene una rueda?", "opts": ["La zanfona.", "El arpa.", "El laúd.", "La flauta."], "a": 0, "why": "La rueda roza las cuerdas." },
    { "id": "mbe2", "type": "tf", "q": "La zanfona tiene teclas.", "a": true, "why": "Las teclas cambian la nota." },
    { "id": "mbe3", "type": "fact", "q": "La música medieval es la más bonita.", "a": "opinión", "why": "Es un gusto: no se puede comprobar." },
    { "id": "mbe4", "type": "order", "q": "Ordena de lo más antiguo a lo más reciente.", "opts": ["Canto gregoriano", "Ars antiqua", "Ars nova"], "why": "Primero, el canto a una voz; después, la polifonía." }
  ]
}
```

**El nivel** (`level`):

| Campo | Qué es | Cabe |
| --- | --- | --- |
| `title` | Nombre del nivel: su punto del mapa, su ficha y el aviso de nivel desbloqueado | 240 px (mejor 140: bajo su punto del mapa) |
| `sub` | Subtítulo de la ficha | 320 px |
| `info` | Dos renglones de la ficha. La tercera línea («9 min · 8 retos») la calcula la consola con la línea de tiempo | 216 px cada uno |
| `icon` | Opcional: el icono de su punto del mapa (`icon` de `catalogo.json`, `draw.icons`); si no, el de su área | |
| `reward` | Opcional: `{ "card": id }`, un cromo de este nivel que se gana al superarlo por primera vez. Es una propuesta: lo que vale es lo que fija Pixelbook al publicar | |

El área, la unidad y el orden en el mapa no van aquí: los fija el encargo y, al publicar, Pixelbook. El nivel se llama como el cartucho y es jefe final si lo dice `cart.json` (`boss`).

**Cromos** (`cards`, 8 como mucho): `id`, `name` (248 px; en el álbum, dos renglones de 66 px), `sub` (248 px), `who` (quién o qué es: dos renglones de 248 px como mucho), `fact` (por qué importa: cuatro renglones de 328 px como mucho) y `art`, cómo se dibuja su retrato en una caja de 64 × 64:

| `art.kind` | Campos | Qué dibuja |
| --- | --- | --- |
| `person` | `base` (una persona del motor, `draw.people`) o `skin` (`A` o `B`), `hair` (una rampa de color o `warm`, `wig`), `style` (`bob`, `bun`, `short`, `wig`, `wavy`, `long`), `clothes` (una rampa o `ink`) y, si se quiere, `beard` (`full`), `glasses`, `cap` y `band` (`true`) | Un busto del motor con ese pelo, esa piel y esa ropa |
| `picto` | `name` (`draw.pictos`), `bg` (rampa del fondo) y `s` (escala, 2 por defecto) | Un pictograma |
| `icon` | `name` (`draw.icons`), `ramp` (su color, turquesa por defecto) y `bg` | Un icono |
| `console` | Ninguno | Los dibujos hechos a mano de los cromos de la consola: solo para sus cartuchos |

Un tipo que la consola no conoce se dibuja como el icono de los cromos. «Cómo se consigue» (en el álbum) lo escribe la consola: «Nivel 2 de Música» si es el premio del nivel o «Logro «Buen oído»» si es el de un logro. Cada cromo es del área de su nivel.

**Logros** (`badges`, 8 como mucho): `id`, `name` (dos renglones de 80 px), `desc` (460 px; con el nombre, mejor 430 en la barra de la pantalla de logros), `icon` (`draw.icons`), `goal` (siempre `1`), `rule` y, si se quiere, `reward` (`{ "card": id }`, un cromo de este nivel). La regla dice cuándo se gana:

| `rule` | Se gana cuando |
| --- | --- |
| `{ "flag": "mbeOido" }` | El nivel ha activado ese logro (`X.flag`, declarado en `cart.json`) |
| `{ "flags": ["mbeOido", "mbeRitmo"] }` | Los ha activado todos |
| `{ "level": "<id>", "stars": 3 }` | El nivel se ha superado con esas estrellas o más |
| `{ "level": "<id>", "done": true }` | El nivel se ha superado |
| `{ "level": "<id>", "maxMistakes": 0 }` | El nivel se ha superado alguna vez con esos fallos o menos |

`level` es siempre el id de este nivel. Una regla de otro tipo nunca se concede (el validador la rechaza).

**Preguntas** (`questions`, 30 como mucho) para el reto del día, que las pregunta cuando el nivel está superado: `id`, `type`, `q` (tres renglones de 400 px), `why` (la explicación, cuatro renglones de 330 px) y, según el tipo:

| `type` | Campos | Cómo se contesta |
| --- | --- | --- |
| `choice` | `opts` (cuatro, de 336 px como mucho) y `a` (la buena, de 0 a 3) | Las opciones salen barajadas |
| `tf` | `a`: `true` o `false` | Verdadero o falso |
| `fact` | `a`: `"hecho"` u `"opinión"` | Hecho u opinión |
| `order` | `opts` (de 2 a 6, ya en su orden, de 148 px como mucho) | Se tocan en orden |

Cada pregunta es de su nivel (la consola le pone `from`). El reto del día baraja con la huella del id de cada pregunta, así que añadir o retirar niveles no cambia el de las demás.

Los cartuchos de la consola llevan además, porque vienen de antes: `level.id` (el id con el que se guardan las partidas), `place` (`{ area, unit, order }`), `level.dur` y `level.gates` (la tercera línea, escrita a mano), `how` en los cromos, retratos `console` y premios de la consola (`acc`, `color`, `shield`). Fuera de la consola, el validador los rechaza con un encargo y avisa sin él. Las unidades de cada área y los huecos «próximamente» están en `app/content/mapa.json`.

## soluciones.json

Cómo se supera cada reto del guion (formato 1). La cocina juega así el nivel entero antes de publicarlo (`tools/cart/jugar-core.mjs`) y comprueba lo esperado.

```json
{
  "format": 1,
  "gates": {
    "precio": [{ "drag": { "from": "slider", "dx": 85, "dy": 0 } }, { "tap": "price-ok" }],
    "pipeta": [{ "hold": "fill", "s": 5 }, { "tap": "check" }],
    "mapa": [{ "wait": 1 }, { "drag": [60, 110, 118, 110] }],
    "quiz": [{ "tap": "opt", "data": 2 }],
    "teclas": [{ "key": "Enter" }],
    "final": []
  },
  "expect": { "stars": 3, "mistakes": 0, "flags": ["mbeOido"], "xpMin": 200 }
}
```

| Acción | Qué hace |
| --- | --- |
| `{ "tap": zona, "data"?: dato }` | Toca la zona táctil (`hit(id, …, { data })`), en cuanto existe |
| `{ "hold": zona, "s": segundos, "data"? }` | La mantiene pulsada esos segundos (30 como mucho) |
| `{ "drag": [x0, y0, x1, y1] }` o `{ "drag": { "from": zona, "data"?, "dx", "dy" } }` | Arrastra en el búfer de 480 × 270 (desde el centro de la zona, si se nombra) |
| `{ "wait": segundos }` | Espera (30 como mucho) |
| `{ "key": tecla }` | Pulsa una tecla (`Enter`, `ArrowDown`…), que recibe el reto abierto |

Cada reto lleva su lista (vacía si se cierra solo). Al abrirse un reto se espera 1 segundo, se hacen sus acciones en orden con 1 segundo entre una y otra (los retos que clasifican no admiten dos toques seguidos) y se espera a que se cierre; si un reto pide más calma (una animación, un barco que navega), se añade un `wait`. Lo mejor es la solución sin fallos. `expect` es lo que tiene que salir al terminar: `stars`, `mistakes`, `flags` (logros que se han activado) y `xpMin` (la XP que han dado los retos del cartucho, sin los combos de la consola). La partida también falla si la consola rechaza algo del cartucho o la página da un error.

## El código (src/*.js)

Los archivos se juntan en orden alfabético y se ejecutan una vez al empezar el nivel. Lo que declaran es privado del cartucho.

### Registrar escenas y retos

- `defScene(id, draw(t, lt, sc), o)`: dibuja la escena `id` del guion. `t` es el tiempo de la historia, `lt` el tiempo dentro de la escena y `sc` sus datos (`t0`, `t1`, `marks`). En `o`: `in` (transición: `cut`, `dissolve`, `wipe`, `iris`, `blocks`, `glitch`, `black`), `inDur`, `caps` (subtítulos) y `noHud`.
- `defInteraction(id, def)`: el reto `id` del guion. `def` puede tener:
  - `init(X)`: estado inicial propio, que se suma a `S`;
  - `open(S, X)`, `frame(S, X, dt)`;
  - `tap`, `down` y `up`, con la forma `(S, X, zona, dato, P)`;
  - `drag(S, X, P)` y `key(S, X, tecla)`;
  - `hits`: zonas que, tocadas antes de que el reto se abra, saltan a él;
  - `clips`: voces que conviene tener preparadas.
- `onEpisodeStart(fn(EPS))`: prepara `EPS`, el estado libre del nivel (por ejemplo, la vida del jefe).
- `ixState(id)`: el estado `S` de un reto, para dibujarlo en su escena. `S.status` puede ser `pending`, `open` o `done`. Además lleva `attempts`, `wrong`, `hint` y lo que añada `init`.

### Hablar con la consola: `X`

| | Qué hace | Límites |
| --- | --- | --- |
| `X.say(id, gap?)` | Pone en cola una línea suelta | Solo líneas de `clips` o de la app |
| `X.wait(s)`, `X.then(fn)` | Espera, o llama a `fn` cuando la cola llegue ahí | Espera de 10 s como mucho |
| `X.busy()`, `X.hush()` | ¿Hay algo sonando o en cola? / Calla y vacía la cola | |
| `X.sfx(nombre, gain?, pan?)` | Efecto de sonido, del catálogo o de la receta | Solo los declarados en `audio.sfx` |
| `X.listen(nombre, gap?)` | Pone en cola un ejemplo para escuchar: un sonido de la receta. Suena en su turno, como una voz (con `X.then` se sabe cuándo acaba), y mientras suena la música de fondo baja mucho | Solo sonidos de la receta declarados en `audio.sfx` |
| `X.xp(n, motivo)` | Da puntos | Solo con un reto abierto y hasta `xpBudget` en total |
| `X.right(aLaPrimera)`, `X.wrong()` | Cuenta un acierto o un fallo. Las estrellas cuentan cada respuesta. La racha de combos va **por retos**: sube una vez por cada reto superado sin ningún fallo (tenga una respuesta o diez) y, desde el tercer reto seguido, la consola da +10 XP al cerrarlo; un fallo la corta. Partir un reto en muchas respuestas no da más combos | Solo con un reto abierto |
| `X.flag(nombre)` | Activa un logro | Solo los declarados en `flags` |
| `X.haptic(tipo)` | Vibración | `ok`, `no` o `heavy` |
| `X.done(espera?)` | Cierra el reto abierto y la historia sigue | Una vez por reto |
| `X.fx(tipo, a)`, `playerFxAt(tipo, dur)` | Efecto visual propio (sacudida, destello) | |
| `X.rand(k)`, `X.pick(lista, k)` | Azar que se repite igual en cada partida | |
| `X.now` | Tiempo de sesión (sigue corriendo con la historia parada) | |

Cada fotograma se admiten 64 peticiones como mucho. Lo que no cumple los límites se descarta y queda anotado para la revisión.

### Lo que hay a mano

- **Motor de dibujo** (`src/engine`): búfer indexado de 480 × 270 con paleta cerrada (`C`, `K`), primitivas (`rect`, `circ`, `ellipse`, `poly`, `line`, `frame`, `rrect`, `fillFn`, `withClip`…), letra de píxel (`drawText`, `wrapText`, `textWidth`), interfaz (`panel`, `tag`, `bar`, `slamText`, `confetti`…), iconos (`icon`, `picto`), personajes (`bit`, `bust`, `speakerDock`, `megaUV`…), decorados del laboratorio y de los informativos, tiempo (`NOW`, `M`, `L`, `W`, `mouthWho`, `talking`), aleatoriedad fija (`rnd`, `srnd`, `noise1`), curvas (`E`, `lerp`, `clamp`, `win`) y efectos del fotograma (`frameFx`).
- **Zonas táctiles**: `hit(id, x, y, w, h, { data })` al dibujar; `isOver(id)` y `POINTER` para el aspecto al pasar o pulsar.
- **Kit de retos**: `choiceIX` (opción múltiple con explicación de cada fallo y pista tras dos), `orderIX`, `binIX`, `drawChoice`, `drawOrder`, `drawBins`, `drawHint`, `button` y `stars`.
- **Vestir a un personaje**: crea una ficha derivada, por ejemplo `const MILAGROS_LAB = { ...MILAGROS, clothes, back, front, extra, bodyExtra }` (rampa de la ropa, pelo por detrás y por delante, complementos de la cara y del cuerpo; las funciones reciben la esquina del busto), dibújala con `bust(x, y, MILAGROS_LAB, …)` y, para que también salga así en el retrato de quien habla durante todo el nivel, `PEOPLE.milagros = MILAGROS_LAB`. Solo cambia dentro de tu cartucho. El laboratorio (`u01-c-lab`, `00-config.js`) viste así a Milagros y a Daniel con bata, gafas de protección y el pelo recogido.
- **Quien juega**: solo `PLAYER.look` (`color` y `acc`), para dibujar a Bit como lo ha vestido. Nada más: ni su nombre, ni su edad, ni su partida.

### Lo que no hay

- **Bloqueado por la caja** aunque se intente: red (`fetch`, WebSocket, importar código de fuera), almacenamiento (IndexedDB, caché) y DOM. Tampoco existen la partida, la cuenta ni el resto de la app.
- **Prohibido por el validador**, porque un nivel no lo necesita: temporizadores (`setTimeout`, `setInterval`; el tiempo es `t`, `NOW` y `X.wait`), `eval`, `Function` e `import`, y los nombres globales de fuera (`window`, `self`, `globalThis`…).

## La receta: música y sonidos propios (receta/*.js)

Si el catálogo no basta (en una asignatura como Música no bastará), el cartucho trae su **receta**: código que solo hace cuentas para fabricar sonido con el **chip** de Pixelbook (85 piezas e instrumentos: osciladores, filtros y sala, y voces de chiptune, de EN DIRECTO, de la Edad Media y de un estudio de hoy). No es una lista de deseos: lleva las notas exactas y cómo suena cada instrumento. Nada de grabaciones: si te falta un instrumento, constrúyelo con las piezas del chip (un violín es una onda de sierra con vibrato, el ataque del arco y los filtros de la caja); si sale bien, Pixelbook puede pasarlo al chip común.

Pixelbook ejecuta la receta en una **caja cerrada** (la misma que el código del cartucho: sin red, sin almacenamiento, nada fuera del chip), **masteriza** lo que sale (cierra cada bucle con sus colas, quita la continua e iguala el nivel con el resto de músicas y efectos de la app) y guarda el resultado. Lo que se revisa es lo que se publica. Tú decides las notas, los instrumentos y el equilibrio entre ellos; el volumen final lo pone la consola.

```js
// receta/10-medieval.js
defSong('estampida', { bpm: 66, steps: 12, bars: 16 }, (out, C) => {          // bucle de 16 compases de 6/8
  const mel = melody('A4 . D5 E5 F5 E5 | D5 . C5 D5 . .', 2);
  for (let b = 0; b < C.bars; b++) {
    for (const { n, s, l } of mel[b % mel.length]) addMono(out, zanfona(C.st * l, hz(n), 0.9), C.T(b, s), 0.5, -0.1);
    addMono(out, mano.dum(), C.T(b, 0), 0.3, 0);
  }
  mixdown(out, { rev: 0.17 });
});
defSong('sintonia', { once: true, min: 6 }, (out, C, D) => {                   // suena una vez, lo que dure su tramo
  addMono(out, campana(D, hz('D4')), 0, 0.5, 0);
});
defSound('muestra-zanfona', () => zanfona(3, hz('D4')));                       // un sonido suelto
```

- **`defSong(id, o, dibuja)`**: una canción. Un **bucle** lleva `{ bpm, bars, steps?, beat?, swing8?, swing16? }`: `bars` compases (1 a 64; 60 s como mucho) que se repiten sin costura. Una que suena **una vez** (sintonía, rebobinado) lleva `{ once: true, min?, bpm? }`: dura lo que su tramo del guion (de su `{ "music" }` a la siguiente música o al final), al menos `min` segundos y 60 como mucho (si el tramo es más largo, después hay silencio hasta la siguiente música); el silencio de su final se quita solo. Solo puede sonar una vez en el guion. `dibuja(out, C, D)` suma en `out` (estéreo, `[L, R]`, de `D` segundos más 4 de colas) o devuelve su propio búfer. `C` es el reloj: `C.T(compás, paso)` da el segundo, `C.st` es un paso, `C.bar` un compás y `C.bars` los compases del bucle. Coloca las notas dentro de los compases: las colas (eco, sala, notas que se apagan) vuelven solas al principio.
- **`defSound(id, o?, fabrica)`**: un sonido suelto de 10 s como mucho (`o = { max }`). `fabrica()` devuelve una señal mono (`Float32Array`) o estéreo. Sirve como efecto (`X.sfx`, o un `{ "sfx" }` del guion) o como ejemplo para escuchar (`X.listen`).
- **Los nombres**: minúsculas, cifras y guiones, distintos de los del catálogo. Las canciones se declaran en `audio.music` y los sonidos en `audio.sfx` de `cart.json`.
- **El chip**: en `catalogo.json` (`receta`) está la lista y en `REFERENCIA.md` («Sonido: el chip de las recetas»), cada pieza con su firma y para qué sirve. Todo trabaja a `SR` muestras por segundo (24.000) y las frecuencias van en hercios (`hz('A4')` es 440).
- **Reglas** (las comprueba el validador): el mismo sonido cada vez, así que nada de `Math.random`, `Date` ni `performance` (el azar, con `rand(semilla)`); nada de audio colado como números o texto (una receta ocupa 64 KB como mucho). Las melodías, propias o de dominio público (el canto gregoriano y las Cantigas lo son).
- **Límites** (en el móvil, toda la música del nivel se carga entera): 8 canciones y 240 s entre todas; 24 sonidos y 90 s entre todos. Pixelbook comprueba fuera de la caja todo lo que sale de ella.
- **A compás con el dibujo**: en la línea de tiempo (`TL.music`), cada tramo de una canción de la receta lleva `song` (su nombre), `t0`, `t1` y, si la receta lo declara, `bpm` y `steps`. Así las escenas pueden latir con la música sin copiar su tempo. `TL.sounds[nombre].dur` es lo que dura cada sonido de la receta (para dibujar mientras suena).

## EN DIRECTO

Los cartuchos de la serie de Geografía e Historia (`"cast": "en-directo"`) suenan como su informativo: su música (`directo-…`) y sus efectos, que se llaman con `d:` delante (`d:tv-ding`, `d:map-pop`), los hace Pixelbook al compilar. Los efectos del guion (`{ "sfx": "d:tv-hit" }`) y los que pide el código (`X.sfx('d:coin')`) se declaran en `audio.sfx` como los demás. La música y los efectos que no son de EN DIRECTO (los demás del catálogo y los de la receta) también valen y suenan tal cual. Lo que el informativo pone además en su mezcla va en `directo`, en datos:

| Campo | Qué es |
| --- | --- |
| `sceneSfx`, `sceneSfxGain`, `noSceneSfx` | El efecto de cada cambio de escena (sin `d:`; `null`, ninguno), su volumen y las escenas que no lo llevan |
| `beds` | Camas de ambiente: `[ambiente, escena, volumen]`, desde el comienzo de esa escena |
| `appSfx` | Efectos que solo suenan con lo que hace quien juega (sin `d:`): se preparan aunque la historia no los use |
| `extra` | Efectos colocados en la historia: `[efecto, ancla, segundos, volumen, panorama]`. El ancla es `["mark", escena, marca]`, `["word", voz, palabra]` (cuando empieza esa palabra), `["scene", escena]` (su comienzo) o `["t"]` (un segundo fijo); los segundos se suman al ancla. La palabra es la primera de esa voz de la historia que empieza así (sin contar mayúsculas, tildes ni signos), y las palabras van de espacio a espacio y sin las etiquetas de voz: en «la ruta Quanzhou-Venecia» vale `"quanzhou"`, no `"venecia"` |

Para colocar todo esto no se ejecuta nada del cartucho, y solo valen los nombres del catálogo. La vista previa del kit (`herramientas/preparar.mjs`) lo coloca y lo sintetiza con el mismo código que la compilación, así que ya suena como el nivel publicado; solo cambian los tiempos, estimados hasta que haya voces (las sintonías que suenan una vez duran lo que su tramo).

En el código, `palScope(true)` deja mezclar colores con toda la paleta (la del informativo y la común), `TV_INSET` aparta del botón de pausa las marcas del canal, `TICKER_XP = () => HUD_XP()` pone los puntos en el teletipo, `EPS.sfxAlias = { correct: 'd:correct' }` cambia los efectos de los retos por los del informativo y el kit de retos de la serie (`tvPoll`, `tvOrderCards`, `tvBins`, `tvItemCard`) dibuja encuestas, tarjetas y cajas con su estética.

## Límites y nombres reservados

- **Ficheros** (lo que se envía): solo `cart.json`, `script.json`, `nivel.json`, `soluciones.json`, `src/<nombre>.js` y `receta/<nombre>.js` (el nombre, en minúsculas, cifras, `-` y `_`, 40 como mucho). Texto UTF-8 sin el carácter NUL, 60 ficheros y 1 MB entre todos como mucho, 256 KB por fichero (32 KB `cart.json`).
- **XP**: `xpBudget` de 0 a 1000. **Logros**: solo letras, 30 como mucho.
- **Logros reservados**: los logros se guardan junto a las estadísticas de la partida, así que nunca pueden llamarse como una (`firstTry`, `bestCombo`, `answers`, `dailyDone`, `dailyPerfect`, `learned`, `bossWins`) ni como un nombre de JavaScript (`constructor`, `toString`…). `improvedReplay`, `bossFlawless`, `perfectPipette` y `perfectOrder` son de la consola, y con un encargo tampoco valen los logros de sus cartuchos.
- **Ids reservados**: los niveles, cromos, logros y preguntas de la consola (`catalogo.json`, `reserved`). Los huecos «próximamente» del mapa no lo están: un encargo puede ocuparlos.

## Encargos

Un encargo de Pixelbook trae `{ "slug", "idPrefix", "version", "cast" }`. Con él:

| Qué | Regla | Ejemplo |
| --- | --- | --- |
| `id` del cartucho y del nivel | El `slug`: `^[a-z][a-z0-9-]{2,39}$` | `m01-b-escucha` |
| `version` y `cast` de `cart.json` | Los del encargo | `1.0.0`, `remix` |
| Cromos y logros (`nivel.json`) | `^<idPrefix>-[a-z0-9-]{1,30}$` | `mbe-zanfona` |
| Preguntas | `^<idPrefix>[a-z0-9]{1,9}$` (12 como mucho) | `mbe4` |
| Logros de `cart.json` (`flags`) | `^<idPrefix>[A-Z][a-zA-Z]{0,25}$` (solo letras) | `mbeOido` |

Y nada que choque con lo reservado de la consola.

## La huella de los textos

Una persona aprueba los textos de un cartucho antes de que se gaste un crédito de voz. Lo aprobado es su huella (`textos-v1`, `tools/cart/textos.mjs`): el SHA-256 del JSON canónico (claves ordenadas en todos los niveles, sin espacios, los números como los escribe JavaScript) de `{ "cart", "script", "nivel", "strings" }`, con los tres JSON ya leídos (`nivel` es `null` si falta) y en `strings` las cadenas no vacías de `src/*.js` (también los trozos de las plantillas `` `…${…}…` ``), sin repetir y ordenadas. Las cadenas se leen con un tokenizador: el código no se ejecuta. Si una versión nueva solo cambia código (nombres, números, comentarios, la receta), la huella no cambia y la aprobación sigue valiendo; si cambia cualquier texto, se vuelve a revisar.

## El validador

`node tools/cart/validate.mjs carts/<id>` dice cada problema con su fichero, su ruta y un código estable. Con `--json` escribe una línea:

```json
{ "ok": false, "cart": "mbe-prueba", "errors": [{ "code": "SCRIPT_UNKNOWN_ROLE", "file": "script.json", "path": "/scenes/2/steps/4/who", "msg": "papel desconocido «darth»", "hint": "Reparto de remix: lia, rec." }], "warnings": [], "summary": { "scenes": 9, "gates": 8 } }
```

`path` es un puntero JSON dentro del fichero; en el código, `line` es el renglón. Los códigos no cambian de una versión a otra: `CART_…` (manifiesto), `SCRIPT_…`, `SCENE_…`, `STEP_…`, `VOICE_…` y `GATE_…` (guion), `TEXT_…` (reglas del texto: `TEXT_EM_DASH`, `TEXT_URL`, `TEXT_DOMAIN`, `TEXT_EMAIL`, `TEXT_PHONE`, `TEXT_HANDLE`; `TEXT_GLYPH`, aviso), `CODE_…` y `RECIPE_…` (código y receta), `DIRECTO_…`, `NIVEL_…`, `LEVEL_…`, `CARD_…`, `ART_…`, `BADGE_…`, `RULE_…`, `REWARD_…` y `QUESTION_…` (`nivel.json`), `SOL_…` (`soluciones.json`), `FILE_…` y `BUNDLE_…` (ficheros), `FLAG_…`, `ID_RESERVED` y `ENCARGO_INVALID`.

Fuera de Pixelbook (`--externo`, el kit y el servidor) no se pueden reutilizar tomas (`from`). Con `--encargo <fichero.json>` se aplican además sus reglas. El servidor de Videolibros usa el mismo validador empaquetado en un solo fichero (`npm run validator:bundle`, `build/validador.mjs`): lee `{ "files": { "<ruta>": "<texto>" }, "encargo": { … } }` por la entrada estándar (`--stdin --json`) y escribe lo mismo, con la huella de los textos.

## Catálogo

- **Reparto** (`cast`): `modo-ciencia` tiene `vega`, `bit`, `galileo`, `milagros`, `daniel` y `megauv`; `en-directo` tiene `alba`, `nora`, `carmen`, `luis`, `ivan`, `pedro` y `lucia`; `remix` (Música) tiene `lia` y `rec`.
- **Música, efectos y ambientes**: los nombres de `tools/music.mjs` (`SONGS`), `tools/sfx-synth.mjs` (`SYNTH_SFX`) y `tools/sfx.mjs` (`AMB`); los de EN DIRECTO, de `tools/directo/music.mjs` y `tools/directo/sfx.mjs` (sus efectos, con `d:` delante).
- **Chip de las recetas**: `tools/receta/chip.mjs` (piezas de `tools/synth.mjs` e instrumentos de `tools/directo/instruments.mjs`, `tools/receta/chiptune.mjs`, `medieval.mjs` y `estudio.mjs`).

La música y los efectos que falten se hacen con la receta. Si un nivel necesita una voz o un ambiente que no están, se pide a Pixelbook; en esta versión no se admiten peticiones en el manifiesto.

## Probar y enviar

Con la API de creadores, sin instalar nada: `POST …/validate` comprueba al momento, `POST …/trials` prueba en la
cocina (capturas, partida automática con `soluciones.json` y vista previa sin voces) y `POST …/versions` envía a
revisión. Todo está en [agentes.md](agentes.md). Reutilizar tomas ya grabadas (`from`) solo lo puede hacer Pixelbook.
