boosterarena-mcp 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LEEME.md CHANGED
@@ -54,7 +54,16 @@ habla con Booster con **tu** sesión.
54
54
  borrador y tú decides cuándo se publica.
55
55
 
56
56
  **Contenido** — poner texto, imágenes, figuras y dibujos de la biblioteca (más de 3.000
57
- ilustraciones y 280 fondos, que se buscan por nombre en español o inglés).
57
+ ilustraciones y 280 fondos, que se buscan por nombre en español o inglés), y animaciones Lottie
58
+ (`poner_lottie`: de la galería de Booster o por URL). El fondo puede ser una imagen tuya
59
+ (`poner_fondo` con `url`), y cada capa puede llevar el suyo (`capa` en `poner_fondo`, o `fondo`
60
+ en `nueva_capa`).
61
+
62
+ **Trayectorias** — `programar … moverPorTrayectoria` con `hasta` {x, y} (a dónde llega el centro
63
+ del objeto, en % del slide) traza el camino solo; `puntos` para curvas o escalas y `duracion` en
64
+ ms. Un objeto puede tener varios caminos con nombre: en una secuencia de clics cada uno sale de
65
+ donde lo dejó el anterior, y `trayectoriaTerminada` apunta a cada uno por su nombre exacto.
66
+ `recolocar` acepta `x`/`y` fuera del slide para que un objeto entre en escena.
58
67
 
59
68
  **Boosters y Boost Arenas** — colocarlos y configurarlos por nombre. `describir` te dice qué
60
69
  acepta cada uno antes de intentarlo, así que el agente no tiene que adivinar los campos.
package/dist/nucleo.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  // ../src/insert/catalogos.ts
2
2
  var ILUS_BASE = "https://assets.boosterarena.com/ilustraciones";
3
+ var ANIM_PRO_BASE = "https://assets.boosterarena.com/animados-pro";
3
4
  var FONDOS_BASE = "https://assets.boosterarena.com/fondos";
4
5
  var FLAG_ICONS = ["us", "gb", "mx", "ca", "es", "fr", "de", "it", "br", "jp", "cn", "kr", "in", "ru", "au", "nl", "pt", "ar", "co", "eg", "za", "sa", "tr", "gr"].map((c) => `circle-flags:${c}`);
5
6
  var ILLUS_ICONS = ["idea", "reading", "graduation-cap", "library", "planner", "calendar", "alarm-clock", "globe", "landscape", "gallery", "camera", "film", "music", "like", "rating", "sports-mode", "night-landscape", "panorama", "picture", "news", "document", "about", "conference-call", "voice-presentation"].map((n2) => `flat-color-icons:${n2}`);
@@ -67,6 +68,7 @@ var limpiarVacias = (ws) => {
67
68
  const utiles = ws.filter((w) => !VACIAS.has(w));
68
69
  return utiles.length ? utiles : ws;
69
70
  };
71
+ var palabrasDeBusqueda = (q) => limpiarVacias(q.split(/[\s/,_-]+/).filter(Boolean)).map(raiz);
70
72
  var raiz = (w) => {
71
73
  if (w.length <= 3) return w;
72
74
  let r = w;
@@ -143,7 +145,7 @@ function cargarBiblioteca() {
143
145
  });
144
146
  return cargando;
145
147
  }
146
- var t\u00E9rminos = (consulta) => limpiarVacias(sinTildes(consulta).split(/\s+/).filter(Boolean)).map(raiz);
148
+ var t\u00E9rminos = (consulta) => palabrasDeBusqueda(sinTildes(consulta));
147
149
  function mejorIlustracion(lista2, consulta) {
148
150
  if (!lista2.length) return null;
149
151
  const palabras = t\u00E9rminos(consulta);
@@ -231,6 +233,37 @@ function buscarFondo(consulta) {
231
233
  parecidos: h.parecidos.map((f) => f.nombre.es)
232
234
  };
233
235
  }
236
+ var anims = null;
237
+ async function cargarAnimaciones() {
238
+ if (anims?.length) return;
239
+ anims = await fetch(`${ANIM_PRO_BASE}/catalogo.json`, { cache: "no-cache" }).then((r) => r.ok ? r.json() : Promise.reject(new Error(String(r.status)))).then((j) => Array.isArray(j?.elementos) ? j.elementos : []).catch(() => []);
240
+ }
241
+ function buscarAnimacion(consulta) {
242
+ const lista2 = anims ?? [];
243
+ if (!lista2.length) return null;
244
+ const palabras = t\u00E9rminos(consulta);
245
+ const ordena = (a, b) => b.p - a.p || a.e.nombre.es.localeCompare(b.e.nombre.es);
246
+ let exacto = true;
247
+ let orden = lista2.map((e2) => ({ e: e2, p: palabras.length ? puntuar(e2, palabras) : 1 })).filter((x) => x.p > 0);
248
+ if (!orden.length) {
249
+ exacto = false;
250
+ orden = lista2.map((e2) => {
251
+ const d = puntuarDetalle(e2, palabras);
252
+ return { e: e2, p: d.encajadas * 1e3 + d.total };
253
+ }).filter((x) => x.p > 0);
254
+ }
255
+ if (!orden.length) return null;
256
+ orden.sort(ordena);
257
+ const e = orden[0].e;
258
+ return {
259
+ src: `${ANIM_PRO_BASE}/${e.archivo}`,
260
+ nombre: e.nombre.es,
261
+ plan: e.plan,
262
+ caja: { w: e.w, h: e.h, dw: e.dw, dh: e.dh },
263
+ exacto,
264
+ parecidos: orden.slice(1, 3).map((x) => x.e.nombre.es)
265
+ };
266
+ }
234
267
 
235
268
  // ../src/shared/textEffects.ts
236
269
  var COLOR_EFFECTS = [
@@ -1330,7 +1363,7 @@ var efectosMov = MOTION_EFFECTS.map((e) => e.id);
1330
1363
  function construirCatalogo(opts) {
1331
1364
  const fn = (name, description, parameters) => ({ type: "function", function: { name, description, parameters } });
1332
1365
  const monedas = ["quitafondo", "magichat", "transformar"].filter((t) => opts.boosters.includes(t));
1333
- return [
1366
+ const lista2 = [
1334
1367
  // ── PONER COSAS EN EL SLIDE ───────────────────────────────────────────────
1335
1368
  fn(
1336
1369
  "poner_texto",
@@ -1656,6 +1689,45 @@ function construirCatalogo(opts) {
1656
1689
  obj({ mensaje: str("Lo que le dices al maestro. Una o dos frases, en espa\xF1ol, en su tono.") }, ["mensaje"])
1657
1690
  )
1658
1691
  ];
1692
+ return opts.sinNavegador ? conLoDelMcp(lista2, fn) : lista2;
1693
+ }
1694
+ var PUNTO = obj({ x: num("En % del ancho del slide."), y: num("En % del alto del slide.") }, ["x", "y"]);
1695
+ function conLoDelMcp(lista2, fn) {
1696
+ const props = (nombre) => (lista2.find((t) => t.function.name === nombre)?.function.parameters).properties;
1697
+ const programar = props("programar");
1698
+ programar.trayectoria = str('Con "moverPorTrayectoria" o el evento "trayectoriaTerminada": el NOMBRE del camino ("Trayectoria 1", "a la boca"). Se busca por nombre EXACTO. Un objeto puede tener varios: dale a cada uno el suyo y as\xED cada `clickStep` o cada `trayectoriaTerminada` apunta al que toca. Si el camino no existe todav\xEDa, hace falta `hasta` o `puntos` para trazarlo.');
1699
+ programar.hasta = { ...PUNTO, description: 'Solo con "moverPorTrayectoria": a D\xD3NDE llega el CENTRO del objeto, en % del slide. El camino se traza en recta desde donde est\xE9 el objeto \u2014 o, en un `clickStep` 2, 3\u2026, desde donde lo dej\xF3 el paso anterior. Si ya existe un camino con ese `trayectoria`, se retraza.' };
1700
+ programar.puntos = { type: "array", items: PUNTO, description: 'Solo con "moverPorTrayectoria": puntos por los que PASA el centro del objeto, en % del slide, para una curva o un recorrido con escalas. El \xFAltimo es la llegada (o, si mandas tambi\xE9n `hasta`, `hasta` va detr\xE1s).' };
1701
+ programar.duracion = num('Solo con "moverPorTrayectoria": cu\xE1nto tarda el recorrido, en milisegundos. Sin esto, 2000.');
1702
+ const fondo = props("poner_fondo");
1703
+ fondo.url = str("Una imagen ya SUBIDA (con `subir_imagen`), https://. Usa esto, `busca` o `color`: uno solo.");
1704
+ fondo.capa = str("El `ref` o el nombre de una CAPA para ponerle el fondo a ella en vez de al slide. Cada capa puede llevar el suyo.");
1705
+ props("nueva_capa").fondo = str("Fondo propio de la capa: la URL https:// de una imagen ya subida o un color en hex. Sin esto, la capa deja ver el slide de debajo.");
1706
+ for (const t of lista2) {
1707
+ const p = t.function.parameters.properties;
1708
+ if (!t.function.name.startsWith("poner_") || !p.x || t.function.name === "poner_texto_plantilla") continue;
1709
+ p.x = num("Borde IZQUIERDO, en % del ancho del slide. Puede ser NEGATIVO o pasar de 100 para que nazca FUERA del slide y entre con una trayectoria.");
1710
+ p.y = num("Borde SUPERIOR, en % del alto del slide. Tambi\xE9n puede quedar fuera, como `x`.");
1711
+ }
1712
+ const recolocar = props("recolocar");
1713
+ recolocar.x = num("Nuevo borde izquierdo en %. Puede ser NEGATIVO o pasar de 100 para dejar el objeto FUERA del slide y que entre con una trayectoria (el monstruo que entra solo: x = -50).");
1714
+ recolocar.y = num("Nuevo borde superior en %. Tambi\xE9n puede quedar fuera del slide, como `x`.");
1715
+ props("cambiar_estilo").gris_al_tocar = bool("true = en el \u25B6 Player, al tocar el objeto se vuelve gris (y otro toque le devuelve el color). Le dice al coach qu\xE9 botones ya us\xF3. false lo quita.");
1716
+ const i = lista2.findIndex((t) => t.function.name === "poner_emoji");
1717
+ lista2.splice(i + 1, 0, fn(
1718
+ "poner_lottie",
1719
+ "Pone una ANIMACI\xD3N Lottie: de la galer\xEDa de animaciones de Booster (busc\xE1ndola por palabra) o una URL propia de un .json de Lottie. Hace de decoraci\xF3n (las esquinas) o de bot\xF3n: se programa con `programar` como cualquier objeto.",
1720
+ obj({
1721
+ ref: REF,
1722
+ x: X,
1723
+ y: Y,
1724
+ w: W,
1725
+ capa: CAPA,
1726
+ busca: str('Qu\xE9 animaci\xF3n buscar en la galer\xEDa, en espa\xF1ol, ingl\xE9s o portugu\xE9s: "regalo", "trofeo", "estrella", "altavoz".'),
1727
+ url: str("En vez de `busca`: la URL https:// de un Lottie .json (lottie.host, o uno subido).")
1728
+ }, ["ref", "x", "y", "w"])
1729
+ ));
1730
+ return lista2;
1659
1731
  }
1660
1732
 
1661
1733
  // ../src/insert/shapes.ts
@@ -1711,6 +1783,33 @@ var FORMA_DEF = {
1711
1783
  blob: { anch: 26, alto: "cuadrado", color: "#c4b5fd" }
1712
1784
  };
1713
1785
 
1786
+ // ../src/canvas/audioVol.ts
1787
+ var AUDIO_VOL_INICIAL = 0.7;
1788
+
1789
+ // ../src/canvas/bucleCapas.ts
1790
+ var alMostrar = (t) => t.event.type === "layerShown" || t.event.type === "slideStart";
1791
+ function capasQueAbre(t) {
1792
+ return [t.action, ...(t.luego ?? []).map((p) => p.action)].filter((a) => a.type === "showLayer" && a.layerId).map((a) => a.layerId);
1793
+ }
1794
+ function abreAlMostrarse(slide, capaId) {
1795
+ const capa = slide.layers.find((l) => l.id === capaId);
1796
+ if (!capa) return [];
1797
+ return capa.els.flatMap((el) => [...el.triggers ?? [], ...el.gtriggers ?? []]).filter(alMostrar).flatMap(capasQueAbre);
1798
+ }
1799
+ function abreEnBucle(slide, desde, t) {
1800
+ if (!alMostrar(t)) return false;
1801
+ const vistas = /* @__PURE__ */ new Set();
1802
+ const pendientes = [...capasQueAbre(t)];
1803
+ while (pendientes.length) {
1804
+ const id = pendientes.pop();
1805
+ if (id === desde) return true;
1806
+ if (vistas.has(id)) continue;
1807
+ vistas.add(id);
1808
+ pendientes.push(...abreAlMostrarse(slide, id));
1809
+ }
1810
+ return false;
1811
+ }
1812
+
1714
1813
  // ../src/shared/fontsAncho.ts
1715
1814
  var ANCHO_LETRA = {
1716
1815
  "Poppins": { 100: [712, 689], 200: [724, 692], 300: [736, 696], 400: [747, 699], 500: [760, 703], 600: [770, 708], 700: [782, 715], 800: [793, 723], 900: [805, 731] },
@@ -1849,11 +1948,28 @@ var ESQUEMAS_BOOSTERS = {
1849
1948
  campos: {
1850
1949
  images: lista("Las im\xE1genes que la varita saca una a una.", objeto("Una imagen.", {
1851
1950
  src: imagen("La imagen.", { obligatorio: true }),
1852
- label: texto("R\xF3tulo que se planta debajo. Vac\xEDo = sin r\xF3tulo.")
1951
+ label: texto("R\xF3tulo que se planta con ella. Vac\xEDo = sin r\xF3tulo."),
1952
+ dest: objeto("D\xD3NDE cae la imagen al salir de la varita, en % del slide. Sin esto, se reparten solas en filas de 4.", {
1953
+ x: num2("Borde izquierdo, en % del ancho.", { entero: false, obligatorio: true }),
1954
+ y: num2("Borde superior, en % del alto.", { entero: false, obligatorio: true }),
1955
+ w: num2("Ancho, en % del ancho del slide. El alto sale de la imagen.", { entero: false, min: 2, max: 100, obligatorio: true })
1956
+ }),
1957
+ labelDest: objeto("D\xF3nde cae el R\xD3TULO, aparte de la imagen, en % del slide. Sin esto, justo debajo de ella.", {
1958
+ x: num2("Borde izquierdo, en % del ancho.", { entero: false, obligatorio: true }),
1959
+ y: num2("Borde superior, en % del alto.", { entero: false, obligatorio: true }),
1960
+ w: num2("Ancho, en % del ancho del slide.", { entero: false, min: 2, max: 100, obligatorio: true })
1961
+ }),
1962
+ labelColor: color('Color del r\xF3tulo plantado. Sin esto, casi negro: sobre un fondo oscuro pon "#ffffff".')
1853
1963
  }), { max: 30 })
1854
- }
1964
+ },
1965
+ nota: "Pop-up City: cada lugar sale de la varita y cae en SU sitio de la ciudad con `dest`, y su r\xF3tulo en el suyo con `labelDest`."
1855
1966
  },
1856
- ejemplo: { images: [{ src: "https://assets.boosterarena.com/u/ej/cat.webp", label: "cat" }] }
1967
+ ejemplo: {
1968
+ images: [
1969
+ { src: "https://assets.boosterarena.com/u/ej/cat.webp", label: "cat" },
1970
+ { src: "https://assets.boosterarena.com/u/ej/bank.webp", label: "bank", dest: { x: 62, y: 40, w: 16 }, labelDest: { x: 62, y: 72, w: 16 }, labelColor: "#ffffff" }
1971
+ ]
1972
+ }
1857
1973
  },
1858
1974
  tombola: {
1859
1975
  donde: "wcfg.ext",
@@ -2759,8 +2875,12 @@ var slideDe = (b) => {
2759
2875
  return sc.slides[Math.min(b.slide, sc.slides.length - 1)];
2760
2876
  };
2761
2877
  function listaDestino(b, capa) {
2762
- const s = slideDe(b);
2763
2878
  const ref = txt(capa).trim();
2879
+ if (!ref && slideDe(b).base.some((e) => e.kind === "arena")) {
2880
+ nuevoSlide(b, "");
2881
+ b.bitacora.push("\u{1F4C4} Slide nuevo: el de la arena es solo para la arena");
2882
+ }
2883
+ const s = slideDe(b);
2764
2884
  if (!ref) return { els: s.base };
2765
2885
  const capaObj = capaPorNombre(b, ref) ?? crearCapa(b, ref);
2766
2886
  b.capaDeTrabajo = capaObj.id;
@@ -2839,7 +2959,33 @@ function encajar(el) {
2839
2959
  el.y = +Math.max(0, Math.min(100 - el.h, el.y)).toFixed(2);
2840
2960
  return el;
2841
2961
  }
2842
- function armarEvento(a, el) {
2962
+ function caminoPorNombre(suyas, nombre, estricto) {
2963
+ const q = nombre.trim().toLowerCase();
2964
+ if (!q) return suyas[0];
2965
+ return suyas.find((m) => m.name.trim().toLowerCase() === q) ?? (estricto ? void 0 : suyas.find((m) => m.name.toLowerCase().includes(q)) ?? suyas[0]);
2966
+ }
2967
+ function puntosPedidos(a) {
2968
+ const esPt = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
2969
+ const pts = Array.isArray(a.puntos) ? a.puntos.filter(esPt) : [];
2970
+ if (esPt(a.hasta)) pts.push(a.hasta);
2971
+ return pts.map((p) => ({ x: Math.max(-40, Math.min(140, p.x)), y: Math.max(-40, Math.min(140, p.y)) }));
2972
+ }
2973
+ function salidaDelCamino(el, movido, a) {
2974
+ const paso = Math.round(num3(a.paso, 1));
2975
+ if (txt(a.evento) !== "clickStep" || paso < 2) return { x: 0, y: 0 };
2976
+ const previo = (el.triggers ?? []).find((t) => t.event.type === "clickStep" && t.event.step === paso - 1 && t.action.type === "playMotion" && t.action.targetId === movido.id);
2977
+ const idPrevio = previo?.action.type === "playMotion" ? previo.action.motionId : "";
2978
+ const cam = (movido.motions ?? []).find((m) => m.id === idPrevio);
2979
+ return cam?.path.length ? { ...cam.path[cam.path.length - 1] } : { x: 0, y: 0 };
2980
+ }
2981
+ function trazarCamino(a, destinos) {
2982
+ const r2 = (p) => ({ x: +p.x.toFixed(2), y: +p.y.toFixed(2) });
2983
+ if (destinos.length > 1) return [a, ...destinos].map(r2);
2984
+ const z = destinos[0];
2985
+ const n2 = Math.max(4, Math.min(40, Math.round(Math.hypot(z.x - a.x, z.y - a.y) / 2.5)));
2986
+ return Array.from({ length: n2 + 1 }, (_, i) => r2({ x: a.x + (z.x - a.x) * i / n2, y: a.y + (z.y - a.y) * i / n2 }));
2987
+ }
2988
+ function armarEvento(a, el, estricto) {
2843
2989
  switch (txt(a.evento)) {
2844
2990
  case "hover":
2845
2991
  return { type: "hover" };
@@ -2852,9 +2998,7 @@ function armarEvento(a, el) {
2852
2998
  case "clickStep":
2853
2999
  return { type: "clickStep", step: Math.max(1, Math.round(num3(a.paso, 1))) };
2854
3000
  case "trayectoriaTerminada": {
2855
- const nombre = txt(a.trayectoria).trim().toLowerCase();
2856
- const suyas = el.motions ?? [];
2857
- const cam = (nombre ? suyas.find((m) => m.name.toLowerCase().includes(nombre)) : void 0) ?? suyas[0];
3001
+ const cam = caminoPorNombre(el.motions ?? [], txt(a.trayectoria), estricto);
2858
3002
  return cam ? { type: "motionEnd", motionId: cam.id } : { type: "click" };
2859
3003
  }
2860
3004
  default:
@@ -2910,10 +3054,32 @@ function armarAccion(b, a, el) {
2910
3054
  if (d && !quien) return { accion: null, fallo: `No encuentro el objeto "${d}" para moverlo.` };
2911
3055
  const movido = quien ?? buscarEl(b, txt(a.objetivo));
2912
3056
  if (!movido) return { accion: null, fallo: "No s\xE9 qu\xE9 objeto tiene que moverse." };
2913
- const nombre = txt(a.trayectoria).trim().toLowerCase();
3057
+ const nombre = txt(a.trayectoria).trim();
2914
3058
  const suyas = movido.motions ?? [];
2915
- const cam = (nombre ? suyas.find((m) => m.name.toLowerCase().includes(nombre)) : void 0) ?? suyas[0];
3059
+ const pts = puntosPedidos(a);
3060
+ if (pts.length) {
3061
+ const c = { x: movido.x + movido.w / 2, y: movido.y + movido.h / 2 };
3062
+ const path = trazarCamino(salidaDelCamino(el, movido, a), pts.map((p) => ({ x: p.x - c.x, y: p.y - c.y })));
3063
+ const duration = Math.round(Math.max(200, Math.min(6e4, num3(a.duracion, 2e3))));
3064
+ const igual = nombre ? suyas.find((m) => m.name.trim().toLowerCase() === nombre.toLowerCase()) : void 0;
3065
+ if (igual) {
3066
+ igual.path = path;
3067
+ igual.duration = duration;
3068
+ } else {
3069
+ let n2 = suyas.length + 1;
3070
+ while (suyas.some((m) => m.name === `Trayectoria ${n2}`)) n2 += 1;
3071
+ movido.motions = [...suyas, { id: nid("mo"), name: nombre || `Trayectoria ${n2}`, path, duration }];
3072
+ }
3073
+ const cam2 = igual ?? movido.motions[movido.motions.length - 1];
3074
+ b.tocados.push(movido.id);
3075
+ b.bitacora.push(`\u219D ${describirEl(movido)}: camino \xAB${cam2.name}\xBB${igual ? " retrazado" : ""}`);
3076
+ return { accion: { type: "playMotion", targetId: movido.id, motionId: cam2.id } };
3077
+ }
3078
+ const cam = caminoPorNombre(suyas, nombre, b.estricto);
2916
3079
  if (cam) return { accion: { type: "playMotion", targetId: movido.id, motionId: cam.id } };
3080
+ if (b.estricto) {
3081
+ return { accion: null, fallo: `${nombre && suyas.length ? `${describirEl(movido)} no tiene ning\xFAn camino \xAB${nombre}\xBB (tiene: ${suyas.map((m) => `\xAB${m.name}\xBB`).join(", ")}). ` : ""}Para trazarlo, pasa \`hasta\` {x, y} \u2014a d\xF3nde llega el centro del objeto, en % del slide\u2014 o \`puntos\`.` };
3082
+ }
2917
3083
  const nuevo = { id: nid("mo"), name: txt(a.trayectoria) || "Trayectoria 1", path: [], duration: 2e3 };
2918
3084
  movido.motions = [...suyas, nuevo];
2919
3085
  b.tocados.push(movido.id);
@@ -2951,6 +3117,25 @@ function armarAccion(b, a, el) {
2951
3117
  }
2952
3118
  }
2953
3119
  function ejecutar(b, nombre, args, fab) {
3120
+ const antes = b.creados.length;
3121
+ const dijo = ejecutarUna(b, nombre, args, fab);
3122
+ return b.estricto && !b.fallo ? dijo + dejarFuera(b, nombre, args, antes) : dijo;
3123
+ }
3124
+ var enElAreaDeTrabajo = (v, lado) => Math.max(-lado - 30, Math.min(130, v));
3125
+ function dejarFuera(b, nombre, args, antes) {
3126
+ if (!nombre.startsWith("poner_") || ["poner_fondo", "poner_arena", "poner_audio", "poner_texto_plantilla"].includes(nombre)) return "";
3127
+ if (b.creados.length !== antes + 1) return "";
3128
+ const el = buscarEl(b, b.creados[antes]);
3129
+ if (!el) return "";
3130
+ const x = typeof args.x === "number" && Number.isFinite(args.x) ? args.x : null;
3131
+ const y = typeof args.y === "number" && Number.isFinite(args.y) ? args.y : null;
3132
+ const sale = (v, lado) => v !== null && (v < 0 || v + lado > 100);
3133
+ if (!sale(x, el.w) && !sale(y, el.h)) return "";
3134
+ if (sale(x, el.w)) el.x = +enElAreaDeTrabajo(x, el.w).toFixed(2);
3135
+ if (sale(y, el.h)) el.y = +enElAreaDeTrabajo(y, el.h).toFixed(2);
3136
+ return " Qued\xF3 FUERA del slide (o asomando): no se ve entero hasta que una trayectoria lo meta.";
3137
+ }
3138
+ function ejecutarUna(b, nombre, args, fab) {
2954
3139
  switch (nombre) {
2955
3140
  // ── Poner cosas ──────────────────────────────────────────────────────────
2956
3141
  case "poner_texto": {
@@ -3108,6 +3293,36 @@ El ref "${refBase}" es el rengl\xF3n grande; cada uno es tambi\xE9n "${refBase}_
3108
3293
  b.bitacora.push(`\u{1F5BC}\uFE0F Imagen subida${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
3109
3294
  return `Puesta la imagen (id ${el.id}).`;
3110
3295
  }
3296
+ // Solo lo ofrece el MCP (`catalogo.ts`, `sinNavegador`): el Robot no pone animaciones.
3297
+ case "poner_lottie": {
3298
+ const url = txt(args.url).trim();
3299
+ const busca = txt(args.busca).trim();
3300
+ if (!url && !busca) return no(b, "Dime qu\xE9 animaci\xF3n buscar (`busca`) o p\xE1same la `url` de un Lottie.");
3301
+ if (url && !/^https?:\/\//i.test(url)) return no(b, "La `url` tiene que ser una direcci\xF3n https:// de un .json de Lottie.");
3302
+ const hallada = url ? null : buscarAnimacion(busca);
3303
+ if (!url && !hallada) {
3304
+ return no(b, `La galer\xEDa de animaciones no tiene nada parecido a "${busca}" (o no contest\xF3). Prueba otra palabra, en singular, o pon un dibujo con \`poner_dibujo\`.`);
3305
+ }
3306
+ if (hallada?.plan === "pro" && !b.premium) {
3307
+ return no(b, `La animaci\xF3n \xAB${hallada.nombre}\xBB es del plan Pro y esta cuenta no lo tiene. Se activa desde boosterarena.com.`);
3308
+ }
3309
+ const w = pct(args.w, 20);
3310
+ const prop = hallada?.caja.w && hallada.caja.h ? hallada.caja.w / hallada.caja.h : 1;
3311
+ const el = encajar({
3312
+ id: nid("lottie"),
3313
+ kind: "lottie",
3314
+ x: pct(args.x, 40),
3315
+ y: pct(args.y, 30),
3316
+ w,
3317
+ h: +(w / prop * (1920 / 1080)).toFixed(1),
3318
+ src: hallada?.src ?? url
3319
+ });
3320
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
3321
+ b.bitacora.push(`\u2728 Animaci\xF3n ${hallada ? `\xAB${hallada.nombre}\xBB` : "propia"}${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
3322
+ if (!hallada) return `Animaci\xF3n puesta (id ${el.id}).`;
3323
+ const otros = hallada.parecidos.length ? ` Otras parecidas: ${hallada.parecidos.map((x) => `"${x}"`).join(", ")}.` : "";
3324
+ return hallada.exacto ? `Animaci\xF3n "${hallada.nombre}" puesta (id ${el.id}, exacto: true).${otros}` : `Animaci\xF3n "${hallada.nombre}" puesta (id ${el.id}, exacto: false). \u26A0\uFE0F OJO: no hay ninguna que sea "${busca}"; es la m\xE1s cercana.${otros}`;
3325
+ }
3111
3326
  case "poner_audio": {
3112
3327
  const url = txt(args.url).trim();
3113
3328
  if (!/^https?:\/\//i.test(url)) {
@@ -3121,7 +3336,8 @@ El ref "${refBase}" es el rengl\xF3n grande; cada uno es tambi\xE9n "${refBase}_
3121
3336
  w: 16,
3122
3337
  h: 7,
3123
3338
  src: url,
3124
- name: txt(args.nombre) || "Audio"
3339
+ name: txt(args.nombre) || "Audio",
3340
+ vol: AUDIO_VOL_INICIAL
3125
3341
  };
3126
3342
  plantar(b, el, args.ref, void 0);
3127
3343
  b.bitacora.push(`\u{1F50A} Audio \xAB${el.name}\xBB`);
@@ -3206,21 +3422,35 @@ El ref "${refBase}" es el rengl\xF3n grande; cada uno es tambi\xE9n "${refBase}_
3206
3422
  case "poner_fondo": {
3207
3423
  const color2 = txt(args.color).trim();
3208
3424
  const busca = txt(args.busca).trim();
3425
+ const url = txt(args.url).trim();
3426
+ const refCapa = txt(args.capa).trim();
3427
+ const capa = refCapa ? capaPorNombre(b, refCapa) : void 0;
3428
+ if (refCapa && !capa) {
3429
+ return no(b, `No hay ninguna capa "${refCapa}" en este slide. Cr\xE9ala antes con \`nueva_capa\` (que tambi\xE9n acepta \`fondo\`).`);
3430
+ }
3431
+ const destino = capa ?? slideDe(b);
3432
+ const d\u00F3nde = capa ? ` de la capa \xAB${capa.name}\xBB` : "";
3433
+ if (url) {
3434
+ if (!/^https?:\/\//i.test(url)) return no(b, "La `url` tiene que ser una imagen ya SUBIDA (https://). S\xFAbela antes con `subir_imagen`.");
3435
+ destino.background = url;
3436
+ b.bitacora.push(`\u{1F3DE}\uFE0F Fondo propio${d\u00F3nde}`);
3437
+ return `Fondo${d\u00F3nde} puesto con esa imagen.`;
3438
+ }
3209
3439
  if (busca) {
3210
3440
  const f = buscarFondo(busca);
3211
3441
  if (!f) return no(b, `No hay ning\xFAn fondo parecido a "${busca}". Puedes poner un color liso.`);
3212
- slideDe(b).background = f.src;
3213
- b.bitacora.push(`\u{1F3DE}\uFE0F Fondo: ${f.nombre}`);
3442
+ destino.background = f.src;
3443
+ b.bitacora.push(`\u{1F3DE}\uFE0F Fondo${d\u00F3nde}: ${f.nombre}`);
3214
3444
  const otros = f.parecidos.length ? ` Otros parecidos: ${f.parecidos.map((x) => `"${x}"`).join(", ")}.` : "";
3215
- return f.exacto ? `Fondo puesto: "${f.nombre}" (exacto: true).${otros}` : `Fondo puesto: "${f.nombre}" (exacto: false). \u26A0\uFE0F OJO: no hay ning\xFAn fondo que sea "${busca}"; es lo m\xE1s cercano.${otros} Si no sirve, prueba otra palabra o pon un color liso.`;
3445
+ return f.exacto ? `Fondo${d\u00F3nde} puesto: "${f.nombre}" (exacto: true).${otros}` : `Fondo${d\u00F3nde} puesto: "${f.nombre}" (exacto: false). \u26A0\uFE0F OJO: no hay ning\xFAn fondo que sea "${busca}"; es lo m\xE1s cercano.${otros} Si no sirve, prueba otra palabra o pon un color liso.`;
3216
3446
  }
3217
3447
  if (!color2) return no(b, "Dime un color o una escena a buscar.");
3218
3448
  if (!COLOR_DE_FONDO.test(color2)) {
3219
3449
  return no(b, `"${color2}" no es un color que el slide sepa pintar. M\xE1ndalo en hex ("#0f172a"), rgb(), hsl() o como un degradado CSS.`);
3220
3450
  }
3221
- slideDe(b).background = color2;
3222
- b.bitacora.push(`\u{1F3A8} Fondo ${color2}`);
3223
- return "Fondo de color puesto.";
3451
+ destino.background = color2;
3452
+ b.bitacora.push(`\u{1F3A8} Fondo ${color2}${d\u00F3nde}`);
3453
+ return `Fondo de color${d\u00F3nde} puesto.`;
3224
3454
  }
3225
3455
  case "poner_booster": {
3226
3456
  const tipo = txt(args.tipo);
@@ -3348,6 +3578,12 @@ ${fallosEnTexto(r.fallos)}
3348
3578
 
3349
3579
  Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarlo.`);
3350
3580
  }
3581
+ if (el.widget === "magicwand") {
3582
+ const imgs = cfg.valor.images ?? [];
3583
+ imgs.forEach((im) => {
3584
+ if (!im.id) im.id = nid("wand");
3585
+ });
3586
+ }
3351
3587
  el.wcfg = ficha.donde === "wcfg.ext" ? { ...el.wcfg, ext: cfg.valor } : { ...el.wcfg?.ext ? { ext: el.wcfg.ext } : {}, ...cfg.valor };
3352
3588
  b.tocados.push(el.id);
3353
3589
  b.bitacora.push(`\u270E Booster ${el.widget} configurado`);
@@ -3361,9 +3597,14 @@ Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarl
3361
3597
  }
3362
3598
  case "nueva_capa": {
3363
3599
  const r = txt(args.ref).trim();
3600
+ const fondo = txt(args.fondo).trim();
3601
+ if (fondo && !/^https?:\/\//i.test(fondo) && !COLOR_DE_FONDO.test(fondo)) {
3602
+ return no(b, `"${fondo}" no sirve de fondo: tiene que ser la URL https:// de una imagen ya subida o un color en hex.`);
3603
+ }
3364
3604
  const capa = crearCapa(b, txt(args.nombre) || r);
3365
3605
  if (r) b.aliasCapa.set(r, capa.id);
3366
- return `Capa "${capa.name}" creada. Para poner cosas dentro, p\xE1sale capa="${r || capa.name}".`;
3606
+ if (fondo) capa.background = fondo;
3607
+ return `Capa "${capa.name}" creada${fondo ? " con su fondo" : ""}. Para poner cosas dentro, p\xE1sale capa="${r || capa.name}".`;
3367
3608
  }
3368
3609
  case "nueva_escena": {
3369
3610
  const nombre2 = txt(args.nombre) || `Escena ${b.doc.length + 1}`;
@@ -3389,9 +3630,22 @@ Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarl
3389
3630
  b.bitacora.push(`\u{1F9F9} Sin programaci\xF3n: ${describirEl(el)}`);
3390
3631
  return `Le quit\xE9 ${n0} disparador(es).`;
3391
3632
  }
3633
+ if (el.kind === "widget" || el.kind === "arena") {
3634
+ return no(b, `${describirEl(el)} es un booster o una arena: no lleva disparadores, ya trae su propio funcionamiento. Programa otro objeto del slide (un texto, una imagen, un bot\xF3n).`);
3635
+ }
3392
3636
  const { accion, fallo } = armarAccion(b, args, el);
3393
3637
  if (!accion) return no(b, fallo ?? "No entend\xED qu\xE9 acci\xF3n quer\xEDas.");
3394
- const trig = { id: nid("trig"), event: armarEvento(args, el), action: accion };
3638
+ if (b.estricto && txt(args.evento) === "trayectoriaTerminada" && !caminoPorNombre(el.motions ?? [], txt(args.trayectoria), true)) {
3639
+ return no(b, `${describirEl(el)} no tiene ${txt(args.trayectoria) ? `ning\xFAn camino \xAB${txt(args.trayectoria)}\xBB` : "ning\xFAn camino"}, as\xED que \xABal terminar su trayectoria\xBB no tendr\xEDa cu\xE1ndo dispararse. Progr\xE1male antes \`moverPorTrayectoria\` con \`hasta\`.`);
3640
+ }
3641
+ const trig = { id: nid("trig"), event: armarEvento(args, el, b.estricto), action: accion };
3642
+ if (trig.event.type === "motionEnd" && accion.type === "playMotion" && trig.event.motionId === accion.motionId) {
3643
+ return no(b, "Eso har\xEDa un BUCLE: al terminar el camino volver\xEDa a empezar el mismo camino, sin fin. Si quieres que d\xE9 vueltas, usa otro evento; si quieres encadenar, dale otro nombre al segundo camino.");
3644
+ }
3645
+ const capaDelEl = slideDe(b).layers.find((l) => l.els.some((e) => e.id === el.id));
3646
+ if (capaDelEl && abreEnBucle(slideDe(b), capaDelEl.id, trig)) {
3647
+ return no(b, `Eso har\xEDa un BUCLE: al mostrarse la capa \xAB${capaDelEl.name}\xBB se volver\xEDa a abrir a s\xED misma sin parar y la clase se congelar\xEDa. Usa otra capa, \`cerrarCapa\` o el evento de clic.`);
3648
+ }
3395
3649
  el.triggers = args.primero === true ? [trig, ...el.triggers ?? []] : [...el.triggers ?? [], trig];
3396
3650
  b.tocados.push(el.id);
3397
3651
  b.bitacora.push(`\u26A1 ${describirEl(el)}: ${describirTrigger(trig, b)}`);
@@ -3411,12 +3665,13 @@ Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarl
3411
3665
  el.w = w;
3412
3666
  }
3413
3667
  if (num3(args.alto, 0) > 0) el.h = pct(args.alto, el.h);
3414
- if (typeof args.x === "number") el.x = pct(args.x, el.x);
3415
- if (typeof args.y === "number") el.y = pct(args.y, el.y);
3416
- encajar(el);
3668
+ if (typeof args.x === "number") el.x = b.estricto ? +enElAreaDeTrabajo(args.x, el.w).toFixed(2) : pct(args.x, el.x);
3669
+ if (typeof args.y === "number") el.y = b.estricto ? +enElAreaDeTrabajo(args.y, el.h).toFixed(2) : pct(args.y, el.y);
3670
+ const afuera = b.estricto && (el.x < 0 || el.y < 0 || el.x + el.w > 100 || el.y + el.h > 100);
3671
+ if (!afuera) encajar(el);
3417
3672
  b.tocados.push(el.id);
3418
3673
  b.bitacora.push(`\u2194\uFE0F Recolocado: ${describirEl(el)}`);
3419
- return "Movido.";
3674
+ return afuera ? "Movido. Qued\xF3 FUERA del slide: no se ve hasta que una trayectoria lo meta." : "Movido.";
3420
3675
  }
3421
3676
  case "cambiar_estilo": {
3422
3677
  const el = buscarEl(b, txt(args.objetivo));
@@ -3438,6 +3693,7 @@ Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarl
3438
3693
  if (txt(args.align)) el.align = txt(args.align);
3439
3694
  if (typeof args.opacidad === "number") el.opacity = Math.max(0, Math.min(1, args.opacidad));
3440
3695
  if (typeof args.giro === "number") el.rot = args.giro || void 0;
3696
+ if (typeof args.gris_al_tocar === "boolean") el.grayClick = args.gris_al_tocar || void 0;
3441
3697
  b.tocados.push(el.id);
3442
3698
  b.bitacora.push(`\u{1F3A8} Retocado: ${describirEl(el)}`);
3443
3699
  return "Estilo cambiado.";
@@ -4026,6 +4282,7 @@ export {
4026
4282
  FORMA_DEF,
4027
4283
  buscarDibujo,
4028
4284
  buscarFondo,
4285
+ cargarAnimaciones,
4029
4286
  cargarBiblioteca,
4030
4287
  construirArena,
4031
4288
  construirCatalogo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boosterarena-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "El MCP de Booster: tu agente crea y edita presentaciones dentro de boosterarena.com",
5
5
  "keywords": [
6
6
  "mcp",
package/proyectos.mjs CHANGED
@@ -50,7 +50,8 @@ async function pedir(token, ruta, opciones = {}) {
50
50
 
51
51
  /** Los proyectos del maestro, del más reciente al más viejo. Sin el documento. */
52
52
  export async function listar(token, { tipo, limite = 50 } = {}) {
53
- const q = new URLSearchParams({ select: COLUMNAS_LISTA, order: 'updated_at.desc', limit: String(limite) });
53
+ // `deleted_at=is.null`: lo que el maestro mandó a la Papelera no se lista.
54
+ const q = new URLSearchParams({ select: COLUMNAS_LISTA, order: 'updated_at.desc', limit: String(limite), deleted_at: 'is.null' });
54
55
  if (tipo) q.set('type', `eq.${tipo}`);
55
56
  return pedir(token, `projects?${q}`);
56
57
  }
package/servidor.mjs CHANGED
@@ -138,6 +138,7 @@ function herramientasDelLienzo() {
138
138
  boosters: nucleo.ALL_WIDGETS.filter((w) => w.ready).map((w) => w.type),
139
139
  arenas: nucleo.ARENAS.filter((a) => a.ready).map((a) => a.type),
140
140
  formas: Object.keys(nucleo.FORMA_DEF),
141
+ sinNavegador: true, // trazar trayectorias, lotties, fondos por URL y en capas, gris al tocar
141
142
  }).map((t) => ({
142
143
  name: t.function.name,
143
144
  description: t.function.description,
@@ -281,6 +282,7 @@ function queEs(e) {
281
282
  if (e.kind === 'arena') return `ARENA ${e.arena}`;
282
283
  if (e.kind === 'widget') return `booster ${e.widget}${e.widget === 'instructions' ? ` «${corto(e.wcfg?.ext?.title, 24)}»` : ''}`;
283
284
  if (e.kind === 'image') return 'imagen';
285
+ if (e.kind === 'lottie') return 'animación';
284
286
  if (e.kind === 'audio') return `audio «${corto(e.name, 24)}»`;
285
287
  if (e.kind === 'shape' && e.shape === 'button') return `botón «${corto(e.text, 24)}»`;
286
288
  if (e.kind === 'shape' && e.hotspot) return 'zona caliente';
@@ -294,6 +296,14 @@ function queHace(t, doc, slide) {
294
296
  const cuando = { click: 'al tocarlo', hover: 'al pasar encima', slideStart: 'al entrar al slide',
295
297
  layerShown: 'al abrirse su capa', time: `a los ${ev.seconds} s`, clickStep: `en el clic ${ev.step}`,
296
298
  motionEnd: 'al terminar su trayectoria' }[ev.type] ?? ev.type;
299
+ // El nombre del camino, que es por lo que se le apunta: «al terminar "Trayectoria 2"».
300
+ const camino = (id) => {
301
+ for (const x of [...slide.base, ...slide.layers.flatMap((l) => l.els)]) {
302
+ const m = (x.motions ?? []).find((mm) => mm.id === id);
303
+ if (m) return `«${m.name}»`;
304
+ }
305
+ return 'su trayectoria';
306
+ };
297
307
  const a = t.action ?? {};
298
308
  const capa = (id) => slide.layers.find((l) => l.id === id)?.name ?? id;
299
309
  const qué = a.type === 'showLayer' ? `abre la capa «${capa(a.layerId)}»`
@@ -304,17 +314,22 @@ function queHace(t, doc, slide) {
304
314
  : a.type === 'speak' ? `lee «${corto(a.text, 24)}»`
305
315
  : a.type === 'openUrl' ? `abre ${a.url}`
306
316
  : a.type === 'hideEl' ? `oculta ${a.targetId || 'el objeto'}`
307
- : a.type === 'playMotion' ? 'recorre su trayectoria'
317
+ : a.type === 'playMotion' ? `recorre ${camino(a.motionId)}${a.targetId !== t.ownerId ? ` (${a.targetId})` : ''}`
308
318
  : a.type;
309
319
  const luego = (t.luego ?? []).length ? ` (+${t.luego.length} en cadena)` : '';
310
- return `${cuando} → ${qué}${luego}`;
320
+ return `${ev.type === 'motionEnd' ? `al terminar ${camino(ev.motionId)}` : cuando} → ${qué}${luego}`;
311
321
  }
312
322
 
313
323
  function listarEls(els, doc, slide, sangria) {
314
324
  const filas = [];
315
325
  for (const e of els) {
316
- filas.push(`${sangria}· ${queEs(e)} [${e.id}]`);
317
- for (const t of e.triggers ?? []) filas.push(`${sangria} ⚡ ${queHace(t, doc, slide)}`);
326
+ const donde = `@${Math.round(e.x)},${Math.round(e.y)} ${Math.round(e.w)}×${Math.round(e.h)}`;
327
+ filas.push(`${sangria}· ${queEs(e)} ${donde}${e.grayClick ? ' · gris al tocar' : ''} [${e.id}]`);
328
+ for (const m of e.motions ?? []) {
329
+ const fin = m.path[m.path.length - 1];
330
+ filas.push(`${sangria} ↝ «${m.name}» ${m.duration} ms${fin ? `, termina desplazado (${Math.round(fin.x)}, ${Math.round(fin.y)})` : ' — ⚠️ SIN CAMINO: no se mueve'}`);
331
+ }
332
+ for (const t of e.triggers ?? []) filas.push(`${sangria} ⚡ ${queHace({ ...t, ownerId: e.id }, doc, slide)}`);
318
333
  }
319
334
  return filas;
320
335
  }
@@ -330,7 +345,7 @@ function resumenDelDoc(doc, { escena, slide } = {}) {
330
345
  filas.push(` slide ${j + 1}${s.name ? ` «${s.name}»` : ''}${fondo}${s.base.length || s.layers.length ? '' : ' (vacío)'}`);
331
346
  filas.push(...listarEls(s.base, doc, s, ' '));
332
347
  for (const l of s.layers) {
333
- filas.push(` ▣ capa «${l.name}» [${l.id}] — ${l.els.length} objeto(s)${l.background ? ` · fondo ${l.background}` : ''}`);
348
+ filas.push(` ▣ capa «${l.name}» [${l.id}] — ${l.els.length} objeto(s)${l.background ? ` · fondo ${/^https?:/.test(l.background) ? 'imagen' : l.background}` : ''}`);
334
349
  filas.push(...listarEls(l.els, doc, s, ' '));
335
350
  }
336
351
  });
@@ -370,7 +385,9 @@ async function llamar(nombre, args = {}) {
370
385
  yo.proyecto = p;
371
386
  yo.borrador = nucleo.nuevoBorrador(Array.isArray(p.document) ? p.document : [], 0, 0, '');
372
387
  yo.borrador.estricto = true; // sin nadie delante: lo que el Robot resuelve solo, aquí se niega
373
- await nucleo.cargarBiblioteca(); // `poner_dibujo` busca en caliente
388
+ yo.borrador.premium = yo.plan === 'pro';
389
+ // `poner_dibujo`, `poner_fondo` y `poner_lottie` buscan en caliente.
390
+ await Promise.all([nucleo.cargarBiblioteca(), nucleo.cargarAnimaciones()]);
374
391
  // El id va en la PRIMERA línea y con su nombre: antes no venía y había que llamar a
375
392
  // `listar_proyectos` y buscar por título, que con ocho pilotos de nombre parecido es jugársela.
376
393
  // No hay URL propia de un proyecto: el editor no abre proyectos por dirección, se entra desde la