boosterarena-mcp 0.1.0

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.
@@ -0,0 +1,2688 @@
1
+ // ../src/insert/catalogos.ts
2
+ var ILUS_BASE = "https://assets.boosterarena.com/ilustraciones";
3
+ var FONDOS_BASE = "https://assets.boosterarena.com/fondos";
4
+ 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
+ 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}`);
6
+ var ANIMAL_ICONS = ["dog", "cat", "mouse-face", "hamster", "rabbit-face", "fox", "bear", "panda", "koala", "tiger-face", "lion", "cow-face", "pig-face", "frog", "monkey-face", "chicken", "penguin", "bird", "turtle", "snake", "tropical-fish", "dolphin", "elephant", "horse-face", "butterfly", "unicorn", "owl"].map((n2) => `noto:${n2}`);
7
+ var sinTildes = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
8
+ var VACIAS = /* @__PURE__ */ new Set([
9
+ "un",
10
+ "una",
11
+ "unos",
12
+ "unas",
13
+ "el",
14
+ "la",
15
+ "lo",
16
+ "los",
17
+ "las",
18
+ "de",
19
+ "del",
20
+ "al",
21
+ "con",
22
+ "sin",
23
+ "para",
24
+ "por",
25
+ "que",
26
+ "y",
27
+ "e",
28
+ "o",
29
+ "u",
30
+ "en",
31
+ "a",
32
+ "mi",
33
+ "su",
34
+ "este",
35
+ "esta",
36
+ "ese",
37
+ "esa",
38
+ "the",
39
+ "an",
40
+ "of",
41
+ "with",
42
+ "for",
43
+ "and",
44
+ "or",
45
+ "in",
46
+ "on",
47
+ "to",
48
+ "my",
49
+ "this",
50
+ "that",
51
+ "um",
52
+ "uma",
53
+ "os",
54
+ "as",
55
+ "do",
56
+ "da",
57
+ "dos",
58
+ "das",
59
+ "com",
60
+ "sem",
61
+ "no",
62
+ "na",
63
+ "e",
64
+ "ou"
65
+ ]);
66
+ var limpiarVacias = (ws) => {
67
+ const utiles = ws.filter((w) => !VACIAS.has(w));
68
+ return utiles.length ? utiles : ws;
69
+ };
70
+ var raiz = (w) => {
71
+ if (w.length <= 3) return w;
72
+ let r = w;
73
+ if (/[bcdfghjklmnpqrstvwxyz]es$/.test(r)) r = r.slice(0, -2);
74
+ else if (/s$/.test(r)) r = r.slice(0, -1);
75
+ if (r.length >= 6 && /[oa]$/.test(r)) r = r.slice(0, -1);
76
+ return r;
77
+ };
78
+ var distancia = (a, b, tope) => {
79
+ if (Math.abs(a.length - b.length) > tope) return tope + 1;
80
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
81
+ for (let i = 1; i <= a.length; i++) {
82
+ const fila = [i];
83
+ let mejorFila = i;
84
+ for (let j = 1; j <= b.length; j++) {
85
+ const coste = a[i - 1] === b[j - 1] ? 0 : 1;
86
+ const v = Math.min(prev[j] + 1, fila[j - 1] + 1, prev[j - 1] + coste);
87
+ fila.push(v);
88
+ if (v < mejorFila) mejorFila = v;
89
+ }
90
+ if (mejorFila > tope) return tope + 1;
91
+ prev = fila;
92
+ }
93
+ return prev[b.length];
94
+ };
95
+ var erratasQueSePerdonan = (w) => w.length >= 7 ? 2 : w.length >= 5 ? 1 : 0;
96
+ var CAMPOS = [["bn", 100], ["bs", 60], ["bm", 45], ["bt", 30], ["bf", 12]];
97
+ var puntuarDetalle = (e, palabras) => {
98
+ let total = 0, encajadas = 0;
99
+ for (const w of palabras) {
100
+ let mejor = 0;
101
+ for (const [campo, peso] of CAMPOS) {
102
+ const texto2 = e[campo];
103
+ if (!texto2) continue;
104
+ const ws = texto2.split(" ");
105
+ if (ws.includes(w)) {
106
+ mejor = Math.max(mejor, peso);
107
+ continue;
108
+ }
109
+ if (ws.some((x) => x.startsWith(w))) {
110
+ mejor = Math.max(mejor, peso * 0.8);
111
+ continue;
112
+ }
113
+ const tope = erratasQueSePerdonan(w);
114
+ if (tope && ws.some((x) => distancia(w, x, tope) <= tope)) mejor = Math.max(mejor, peso * 0.45);
115
+ }
116
+ if (mejor) {
117
+ encajadas++;
118
+ total += mejor;
119
+ }
120
+ }
121
+ return { total, encajadas };
122
+ };
123
+ var puntuar = (e, palabras) => {
124
+ const { total, encajadas } = puntuarDetalle(e, palabras);
125
+ return encajadas === palabras.length ? total : 0;
126
+ };
127
+
128
+ // ../src/canvas/robot/biblioteca.ts
129
+ var ilus = null;
130
+ var fondos = null;
131
+ var cargando = null;
132
+ function cargarBiblioteca() {
133
+ if (ilus && fondos) return Promise.resolve();
134
+ if (cargando) return cargando;
135
+ const pedir = (url) => fetch(url, { 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(() => []);
136
+ cargando = Promise.all([
137
+ pedir(`${ILUS_BASE}/catalogo.json`),
138
+ pedir(`${FONDOS_BASE}/catalogo.json`)
139
+ ]).then(([a, b]) => {
140
+ ilus = a;
141
+ fondos = b;
142
+ cargando = null;
143
+ });
144
+ return cargando;
145
+ }
146
+ var t\u00E9rminos = (consulta) => limpiarVacias(sinTildes(consulta).split(/\s+/).filter(Boolean)).map(raiz);
147
+ function mejorIlustracion(lista2, consulta) {
148
+ if (!lista2.length) return null;
149
+ const palabras = t\u00E9rminos(consulta);
150
+ if (!palabras.length) return { e: lista2[0], exacto: false, encajadas: 0, pedidas: 0 };
151
+ let top = null;
152
+ let mejorP = 0;
153
+ for (const e of lista2) {
154
+ const p = puntuar(e, palabras);
155
+ if (p > mejorP) {
156
+ mejorP = p;
157
+ top = e;
158
+ }
159
+ }
160
+ if (top) return { e: top, exacto: true, encajadas: palabras.length, pedidas: palabras.length };
161
+ let mejorEncajadas = 0;
162
+ for (const e of lista2) {
163
+ const p = puntuarDetalle(e, palabras);
164
+ const v = p.encajadas * 1e3 + p.total;
165
+ if (v > mejorP) {
166
+ mejorP = v;
167
+ top = e;
168
+ mejorEncajadas = p.encajadas;
169
+ }
170
+ }
171
+ return top ? { e: top, exacto: false, encajadas: mejorEncajadas, pedidas: palabras.length } : null;
172
+ }
173
+ function mejorFondo(lista2, consulta) {
174
+ if (!lista2.length) return null;
175
+ const palabras = t\u00E9rminos(consulta);
176
+ if (!palabras.length) return lista2[0];
177
+ let top = null;
178
+ let mejorP = 0;
179
+ for (const f of lista2) {
180
+ let p = 0;
181
+ for (const w of palabras) {
182
+ if (f.bn?.includes(w)) p += 100;
183
+ else if (f.bt?.includes(w)) p += 30;
184
+ else if (f.be?.includes(w)) p += 20;
185
+ }
186
+ if (p > mejorP) {
187
+ mejorP = p;
188
+ top = f;
189
+ }
190
+ }
191
+ return top;
192
+ }
193
+ function buscarDibujo(consulta) {
194
+ const h = mejorIlustracion(ilus ?? [], consulta);
195
+ if (!h) return null;
196
+ return {
197
+ src: `${ILUS_BASE}/${h.e.archivo}`,
198
+ nombre: h.e.nombre.es,
199
+ aspect: h.e.w && h.e.h ? h.e.w / h.e.h : 1,
200
+ exacto: h.exacto,
201
+ encajadas: h.encajadas,
202
+ pedidas: h.pedidas
203
+ };
204
+ }
205
+ function buscarFondo(consulta) {
206
+ const e = mejorFondo(fondos ?? [], consulta);
207
+ if (!e) return null;
208
+ return { src: `${FONDOS_BASE}/${e.archivo}`, nombre: e.nombre.es };
209
+ }
210
+
211
+ // ../src/shared/textEffects.ts
212
+ var COLOR_EFFECTS = [
213
+ { id: "none", name: "Ninguno" },
214
+ { id: "shadow", name: "Sombra" },
215
+ { id: "lift", name: "Elevado" },
216
+ { id: "outline", name: "Contorno" },
217
+ { id: "neon", name: "Ne\xF3n" },
218
+ { id: "glow", name: "Glow" },
219
+ // Los cuatro que Canva tiene y aquí faltaban. Los tres primeros son `text-shadow` puro
220
+ // (baratos); 'fondo' es una caja detrás de las letras.
221
+ // El golpe de las pegatinas de póster: una copia MACIZA desplazada, sin nada de
222
+ // desenfoque, en un color que contrasta. Es la Sombra de Canva con el desenfoque a
223
+ // cero; aquí va aparte porque nuestra Sombra siempre lleva algo de difuminado y
224
+ // cambiarla movería lo que ya hay hecho en los proyectos guardados.
225
+ { id: "block", name: "Bloque" },
226
+ { id: "splice", name: "Empalme" },
227
+ { id: "echo", name: "Eco" },
228
+ { id: "glitch", name: "Glitch" },
229
+ { id: "bgbox", name: "Fondo" },
230
+ { id: "retro", name: "Retro" },
231
+ { id: "gradient", name: "Degradado" },
232
+ { id: "shimmer", name: "Brillo" }
233
+ ];
234
+ var MOTION_EFFECTS = [
235
+ { id: "none", name: "Ninguno" },
236
+ { id: "float", name: "Flotar" },
237
+ { id: "pulse", name: "Pulso" },
238
+ { id: "swing", name: "Balanceo" },
239
+ { id: "wave", name: "Onda" },
240
+ { id: "sweep", name: "Barrido" },
241
+ // Seis más (2026-08-31), del mismo tipo que los de arriba: BUCLES de ambiente, no
242
+ // animaciones de entrada. Todos animan SOLO `transform`, así que combinan con los trece
243
+ // efectos de color sin revisar nada — que es justo la garantía de las dos categorías.
244
+ // Los que se mueven en horizontal miden en `em` y no en %: el % de una caja de texto es
245
+ // su ANCHO, y con eso un titular largo se sacudía cinco veces más que una palabra suelta.
246
+ // En `em` el movimiento va con el tamaño de la letra, que es lo que el ojo compara.
247
+ { id: "bounce", name: "Rebote" },
248
+ { id: "heartbeat", name: "Latido" },
249
+ { id: "shake", name: "Temblor" },
250
+ { id: "glide", name: "Vaiv\xE9n" },
251
+ { id: "orbit", name: "\xD3rbita" },
252
+ { id: "elastic", name: "El\xE1stico" }
253
+ ];
254
+
255
+ // ../src/canvas/robot/catalogo.ts
256
+ var obj = (props, req = []) => ({ type: "object", properties: props, required: req, additionalProperties: false });
257
+ var str = (description, opts = {}) => ({ type: "string", description, ...opts.enum ? { enum: opts.enum } : {} });
258
+ var num = (description) => ({ type: "number", description });
259
+ var bool = (description) => ({ type: "boolean", description });
260
+ var REF = str('Etiqueta corta que T\xDA inventas para poder referirte a esto despu\xE9s en esta misma tanda (por ejemplo al programarlo). Ej: "titulo", "boton_dia2".');
261
+ var X = num("Borde IZQUIERDO, en % del ancho del slide (0 = pegado a la izquierda, 100 = derecha).");
262
+ var Y = num("Borde SUPERIOR, en % del alto del slide (0 = arriba del todo, 100 = abajo).");
263
+ var W = num("ANCHO en % del ancho del slide. No pongas alto: lo calcula el editor.");
264
+ var CAPA = str("Deja esto vac\xEDo para poner la cosa en el slide. Si quieres que caiga DENTRO de una capa, pon aqu\xED el `ref` o el nombre de esa capa.");
265
+ var efectosColor = COLOR_EFFECTS.map((e) => e.id);
266
+ var efectosMov = MOTION_EFFECTS.map((e) => e.id);
267
+ function construirCatalogo(opts) {
268
+ const fn = (name, description, parameters) => ({ type: "function", function: { name, description, parameters } });
269
+ const monedas = ["quitafondo", "magichat", "transformar"].filter((t) => opts.boosters.includes(t));
270
+ return [
271
+ // ── PONER COSAS EN EL SLIDE ───────────────────────────────────────────────
272
+ fn(
273
+ "poner_texto",
274
+ "Pone un texto en el slide. Es la herramienta que m\xE1s vas a usar: t\xEDtulos, enunciados, opciones de respuesta, etiquetas.",
275
+ obj({
276
+ ref: REF,
277
+ x: X,
278
+ y: Y,
279
+ w: W,
280
+ capa: CAPA,
281
+ texto: str("El texto que se lee. En el idioma que pida el maestro."),
282
+ tamano: num("Tama\xF1o de letra en p\xEDxeles sobre un alto de 1080. Referencias: t\xEDtulo grande 90, subt\xEDtulo 56, texto normal 40, letra chica 28. Si dudas, 44."),
283
+ color: str('Color del texto en hex, ej "#111827". Oscuro sobre fondo claro y al rev\xE9s.'),
284
+ efecto: str("Efecto de COLOR de las letras. Ojo: el ne\xF3n pinta las letras del color DEL EFECTO, as\xED que \xABtexto blanco con ne\xF3n\xBB es color blanco Y color_efecto blanco.", { enum: efectosColor }),
285
+ color_efecto: str("Color del efecto en hex: el brillo del ne\xF3n, la sombra, el borde, el bloque. Si no lo mandas, cada efecto usa el suyo."),
286
+ movimiento: str("Efecto de MOVIMIENTO de las letras. Se combina con el de color. Con cuentagotas: un slide entero movi\xE9ndose marea.", { enum: efectosMov }),
287
+ mayusculas: bool("Ponerlo TODO EN MAY\xDASCULAS sin cambiar lo que se escribi\xF3."),
288
+ peso: num("Grosor de la letra, de 400 (normal) a 900 (negr\xEDsima). Los t\xEDtulos piden 800."),
289
+ align: str("Alineaci\xF3n del p\xE1rrafo.", { enum: ["left", "center", "right", "justify"] })
290
+ }, ["ref", "x", "y", "w", "texto"])
291
+ ),
292
+ fn(
293
+ "poner_dibujo",
294
+ 'Busca un dibujo en NUESTRA biblioteca (miles de ilustraciones con fondo transparente) y lo pone en el slide. Descr\xEDbelo en espa\xF1ol y en singular: "le\xF3n", "manzana", "ni\xF1a leyendo". No necesitas saber qu\xE9 hay: el buscador es del editor y te contesta qu\xE9 encontr\xF3.',
295
+ obj({
296
+ ref: REF,
297
+ x: X,
298
+ y: Y,
299
+ w: W,
300
+ capa: CAPA,
301
+ busca: str("Qu\xE9 dibujo quieres, en espa\xF1ol. Una o dos palabras funcionan mejor que una frase.")
302
+ }, ["ref", "x", "y", "w", "busca"])
303
+ ),
304
+ // ── Archivos de fuera ─────────────────────────────────────────────────────
305
+ // La puerta por la que entra lo que genera otra fábrica (imágenes, voz). Booster no genera
306
+ // ninguna de las dos cosas —se decidió así por costo— pero sí las recibe: para el documento,
307
+ // un MP3 de un TTS y un MP3 que subió un maestro son el mismo elemento con una URL de R2.
308
+ fn(
309
+ "poner_imagen",
310
+ "Pone una imagen que YA est\xE1 subida, por su URL. Es para lo que trae quien te llama (una ilustraci\xF3n generada, una foto del maestro). Si lo que quieres es un dibujo de NUESTRA biblioteca, usa `poner_dibujo`, que busca por palabras.",
311
+ obj({
312
+ ref: REF,
313
+ x: X,
314
+ y: Y,
315
+ w: W,
316
+ capa: CAPA,
317
+ url: str("La direcci\xF3n https:// del archivo ya subido."),
318
+ proporcion: num("Ancho dividido entre alto de la imagen (1 = cuadrada, 1.78 = apaisada 16:9). Si no la sabes, d\xE9jalo: se asume cuadrada y el maestro la ajusta.")
319
+ }, ["ref", "x", "y", "w", "url"])
320
+ ),
321
+ fn(
322
+ "poner_audio",
323
+ "Pone un audio que YA est\xE1 subido, por su URL: una grabaci\xF3n, una pista, una voz generada. Aparece como un chip de altavoz FUERA del slide (es un mando del maestro, no parte de la l\xE1mina). \u26A0\uFE0F Ponerlo NO hace que suene: hay que programarlo despu\xE9s.",
324
+ obj({
325
+ ref: REF,
326
+ url: str("La direcci\xF3n https:// del audio ya subido."),
327
+ nombre: str('C\xF3mo se llama, para que el maestro lo reconozca: "Listening 1", "Instrucciones".')
328
+ }, ["ref", "url"])
329
+ ),
330
+ fn(
331
+ "poner_figura",
332
+ "Pone una figura vectorial: globos de di\xE1logo, flechas, marcas de correcto/incorrecto, marcos, resaltadores, geometr\xEDa.",
333
+ obj({
334
+ ref: REF,
335
+ x: X,
336
+ y: Y,
337
+ w: W,
338
+ capa: CAPA,
339
+ forma: str("Qu\xE9 figura.", { enum: opts.formas }),
340
+ color: str("Color de relleno en hex. Si lo omites entra con el color que le toca por defecto (la palomita verde, la cruz roja...), que casi siempre es el bueno.")
341
+ }, ["ref", "x", "y", "w", "forma"])
342
+ ),
343
+ fn(
344
+ "poner_boton",
345
+ 'Pone un BOT\xD3N 3D con texto, listo para programarle qu\xE9 hace al tocarlo. Para men\xFAs, "siguiente", "ver la respuesta", "volver".',
346
+ obj({
347
+ ref: REF,
348
+ x: X,
349
+ y: Y,
350
+ w: W,
351
+ capa: CAPA,
352
+ texto: str('Lo que dice el bot\xF3n. Corto: "Ver respuesta", "D\xEDa 2".'),
353
+ color: str("Color del bot\xF3n en hex."),
354
+ icono: str("Icono opcional dentro del bot\xF3n.", {
355
+ enum: ["none", "next", "back", "home", "play", "pause", "check", "star"]
356
+ })
357
+ }, ["ref", "x", "y", "w", "texto"])
358
+ ),
359
+ fn(
360
+ "poner_emoji",
361
+ "Pone un emoji grande como elemento del slide.",
362
+ obj(
363
+ { ref: REF, x: X, y: Y, w: W, capa: CAPA, emoji: str('Un solo emoji, ej "\u{1F389}".') },
364
+ ["ref", "x", "y", "w", "emoji"]
365
+ )
366
+ ),
367
+ fn(
368
+ "poner_zona_caliente",
369
+ "Pone una ZONA CALIENTE: una figura invisible en la proyecci\xF3n que captura el clic. Sirve para hacer clicable un trozo de una imagen de fondo (un pa\xEDs en un mapa, un \xF3rgano en un dibujo). Progr\xE1mala despu\xE9s con `programar`.",
370
+ obj({
371
+ ref: REF,
372
+ x: X,
373
+ y: Y,
374
+ w: W,
375
+ capa: CAPA,
376
+ forma: str("Forma de la zona.", { enum: ["ellipse", "rect", "triangle"] }),
377
+ alto: num("Alto en % del alto del slide. Aqu\xED S\xCD se pide, porque una zona se ajusta a lo que tapa.")
378
+ }, ["ref", "x", "y", "w", "forma"])
379
+ ),
380
+ fn(
381
+ "poner_fondo",
382
+ "Pone el FONDO del slide: o una escena de nuestra biblioteca (aula, cocina, bosque, hospital...) busc\xE1ndola por palabra, o un color liso. El fondo no es un objeto: no se mueve ni se programa.",
383
+ obj({
384
+ busca: str('Escena a buscar en la biblioteca de fondos, en espa\xF1ol: "aula", "cocina", "playa".'),
385
+ color: str('Color liso en hex, ej "#0f172a". Usa esto O `busca`, no los dos.')
386
+ }, [])
387
+ ),
388
+ fn(
389
+ "poner_booster",
390
+ "Pone un BOOSTER: una herramienta interactiva que el maestro dispara en clase (dado, ruleta, cron\xF3metro, marcador, sem\xE1foro...). Ocupa un trozo del slide y convive con lo dem\xE1s.",
391
+ obj({
392
+ ref: REF,
393
+ x: X,
394
+ y: Y,
395
+ w: W,
396
+ capa: CAPA,
397
+ tipo: str("Cu\xE1l booster.", { enum: opts.boosters })
398
+ }, ["ref", "x", "y", "w", "tipo"])
399
+ ),
400
+ ...monedas.length ? [fn(
401
+ "aplicar_booster",
402
+ "USA uno de los boosters que se ARRASTRAN sobre un objeto, y lo deja ya aplicado. Es la herramienta para \xABqu\xEDtale el fondo a esta foto\xBB (quitafondo), \xABque esta imagen salga en negro y se revele al tocarla\xBB (magichat) y \xABque al hacer clic se convierta en otra cosa\xBB (transformar). \u26A0\uFE0F Para esto NO uses `poner_booster`: eso solo deja la moneda tirada en el slide sin aplicar nada, y el maestro tendr\xEDa que arrastrarla \xE9l. Y NO uses `borrar`: quitarle el fondo a una imagen no es quitar la imagen.",
403
+ obj({
404
+ tipo: str("Cu\xE1l de las tres monedas.", { enum: monedas }),
405
+ objetivo: str("El `ref` o el id del objeto sobre el que se suelta. Tiene que estar en el slide (no dentro de una capa). Para quitafondo y magichat, una imagen.")
406
+ }, ["tipo", "objetivo"])
407
+ )] : [],
408
+ fn(
409
+ "poner_arena",
410
+ "Pone una BOOST ARENA: un juego completo que ocupa el SLIDE ENTERO (clasificaci\xF3n, carrera, memoria, rompecabezas, tragamonedas...). Como se come el slide, va siempre en un slide para ella sola: si el slide de ahora ya tiene cosas, llama antes a `nuevo_slide`. Sus preguntas y palabras las carga el maestro despu\xE9s, en el editor de la arena.",
411
+ obj({
412
+ ref: REF,
413
+ tipo: str("Cu\xE1l arena.", { enum: opts.arenas })
414
+ }, ["ref", "tipo"])
415
+ ),
416
+ // ── LLENAR LO QUE SE PONE ─────────────────────────────────────────────────
417
+ // Poner una arena vacía no adelanta nada: para una fábrica, LLENAR es todo el trabajo.
418
+ // Estas tres van juntas y en este orden: `describir` trae la forma, `configurar_*` la llena.
419
+ //
420
+ // ⚠️ El schema NO viaja en el catálogo, a posta. `functions/api/openai.ts` rechaza catálogos
421
+ // de más de 40.000 bytes, y ese tope ya estuvo a un 5 % de romperse (22.731 bytes medidos el
422
+ // 2026-09-04 contra uno de 24.000). Solo la Batalla Pirata ocupa más que varias herramientas
423
+ // juntas. Con `describir` el catálogo NO crece aunque se añadan veinte arenas más.
424
+ fn(
425
+ "describir",
426
+ "Dice QU\xC9 se le puede llenar a un booster o a una arena: sus campos, sus trampas y un ejemplo que funciona. Ll\xE1malo ANTES de `configurar_arena` o `configurar_booster` \u2014 los nombres de los campos no son los que parecen (la Batalla Pirata guarda en `pirateQs`, no en `questions`) y adivinarlos hace que el maestro se encuentre la actividad vac\xEDa en plena clase.",
427
+ obj({ tipo: str('El tipo del booster o de la arena: "pirate", "gameshow", "cycle"\u2026') }, ["tipo"])
428
+ ),
429
+ fn(
430
+ "configurar_arena",
431
+ "LLENA una arena que ya est\xE1 puesta: sus preguntas, sus palabras, sus parejas. Es lo que convierte una arena de muestra en la actividad de ESTA clase. Reemplaza la configuraci\xF3n entera, as\xED que manda todos los campos que quieras conservar, no solo los que cambias.",
432
+ obj({
433
+ objetivo: str("El `ref` o el id de la arena. Si acabas de ponerla en esta misma tanda, usa su `ref`. Puedes dejarlo vac\xEDo: sin \xE9l se llena la arena del slide en el que est\xE1s, que solo puede ser una."),
434
+ cfg: str('La configuraci\xF3n, como TEXTO JSON. La forma exacta te la da `describir(tipo)`. Ejemplo para gameshow: {"questions":[{"q":"\u2026","options":["a","b"],"answer":0}]}')
435
+ }, ["cfg"])
436
+ ),
437
+ fn(
438
+ "configurar_booster",
439
+ "LLENA un booster que ya est\xE1 puesto: los nombres de la ruleta, los segundos del cron\xF3metro, las etapas del Ciclo, el enlace del QR. Reemplaza la configuraci\xF3n entera.",
440
+ obj({
441
+ objetivo: str("El `ref` o el id del booster. Si acabas de ponerlo en esta misma tanda, usa su `ref`."),
442
+ cfg: str('La configuraci\xF3n, como TEXTO JSON. La forma exacta te la da `describir(tipo)`. Ejemplo para timer: {"seconds":90}')
443
+ }, ["objetivo", "cfg"])
444
+ ),
445
+ // ── ESTRUCTURA DEL CURSO ──────────────────────────────────────────────────
446
+ fn(
447
+ "nuevo_slide",
448
+ "Crea un slide NUEVO justo despu\xE9s del actual y se pone a trabajar en \xE9l: todo lo que pongas a partir de aqu\xED cae ah\xED. Es como pasar a la diapositiva siguiente cuando te piden varias.",
449
+ obj({ nombre: str('Nombre del slide, corto y descriptivo: "Vocabulario", "Pr\xE1ctica 1".') }, [])
450
+ ),
451
+ fn(
452
+ "nueva_capa",
453
+ "Crea una CAPA en el slide actual: una ventana que se abre encima del slide cuando algo la llama (la respuesta de un ejercicio, una explicaci\xF3n, una imagen ampliada). Nace vac\xEDa; para poner cosas DENTRO, p\xE1sale su `ref` en el campo `capa` de las herramientas de poner. \u26A0\uFE0F Crearla NO hace que se abra: para que se abra al tocar algo hay que `programar` ese algo con accion abrirCapa y el `ref` de esta capa.",
454
+ obj({ ref: REF, nombre: str('Nombre de la capa: "Respuesta 1", "Pista".') }, ["ref", "nombre"])
455
+ ),
456
+ fn(
457
+ "nueva_escena",
458
+ "Crea una ESCENA nueva (un d\xEDa de clase, un bloque del curso) con un slide vac\xEDo, y se pone a trabajar ah\xED. Solo si el maestro pide otro d\xEDa u otro bloque \u2014 no para un slide m\xE1s.",
459
+ obj({ nombre: str('Nombre de la escena: "D\xEDa 3", "Repaso".') }, ["nombre"])
460
+ ),
461
+ // ── PROGRAMAR (los disparadores) ──────────────────────────────────────────
462
+ fn(
463
+ "programar",
464
+ "Le da COMPORTAMIENTO a un objeto: cu\xE1ndo pasa algo (evento) y qu\xE9 pasa (acci\xF3n). Es lo que convierte un slide en una actividad. Puedes programar algo que acabas de poner usando su `ref`, o algo que ya estaba usando el id que te di en la lista del slide.",
465
+ obj({
466
+ objetivo: str("A qu\xE9 objeto se le pone. Su `ref` (si lo acabas de crear) o su id (si ya exist\xEDa)."),
467
+ evento: str(
468
+ "CU\xC1NDO se dispara. click = al tocarlo \xB7 hover = al pasarle por encima \xB7 slideStart = al entrar al slide \xB7 layerShown = al abrirse la capa donde vive \xB7 time = a los N segundos \xB7 clickStep = en el N-\xE9simo clic (secuencias de clic) \xB7 trayectoriaTerminada = cuando el objeto acaba de recorrer su camino (para encadenar: \xABcuando llegue, que desaparezca\xBB).",
469
+ { enum: ["click", "hover", "slideStart", "layerShown", "time", "clickStep", "trayectoriaTerminada"] }
470
+ ),
471
+ segundos: num('Solo con evento "time": a los cu\xE1ntos segundos.'),
472
+ paso: num('Solo con evento "clickStep": en qu\xE9 n\xFAmero de clic (1, 2, 3...).'),
473
+ accion: str(
474
+ "QU\xC9 pasa. abrirCapa = muestra una capa \xB7 irAEscena = salta a otra escena \xB7 irASlide = salta a un slide \xB7 cerrarCapa = cierra la capa abierta \xB7 leerEnVoz = lo lee el navegador en voz alta \xB7 abrirEnlace = abre una URL \xB7 ocultar = el objeto desaparece con una nube de humo \xB7 moverPorTrayectoria = el objeto RECORRE un camino dibujado \xB7 sonar = suena un audio que ya est\xE1 puesto en el slide o en una capa (para \xABque suene al abrirse la capa\xBB: objetivo = ese audio, evento = layerShown, destino = ese mismo audio; para \xABal tocar X\xBB: objetivo = X, destino = el audio) \xB7 reiniciarSecuencia = devuelve el slide a como empez\xF3 \xB7 quitar = le borra todo lo que tuviera programado.",
475
+ {
476
+ enum: [
477
+ "abrirCapa",
478
+ "irAEscena",
479
+ "irASlide",
480
+ "cerrarCapa",
481
+ "leerEnVoz",
482
+ "abrirEnlace",
483
+ "ocultar",
484
+ "moverPorTrayectoria",
485
+ "sonar",
486
+ "reiniciarSecuencia",
487
+ "quitar"
488
+ ]
489
+ }
490
+ ),
491
+ capa: str('Solo con "abrirCapa": el `ref` o el nombre de la capa que se abre.'),
492
+ escena: str('Solo con "irAEscena": el nombre de la escena destino.'),
493
+ slide: num('Solo con "irASlide": el n\xFAmero de slide dentro de la escena actual (empieza en 1).'),
494
+ texto: str('Solo con "leerEnVoz": qu\xE9 se lee en voz alta.'),
495
+ idioma: str('Solo con "leerEnVoz": "en-US" para ingl\xE9s, "es-ES" para espa\xF1ol.'),
496
+ url: str('Solo con "abrirEnlace": la direcci\xF3n completa (https://...).'),
497
+ primero: bool("Ponlo en true si esta acci\xF3n tiene que dispararse ANTES de las que el objeto ya tiene. Los disparadores de un mismo evento corren EN ORDEN, as\xED que para \xABantes de que desaparezca, que abra una capa\xBB la de abrir la capa lleva primero=true."),
498
+ destino: str('El objeto sobre el que RECAE la acci\xF3n, cuando no es el mismo que la dispara. Con "ocultar": el que desaparece (si lo omites, el que dispara). Con "moverPorTrayectoria": el que se mueve (si lo omites, el que dispara). Con "sonar": el AUDIO que suena \u2014 este es obligatorio, y tiene que ser uno de los audios que te listo en el slide.'),
499
+ trayectoria: str('Solo con "moverPorTrayectoria" o el evento "trayectoriaTerminada": el NOMBRE del camino. Si el objeto no tiene ninguno, NO pasa nada malo: p\xEDdelo igual y el editor le crea uno vac\xEDo y pone al maestro a dibujarlo ah\xED mismo, con la programaci\xF3n ya hecha. Es lo correcto \u2014 t\xFA no puedes dibujar el camino, pero s\xED dej\xE1rselo listo.')
500
+ }, ["objetivo", "evento", "accion"])
501
+ ),
502
+ // ── RETOCAR LO QUE YA ESTÁ ────────────────────────────────────────────────
503
+ // Se llamaba `mover`, y ese nombre le robaba las peticiones a `programar` con
504
+ // `moverPorTrayectoria`: a «quiero que al hacer clic el tarro SE MUEVA por una trayectoria»
505
+ // el modelo contestaba recolocando el tarro. Medido contra la API, fallaba siempre. El
506
+ // arreglo es el nombre, no una regla más en el prompt: aquí «mover» es de animación.
507
+ fn(
508
+ "recolocar",
509
+ "Cambia de SITIO o de TAMA\xD1O un objeto que ya est\xE1 en el slide, al instante y para siempre. NO es una animaci\xF3n ni un recorrido: para que un objeto se mueva en la proyecci\xF3n, usa `programar` con la acci\xF3n `moverPorTrayectoria`.",
510
+ obj({
511
+ objetivo: str("El `ref` o el id del objeto."),
512
+ x: num("Nuevo borde izquierdo en %. Om\xEDtelo para no moverlo en horizontal."),
513
+ y: num("Nuevo borde superior en %. Om\xEDtelo para no moverlo en vertical."),
514
+ w: num("Nuevo ancho en %. El alto se reajusta solo guardando la proporci\xF3n.")
515
+ }, ["objetivo"])
516
+ ),
517
+ // ── ESTILO ────────────────────────────────────────────────────────────────
518
+ // Sin estas dos, el robot podía CREAR bonito pero no RETOCAR: «ponlo blanco» o «pon a este
519
+ // sticker los mismos efectos que a la abeja» no tenían herramienta, y son de lo primero que
520
+ // pide cualquiera que esté componiendo un slide.
521
+ fn(
522
+ "cambiar_estilo",
523
+ "Cambia c\xF3mo SE VE un objeto que ya est\xE1 puesto, sin tocar su contenido ni su sitio. Manda solo lo que quieras cambiar; lo que no mandes se queda como estaba.",
524
+ obj({
525
+ objetivo: str("El `ref` o el id del objeto."),
526
+ color: str("Color del TEXTO en hex."),
527
+ relleno: str("Color de RELLENO de una figura o un bot\xF3n, en hex."),
528
+ tamano: num("Tama\xF1o de letra en p\xEDxeles sobre 1080."),
529
+ efecto: str("Efecto de COLOR de las letras. Ojo: el ne\xF3n pinta las letras del color DEL EFECTO, as\xED que \xABtexto blanco con ne\xF3n\xBB es color blanco Y color_efecto blanco.", { enum: efectosColor }),
530
+ color_efecto: str("Color del efecto en hex: el brillo del ne\xF3n, la sombra, el borde, el bloque. Si no lo mandas, cada efecto usa el suyo."),
531
+ movimiento: str("Efecto de MOVIMIENTO.", { enum: efectosMov }),
532
+ peso: num("Grosor de la letra, 400 a 900."),
533
+ mayusculas: bool("TODO EN MAY\xDASCULAS (o false para quitarlo)."),
534
+ align: str("Alineaci\xF3n del p\xE1rrafo.", { enum: ["left", "center", "right", "justify"] }),
535
+ opacidad: num("Transparencia de 0 (invisible) a 1 (opaco)."),
536
+ giro: num("Giro en grados. 0 = derecho.")
537
+ }, ["objetivo"])
538
+ ),
539
+ fn(
540
+ "copiar_estilo",
541
+ "Copia el ASPECTO de un objeto a otro: colores, tama\xF1o de letra, fuente, grosor, may\xFAsculas y los dos efectos. No copia el texto, ni el sitio, ni el tama\xF1o de la caja, ni la programaci\xF3n. Es lo que resuelve \xABponle a este sticker los mismos efectos que le pusiste a la abeja\xBB: no tienes que acordarte de qu\xE9 le pusiste, se copia y ya.",
542
+ obj({
543
+ desde: str("El `ref` o el id del objeto que ya est\xE1 como quieres."),
544
+ hacia: str("El `ref` o el id del objeto que tiene que quedar igual.")
545
+ }, ["desde", "hacia"])
546
+ ),
547
+ fn(
548
+ "cambiar_texto",
549
+ "Reescribe el texto de un objeto que ya est\xE1 en el slide.",
550
+ obj({
551
+ objetivo: str("El `ref` o el id del objeto."),
552
+ texto: str("El texto nuevo.")
553
+ }, ["objetivo", "texto"])
554
+ ),
555
+ fn(
556
+ "borrar",
557
+ "Hace DESAPARECER un objeto entero del slide. \u26A0\uFE0F DESTRUCTIVA: \xFAsala SOLO si el maestro te pide borrar, quitar o eliminar EL OBJETO con esas palabras. Nunca porque te parezca que sobra, ni para \xABlimpiar\xBB, ni al reordenar un slide. Si el mensaje no pide un borrado, usa `responder`. \u26A0\uFE0F Y quitarle a algo el FONDO, el color, el borde o la sombra NO es borrarlo: el fondo de una imagen se quita con `aplicar_booster`, y el objeto se queda donde est\xE1.",
558
+ obj({ objetivo: str("El `ref` o el id del objeto a quitar.") }, ["objetivo"])
559
+ ),
560
+ // ── HABLAR, QUE TAMBIÉN ES UNA RESPUESTA VÁLIDA ───────────────────────────
561
+ // Al robot se le OBLIGA a llamar a una herramienta (`tool_choice: 'required'`), porque sin
562
+ // obligarlo contestaba «voy a programar la acción… procedo a añadirla» sin llamar a nada.
563
+ // Pero esa obligación tiene un filo: si el maestro NO pide nada, el modelo se inventa una
564
+ // operación con tal de obedecer. Pasó, y de la peor forma: Antonio escribió «listo ya le
565
+ // agregué trayectoria» —una simple confirmación— y el robot llamó a `borrar` y le borró el
566
+ // dibujo. Por eso esta herramienta se describe como lo NORMAL y no como la excepción: en
567
+ // una conversación, la mitad de los mensajes no piden ningún cambio.
568
+ fn(
569
+ "responder",
570
+ "Hablarle al maestro SIN tocar el lienzo. Es una respuesta tan v\xE1lida como cualquier otra y la vas a usar mucho: el maestro te habla como a una persona. \xDAsala SIEMPRE que su mensaje no pida un cambio concreto \u2014 cuando te confirma algo (\xABlisto\xBB, \xABya lo hice\xBB, \xABya dibuj\xE9 la trayectoria\xBB), cuando te da las gracias, cuando te pregunta, o cuando te falta un dato para actuar. Ante la duda de si te est\xE1n pidiendo un cambio o solo cont\xE1ndote algo: RESPONDE, no toques nada.",
571
+ obj({ mensaje: str("Lo que le dices al maestro. Una o dos frases, en espa\xF1ol, en su tono.") }, ["mensaje"])
572
+ )
573
+ ];
574
+ }
575
+
576
+ // ../src/insert/shapes.ts
577
+ var squareH = (w) => +(w * (16 / 9)).toFixed(1);
578
+ var FORMA_DEF = {
579
+ rect: { anch: 24, alto: 0.667, color: "#6366f1" },
580
+ ellipse: { anch: 16, alto: "cuadrado", color: "#22c55e" },
581
+ star: { anch: 16, alto: "cuadrado", color: "#f59e0b" },
582
+ triangle: { anch: 18, alto: "cuadrado", color: "#8b5cf6" },
583
+ // Globos: apaisados, y la cola necesita sitio abajo.
584
+ bubble: { anch: 26, alto: 0.85, color: "#38bdf8" },
585
+ "bubble-r": { anch: 26, alto: 0.85, color: "#f97316" },
586
+ "bubble-think": { anch: 24, alto: 1.05, color: "#a855f7" },
587
+ "bubble-shout": { anch: 22, alto: "cuadrado", color: "#ef4444" },
588
+ "bubble-oval": { anch: 26, alto: 0.85, color: "#38bdf8" },
589
+ "bubble-round": { anch: 22, alto: "cuadrado", color: "#38bdf8" },
590
+ "bubble-cloud": { anch: 28, alto: 0.8, color: "#a855f7" },
591
+ "bubble-brain": { anch: 26, alto: 0.9, color: "#f472b6" },
592
+ "bubble-flower": { anch: 24, alto: "cuadrado", color: "#a855f7" },
593
+ "bubble-up": { anch: 26, alto: 0.85, color: "#22c55e" },
594
+ "bubble-heart": { anch: 22, alto: "cuadrado", color: "#f43f5e" },
595
+ "bubble-whisper": { anch: 22, alto: 0.9, color: "#94a3b8" },
596
+ "bubble-double": { anch: 28, alto: 0.95, color: "#0ea5e9" },
597
+ "bubble-radio": { anch: 24, alto: "cuadrado", color: "#f59e0b" },
598
+ "bubble-scroll": { anch: 28, alto: 0.85, color: "#d4a373" },
599
+ "bubble-pointer": { anch: 30, alto: 0.7, color: "#14b8a6" },
600
+ // Flechas: anchas y bajas, que es como apunta una flecha.
601
+ "arrow-block": { anch: 26, alto: 0.62, color: "#0ea5e9" },
602
+ "arrow-double": { anch: 26, alto: 0.62, color: "#0ea5e9" },
603
+ "arrow-bent": { anch: 22, alto: 0.9, color: "#0ea5e9" },
604
+ "arrow-curved": { anch: 22, alto: 1, color: "#0ea5e9" },
605
+ "arrow-circular": { anch: 18, alto: "cuadrado", color: "#0ea5e9" },
606
+ chevron: { anch: 14, alto: 1.1, color: "#0ea5e9" },
607
+ // Marcas: verde lo correcto, rojo lo incorrecto. Sale así sin tocar nada.
608
+ check: { anch: 14, alto: "cuadrado", color: "#22c55e" },
609
+ cross: { anch: 13, alto: "cuadrado", color: "#ef4444" },
610
+ question: { anch: 12, alto: "cuadrado", color: "#f59e0b" },
611
+ exclaim: { anch: 9, alto: "cuadrado", color: "#f59e0b" },
612
+ // Geometría: cuadradas de verdad, porque en clase se miden y se comparan.
613
+ diamond: { anch: 16, alto: "cuadrado", color: "#8b5cf6" },
614
+ pentagon: { anch: 16, alto: "cuadrado", color: "#8b5cf6" },
615
+ hexagon: { anch: 16, alto: "cuadrado", color: "#8b5cf6" },
616
+ trapezoid: { anch: 18, alto: 0.9, color: "#8b5cf6" },
617
+ parallelogram: { anch: 20, alto: 0.7, color: "#8b5cf6" },
618
+ // Marcos y cintas: anchos, que es lo que enmarcan.
619
+ banner: { anch: 34, alto: 0.45, color: "#f43f5e" },
620
+ frame: { anch: 34, alto: 1, color: "#334155" },
621
+ note: { anch: 20, alto: 1, color: "#fbbf24" },
622
+ tag: { anch: 22, alto: 0.6, color: "#14b8a6" },
623
+ // Resaltadores: van encima de otra cosa, así que entran grandes.
624
+ "circle-mark": { anch: 30, alto: 0.6, color: "#ef4444" },
625
+ underline: { anch: 28, alto: 0.3, color: "#facc15" },
626
+ blob: { anch: 26, alto: "cuadrado", color: "#c4b5fd" }
627
+ };
628
+
629
+ // ../src/registro/esquemas.ts
630
+ var texto = (que, o = {}) => ({ tipo: "texto", que, ...o });
631
+ var html = (que) => ({ tipo: "html", que });
632
+ var num2 = (que, o = {}) => ({ tipo: "numero", que, entero: true, ...o });
633
+ var bool2 = (que) => ({ tipo: "booleano", que });
634
+ var opcion = (que, valores, o = {}) => ({ tipo: "opcion", que, valores, ...o });
635
+ var color = (que) => ({ tipo: "color", que });
636
+ var imagen = (que, o = {}) => ({ tipo: "archivo", medio: "imagen", que, ...o });
637
+ var audio = (que) => ({ tipo: "archivo", medio: "audio", que });
638
+ var lista = (que, de, o = {}) => ({ tipo: "lista", que, de, ...o });
639
+ var objeto = (que, campos, o = {}) => ({ tipo: "objeto", que, campos, ...o });
640
+ var indice = (que, dentroDe) => ({ tipo: "indice", que, dentroDe, obligatorio: true });
641
+ var sinConfig = () => ({ cfgSchema: null, ejemplo: null, donde: null });
642
+ var ESQUEMAS_BOOSTERS = {
643
+ dice: {
644
+ donde: "wcfg",
645
+ cfgSchema: { campos: { dice: num2("Cu\xE1ntos dados se lanzan a la vez.", { min: 1, max: 6 }) } },
646
+ ejemplo: { dice: 2 }
647
+ },
648
+ wheel: {
649
+ donde: "wcfg",
650
+ cfgSchema: { campos: { names: lista("Los nombres entre los que sortea.", texto("Un nombre."), { min: 2, max: 60, obligatorio: true }) } },
651
+ ejemplo: { names: ["Ana", "Luis", "Mar\xEDa", "Carlos"] }
652
+ },
653
+ timer: {
654
+ donde: "wcfg",
655
+ cfgSchema: { campos: { seconds: num2("Cu\xE1nto dura la cuenta atr\xE1s, en segundos.", { min: 5, max: 3600 }) } },
656
+ ejemplo: { seconds: 90 }
657
+ },
658
+ score: {
659
+ donde: "wcfg",
660
+ cfgSchema: { campos: { teams: lista("Los equipos que se punt\xFAan.", texto("Nombre del equipo."), { min: 2, max: 8, obligatorio: true }) } },
661
+ ejemplo: { teams: ["Equipo A", "Equipo B", "Equipo C"] }
662
+ },
663
+ groups: {
664
+ donde: "wcfg",
665
+ cfgSchema: {
666
+ campos: {
667
+ names: lista("Los participantes que se van a repartir.", texto("Un nombre."), { min: 2, max: 60, obligatorio: true }),
668
+ teamCount: num2("En cu\xE1ntos equipos se reparten.", { min: 2, max: 8 })
669
+ }
670
+ },
671
+ ejemplo: { names: ["Ana", "Luis", "Mar\xEDa", "Carlos", "Sof\xEDa", "Diego"], teamCount: 3 }
672
+ },
673
+ soundboard: {
674
+ donde: "wcfg",
675
+ cfgSchema: {
676
+ campos: {
677
+ sounds: lista("Los botones de la botonera.", objeto("Un bot\xF3n.", {
678
+ label: texto("Lo que dice el bot\xF3n.", { obligatorio: true }),
679
+ src: audio("El sonido que suelta.")
680
+ }), { max: 12 })
681
+ },
682
+ nota: "Si un bot\xF3n no trae `src`, suena el efecto de f\xE1brica que le toca por posici\xF3n."
683
+ },
684
+ ejemplo: { sounds: [{ label: "Aplausos" }, { label: "Ding" }, { label: "Buzzer" }] }
685
+ },
686
+ mystery: {
687
+ donde: "wcfg",
688
+ cfgSchema: { campos: { items: lista("Lo que sale al azar, sin repetir hasta agotarse.", texto("Una palabra o pregunta."), { min: 2, max: 60, obligatorio: true }) } },
689
+ ejemplo: { items: ["apple", "happy", "run", "book", "blue", "friend"] }
690
+ },
691
+ // Un botón que suena y destella. No hay nada que llenar.
692
+ buzzer: sinConfig(),
693
+ // Las tres MONEDAS-HERRAMIENTA: no se configuran, se arrastran sobre otro objeto y actúan
694
+ // sobre ÉL (le cambian el `src`, le ponen `magic:true`, le cuelgan la lista `morph`).
695
+ // Para usarlas desde una máquina está `aplicar_booster`, no `configurar_booster`.
696
+ quitafondo: sinConfig(),
697
+ magichat: sinConfig(),
698
+ transformar: sinConfig(),
699
+ instructions: {
700
+ donde: "wcfg.ext",
701
+ cfgSchema: {
702
+ campos: {
703
+ title: texto("T\xEDtulo del panel.", { max: 80 }),
704
+ icon: texto("Un emoji para la moneda.", { max: 4 }),
705
+ bodyHtml: html("El contenido, con formato. Manda sobre `body` si vienen los dos."),
706
+ body: texto("Legado: texto plano, una instrucci\xF3n por l\xEDnea.")
707
+ },
708
+ nota: "Escribe en `bodyHtml`. `body` solo existe para los slides viejos."
709
+ },
710
+ ejemplo: {
711
+ title: "Instructions",
712
+ icon: "\u{1F4A1}",
713
+ bodyHtml: "<ol><li>Read the text on the board.</li><li>Discuss it with your partner.</li><li>Write three sentences.</li></ol>"
714
+ }
715
+ },
716
+ magicwand: {
717
+ donde: "wcfg.ext",
718
+ cfgSchema: {
719
+ campos: {
720
+ images: lista("Las im\xE1genes que la varita saca una a una.", objeto("Una imagen.", {
721
+ src: imagen("La imagen.", { obligatorio: true }),
722
+ label: texto("R\xF3tulo que se planta debajo. Vac\xEDo = sin r\xF3tulo.")
723
+ }), { max: 30 })
724
+ }
725
+ },
726
+ ejemplo: { images: [{ src: "https://assets.boosterarena.com/u/ej/cat.webp", label: "cat" }] }
727
+ },
728
+ tombola: {
729
+ donde: "wcfg.ext",
730
+ cfgSchema: {
731
+ campos: {
732
+ kind: opcion("Qu\xE9 sortea el bombo.", ["numbers", "words", "images"], { obligatorio: true }),
733
+ from: num2('Primer n\xFAmero del rango. Solo con `kind: "numbers"`.', { min: 0, max: 999 }),
734
+ to: num2('\xDAltimo n\xFAmero del rango. Solo con `kind: "numbers"`.', { min: 1, max: 999 }),
735
+ words: lista("Las bolas, si son palabras.", texto("Una palabra."), { max: 60 }),
736
+ images: lista("Las bolas, si son dibujos.", objeto("Una bola con dibujo.", {
737
+ src: imagen("El dibujo.", { obligatorio: true }),
738
+ label: texto("Nombre del dibujo.")
739
+ }), { max: 40 })
740
+ },
741
+ nota: "Llena SOLO el juego que corresponde a `kind`. Los topes son 200 n\xFAmeros, 60 palabras, 40 im\xE1genes."
742
+ },
743
+ ejemplo: { kind: "words", words: ["cat", "dog", "bird", "fish", "horse"] }
744
+ },
745
+ trafficlight: {
746
+ donde: "wcfg.ext",
747
+ cfgSchema: {
748
+ campos: {
749
+ start: opcion("Luz encendida al abrir el slide.", ["red", "amber", "green"]),
750
+ sound: bool2("Que suene al cambiar de luz."),
751
+ housing: imagen("PNG de la carcasa. Sin \xE9l, se dibuja con degradado.")
752
+ }
753
+ },
754
+ ejemplo: { start: "green", sound: true }
755
+ },
756
+ labelit: {
757
+ donde: "wcfg.ext",
758
+ cfgSchema: {
759
+ campos: {
760
+ src: imagen("La l\xE1mina que se etiqueta (c\xE9lula, mapa, un coraz\xF3n\u2026).", { obligatorio: true }),
761
+ aspect: { tipo: "numero", que: "Ancho/alto de la l\xE1mina. Lo calcula el editor al subirla.", min: 0.1, max: 10 },
762
+ title: texto("T\xEDtulo de la actividad."),
763
+ mode: opcion("C\xF3mo se juega: revelar al tocar, arrastrar las palabras, o escribirlas.", ["reveal", "drag", "type"]),
764
+ points: lista("Los puntos clavados sobre la l\xE1mina.", objeto("Un punto.", {
765
+ x: { tipo: "numero", que: "Posici\xF3n horizontal, en % del ancho de la l\xE1mina.", min: 0, max: 100, obligatorio: true },
766
+ y: { tipo: "numero", que: "Posici\xF3n vertical, en % del alto de la l\xE1mina.", min: 0, max: 100, obligatorio: true },
767
+ text: texto("C\xF3mo se llama esa parte.", { obligatorio: true })
768
+ }), { max: 20 }),
769
+ shuffle: bool2("Barajar las fichas en el modo arrastrar."),
770
+ sound: bool2("Sonidos de acierto y error."),
771
+ fontScale: { tipo: "numero", que: "Tama\xF1o del texto de los r\xF3tulos (1 = normal).", min: 0.7, max: 2.6 },
772
+ leadLen: { tipo: "numero", que: "Largo de la barrita del r\xF3tulo al punto, en % de la imagen.", min: 4, max: 45 }
773
+ },
774
+ nota: "Sin `src` no hay actividad: la l\xE1mina la pone el maestro o la f\xE1brica, nunca sale de f\xE1brica."
775
+ },
776
+ ejemplo: {
777
+ src: "https://assets.boosterarena.com/u/ej/celula.webp",
778
+ mode: "reveal",
779
+ shuffle: true,
780
+ sound: true,
781
+ points: [{ x: 50, y: 42, text: "nucleus" }, { x: 22, y: 61, text: "cytoplasm" }]
782
+ }
783
+ },
784
+ cycle: {
785
+ donde: "wcfg.ext",
786
+ cfgSchema: {
787
+ campos: {
788
+ title: texto('T\xEDtulo del proceso: "El ciclo del agua".'),
789
+ stages: lista("Las etapas de la rueda, en orden.", objeto("Una etapa.", {
790
+ id: texto("Identificador propio de la etapa. Si lo omites, lo pone el editor."),
791
+ text: texto("C\xF3mo se llama la etapa.", { obligatorio: true }),
792
+ src: imagen("Dibujo de la etapa."),
793
+ body: texto("Lo que se cuenta de esa etapa en el panel del centro.")
794
+ }), { min: 2, max: 8, obligatorio: true }),
795
+ closed: bool2("El proceso se repite (rueda) o termina (cadena)."),
796
+ guess: bool2("Modo Adivinar: los nombres salen ocultos hasta el clic."),
797
+ sound: bool2("Sonido al pasar de etapa.")
798
+ }
799
+ },
800
+ ejemplo: {
801
+ title: "The water cycle",
802
+ closed: true,
803
+ guess: false,
804
+ sound: true,
805
+ stages: [
806
+ { text: "Evaporation", body: "The sun heats the water and it rises as vapour." },
807
+ { text: "Condensation", body: "The vapour cools down and forms clouds." },
808
+ { text: "Precipitation", body: "The water falls as rain or snow." },
809
+ { text: "Collection", body: "The water gathers in rivers, lakes and the sea." }
810
+ ]
811
+ }
812
+ },
813
+ fracplot: {
814
+ donde: "wcfg.ext",
815
+ cfgSchema: {
816
+ campos: {
817
+ shape: opcion("Con qu\xE9 figura se dibuja la fracci\xF3n.", ["pie", "bar"]),
818
+ label: bool2("Escribir la fracci\xF3n dentro de la figura."),
819
+ color: num2("\xCDndice del color de la pr\xF3xima figura. Rota solo al estampar.", { min: 0, max: 11 }),
820
+ sound: bool2("Sonido al estampar.")
821
+ },
822
+ nota: "Es una HERRAMIENTA DE AUTOR\xCDA: no guarda las fracciones, las estampa en el slide como figuras. Configurarlo solo cambia c\xF3mo sale la SIGUIENTE."
823
+ },
824
+ ejemplo: { shape: "pie", label: true, color: 0, sound: true }
825
+ },
826
+ qr: {
827
+ donde: "wcfg.ext",
828
+ cfgSchema: {
829
+ campos: {
830
+ text: texto("Lo que se codifica: el enlace o el texto.", { obligatorio: true }),
831
+ kind: opcion("Si lo de arriba es un enlace o texto suelto.", ["url", "text"]),
832
+ dark: color("Color de los m\xF3dulos. Tiene que ser el OSCURO de los dos."),
833
+ light: color("Color del fondo, zona muda incluida."),
834
+ label: texto("R\xF3tulo debajo del c\xF3digo. Vac\xEDo = sin r\xF3tulo."),
835
+ plate: bool2("Tarjeta blanca con esquinas redondas y sombra."),
836
+ sound: bool2("El sonido de la boca al comerse el enlace.")
837
+ },
838
+ nota: "El c\xF3digo es EST\xC1TICO: el enlace va grabado en el dibujo, as\xED que sigue funcionando dentro del .zip exportado. Si cambias `text`, hay que regenerarlo."
839
+ },
840
+ ejemplo: { text: "https://boosterarena.com", kind: "url", label: "Escanea aqu\xED", plate: true, sound: true }
841
+ },
842
+ plotter: {
843
+ donde: "wcfg.ext",
844
+ cfgSchema: {
845
+ campos: {
846
+ eqs: lista("Las ecuaciones que se dibujan.", objeto("Una ecuaci\xF3n.", {
847
+ txt: texto('La ecuaci\xF3n tal cual se escribe: "y = x^2 - 3".', { obligatorio: true }),
848
+ color: color("Color de la curva."),
849
+ on: bool2("Si sale dibujada o apagada.")
850
+ }), { min: 1, max: 8, obligatorio: true }),
851
+ mode: opcion("Estudio plano o superficie girable.", ["2d", "3d"]),
852
+ dark: bool2("Cuadr\xEDcula sobre fondo oscuro."),
853
+ piAxis: bool2("Marcar el eje X en m\xFAltiplos de \u03C0."),
854
+ showCuts: bool2("Marcar los cortes entre curvas con sus coordenadas.")
855
+ },
856
+ nota: "Cada letra suelta (a, b, c) de una ecuaci\xF3n se vuelve un deslizador en el Player."
857
+ },
858
+ ejemplo: { mode: "2d", dark: true, showCuts: true, eqs: [{ txt: "y = x^2 - 3", on: true }, { txt: "y = 2x + 1", on: true }] }
859
+ },
860
+ // Retirado del catálogo el 2026-08-12 (Antonio se quedó con `fracplot`), pero su código sigue
861
+ // cableado para que los slides que ya lo tengan puesto sigan abriendo. Ficha declarada para
862
+ // que el candado no lo cuente como olvidado si algún día vuelve la tarjeta.
863
+ fractions: {
864
+ donde: "wcfg.ext",
865
+ cfgSchema: {
866
+ campos: {
867
+ mode: opcion("Qu\xE9 operaci\xF3n se manipula.", ["add", "compare"]),
868
+ title: texto("T\xEDtulo del riel."),
869
+ a: texto('Primera fracci\xF3n, escrita "1/2".', { obligatorio: true }),
870
+ b: texto('Segunda fracci\xF3n, escrita "1/3".', { obligatorio: true }),
871
+ labels: bool2("Escribir el valor en cada pieza."),
872
+ sound: bool2("Sonido al cortar una regleta.")
873
+ },
874
+ nota: "\u26A0\uFE0F RETIRADO de la galer\xEDa. No lo pongas en slides nuevos \u2014 usa `fracplot`."
875
+ },
876
+ ejemplo: { mode: "add", a: "1/2", b: "1/3", labels: true, sound: true }
877
+ }
878
+ };
879
+ var ESQUEMAS_ARENAS = {
880
+ classification: {
881
+ donde: "acfg",
882
+ cfgSchema: {
883
+ campos: {
884
+ categories: lista("Los montones a los que se arrastra.", texto("Nombre del mont\xF3n."), { min: 2, max: 4, obligatorio: true }),
885
+ items: lista("Las palabras que se reparten.", objeto("Una palabra y su mont\xF3n.", {
886
+ text: texto("La palabra.", { obligatorio: true }),
887
+ cat: indice("A qu\xE9 mont\xF3n pertenece (0 = el primero de `categories`).", "/categories")
888
+ }), { min: 2, max: 24, obligatorio: true })
889
+ },
890
+ nota: "Reparte las palabras entre TODAS las categor\xEDas: un mont\xF3n vac\xEDo se ve como un error en clase."
891
+ },
892
+ ejemplo: {
893
+ categories: ["Animals", "Plants"],
894
+ items: [
895
+ { text: "cat", cat: 0 },
896
+ { text: "hamster", cat: 0 },
897
+ { text: "dog", cat: 0 },
898
+ { text: "daisy", cat: 1 },
899
+ { text: "oak tree", cat: 1 },
900
+ { text: "tulip", cat: 1 }
901
+ ]
902
+ }
903
+ },
904
+ gameshow: {
905
+ donde: "acfg",
906
+ cfgSchema: {
907
+ campos: {
908
+ questions: lista("Las preguntas del concurso.", objeto("Una pregunta.", {
909
+ q: texto("El enunciado.", { obligatorio: true }),
910
+ options: lista("Las respuestas que se ofrecen.", texto("Una respuesta."), { min: 2, max: 4, obligatorio: true }),
911
+ answer: indice("Cu\xE1l de `options` es la correcta (0 = la primera).", "options")
912
+ }), { min: 1, max: 30, obligatorio: true })
913
+ },
914
+ nota: "Aqu\xED las opciones son TEXTO PLANO, no objetos. La Pirata y el Escape s\xED admiten imagen; este no."
915
+ },
916
+ ejemplo: {
917
+ questions: [
918
+ { q: "Which planet is the Red Planet?", options: ["Venus", "Mars", "Jupiter", "Saturn"], answer: 1 },
919
+ { q: "A group of wolves is called a ___.", options: ["pack", "herd", "school", "flock"], answer: 0 }
920
+ ]
921
+ }
922
+ },
923
+ race: {
924
+ donde: "acfg",
925
+ cfgSchema: {
926
+ campos: {
927
+ track: num2("Qu\xE9 circuito se corre.", { min: 0, max: 3 }),
928
+ laps: num2("Cu\xE1ntas vueltas dura la carrera.", { min: 1, max: 5 }),
929
+ questions: lista("Las preguntas que dan nitro.", objeto("Una pregunta.", {
930
+ q: texto("El enunciado.", { obligatorio: true }),
931
+ options: lista("Las respuestas que se ofrecen.", texto("Una respuesta."), { min: 2, max: 4, obligatorio: true }),
932
+ answer: indice("Cu\xE1l de `options` es la correcta (0 = la primera).", "options")
933
+ }), { min: 1, max: 30, obligatorio: true })
934
+ },
935
+ nota: "Misma forma de pregunta que el Game Show: opciones de texto plano."
936
+ },
937
+ ejemplo: {
938
+ track: 0,
939
+ questions: [
940
+ { q: "A baby dog is called a ___.", options: ["kitten", "puppy", "cub", "foal"], answer: 1 },
941
+ { q: "Water boils at 100 degrees ___.", options: ["Fahrenheit", "Celsius", "Kelvin", "Newton"], answer: 1 }
942
+ ]
943
+ }
944
+ },
945
+ pirate: {
946
+ donde: "acfg",
947
+ cfgSchema: {
948
+ campos: {
949
+ pirateQs: lista("Las preguntas de la batalla.", objeto("Una pregunta.", {
950
+ q: texto("El enunciado.", { obligatorio: true }),
951
+ image: imagen("Dibujo del enunciado."),
952
+ options: lista("Las respuestas.", objeto("Una respuesta.", {
953
+ text: texto("Lo que dice la respuesta."),
954
+ image: imagen("Dibujo de la respuesta.")
955
+ }), { min: 2, max: 4, obligatorio: true }),
956
+ answer: indice("Cu\xE1l de `options` es la correcta (0 = la primera).", "options")
957
+ }), { min: 1, max: 30, obligatorio: true }),
958
+ secs: num2("Segundos para contestar cada pregunta.", { min: 5, max: 120 }),
959
+ chests: bool2("Que el que acierta abra un cofre y pueda quitarle oro a otro.")
960
+ },
961
+ nota: "\u26A0\uFE0F EL CAMPO ES `pirateQs`, NO `questions`. Escribir en `questions` fue el fallo que tuvo esta arena hasta el 2026-08-14: se guardaba ah\xED, se le\xEDa de `pirateQs`, y la partida jugaba siempre las tres preguntas de f\xE1brica. Y sus opciones son OBJETOS `{text, image}`, no cadenas."
962
+ },
963
+ ejemplo: {
964
+ secs: 20,
965
+ chests: true,
966
+ pirateQs: [
967
+ { q: "Which sentence is correct?", answer: 0, options: [{ text: "She goes to school." }, { text: "She go to school." }, { text: "She going to school." }] },
968
+ { q: "Yesterday I ___ to the market.", answer: 1, options: [{ text: "go" }, { text: "went" }, { text: "gone" }] }
969
+ ]
970
+ }
971
+ },
972
+ mole: {
973
+ donde: "acfg",
974
+ cfgSchema: {
975
+ campos: {
976
+ prompt: texto('La consigna del campo: "\xA1Aplasta los topos con la oraci\xF3n correcta!".', { obligatorio: true }),
977
+ secs: num2("Cu\xE1nto dura la partida, en segundos.", { min: 20, max: 180 }),
978
+ sets: lista("Las tandas de carteles que van saliendo.", objeto("Una tanda.", {
979
+ options: lista("Los carteles de la tanda.", objeto("Un cartel.", {
980
+ text: texto("Lo que dice el cartel."),
981
+ image: imagen("Dibujo del cartel.")
982
+ }), { min: 2, max: 6, obligatorio: true }),
983
+ answer: indice("Cu\xE1l de `options` es la buena (0 = el primero).", "options")
984
+ }), { min: 1, max: 20, obligatorio: true })
985
+ },
986
+ nota: "Las opciones son objetos `{text, image}` \u2014 el campo de imagen se llama `image`, como en la Pirata."
987
+ },
988
+ ejemplo: {
989
+ prompt: "\xA1Aplasta los topos con la oraci\xF3n correcta!",
990
+ secs: 60,
991
+ sets: [
992
+ { answer: 0, options: [{ text: "She goes to school." }, { text: "She go to school." }, { text: "She going to school." }] },
993
+ { answer: 1, options: [{ text: "He don't like it." }, { text: "He doesn't like it." }, { text: "He not like it." }] }
994
+ ]
995
+ }
996
+ },
997
+ jigsaw: {
998
+ donde: "acfg",
999
+ cfgSchema: {
1000
+ campos: {
1001
+ imgs: lista("Los dibujos que se arman, uno por ronda.", objeto("Un dibujo.", {
1002
+ src: imagen("El dibujo.", { obligatorio: true }),
1003
+ title: texto('C\xF3mo se llama. Se ense\xF1a al terminar: "\xA1Armaste: The Solar System!".')
1004
+ }), { min: 1, max: 8, obligatorio: true }),
1005
+ level: opcion("En cu\xE1ntas piezas se corta cada dibujo.", ["easy", "normal", "hard"]),
1006
+ prompt: texto("La consigna. Comparte campo con Topos.")
1007
+ },
1008
+ nota: "Con una imagen basta. Cada imagen de m\xE1s es una ronda m\xE1s."
1009
+ },
1010
+ ejemplo: {
1011
+ level: "normal",
1012
+ imgs: [{ src: "https://assets.boosterarena.com/u/ej/solar-system.webp", title: "The Solar System" }]
1013
+ }
1014
+ },
1015
+ memory: {
1016
+ donde: "acfg",
1017
+ cfgSchema: {
1018
+ campos: {
1019
+ pairs: lista("Las parejas que hay que juntar.", objeto("Una pareja: sus dos caras.", {
1020
+ a: objeto("Primera carta.", {
1021
+ text: texto("Lo que dice la carta."),
1022
+ image: imagen("Dibujo de la carta."),
1023
+ audio: audio("Sonido al destaparla.")
1024
+ }, { obligatorio: true }),
1025
+ b: objeto("Segunda carta.", {
1026
+ text: texto("Lo que dice la carta."),
1027
+ image: imagen("Dibujo de la carta."),
1028
+ audio: audio("Sonido al destaparla.")
1029
+ }, { obligatorio: true })
1030
+ }), { min: 2, max: 12, obligatorio: true }),
1031
+ back: texto("Id del dorso de las cartas."),
1032
+ mode: opcion("Individual o por equipos.", ["solo", "teams"]),
1033
+ teams: num2('Cu\xE1ntos equipos. Solo cuenta con `mode: "teams"`.', { min: 2, max: 4 })
1034
+ },
1035
+ nota: "Cada carta admite texto, dibujo y sonido a la vez: imagen\u2194imagen, imagen\u2194palabra o palabra\u2194definici\xF3n. Una carta sin nada de eso sale en blanco."
1036
+ },
1037
+ ejemplo: {
1038
+ back: "booster",
1039
+ mode: "solo",
1040
+ teams: 2,
1041
+ pairs: [
1042
+ { a: { text: "apple" }, b: { text: "a round red or green fruit" } },
1043
+ { a: { text: "run" }, b: { text: "to move fast on your feet" } },
1044
+ { a: { text: "winter" }, b: { text: "the coldest season of the year" } }
1045
+ ]
1046
+ }
1047
+ },
1048
+ "booster-escape": {
1049
+ donde: "acfg",
1050
+ cfgSchema: {
1051
+ campos: {
1052
+ escapeQuestions: lista("Las preguntas del laberinto.", objeto("Una pregunta.", {
1053
+ text: texto("El enunciado."),
1054
+ image: imagen("Dibujo del enunciado."),
1055
+ audio: audio("Audio del enunciado."),
1056
+ options: lista("Las salidas entre las que elegir.", objeto("Una salida.", {
1057
+ text: texto("Lo que dice la salida."),
1058
+ img: imagen("Dibujo de la salida.")
1059
+ }), { min: 2, max: 4, obligatorio: true }),
1060
+ answer: indice("Cu\xE1l de `options` es la correcta (0 = la primera).", "options")
1061
+ }), { min: 1, max: 20, obligatorio: true }),
1062
+ playerNames: lista("Los dos nombres del modo Versus.", texto("Nombre del jugador."), { min: 2, max: 2 })
1063
+ },
1064
+ nota: "\u26A0\uFE0F EL CAMPO ES `escapeQuestions`, NO `questions`. Y aqu\xED la imagen de una opci\xF3n se llama `img` (en la Pirata y en Topos se llama `image`). Es el nombre con el que la arena ven\xEDa de we-boosters y no se cambi\xF3, para que los proyectos ya hechos no salgan vac\xEDos."
1065
+ },
1066
+ ejemplo: {
1067
+ escapeQuestions: [
1068
+ { text: 'The opposite of "hot" is\u2026', answer: 1, options: [{ text: "warm" }, { text: "cold" }, { text: "dry" }] },
1069
+ { text: "She ___ a book every week.", answer: 0, options: [{ text: "reads" }, { text: "read" }, { text: "reading" }] }
1070
+ ]
1071
+ }
1072
+ },
1073
+ "las-vegas": {
1074
+ donde: "acfg",
1075
+ cfgSchema: {
1076
+ campos: {
1077
+ reels: lista(
1078
+ "Los rodillos, de izquierda a derecha. Cada uno es una columna que gira.",
1079
+ lista("Las casillas de ese rodillo.", objeto("Una casilla: palabra o dibujo.", {
1080
+ texto: texto("La palabra de la casilla."),
1081
+ src: imagen("El dibujo de la casilla.")
1082
+ }), { min: 2, max: 20 }),
1083
+ { min: 1, max: 4, obligatorio: true }
1084
+ )
1085
+ },
1086
+ nota: "Los rodillos construyen una FRASE al parar: SHE \xB7 WRITTEN \xB7 A LETTER. Piensa cada rodillo como una posici\xF3n de la oraci\xF3n, no como una lista de palabras sueltas. \u26A0\uFE0F La casilla usa `texto` y `src` (en espa\xF1ol), no `text`/`image` como el resto de las arenas."
1087
+ },
1088
+ ejemplo: {
1089
+ reels: [
1090
+ [{ texto: "SHE" }, { texto: "HE" }, { texto: "THEY" }],
1091
+ [{ texto: "HAS WRITTEN" }, { texto: "HAVE WRITTEN" }],
1092
+ [{ texto: "A LETTER" }, { texto: "A BOOK" }, { texto: "AN EMAIL" }]
1093
+ ]
1094
+ }
1095
+ },
1096
+ "word-storm": {
1097
+ donde: "acfg",
1098
+ cfgSchema: {
1099
+ campos: {
1100
+ words: lista("El mazo de palabras que caen y hay que teclear.", texto("Una palabra."), { min: 4, max: 80, obligatorio: true })
1101
+ },
1102
+ nota: "Palabras que se escriban con el teclado: sin tildes raras ni espacios si el nivel es bajo. La dificultad la sube la arena sola."
1103
+ },
1104
+ ejemplo: { words: ["apple", "house", "friend", "water", "school", "happy", "winter", "teacher"] }
1105
+ }
1106
+ };
1107
+ var ESQUEMAS = { ...ESQUEMAS_BOOSTERS, ...ESQUEMAS_ARENAS };
1108
+ ESQUEMAS["classification-jungle"] = {
1109
+ ...ESQUEMAS_ARENAS.classification,
1110
+ ejemplo: {
1111
+ categories: ["verbs", "nouns"],
1112
+ items: [
1113
+ { text: "run", cat: 0 },
1114
+ { text: "jump", cat: 0 },
1115
+ { text: "eat", cat: 0 },
1116
+ { text: "cat", cat: 1 },
1117
+ { text: "house", cat: 1 },
1118
+ { text: "tree", cat: 1 }
1119
+ ]
1120
+ }
1121
+ };
1122
+ ESQUEMAS_ARENAS["classification-jungle"] = ESQUEMAS["classification-jungle"];
1123
+ var menuDeCurso = (queEs, propios, ejemplo) => ({
1124
+ donde: "acfg",
1125
+ cfgSchema: {
1126
+ campos: {
1127
+ ...propios,
1128
+ title: texto("T\xEDtulo grande de bienvenida."),
1129
+ subtitle: texto("Subt\xEDtulo bajo el t\xEDtulo."),
1130
+ intro: texto("P\xE1rrafo de bienvenida."),
1131
+ footer: texto("Texto de la barra inferior."),
1132
+ logoImage: imagen("Logo. Si no hay, se usa `logoText`."),
1133
+ logoText: texto("Texto del logo cuando no hay imagen."),
1134
+ items: lista(
1135
+ "Las tarjetas del men\xFA. Cada una es un DESTINO al que se puede navegar.",
1136
+ objeto("Una tarjeta.", {
1137
+ id: texto("Clave estable de la tarjeta."),
1138
+ color: color("Color de acento de la tarjeta."),
1139
+ title: texto('T\xEDtulo corto: "CONTENIDO".', { obligatorio: true }),
1140
+ desc: texto("Descripci\xF3n breve."),
1141
+ icon: imagen("Icono. Admite tambi\xE9n un emoji corto, que se pinta como texto.")
1142
+ }),
1143
+ { min: 1, max: 8, obligatorio: true }
1144
+ )
1145
+ },
1146
+ nota: queEs + " \u26A0\uFE0F Deja `id` puesto en cada tarjeta: es lo que ata el destino de navegaci\xF3n aunque el maestro reordene o renombre. Sin `id` se cae a la POSICI\xD3N, y reordenar rompe la programaci\xF3n que ya hubiera hecho."
1147
+ },
1148
+ ejemplo
1149
+ });
1150
+ var TARJETAS_EJ = [
1151
+ { id: "s1", color: "#14b8a6", title: "CONTENIDO", desc: "Los temas del curso." },
1152
+ { id: "s2", color: "#3b82f6", title: "OBJETIVOS", desc: "Qu\xE9 vas a aprender." },
1153
+ { id: "s3", color: "#8b5cf6", title: "EVALUACI\xD3N", desc: "Pon a prueba lo aprendido." }
1154
+ ];
1155
+ ESQUEMAS_ARENAS["course-menu"] = menuDeCurso(
1156
+ "Panel diagonal a la izquierda y tarjetas a la derecha.",
1157
+ {
1158
+ panelColor: color("Color del panel diagonal de la izquierda."),
1159
+ accentColor: color("Color de acento: t\xEDtulo, rayitas, logo.")
1160
+ },
1161
+ { title: "BIENVENIDO", intro: "Elige una secci\xF3n para empezar.", accentColor: "#14b8a6", items: TARJETAS_EJ }
1162
+ );
1163
+ ESQUEMAS_ARENAS["course-menu-kids"] = menuDeCurso(
1164
+ "Versi\xF3n infantil: cielo, personajes a los lados y tarjetas redondas.",
1165
+ {
1166
+ bgImage: imagen("Fondo. Si no hay, se dibuja el cielo con `bgColor`."),
1167
+ bgColor: color("Color del cielo."),
1168
+ leftImage: imagen("Personaje de la izquierda."),
1169
+ rightImage: imagen("Personaje de la derecha."),
1170
+ accentColor: color("Color de acento.")
1171
+ },
1172
+ { title: "\xA1BIENVENIDO!", subtitle: "Vamos a aprender", bgColor: "#7dd3fc", items: TARJETAS_EJ }
1173
+ );
1174
+ ESQUEMAS_ARENAS["course-menu-neon"] = menuDeCurso(
1175
+ "Est\xE9tica teen oscura, con tarjetas que respiran.",
1176
+ {
1177
+ bgImage: imagen("Fondo. Si no hay, se usa el oscuro con `bgColor`."),
1178
+ bgColor: color("Color del fondo oscuro."),
1179
+ sideImage: imagen("Imagen de la derecha (estudiantes, personaje\u2026).")
1180
+ },
1181
+ { title: "\xA1BIENVENIDO!", subtitle: "Empieza aqu\xED", bgColor: "#0b1220", items: TARJETAS_EJ }
1182
+ );
1183
+ ESQUEMAS_ARENAS["course-menu-sparta"] = menuDeCurso(
1184
+ "Mitolog\xEDa griega: medallones de bronce sobre piedra.",
1185
+ {
1186
+ bgImage: imagen("Fondo. Si no hay, se dibuja el \xE9pico con `bgColor`."),
1187
+ bgColor: color("Color del fondo."),
1188
+ sideImage: imagen("Guerrero o personaje de la derecha."),
1189
+ motto: texto("Lema grabado bajo el t\xEDtulo."),
1190
+ accentColor: color("Dorado de acento de los medallones.")
1191
+ },
1192
+ { title: "BIENVENIDO", motto: "Con esfuerzo, todo", accentColor: "#c9a24b", items: TARJETAS_EJ }
1193
+ );
1194
+ ESQUEMAS_ARENAS["duck-shooting"] = {
1195
+ donde: "acfg",
1196
+ cfgSchema: {
1197
+ campos: {
1198
+ questions: lista("Las preguntas de la caseta de tiro.", objeto("Una pregunta.", {
1199
+ q: texto("El enunciado.", { obligatorio: true }),
1200
+ options: lista("Los patos entre los que disparar.", texto("Una respuesta."), { min: 2, max: 4, obligatorio: true }),
1201
+ answer: indice("Cu\xE1l de `options` es la correcta (0 = la primera).", "options")
1202
+ }), { min: 1, max: 30, obligatorio: true })
1203
+ },
1204
+ nota: "Opciones de TEXTO PLANO, como el Game Show. Cortas: van escritas sobre un pato en movimiento."
1205
+ },
1206
+ ejemplo: {
1207
+ questions: [
1208
+ { q: "A baby cat is called a ___.", options: ["puppy", "kitten", "cub"], answer: 1 },
1209
+ { q: 'The opposite of "big" is ___.', options: ["small", "tall", "wide"], answer: 0 }
1210
+ ]
1211
+ }
1212
+ };
1213
+ ESQUEMAS_ARENAS.jenga = {
1214
+ donde: "acfg",
1215
+ cfgSchema: {
1216
+ campos: {
1217
+ questions: lista(
1218
+ "Las preguntas. Acertar no toca la torre; fallar saca una pieza hasta derrumbarla.",
1219
+ objeto("Una pregunta.", {
1220
+ q: texto("El enunciado.", { obligatorio: true }),
1221
+ options: lista("Las respuestas.", texto("Una respuesta."), { min: 2, max: 4, obligatorio: true }),
1222
+ answer: indice("Cu\xE1l de `options` es la correcta (0 = la primera).", "options")
1223
+ }),
1224
+ { min: 1, max: 30, obligatorio: true }
1225
+ )
1226
+ },
1227
+ nota: "La torre es 3D con derrumbe f\xEDsico: pon preguntas de sobra para que la partida dure, porque la gracia est\xE1 en ver cu\xE1nto aguanta."
1228
+ },
1229
+ ejemplo: {
1230
+ questions: [
1231
+ { q: "She ___ to school every day.", options: ["go", "goes", "going"], answer: 1 },
1232
+ { q: "They ___ playing football now.", options: ["is", "am", "are"], answer: 2 }
1233
+ ]
1234
+ }
1235
+ };
1236
+ ESQUEMAS_ARENAS["we-spelling-jenga"] = {
1237
+ donde: "acfg",
1238
+ cfgSchema: {
1239
+ campos: {
1240
+ words: lista("Las palabras que hay que deletrear en orden.", texto("Una palabra."), { min: 4, max: 60, obligatorio: true }),
1241
+ playerNames: lista("Los nombres de los jugadores.", texto("Nombre."), { min: 2, max: 4 })
1242
+ },
1243
+ nota: "En MAY\xDASCULAS y sin tildes ni espacios: se deletrean letra a letra. De 5 a 8 letras van bien; las largas lucen la escala autom\xE1tica."
1244
+ },
1245
+ ejemplo: {
1246
+ words: ["HOUSE", "WATER", "GREEN", "MUSIC", "FRIEND", "SCHOOL"],
1247
+ playerNames: ["Player 1", "Player 2", "Player 3", "Player 4"]
1248
+ }
1249
+ };
1250
+ ESQUEMAS_ARENAS["cellphone-challenge"] = {
1251
+ donde: "acfg",
1252
+ cfgSchema: {
1253
+ campos: {
1254
+ normalWords: lista("Reto NORMAL (multi-tap): palabras que se teclean.", texto("Una palabra."), { max: 40 }),
1255
+ predictiveWords: lista("Reto PREDICTIVO: palabras que se teclean; son adem\xE1s el diccionario.", texto("Una palabra."), { max: 40 }),
1256
+ decodeWords: lista("Reto DECODE: se ense\xF1an sus N\xDAMEROS y el alumno adivina la palabra.", texto("Una palabra."), { max: 40 }),
1257
+ conversations: lista(
1258
+ "Reto CONVERSATION: chats guiados donde el alumno teclea sus respuestas.",
1259
+ objeto("Una conversaci\xF3n.", {
1260
+ turns: lista("Los turnos, en orden.", objeto("Un turno.", {
1261
+ from: opcion("Qui\xE9n habla: el otro o el alumno.", ["them", "you"], { obligatorio: true }),
1262
+ text: texto("Lo que se dice. Si es del alumno, es lo que tiene que teclear.", { obligatorio: true })
1263
+ }), { min: 2, max: 12, obligatorio: true })
1264
+ }),
1265
+ { max: 10 }
1266
+ )
1267
+ },
1268
+ nota: "Son CUATRO retos independientes: llena el que vayas a usar, no hace falta llenarlos todos. En MAY\xDASCULAS y sin signos: se teclea con el teclado num\xE9rico de un tel\xE9fono viejo."
1269
+ },
1270
+ ejemplo: {
1271
+ normalWords: ["HELLO", "FRIEND", "SCHOOL"],
1272
+ decodeWords: ["WATER", "HOUSE"],
1273
+ conversations: [{
1274
+ turns: [
1275
+ { from: "them", text: "HEY WHAT ARE YOU DOING" },
1276
+ { from: "you", text: "I AM STUDYING ENGLISH" }
1277
+ ]
1278
+ }]
1279
+ }
1280
+ };
1281
+ ESQUEMAS_BOOSTERS.revealer = {
1282
+ donde: "wcfg.ext",
1283
+ cfgSchema: {
1284
+ campos: {
1285
+ label: texto('El s\xEDmbolo grande de la tapa: "?", "1", una letra\u2026', { max: 4 }),
1286
+ hint: texto('Pista peque\xF1a bajo el s\xEDmbolo: "Tap to reveal".', { max: 40 }),
1287
+ theme: opcion("La car\xE1tula de la tapa.", ["aurora", "sunset", "ocean", "candy", "galaxy", "gold"])
1288
+ },
1289
+ nota: "Se coloca ENCIMA de una respuesta escrita en el slide y, al hacer clic, explota en confeti y revela lo de atr\xE1s. La tapa NO guarda la respuesta: la tapa solo tapa."
1290
+ },
1291
+ ejemplo: { label: "?", hint: "Tap to reveal", theme: "aurora" }
1292
+ };
1293
+ Object.assign(ESQUEMAS, ESQUEMAS_BOOSTERS, ESQUEMAS_ARENAS);
1294
+
1295
+ // ../src/registro/validar.ts
1296
+ var esObjeto = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1297
+ var ES_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
1298
+ var ES_ARCHIVO = /^(?:https?:\/\/|data:)/i;
1299
+ function mirarCampo(campo, valor, ruta, hermanos, raiz2, fallos) {
1300
+ const mal = (dice) => {
1301
+ fallos.push({ ruta, dice });
1302
+ };
1303
+ switch (campo.tipo) {
1304
+ case "texto":
1305
+ case "html": {
1306
+ if (typeof valor !== "string") return mal(`tiene que ser texto, y lleg\xF3 ${tipoDe(valor)}`);
1307
+ if (campo.tipo === "texto" && campo.max && valor.length > campo.max) mal(`se pasa de largo (${valor.length} de ${campo.max} caracteres como mucho)`);
1308
+ return;
1309
+ }
1310
+ case "numero": {
1311
+ if (typeof valor !== "number" || Number.isNaN(valor)) return mal(`tiene que ser un n\xFAmero, y lleg\xF3 ${tipoDe(valor)}`);
1312
+ if (campo.entero && !Number.isInteger(valor)) mal(`tiene que ser un n\xFAmero entero, y lleg\xF3 ${valor}`);
1313
+ if (campo.min !== void 0 && valor < campo.min) mal(`se queda corto (${valor}; el m\xEDnimo es ${campo.min})`);
1314
+ if (campo.max !== void 0 && valor > campo.max) mal(`se pasa (${valor}; el m\xE1ximo es ${campo.max})`);
1315
+ return;
1316
+ }
1317
+ case "booleano":
1318
+ if (typeof valor !== "boolean") mal(`tiene que ser true o false, y lleg\xF3 ${tipoDe(valor)}`);
1319
+ return;
1320
+ case "opcion":
1321
+ if (typeof valor !== "string" || !campo.valores.includes(valor))
1322
+ mal(`solo admite ${campo.valores.map((v) => `"${v}"`).join(", ")} \u2014 y lleg\xF3 ${JSON.stringify(valor)}`);
1323
+ return;
1324
+ case "color":
1325
+ if (typeof valor !== "string" || !ES_COLOR.test(valor)) mal(`tiene que ser un color hex tipo "#0f172a", y lleg\xF3 ${JSON.stringify(valor)}`);
1326
+ return;
1327
+ case "archivo":
1328
+ if (typeof valor !== "string" || !ES_ARCHIVO.test(valor))
1329
+ mal(`tiene que ser una URL de ${campo.medio} (https:// o data:), y lleg\xF3 ${JSON.stringify(valor)}`);
1330
+ else if (valor.startsWith("blob:"))
1331
+ mal("es una URL `blob:`, y esas no sobreviven al guardado \u2014 sube el archivo y usa la URL que devuelve");
1332
+ return;
1333
+ case "lista": {
1334
+ if (!Array.isArray(valor)) return mal(`tiene que ser una lista, y lleg\xF3 ${tipoDe(valor)}`);
1335
+ if (campo.min !== void 0 && valor.length < campo.min) mal(`necesita al menos ${campo.min} (llegaron ${valor.length})`);
1336
+ if (campo.max !== void 0 && valor.length > campo.max) mal(`admite ${campo.max} como mucho (llegaron ${valor.length})`);
1337
+ valor.forEach((v, i) => mirarCampo(campo.de, v, `${ruta}[${i}]`, hermanos, raiz2, fallos));
1338
+ return;
1339
+ }
1340
+ case "objeto": {
1341
+ if (!esObjeto(valor)) return mal(`tiene que ser un objeto, y lleg\xF3 ${tipoDe(valor)}`);
1342
+ mirarCampos(campo.campos, valor, ruta, raiz2, fallos);
1343
+ return;
1344
+ }
1345
+ case "indice": {
1346
+ if (!Number.isInteger(valor)) return mal(`tiene que ser un n\xFAmero entero, y lleg\xF3 ${tipoDe(valor)}`);
1347
+ const desdeRaiz = campo.dentroDe.startsWith("/");
1348
+ const nombre = desdeRaiz ? campo.dentroDe.slice(1) : campo.dentroDe;
1349
+ const donde = desdeRaiz ? raiz2 : hermanos;
1350
+ const listaApuntada = donde[nombre];
1351
+ if (!Array.isArray(listaApuntada)) {
1352
+ return;
1353
+ }
1354
+ const n2 = valor;
1355
+ if (n2 < 0 || n2 >= listaApuntada.length)
1356
+ mal(`apunta a la posici\xF3n ${n2} de \`${nombre}\`, que solo tiene ${listaApuntada.length} (las posiciones v\xE1lidas van de 0 a ${listaApuntada.length - 1})`);
1357
+ return;
1358
+ }
1359
+ }
1360
+ }
1361
+ function mirarCampos(campos, valor, ruta, raiz2, fallos) {
1362
+ const bajo = (k) => ruta ? `${ruta}.${k}` : k;
1363
+ for (const [nombre, campo] of Object.entries(campos)) {
1364
+ const v = valor[nombre];
1365
+ if (v === void 0 || v === null) {
1366
+ if (campo.obligatorio) fallos.push({ ruta: bajo(nombre), dice: `falta, y hace falta: ${campo.que}` });
1367
+ continue;
1368
+ }
1369
+ mirarCampo(campo, v, bajo(nombre), valor, raiz2, fallos);
1370
+ }
1371
+ for (const k of Object.keys(valor)) {
1372
+ if (k in campos) continue;
1373
+ const parecido = Object.keys(campos).find((c) => c.toLowerCase().includes(k.toLowerCase()) || k.toLowerCase().includes(c.toLowerCase()));
1374
+ fallos.push({
1375
+ ruta: bajo(k),
1376
+ dice: `este campo no existe aqu\xED${parecido ? ` \u2014 \xBFquer\xEDas decir \`${parecido}\`?` : "."} Los que hay son: ${Object.keys(campos).join(", ")}`
1377
+ });
1378
+ }
1379
+ }
1380
+ function validarContra(schema, cfg) {
1381
+ if (!esObjeto(cfg)) return [{ ruta: "(ra\xEDz)", dice: `la config tiene que ser un objeto, y lleg\xF3 ${tipoDe(cfg)}` }];
1382
+ const fallos = [];
1383
+ mirarCampos(schema.campos, cfg, "", cfg, fallos);
1384
+ return fallos;
1385
+ }
1386
+ function validarCfgDe(tipo, cfg) {
1387
+ const ficha = ESQUEMAS[tipo];
1388
+ if (!ficha) {
1389
+ const cerca = Object.keys(ESQUEMAS).filter((t) => t.includes(tipo) || tipo.includes(t));
1390
+ return { ok: false, fallos: [{ ruta: "(tipo)", dice: `no existe ning\xFAn booster ni arena "${tipo}"${cerca.length ? ` \u2014 \xBFera "${cerca[0]}"?` : ""}` }] };
1391
+ }
1392
+ if (!ficha.cfgSchema) {
1393
+ return { ok: false, fallos: [{ ruta: "(tipo)", dice: `"${tipo}" no tiene nada que configurar` }] };
1394
+ }
1395
+ const fallos = validarContra(ficha.cfgSchema, cfg);
1396
+ return { ok: fallos.length === 0, fallos };
1397
+ }
1398
+ function tipoDe(v) {
1399
+ if (v === null) return "null";
1400
+ if (Array.isArray(v)) return "una lista";
1401
+ switch (typeof v) {
1402
+ case "string":
1403
+ return "texto";
1404
+ case "number":
1405
+ return "un n\xFAmero";
1406
+ case "boolean":
1407
+ return "true/false";
1408
+ case "object":
1409
+ return "un objeto";
1410
+ default:
1411
+ return typeof v;
1412
+ }
1413
+ }
1414
+ function fallosEnTexto(fallos) {
1415
+ return fallos.map((f) => `\xB7 ${f.ruta}: ${f.dice}`).join("\n");
1416
+ }
1417
+
1418
+ // ../src/registro/describir.ts
1419
+ function linea(nombre, campo, sangria) {
1420
+ const marca = campo.obligatorio ? ", obligatorio" : "";
1421
+ const salida = [];
1422
+ switch (campo.tipo) {
1423
+ case "lista": {
1424
+ const cuantos = campo.min !== void 0 || campo.max !== void 0 ? ` de ${campo.min ?? 1} a ${campo.max ?? "\u2026"}` : "";
1425
+ salida.push(`${sangria}${nombre} (lista${cuantos}${marca}) \u2014 ${campo.que}`);
1426
+ if (campo.de.tipo === "objeto") {
1427
+ for (const [k, c] of Object.entries(campo.de.campos)) salida.push(...linea(k, c, `${sangria} `));
1428
+ } else if (campo.de.tipo === "lista") {
1429
+ salida.push(...linea("cada una", campo.de, `${sangria} `));
1430
+ }
1431
+ return salida;
1432
+ }
1433
+ case "objeto":
1434
+ salida.push(`${sangria}${nombre} (objeto${marca}) \u2014 ${campo.que}`);
1435
+ for (const [k, c] of Object.entries(campo.campos)) salida.push(...linea(k, c, `${sangria} `));
1436
+ return salida;
1437
+ case "opcion":
1438
+ return [`${sangria}${nombre} (uno de: ${campo.valores.join(" | ")}${marca}) \u2014 ${campo.que}`];
1439
+ case "numero": {
1440
+ const rango = campo.min !== void 0 && campo.max !== void 0 ? ` ${campo.min}\u2013${campo.max}` : "";
1441
+ return [`${sangria}${nombre} (n\xFAmero${rango}${marca}) \u2014 ${campo.que}`];
1442
+ }
1443
+ case "indice":
1444
+ return [`${sangria}${nombre} (posici\xF3n dentro de \`${campo.dentroDe}\`, empezando en 0${marca}) \u2014 ${campo.que}`];
1445
+ case "archivo":
1446
+ return [`${sangria}${nombre} (URL de ${campo.medio}${marca}) \u2014 ${campo.que}`];
1447
+ case "booleano":
1448
+ return [`${sangria}${nombre} (true/false${marca}) \u2014 ${campo.que}`];
1449
+ case "color":
1450
+ return [`${sangria}${nombre} (color hex, "#0f172a"${marca}) \u2014 ${campo.que}`];
1451
+ default:
1452
+ return [`${sangria}${nombre} (texto${marca}) \u2014 ${campo.que}`];
1453
+ }
1454
+ }
1455
+ function cuerpo(schema) {
1456
+ const filas = [];
1457
+ for (const [k, c] of Object.entries(schema.campos)) filas.push(...linea(k, c, " "));
1458
+ return filas.join("\n");
1459
+ }
1460
+ function describirTipo(tipo) {
1461
+ const ficha = ESQUEMAS[tipo];
1462
+ if (!ficha) {
1463
+ const cerca = Object.keys(ESQUEMAS).filter((t) => t.includes(tipo) || tipo.includes(t)).slice(0, 3);
1464
+ return `No existe ning\xFAn booster ni arena "${tipo}".${cerca.length ? ` \xBFEra ${cerca.map((c) => `"${c}"`).join(" o ")}?` : ""}`;
1465
+ }
1466
+ if (!ficha.cfgSchema) return `"${tipo}" no tiene nada que configurar: se pone y ya funciona.`;
1467
+ const partes = [
1468
+ `Config de "${tipo}" (se guarda en ${ficha.donde}):`,
1469
+ cuerpo(ficha.cfgSchema)
1470
+ ];
1471
+ if (ficha.cfgSchema.nota) {
1472
+ const n2 = ficha.cfgSchema.nota;
1473
+ partes.push(n2.startsWith("\u26A0") ? `
1474
+ ${n2}` : `
1475
+ \u26A0\uFE0F ${n2}`);
1476
+ }
1477
+ if (ficha.ejemplo) partes.push(`
1478
+ Ejemplo que funciona:
1479
+ ${JSON.stringify(ficha.ejemplo, null, 2)}`);
1480
+ return partes.join("\n");
1481
+ }
1482
+
1483
+ // ../src/canvas/robot/ejecutor.ts
1484
+ function porQueNo(r, familia) {
1485
+ if (r.motivo === "premium") {
1486
+ return `\xAB${r.etiqueta}\xBB es del plan Pro y esta cuenta no lo tiene, as\xED que no lo puedo poner. D\xCDSELO con \`responder\`, sin rodeos: que se activa desde boosterarena.com. Y ofr\xE9cele seguir con ${familia === "arena" ? "otra arena de las incluidas" : "otro booster de los incluidos"}, que quedarse sin nada es peor.`;
1487
+ }
1488
+ if (r.motivo === "no-listo") {
1489
+ return `\xAB${r.etiqueta}\xBB todav\xEDa no est\xE1 construido: sale en el cat\xE1logo pero no se puede usar. Elige otro y dilo con \`responder\`.`;
1490
+ }
1491
+ return `No existe ning${familia === "arena" ? "una arena" : "\xFAn booster"} "${r.tipo}". Mira la lista que te di y usa uno de esos.`;
1492
+ }
1493
+ var n = 0;
1494
+ var nid = (p) => `rb${p}-${Date.now().toString(36)}-${(n++).toString(36)}${Math.random().toString(36).slice(2, 5)}`;
1495
+ var num3 = (v, def) => typeof v === "number" && Number.isFinite(v) ? v : def;
1496
+ var txt = (v) => typeof v === "string" ? v : "";
1497
+ var pct = (v, def) => Math.max(-40, Math.min(140, num3(v, def)));
1498
+ function nuevoBorrador(doc, escena, slide, pedido = "") {
1499
+ return {
1500
+ doc: structuredClone(doc),
1501
+ escena,
1502
+ slide,
1503
+ pedido,
1504
+ alias: /* @__PURE__ */ new Map(),
1505
+ aliasCapa: /* @__PURE__ */ new Map(),
1506
+ creados: [],
1507
+ tocados: [],
1508
+ bitacora: []
1509
+ };
1510
+ }
1511
+ var PALABRAS_DE_BORRAR = /(borr|quit|elimin|saca|remov|delete|suprim|deshaz)/i;
1512
+ var QUITAR_UN_RASGO = /(?:quit|borr|elimin|sac|remov|limpi)\w*\s+(?:(?:el|la|los|las|su|sus|ese|esa|este|esta)\s+)*(?:fondo|fondos|color|colores|borde|bordes|sombra|sombras|relleno|marco|contorno|opacidad|efecto|efectos|movimiento|animacion)/gi;
1513
+ function pidi\u00F3Borrar(pedido) {
1514
+ const plano = pedido.normalize("NFD").replace(/[̀-ͯ]/g, "");
1515
+ return PALABRAS_DE_BORRAR.test(plano.replace(QUITAR_UN_RASGO, " "));
1516
+ }
1517
+ function colorDelEfecto(efecto, colorEfecto, colorTexto) {
1518
+ if (!efecto || efecto === "none") return {};
1519
+ if (colorEfecto) return { fxColor: colorEfecto };
1520
+ if (efecto === "neon" && colorTexto) return { fxColor: colorTexto };
1521
+ return {};
1522
+ }
1523
+ var escenaDe = (b) => b.doc[Math.min(b.escena, b.doc.length - 1)];
1524
+ var slideDe = (b) => {
1525
+ const sc = escenaDe(b);
1526
+ return sc.slides[Math.min(b.slide, sc.slides.length - 1)];
1527
+ };
1528
+ function listaDestino(b, capa) {
1529
+ const s = slideDe(b);
1530
+ const ref = txt(capa).trim();
1531
+ if (!ref) return { els: s.base };
1532
+ const capaObj = capaPorNombre(b, ref) ?? crearCapa(b, ref);
1533
+ b.capaDeTrabajo = capaObj.id;
1534
+ return { els: capaObj.els, dentroDe: capaObj.name };
1535
+ }
1536
+ function capaPorNombre(b, ref) {
1537
+ const s = slideDe(b);
1538
+ const id = b.aliasCapa.get(ref);
1539
+ return s.layers.find((l) => l.id === id) ?? s.layers.find((l) => l.name.toLowerCase() === ref.toLowerCase()) ?? s.layers.find((l) => l.name.toLowerCase().includes(ref.toLowerCase()));
1540
+ }
1541
+ function crearCapa(b, ref) {
1542
+ const s = slideDe(b);
1543
+ const capa = { id: nid("capa"), name: ref || `Capa ${s.layers.length + 1}`, els: [] };
1544
+ s.layers.push(capa);
1545
+ if (ref) b.aliasCapa.set(ref, capa.id);
1546
+ b.capaDeTrabajo = capa.id;
1547
+ b.bitacora.push(`\u{1F4D1} Capa \xAB${capa.name}\xBB`);
1548
+ return capa;
1549
+ }
1550
+ function* todasLasListas(b) {
1551
+ for (const sc of b.doc) for (const sl of sc.slides) {
1552
+ yield sl.base;
1553
+ for (const l of sl.layers) yield l.els;
1554
+ }
1555
+ }
1556
+ function buscarEl(b, ref) {
1557
+ const id = b.alias.get(ref.trim()) ?? ref.trim();
1558
+ const s = slideDe(b);
1559
+ const aqu\u00ED = s.base.find((e) => e.id === id) ?? s.layers.flatMap((l) => l.els).find((e) => e.id === id);
1560
+ if (aqu\u00ED) return aqu\u00ED;
1561
+ for (const lista2 of todasLasListas(b)) {
1562
+ const e = lista2.find((x) => x.id === id);
1563
+ if (e) return e;
1564
+ }
1565
+ return null;
1566
+ }
1567
+ function buscarCapa(b, id) {
1568
+ for (const sc of b.doc) for (const sl of sc.slides) {
1569
+ const l = sl.layers.find((x) => x.id === id);
1570
+ if (l) return l;
1571
+ }
1572
+ return void 0;
1573
+ }
1574
+ function plantar(b, el, ref, capa) {
1575
+ const { els, dentroDe } = listaDestino(b, capa);
1576
+ els.push(el);
1577
+ const r = txt(ref).trim();
1578
+ if (r) b.alias.set(r, el.id);
1579
+ b.creados.push(el.id);
1580
+ b.tocados.push(el.id);
1581
+ return { el, dentroDe };
1582
+ }
1583
+ function altoTexto(texto2, w, fontSize) {
1584
+ const anchoPx = w / 100 * 1920;
1585
+ const porRenglon = Math.max(1, Math.floor(anchoPx / (fontSize * 0.55)));
1586
+ const renglones = texto2.split("\n").reduce((t, linea2) => t + Math.max(1, Math.ceil(linea2.length / porRenglon)), 0);
1587
+ return Math.max(4, +(renglones * fontSize * 1.3 / 1080 * 100).toFixed(1));
1588
+ }
1589
+ var ALTO_MAX_DIBUJO = 58;
1590
+ function cajaImagen(w, aspect) {
1591
+ const alto = w * (16 / 9) / (aspect || 1);
1592
+ if (alto <= ALTO_MAX_DIBUJO) return { w, h: +alto.toFixed(1) };
1593
+ const k = ALTO_MAX_DIBUJO / alto;
1594
+ return { w: +(w * k).toFixed(1), h: ALTO_MAX_DIBUJO };
1595
+ }
1596
+ function encajar(el) {
1597
+ el.w = Math.max(2, Math.min(100, el.w));
1598
+ el.h = Math.max(2, Math.min(100, el.h));
1599
+ el.x = +Math.max(0, Math.min(100 - el.w, el.x)).toFixed(2);
1600
+ el.y = +Math.max(0, Math.min(100 - el.h, el.y)).toFixed(2);
1601
+ return el;
1602
+ }
1603
+ function armarEvento(a, el) {
1604
+ switch (txt(a.evento)) {
1605
+ case "hover":
1606
+ return { type: "hover" };
1607
+ case "slideStart":
1608
+ return { type: "slideStart" };
1609
+ case "layerShown":
1610
+ return { type: "layerShown" };
1611
+ case "time":
1612
+ return { type: "time", seconds: Math.max(0, num3(a.segundos, 3)) };
1613
+ case "clickStep":
1614
+ return { type: "clickStep", step: Math.max(1, Math.round(num3(a.paso, 1))) };
1615
+ case "trayectoriaTerminada": {
1616
+ const nombre = txt(a.trayectoria).trim().toLowerCase();
1617
+ const suyas = el.motions ?? [];
1618
+ const cam = (nombre ? suyas.find((m) => m.name.toLowerCase().includes(nombre)) : void 0) ?? suyas[0];
1619
+ return cam ? { type: "motionEnd", motionId: cam.id } : { type: "click" };
1620
+ }
1621
+ default:
1622
+ return { type: "click" };
1623
+ }
1624
+ }
1625
+ function armarAccion(b, a, el) {
1626
+ const s = slideDe(b);
1627
+ switch (txt(a.accion)) {
1628
+ case "cerrarCapa":
1629
+ return { accion: { type: "closeLayer" } };
1630
+ case "reiniciarSecuencia":
1631
+ return { accion: { type: "resetSequence" } };
1632
+ case "abrirCapa": {
1633
+ const ref = txt(a.capa).trim();
1634
+ const capa = capaPorNombre(b, ref) ?? crearCapa(b, ref);
1635
+ return { accion: { type: "showLayer", layerId: capa.id } };
1636
+ }
1637
+ case "irAEscena": {
1638
+ const q = txt(a.escena).trim().toLowerCase();
1639
+ const sc = b.doc.find((x) => x.name.toLowerCase() === q) ?? b.doc.find((x) => x.name.toLowerCase().includes(q));
1640
+ if (!sc) return { accion: null, fallo: `No hay ninguna escena llamada "${txt(a.escena)}". Las que hay: ${b.doc.map((x) => x.name).join(", ")}.` };
1641
+ return { accion: { type: "goScene", sceneId: sc.id } };
1642
+ }
1643
+ case "irASlide": {
1644
+ const i = Math.round(num3(a.slide, 0)) - 1;
1645
+ const destino = escenaDe(b).slides[i];
1646
+ if (!destino) return { accion: null, fallo: `Esta escena tiene ${escenaDe(b).slides.length} slides; no existe el n\xFAmero ${num3(a.slide, 0)}.` };
1647
+ return { accion: { type: "goSlide", slideId: destino.id } };
1648
+ }
1649
+ case "leerEnVoz": {
1650
+ const t = txt(a.texto).trim();
1651
+ if (!t) return { accion: null, fallo: "Para leer en voz alta hace falta el texto." };
1652
+ return { accion: { type: "speak", text: t, lang: txt(a.idioma) || "en-US" } };
1653
+ }
1654
+ case "abrirEnlace": {
1655
+ const u = txt(a.url).trim();
1656
+ if (!/^https?:\/\//i.test(u)) return { accion: null, fallo: "El enlace tiene que empezar por https://." };
1657
+ return { accion: { type: "openUrl", url: u } };
1658
+ }
1659
+ case "moverPorTrayectoria": {
1660
+ const d = txt(a.destino).trim();
1661
+ const quien = d ? buscarEl(b, d) : null;
1662
+ if (d && !quien) return { accion: null, fallo: `No encuentro el objeto "${d}" para moverlo.` };
1663
+ const movido = quien ?? buscarEl(b, txt(a.objetivo));
1664
+ if (!movido) return { accion: null, fallo: "No s\xE9 qu\xE9 objeto tiene que moverse." };
1665
+ const nombre = txt(a.trayectoria).trim().toLowerCase();
1666
+ const suyas = movido.motions ?? [];
1667
+ const cam = (nombre ? suyas.find((m) => m.name.toLowerCase().includes(nombre)) : void 0) ?? suyas[0];
1668
+ if (cam) return { accion: { type: "playMotion", targetId: movido.id, motionId: cam.id } };
1669
+ const nuevo = { id: nid("mo"), name: txt(a.trayectoria) || "Trayectoria 1", path: [], duration: 2e3 };
1670
+ movido.motions = [...suyas, nuevo];
1671
+ b.tocados.push(movido.id);
1672
+ if (!b.pedirDibujo) b.pedirDibujo = { elId: movido.id, motionId: nuevo.id };
1673
+ b.bitacora.push(`\u270F\uFE0F ${describirEl(movido)} necesita su camino: te dejo listo para dibujarlo`);
1674
+ return { accion: { type: "playMotion", targetId: movido.id, motionId: nuevo.id } };
1675
+ }
1676
+ case "sonar": {
1677
+ const d = txt(a.destino).trim();
1678
+ const audio2 = d ? buscarEl(b, d) : el.kind === "audio" ? el : null;
1679
+ if (audio2 && audio2.kind === "audio" && audio2.id === el.id && txt(a.evento) === "click") {
1680
+ return {
1681
+ accion: null,
1682
+ fallo: "No hace falta: un audio ya suena solo al tocarlo. Para que suene AL ABRIRSE LA CAPA, programa ese mismo audio con evento layerShown (accion sonar, destino \xE9l mismo). Para que suene al tocar OTRO objeto, el objetivo es ese objeto y el destino, el audio."
1683
+ };
1684
+ }
1685
+ if (!audio2) {
1686
+ const hay = [...s.base, ...s.layers.flatMap((l) => l.els)].filter((e) => e.kind === "audio");
1687
+ return {
1688
+ accion: null,
1689
+ fallo: hay.length ? `Dime cu\xE1l audio: ${hay.map((e) => `${e.id} ("${e.name ?? "sin nombre"}")`).join(", ")}.` : "Este slide no tiene ning\xFAn audio puesto. El maestro tiene que subirlo primero con el bot\xF3n Audio."
1690
+ };
1691
+ }
1692
+ if (audio2.kind !== "audio") return { accion: null, fallo: `"${d}" no es un audio.` };
1693
+ return { accion: { type: "playSound", targetId: audio2.id } };
1694
+ }
1695
+ case "ocultar": {
1696
+ const d = txt(a.destino).trim();
1697
+ const otro = d ? buscarEl(b, d) : null;
1698
+ if (d && !otro) return { accion: null, fallo: `No encuentro el objeto "${d}" para ocultarlo.` };
1699
+ return { accion: { type: "hideEl", targetId: otro?.id ?? "" } };
1700
+ }
1701
+ default:
1702
+ return { accion: null };
1703
+ }
1704
+ }
1705
+ function ejecutar(b, nombre, args, fab) {
1706
+ switch (nombre) {
1707
+ // ── Poner cosas ──────────────────────────────────────────────────────────
1708
+ case "poner_texto": {
1709
+ const texto2 = txt(args.texto) || "Texto";
1710
+ const w = pct(args.w, 40);
1711
+ const fontSize = Math.max(10, Math.min(220, num3(args.tamano, 44)));
1712
+ const el = encajar({
1713
+ id: nid("txt"),
1714
+ kind: "text",
1715
+ x: pct(args.x, 30),
1716
+ y: pct(args.y, 40),
1717
+ w,
1718
+ h: altoTexto(texto2, w, fontSize),
1719
+ text: texto2,
1720
+ fontSize,
1721
+ color: txt(args.color) || "#111827",
1722
+ align: ["left", "center", "right", "justify"].find((v) => v === args.align) ?? "center",
1723
+ ...txt(args.efecto) && args.efecto !== "none" ? { effect: txt(args.efecto) } : {},
1724
+ ...colorDelEfecto(txt(args.efecto), txt(args.color_efecto), txt(args.color)),
1725
+ // `motion` se escribe SIEMPRE, aunque sea 'none': es la señal de que el elemento está
1726
+ // en el formato nuevo de dos categorías (ver `normalizeFx` en shared/textEffects).
1727
+ motion: txt(args.movimiento) || "none",
1728
+ ...args.mayusculas === true ? { caps: true } : {},
1729
+ ...num3(args.peso, 0) >= 400 ? { weight: Math.round(num3(args.peso, 400)) } : {}
1730
+ });
1731
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1732
+ b.bitacora.push(`\u{1F163} \xAB${texto2.length > 40 ? `${texto2.slice(0, 40)}\u2026` : texto2}\xBB${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1733
+ return `Texto puesto (id ${el.id}).`;
1734
+ }
1735
+ case "poner_dibujo": {
1736
+ const busca = txt(args.busca);
1737
+ const hallado = buscarDibujo(busca);
1738
+ if (!hallado) {
1739
+ return `La biblioteca no tiene nada parecido a "${busca}". Prueba con otra palabra m\xE1s com\xFAn y en singular, o resu\xE9lvelo con una figura o un emoji.`;
1740
+ }
1741
+ const pedido = pct(args.w, 20);
1742
+ const caja = cajaImagen(pedido, hallado.aspect);
1743
+ const el = encajar({
1744
+ id: nid("img"),
1745
+ kind: "image",
1746
+ x: pct(args.x, 40),
1747
+ y: pct(args.y, 35),
1748
+ w: caja.w,
1749
+ h: caja.h,
1750
+ src: hallado.src,
1751
+ aspect: hallado.aspect
1752
+ });
1753
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1754
+ b.bitacora.push(`\u{1F5BC}\uFE0F ${hallado.nombre}${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1755
+ const encogido = caja.w < pedido - 0.5 ? ` Es un dibujo alto, as\xED que entr\xF3 a ${caja.w}% de ancho y ${caja.h}% de alto en vez de ${pedido}%.` : "";
1756
+ const conformado = hallado.exacto ? "" : ` \u26A0\uFE0F OJO: no hay nada que sea "${busca}" en la biblioteca. Lo m\xE1s cercano fue "${hallado.nombre}"${hallado.pedidas > 1 ? ` (encajaron ${hallado.encajadas} de ${hallado.pedidas} palabras)` : ""}. Si no te sirve, prueba otra palabra m\xE1s com\xFAn, o dile al maestro con \`responder\` que ese dibujo no lo tenemos y que lo suba \xE9l.`;
1757
+ return `Puesto el dibujo "${hallado.nombre}" (id ${el.id}).${encogido}${conformado}`;
1758
+ }
1759
+ // ── Archivos que trae quien llama, no la biblioteca ──────────────────────
1760
+ // `poner_dibujo` busca en NUESTRO catálogo. Estas dos ponen un archivo que ya está subido:
1761
+ // es la puerta por la que entra lo que genera una fábrica de fuera (Flux para las imágenes,
1762
+ // un TTS para la voz) sin que Booster tenga que generarlo. Un MP3 de un TTS y un MP3 que
1763
+ // subió un maestro son el MISMO elemento con una URL de R2: el documento no los distingue.
1764
+ case "poner_imagen": {
1765
+ const url = txt(args.url).trim();
1766
+ if (!/^https?:\/\//i.test(url)) {
1767
+ return "La `url` tiene que ser una direcci\xF3n https:// de un archivo ya SUBIDO. Si el archivo est\xE1 en el disco, s\xFAbelo antes con `subir_imagen` y usa la URL que te devuelva. Una ruta local no vale: el maestro abre el proyecto en otra computadora.";
1768
+ }
1769
+ const pedido = pct(args.w, 25);
1770
+ const proporcion = num3(args.proporcion, 1) || 1;
1771
+ const caja = cajaImagen(pedido, proporcion);
1772
+ const el = encajar({
1773
+ id: nid("img"),
1774
+ kind: "image",
1775
+ x: pct(args.x, 38),
1776
+ y: pct(args.y, 30),
1777
+ w: caja.w,
1778
+ h: caja.h,
1779
+ src: url,
1780
+ aspect: proporcion
1781
+ });
1782
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1783
+ b.bitacora.push(`\u{1F5BC}\uFE0F Imagen subida${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1784
+ return `Puesta la imagen (id ${el.id}).`;
1785
+ }
1786
+ case "poner_audio": {
1787
+ const url = txt(args.url).trim();
1788
+ if (!/^https?:\/\//i.test(url)) {
1789
+ return "La `url` tiene que ser una direcci\xF3n https:// de un audio ya SUBIDO. S\xFAbelo antes con `subir_audio` y usa la URL que te devuelva.";
1790
+ }
1791
+ const el = {
1792
+ id: nid("audio"),
1793
+ kind: "audio",
1794
+ x: 102,
1795
+ y: 4 + b.creados.length % 8 * 8,
1796
+ w: 16,
1797
+ h: 7,
1798
+ src: url,
1799
+ name: txt(args.nombre) || "Audio"
1800
+ };
1801
+ plantar(b, el, args.ref, void 0);
1802
+ b.bitacora.push(`\u{1F50A} Audio \xAB${el.name}\xBB`);
1803
+ return `Puesto el audio "${el.name}" (id ${el.id}). Est\xE1 FUERA del slide, como un mando: para que suene, progr\xE1malo con \`programar\` (al hacer clic \u2192 reproducir audio) o ponlo al iniciar el slide.`;
1804
+ }
1805
+ case "poner_figura": {
1806
+ const forma = txt(args.forma) || "rect";
1807
+ const def = FORMA_DEF[forma] ?? FORMA_DEF.rect;
1808
+ const w = pct(args.w, def.anch);
1809
+ const el = encajar({
1810
+ id: nid("fig"),
1811
+ kind: "shape",
1812
+ shape: forma,
1813
+ x: pct(args.x, 40),
1814
+ y: pct(args.y, 35),
1815
+ w,
1816
+ h: def.alto === "cuadrado" ? squareH(w) : squareH(w) * def.alto,
1817
+ fill: txt(args.color) || def.color,
1818
+ strokeW: 0,
1819
+ strokeStyle: "solid",
1820
+ ...forma === "rect" ? { radius: 0 } : {},
1821
+ ...forma === "star" ? { spikes: 5 } : {}
1822
+ });
1823
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1824
+ b.bitacora.push(`\u25C7 Figura ${forma}${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1825
+ return `Figura puesta (id ${el.id}).`;
1826
+ }
1827
+ case "poner_boton": {
1828
+ const w = pct(args.w, 26);
1829
+ const el = encajar({
1830
+ id: nid("btn"),
1831
+ kind: "shape",
1832
+ shape: "button",
1833
+ btnShape: "pill",
1834
+ btnIcon: txt(args.icono) || "none",
1835
+ x: pct(args.x, 37),
1836
+ y: pct(args.y, 42),
1837
+ w,
1838
+ h: 11,
1839
+ fill: txt(args.color) || "#6366f1",
1840
+ color: "#ffffff",
1841
+ text: txt(args.texto) || "Bot\xF3n",
1842
+ fontSize: 46
1843
+ });
1844
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1845
+ b.bitacora.push(`\u{1F518} Bot\xF3n \xAB${el.text}\xBB${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1846
+ return `Bot\xF3n puesto (id ${el.id}). Progr\xE1malo con \`programar\` si a\xFAn no lo hiciste.`;
1847
+ }
1848
+ case "poner_emoji": {
1849
+ const w = pct(args.w, 12);
1850
+ const el = encajar({
1851
+ id: nid("emo"),
1852
+ kind: "emoji",
1853
+ x: pct(args.x, 44),
1854
+ y: pct(args.y, 40),
1855
+ w,
1856
+ h: squareH(w),
1857
+ text: txt(args.emoji) || "\u2728"
1858
+ });
1859
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1860
+ b.bitacora.push(`${el.text} Emoji${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1861
+ return `Emoji puesto (id ${el.id}).`;
1862
+ }
1863
+ case "poner_zona_caliente": {
1864
+ const forma = ["ellipse", "rect", "triangle"].find((v) => v === args.forma) ?? "ellipse";
1865
+ const w = pct(args.w, 18);
1866
+ const el = encajar({
1867
+ id: nid("zona"),
1868
+ kind: "shape",
1869
+ shape: forma,
1870
+ hotspot: true,
1871
+ x: pct(args.x, 40),
1872
+ y: pct(args.y, 35),
1873
+ w,
1874
+ h: num3(args.alto, 0) > 0 ? pct(args.alto, 20) : squareH(w),
1875
+ ...forma === "rect" ? { radius: 6 } : {}
1876
+ });
1877
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1878
+ b.bitacora.push(`\u{1F3AF} Zona caliente${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1879
+ return `Zona caliente puesta (id ${el.id}). Es invisible al proyectar: progr\xE1male qu\xE9 hace al tocarla.`;
1880
+ }
1881
+ case "poner_fondo": {
1882
+ const color2 = txt(args.color).trim();
1883
+ const busca = txt(args.busca).trim();
1884
+ if (busca) {
1885
+ const f = buscarFondo(busca);
1886
+ if (!f) return `No hay ning\xFAn fondo parecido a "${busca}". Puedes poner un color liso.`;
1887
+ slideDe(b).background = f.src;
1888
+ b.bitacora.push(`\u{1F3DE}\uFE0F Fondo: ${f.nombre}`);
1889
+ return `Fondo puesto: "${f.nombre}".`;
1890
+ }
1891
+ if (!color2) return "Dime un color o una escena a buscar.";
1892
+ slideDe(b).background = color2;
1893
+ b.bitacora.push(`\u{1F3A8} Fondo ${color2}`);
1894
+ return "Fondo de color puesto.";
1895
+ }
1896
+ case "poner_booster": {
1897
+ const tipo = txt(args.tipo);
1898
+ const hecho = fab.nuevoWidget(tipo);
1899
+ if (!hecho.ok) return porQueNo(hecho, "booster");
1900
+ const base = hecho.el;
1901
+ const w = num3(args.w, 0) > 0 ? pct(args.w, base.w) : base.w;
1902
+ const el = encajar({
1903
+ ...base,
1904
+ id: nid("wdg"),
1905
+ x: pct(args.x, base.x),
1906
+ y: pct(args.y, base.y),
1907
+ w,
1908
+ h: +(base.h * (w / base.w)).toFixed(1)
1909
+ });
1910
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
1911
+ b.bitacora.push(`\u2699\uFE0F Booster ${tipo}${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
1912
+ return `Booster "${tipo}" puesto (id ${el.id}).`;
1913
+ }
1914
+ // ── USAR un booster de los que se ARRASTRAN sobre un objeto ───────────────
1915
+ // Tres boosters no se «ponen»: se USAN encima de algo. El maestro coge la moneda y la
1916
+ // suelta sobre la imagen. Hasta ahora el robot solo sabía dejar la moneda tirada en el
1917
+ // slide, así que a «quítale el fondo a esta imagen» no tenía ninguna forma de obedecer
1918
+ // — y acabó llamando a `borrar`.
1919
+ //
1920
+ // Aquí se hace la mitad que cabe en el documento: crear la moneda y CENTRARLA sobre el
1921
+ // objetivo, exactamente donde la deja el maestro al soltarla. La otra mitad —el bicho
1922
+ // comiendo, la llamada al recortador, el diálogo del Transformador— la remata el editor
1923
+ // con `pedirSoltar`, por el mismo camino de siempre (`maybeDropCamaleon` y compañía):
1924
+ // NO se duplica ni una regla, o el robot y el maestro acabarían haciendo cosas distintas.
1925
+ case "aplicar_booster": {
1926
+ const tipo = txt(args.tipo);
1927
+ const el = buscarEl(b, txt(args.objetivo));
1928
+ if (!el) return `No encuentro ning\xFAn objeto "${txt(args.objetivo)}".`;
1929
+ if (b.pedirSoltar) {
1930
+ return "Solo puedo aplicar UNA de estas monedas por tanda (cada una lanza su propia animaci\xF3n y hay que verlas de una en una). Dile al maestro que te pida la siguiente.";
1931
+ }
1932
+ if (!slideDe(b).base.some((x) => x.id === el.id)) {
1933
+ return `${describirEl(el)} no est\xE1 suelto en el slide (est\xE1 dentro de una capa), y estas monedas solo se sueltan sobre lo que est\xE1 en el slide. Dile al maestro que lo saque de la capa, o que lo haga \xE9l arrastrando la moneda.`;
1934
+ }
1935
+ if ((tipo === "quitafondo" || tipo === "magichat") && !(el.kind === "image" && el.src)) {
1936
+ return `El booster "${tipo}" solo funciona sobre una IMAGEN, y ${describirEl(el)} no lo es. D\xEDselo al maestro con \`responder\`.`;
1937
+ }
1938
+ if (tipo === "transformar" && (el.kind === "widget" || el.kind === "arena" || el.kind === "audio")) {
1939
+ return "El Transformador va sobre una imagen, una figura o un texto, no sobre otro booster.";
1940
+ }
1941
+ const hecho = fab.nuevoWidget(tipo);
1942
+ if (!hecho.ok) return porQueNo(hecho, "booster");
1943
+ const base = hecho.el;
1944
+ const moneda = encajar({
1945
+ ...base,
1946
+ id: nid("wdg"),
1947
+ x: el.x + el.w / 2 - base.w / 2,
1948
+ y: el.y + el.h / 2 - base.h / 2
1949
+ });
1950
+ slideDe(b).base.push(moneda);
1951
+ b.tocados.push(el.id);
1952
+ b.pedirSoltar = { tipo, moneda, objetivo: el };
1953
+ b.bitacora.push(tipo === "quitafondo" ? `\u{1F98E} Fondo quitado: ${describirEl(el)}` : tipo === "magichat" ? `\u{1F3A9} En silueta hasta que la toquen: ${describirEl(el)}` : `\u{1F500} Transformador puesto sobre ${describirEl(el)}`);
1954
+ return tipo === "transformar" ? `El Transformador ya est\xE1 sobre ${describirEl(el)} y el editor le abrir\xE1 al maestro el di\xE1logo para elegir en qu\xE9 se convierte. Dile que elija ah\xED las im\xE1genes.` : `Hecho: "${tipo}" aplicado sobre ${describirEl(el)} (id ${el.id}).`;
1955
+ }
1956
+ case "poner_arena": {
1957
+ const tipo = txt(args.tipo);
1958
+ const hecha = fab.nuevaArena(tipo);
1959
+ if (!hecha.ok) return porQueNo(hecha, "arena");
1960
+ const s = slideDe(b);
1961
+ if (s.base.some((e) => e.kind !== "arena")) {
1962
+ nuevoSlide(b, `Actividad \xB7 ${tipo}`);
1963
+ b.bitacora.push("\u{1F4C4} Slide nuevo para la arena (una arena ocupa el slide entero)");
1964
+ }
1965
+ const destino = slideDe(b);
1966
+ const el = { ...hecha.el, id: nid("arena") };
1967
+ destino.base = [...destino.base.filter((e) => e.kind !== "arena"), el];
1968
+ const refArena = txt(args.ref).trim();
1969
+ if (refArena) b.alias.set(refArena, el.id);
1970
+ b.creados.push(el.id);
1971
+ b.tocados.push(el.id);
1972
+ b.bitacora.push(`\u{1F3DF}\uFE0F Arena ${tipo}`);
1973
+ return `Arena "${tipo}" puesta (id ${el.id}). Trae preguntas de MUESTRA, que no son las de esta clase: ll\xE1mala con \`describir("${tipo}")\` y luego \`configurar_arena\` para llenarla con el contenido de verdad. Si la dejas as\xED, el maestro se encuentra las de f\xE1brica en clase.`;
1974
+ }
1975
+ // ── Llenar lo que ya está puesto ─────────────────────────────────────────
1976
+ // Poner una arena vacía no adelanta nada: LLENAR es el trabajo. Ver `src/registro/`.
1977
+ case "describir":
1978
+ return describirTipo(txt(args.tipo));
1979
+ case "configurar_arena": {
1980
+ const objetivo = txt(args.objetivo).trim();
1981
+ const el = objetivo ? buscarEl(b, objetivo) : slideDe(b).base.find((e) => e.kind === "arena") ?? null;
1982
+ if (!el) {
1983
+ return objetivo ? `No encuentro ninguna arena "${objetivo}".` : "En este slide no hay ninguna arena que llenar. Ponla antes con `poner_arena`.";
1984
+ }
1985
+ if (el.kind !== "arena" || !el.arena) return `${describirEl(el)} no es una arena, as\xED que no tiene qu\xE9 llenarle.`;
1986
+ const cfg = leerJson(args.cfg);
1987
+ if ("error" in cfg) return cfg.error;
1988
+ const r = validarCfgDe(el.arena, cfg.valor);
1989
+ if (!r.ok) {
1990
+ return `Esa configuraci\xF3n no vale para la arena "${el.arena}":
1991
+ ${fallosEnTexto(r.fallos)}
1992
+
1993
+ Pide \`describir("${el.arena}")\` para ver la forma exacta y vuelve a intentarlo. NO la dejes a medias.`;
1994
+ }
1995
+ el.acfg = cfg.valor;
1996
+ b.tocados.push(el.id);
1997
+ b.bitacora.push(`\u270E Arena ${el.arena} llenada`);
1998
+ return `Arena "${el.arena}" llenada y validada (id ${el.id}).`;
1999
+ }
2000
+ case "configurar_booster": {
2001
+ const el = buscarEl(b, txt(args.objetivo));
2002
+ if (!el) return `No encuentro ning\xFAn booster "${txt(args.objetivo)}".`;
2003
+ if (el.kind !== "widget" || !el.widget) return `${describirEl(el)} no es un booster, as\xED que no tiene qu\xE9 llenarle.`;
2004
+ const ficha = ESQUEMAS[el.widget];
2005
+ if (!ficha || !ficha.cfgSchema) {
2006
+ return `El booster "${el.widget}" no tiene nada que configurar. Si quer\xEDas APLICARLO sobre otro objeto (quitafondo, magichat, transformar), esa es \`aplicar_booster\`.`;
2007
+ }
2008
+ const cfg = leerJson(args.cfg);
2009
+ if ("error" in cfg) return cfg.error;
2010
+ const r = validarCfgDe(el.widget, cfg.valor);
2011
+ if (!r.ok) {
2012
+ return `Esa configuraci\xF3n no vale para el booster "${el.widget}":
2013
+ ${fallosEnTexto(r.fallos)}
2014
+
2015
+ Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarlo.`;
2016
+ }
2017
+ el.wcfg = ficha.donde === "wcfg.ext" ? { ...el.wcfg, ext: cfg.valor } : { ...el.wcfg?.ext ? { ext: el.wcfg.ext } : {}, ...cfg.valor };
2018
+ b.tocados.push(el.id);
2019
+ b.bitacora.push(`\u270E Booster ${el.widget} configurado`);
2020
+ return `Booster "${el.widget}" configurado y validado (id ${el.id}).`;
2021
+ }
2022
+ // ── Estructura ───────────────────────────────────────────────────────────
2023
+ case "nuevo_slide": {
2024
+ const nombre2 = nuevoSlide(b, txt(args.nombre));
2025
+ b.bitacora.push(`\u{1F4C4} Slide \xAB${nombre2}\xBB`);
2026
+ return `Slide "${nombre2}" creado. A partir de ahora todo lo que pongas cae aqu\xED.`;
2027
+ }
2028
+ case "nueva_capa": {
2029
+ const r = txt(args.ref).trim();
2030
+ const capa = crearCapa(b, txt(args.nombre) || r);
2031
+ if (r) b.aliasCapa.set(r, capa.id);
2032
+ return `Capa "${capa.name}" creada. Para poner cosas dentro, p\xE1sale capa="${r || capa.name}".`;
2033
+ }
2034
+ case "nueva_escena": {
2035
+ const nombre2 = txt(args.nombre) || `Escena ${b.doc.length + 1}`;
2036
+ const sl = { id: nid("slide"), name: "Slide 1", base: [], layers: [] };
2037
+ b.doc.push({ id: nid("esc"), name: nombre2, slides: [sl] });
2038
+ b.escena = b.doc.length - 1;
2039
+ b.slide = 0;
2040
+ b.bitacora.push(`\u{1F3AC} Escena \xAB${nombre2}\xBB`);
2041
+ return `Escena "${nombre2}" creada, con un slide vac\xEDo. Ah\xED es donde est\xE1s trabajando ahora.`;
2042
+ }
2043
+ // ── Programar ────────────────────────────────────────────────────────────
2044
+ case "programar": {
2045
+ const objetivo = txt(args.objetivo);
2046
+ const el = buscarEl(b, objetivo);
2047
+ if (!el) return `No encuentro ning\xFAn objeto "${objetivo}" en este slide.`;
2048
+ if (txt(args.accion) === "quitar") {
2049
+ if (!pidi\u00F3Borrar(b.pedido)) {
2050
+ return "NO se quit\xF3 nada: el maestro no pidi\xF3 borrar ni quitar su programaci\xF3n. Cont\xE9stale con `responder`.";
2051
+ }
2052
+ const n0 = (el.triggers ?? []).length;
2053
+ el.triggers = [];
2054
+ b.tocados.push(el.id);
2055
+ b.bitacora.push(`\u{1F9F9} Sin programaci\xF3n: ${describirEl(el)}`);
2056
+ return `Le quit\xE9 ${n0} disparador(es).`;
2057
+ }
2058
+ const { accion, fallo } = armarAccion(b, args, el);
2059
+ if (!accion) return fallo ?? "No entend\xED qu\xE9 acci\xF3n quer\xEDas.";
2060
+ const trig = { id: nid("trig"), event: armarEvento(args, el), action: accion };
2061
+ el.triggers = args.primero === true ? [trig, ...el.triggers ?? []] : [...el.triggers ?? [], trig];
2062
+ b.tocados.push(el.id);
2063
+ b.bitacora.push(`\u26A1 ${describirEl(el)}: ${describirTrigger(trig, b)}`);
2064
+ return "Programado.";
2065
+ }
2066
+ // ── Retocar ──────────────────────────────────────────────────────────────
2067
+ case "recolocar": {
2068
+ const el = buscarEl(b, txt(args.objetivo));
2069
+ if (!el) return `No encuentro ning\xFAn objeto "${txt(args.objetivo)}".`;
2070
+ if (num3(args.w, 0) > 0) {
2071
+ const w = pct(args.w, el.w);
2072
+ el.h = +(el.h * (w / el.w)).toFixed(1);
2073
+ el.w = w;
2074
+ }
2075
+ if (typeof args.x === "number") el.x = pct(args.x, el.x);
2076
+ if (typeof args.y === "number") el.y = pct(args.y, el.y);
2077
+ encajar(el);
2078
+ b.tocados.push(el.id);
2079
+ b.bitacora.push(`\u2194\uFE0F Recolocado: ${describirEl(el)}`);
2080
+ return "Movido.";
2081
+ }
2082
+ case "cambiar_estilo": {
2083
+ const el = buscarEl(b, txt(args.objetivo));
2084
+ if (!el) return `No encuentro ning\xFAn objeto "${txt(args.objetivo)}".`;
2085
+ if (txt(args.color)) el.color = txt(args.color);
2086
+ if (txt(args.relleno)) el.fill = txt(args.relleno);
2087
+ if (num3(args.tamano, 0) > 0) {
2088
+ el.fontSize = Math.max(10, Math.min(220, num3(args.tamano, 44)));
2089
+ if (el.kind === "text") el.h = altoTexto(el.text ?? "", el.w, el.fontSize);
2090
+ }
2091
+ if (txt(args.efecto)) el.effect = txt(args.efecto) === "none" ? void 0 : txt(args.efecto);
2092
+ Object.assign(el, colorDelEfecto(el.effect ?? "", txt(args.color_efecto), txt(args.color)));
2093
+ if (txt(args.movimiento)) el.motion = txt(args.movimiento);
2094
+ if (num3(args.peso, 0) >= 400) el.weight = Math.round(num3(args.peso, 400));
2095
+ if (typeof args.mayusculas === "boolean") el.caps = args.mayusculas || void 0;
2096
+ if (txt(args.align)) el.align = txt(args.align);
2097
+ if (typeof args.opacidad === "number") el.opacity = Math.max(0, Math.min(1, args.opacidad));
2098
+ if (typeof args.giro === "number") el.rot = args.giro || void 0;
2099
+ b.tocados.push(el.id);
2100
+ b.bitacora.push(`\u{1F3A8} Retocado: ${describirEl(el)}`);
2101
+ return "Estilo cambiado.";
2102
+ }
2103
+ case "copiar_estilo": {
2104
+ const desde = buscarEl(b, txt(args.desde));
2105
+ const hacia = buscarEl(b, txt(args.hacia));
2106
+ if (!desde) return `No encuentro el objeto "${txt(args.desde)}" del que copiar.`;
2107
+ if (!hacia) return `No encuentro el objeto "${txt(args.hacia)}" al que copiar.`;
2108
+ const copiar = (k) => {
2109
+ const v = desde[k];
2110
+ if (v === void 0) delete hacia[k];
2111
+ else hacia[k] = v;
2112
+ };
2113
+ [
2114
+ "color",
2115
+ "fill",
2116
+ "fontSize",
2117
+ "font",
2118
+ "weight",
2119
+ "tracking",
2120
+ "caps",
2121
+ "leading",
2122
+ "align",
2123
+ "effect",
2124
+ "motion",
2125
+ "fxColor",
2126
+ "fxColor2",
2127
+ "fxIntensity",
2128
+ "fxSpeed",
2129
+ "curve",
2130
+ "stroke",
2131
+ "strokeW",
2132
+ "strokeStyle",
2133
+ "opacity"
2134
+ ].forEach(copiar);
2135
+ if (hacia.kind === "text") hacia.h = altoTexto(hacia.text ?? "", hacia.w, hacia.fontSize ?? 44);
2136
+ b.tocados.push(hacia.id);
2137
+ b.bitacora.push(`\u{1F3A8} ${describirEl(hacia)} qued\xF3 como ${describirEl(desde)}`);
2138
+ return "Estilo copiado.";
2139
+ }
2140
+ case "cambiar_texto": {
2141
+ const el = buscarEl(b, txt(args.objetivo));
2142
+ if (!el) return `No encuentro ning\xFAn objeto "${txt(args.objetivo)}".`;
2143
+ el.text = txt(args.texto);
2144
+ delete el.html;
2145
+ if (el.kind === "text") el.h = altoTexto(el.text, el.w, el.fontSize ?? 44);
2146
+ b.tocados.push(el.id);
2147
+ b.bitacora.push(`\u270E \xAB${el.text}\xBB`);
2148
+ return "Texto cambiado.";
2149
+ }
2150
+ case "borrar": {
2151
+ if (!pidi\u00F3Borrar(b.pedido)) {
2152
+ return "NO se borr\xF3 nada: el maestro no pidi\xF3 borrar. Su mensaje no lleva ninguna palabra de borrado, as\xED que probablemente te est\xE1 contando algo o confirm\xE1ndote una acci\xF3n suya. Cont\xE9stale con `responder`.";
2153
+ }
2154
+ const el = buscarEl(b, txt(args.objetivo));
2155
+ if (!el) return `No encuentro ning\xFAn objeto "${txt(args.objetivo)}".`;
2156
+ for (const sc of b.doc) {
2157
+ for (const sl of sc.slides) {
2158
+ sl.base = sl.base.filter((e) => e.id !== el.id);
2159
+ sl.layers.forEach((l) => {
2160
+ l.els = l.els.filter((e) => e.id !== el.id);
2161
+ });
2162
+ }
2163
+ }
2164
+ b.bitacora.push(`\u{1F5D1} Quitado: ${describirEl(el)}`);
2165
+ return "Borrado.";
2166
+ }
2167
+ default:
2168
+ return `No existe la herramienta "${nombre}".`;
2169
+ }
2170
+ }
2171
+ function nuevoSlide(b, nombre) {
2172
+ const sc = escenaDe(b);
2173
+ const sl = { id: nid("slide"), name: nombre || `Slide ${sc.slides.length + 1}`, base: [], layers: [] };
2174
+ sc.slides.splice(b.slide + 1, 0, sl);
2175
+ b.slide += 1;
2176
+ return sl.name;
2177
+ }
2178
+ function leerJson(v) {
2179
+ const t = txt(v).trim();
2180
+ if (!t) return { error: "Me mandaste el `cfg` vac\xEDo. Tiene que ser un texto JSON con la configuraci\xF3n." };
2181
+ try {
2182
+ return { valor: JSON.parse(t) };
2183
+ } catch (e) {
2184
+ return { error: `El \`cfg\` no es JSON v\xE1lido (${e.message}). M\xE1ndalo otra vez como texto JSON, sin comas de m\xE1s ni comentarios.` };
2185
+ }
2186
+ }
2187
+ function describirEl(el) {
2188
+ if (el.text) return `\xAB${el.text.length > 28 ? `${el.text.slice(0, 28)}\u2026` : el.text}\xBB`;
2189
+ if (el.kind === "image") return "el dibujo";
2190
+ if (el.kind === "audio") return `el audio \xAB${el.name ?? "sin nombre"}\xBB`;
2191
+ if (el.kind === "widget") return `el booster ${el.widget}`;
2192
+ if (el.kind === "arena") return `la arena ${el.arena}`;
2193
+ if (el.hotspot) return "la zona caliente";
2194
+ return `la figura ${el.shape ?? ""}`.trim();
2195
+ }
2196
+ function describirTrigger(t, b) {
2197
+ const cuando = t.event.type === "click" ? "al tocarlo" : t.event.type === "hover" ? "al pasarle por encima" : t.event.type === "slideStart" ? "al entrar al slide" : t.event.type === "layerShown" ? "al abrirse su capa" : t.event.type === "time" ? `a los ${t.event.seconds} s` : t.event.type === "clickStep" ? `en el clic n\xBA ${t.event.step}` : t.event.type === "motionEnd" ? "al terminar su trayectoria" : "al dispararse";
2198
+ const a = t.action;
2199
+ const nombreCapa = (id) => buscarCapa(b, id)?.name ?? "una capa";
2200
+ const qu\u00E9 = a.type === "showLayer" ? `abre \xAB${nombreCapa(a.layerId)}\xBB` : a.type === "goScene" ? `salta a \xAB${b.doc.find((s) => s.id === a.sceneId)?.name ?? "otra escena"}\xBB` : a.type === "goSlide" ? "salta a otro slide" : a.type === "closeLayer" ? "cierra la capa" : a.type === "speak" ? `lee \xAB${a.text}\xBB en voz alta` : a.type === "openUrl" ? "abre un enlace" : a.type === "hideEl" ? "desaparece con humo" : a.type === "playMotion" ? "recorre su trayectoria" : a.type === "playSound" ? "suena el audio" : a.type === "resetSequence" ? "reinicia el slide" : "hace lo suyo";
2201
+ return `${cuando}, ${qu\u00E9}`;
2202
+ }
2203
+
2204
+ // ../src/canvas/appNativeArenas.ts
2205
+ var ARENAS_MUDADAS = [
2206
+ {
2207
+ arena: "word-storm",
2208
+ module: "canvas/arenas/ArenaWordStorm",
2209
+ namedExports: { DEFAULT_WORDS: "[]" },
2210
+ motivo: "necesitaba el hub de Booster Live (2026-08-13)"
2211
+ },
2212
+ {
2213
+ arena: "booster-escape",
2214
+ module: "canvas/arenas/ArenaBoosterEscape",
2215
+ // El puente le pide el componente por defecto y nada más (sus preguntas de ejemplo son
2216
+ // internas), así que no hay exports con nombre que reponer.
2217
+ namedExports: {},
2218
+ motivo: "necesitaba el hub de Booster Live (2026-08-22)"
2219
+ }
2220
+ ];
2221
+ var APP_NATIVE_ARENAS = ARENAS_MUDADAS.map((a) => a.arena);
2222
+
2223
+ // ../src/canvas/arenas/wordStormLive.ts
2224
+ var WORDSTORM_SAMPLE = [
2225
+ "apple",
2226
+ "storm",
2227
+ "rainbow",
2228
+ "dragon",
2229
+ "window",
2230
+ "jungle",
2231
+ "pencil",
2232
+ "rocket",
2233
+ "summer",
2234
+ "castle"
2235
+ ];
2236
+
2237
+ // ../src/canvas/widgets/CycleWidget.datos.ts
2238
+ var seq = 0;
2239
+ var cycleUid = () => `cs-${Date.now().toString(36)}-${seq++}`;
2240
+
2241
+ // ../src/canvas/widgets/plot2d.ts
2242
+ var CURVE_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#d97706", "#7c3aed", "#db2777", "#0891b2", "#65a30d"];
2243
+
2244
+ // ../src/canvas/widgets/EquationPlotterWidget.datos.ts
2245
+ var DEFAULT_VIEW = { cx: 0, cy: 0, scale: 46 };
2246
+ var seq2 = 0;
2247
+ var eid = () => `eq-${Date.now().toString(36)}-${seq2++}`;
2248
+ function defaultPlotterCfg() {
2249
+ return {
2250
+ eqs: [{ id: eid(), src: "y = x^2", color: CURVE_COLORS[0], visible: true }],
2251
+ mode: "2d",
2252
+ dark: false,
2253
+ piAxis: false,
2254
+ showCuts: true,
2255
+ view: { ...DEFAULT_VIEW },
2256
+ tMin: 0,
2257
+ tMax: Math.PI * 2,
2258
+ range3d: 5,
2259
+ zClamp: 8,
2260
+ wire: false,
2261
+ quality: "normal",
2262
+ params: {}
2263
+ };
2264
+ }
2265
+
2266
+ // ../src/canvas/widgets/fractionsMath.ts
2267
+ var FRAC_DENOMS = [2, 3, 4, 5, 6, 8, 10, 12];
2268
+
2269
+ // ../src/canvas/widgets/QrWidget.datos.ts
2270
+ var QR_DEFAULTS = {
2271
+ // Nace con un enlace de verdad ya puesto: el maestro suelta el booster y ya tiene
2272
+ // algo que enseñar, en vez de un hueco que hay que configurar antes de ver nada.
2273
+ text: "https://boosterarena.com",
2274
+ kind: "url",
2275
+ dark: "#0f172a",
2276
+ light: "#ffffff",
2277
+ style: "soft",
2278
+ ecc: "M",
2279
+ label: "Escan\xE9ame",
2280
+ plate: true,
2281
+ sound: true,
2282
+ anim: true
2283
+ };
2284
+
2285
+ // ../packages/we-boosters/src/canvas/arenas/ArenaMenu.datos.ts
2286
+ function svgIcon(color2, inner) {
2287
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${color2}' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'>${inner}</svg>`;
2288
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
2289
+ }
2290
+ var DEFAULT_ITEMS = [
2291
+ { id: "s1", color: "#14b8a6", title: "CONTENIDO", desc: "Consulta los temas y subtemas del curso.", icon: svgIcon("#14b8a6", "<path d='M3 5.5c2.7-1 5.3-1 8 0v13c-2.7-1-5.3-1-8 0z'/><path d='M21 5.5c-2.7-1-5.3-1-8 0v13c2.7-1 5.3-1 8 0z'/>") },
2292
+ { id: "s2", color: "#3b82f6", title: "OBJETIVOS", desc: "Conoce los objetivos de aprendizaje.", icon: svgIcon("#3b82f6", "<circle cx='12' cy='12' r='7'/><circle cx='12' cy='12' r='3.4'/><path d='M12 12l7-7'/><path d='M16 5h3v3'/>") },
2293
+ { id: "s3", color: "#8b5cf6", title: "EVALUACI\xD3N", desc: "Pon a prueba tus conocimientos.", icon: svgIcon("#8b5cf6", "<rect x='6' y='4.5' width='12' height='16' rx='2'/><rect x='9' y='3' width='6' height='3.2' rx='1'/><path d='M8.6 11l1.4 1.4 2.6-2.6'/><path d='M8.6 15.6l1.4 1.4 2.6-2.6'/>") },
2294
+ { id: "s4", color: "#84cc16", title: "RECURSOS", desc: "Accede a materiales de apoyo adicionales.", icon: svgIcon("#84cc16", "<path d='M4 20h16'/><rect x='7' y='11' width='2.6' height='6' rx='.5'/><rect x='11.7' y='8' width='2.6' height='9' rx='.5'/><rect x='16.4' y='5' width='2.6' height='12' rx='.5'/>") },
2295
+ { id: "s5", color: "#f59e0b", title: "INFORMACI\xD3N", desc: "Detalles importantes sobre el curso.", icon: svgIcon("#f59e0b", "<circle cx='12' cy='8.5' r='3.6'/><path d='M5.5 19.5a6.5 6.5 0 0 1 13 0'/>") }
2296
+ ];
2297
+
2298
+ // ../packages/we-boosters/src/canvas/arenas/ArenaMenuNeon.datos.ts
2299
+ function svgIcon2(color2, inner) {
2300
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${color2}' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'>${inner}</svg>`;
2301
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
2302
+ }
2303
+ var NEON_DEFAULT_ITEMS = [
2304
+ { id: "s1", color: "#2dd4bf", title: "CONTENIDO", desc: "Consulta los temas y subtemas del curso.", icon: svgIcon2("#2dd4bf", "<path d='M3 5.5c2.7-1 5.3-1 8 0v13c-2.7-1-5.3-1-8 0z'/><path d='M21 5.5c-2.7-1-5.3-1-8 0v13c2.7-1 5.3-1 8 0z'/>") },
2305
+ { id: "s2", color: "#3b82f6", title: "OBJETIVOS", desc: "Conoce los objetivos de aprendizaje.", icon: svgIcon2("#3b82f6", "<circle cx='12' cy='12' r='7'/><circle cx='12' cy='12' r='3.4'/><path d='M12 12l7-7'/><path d='M16 5h3v3'/>") },
2306
+ { id: "s3", color: "#a855f7", title: "EVALUACI\xD3N", desc: "Pon a prueba tus conocimientos.", icon: svgIcon2("#a855f7", "<rect x='6' y='4.5' width='12' height='16' rx='2'/><rect x='9' y='3' width='6' height='3.2' rx='1'/><path d='M8.6 11l1.4 1.4 2.6-2.6'/><path d='M8.6 15.6l1.4 1.4 2.6-2.6'/>") },
2307
+ { id: "s4", color: "#ec4899", title: "RECURSOS", desc: "Accede a materiales de apoyo adicionales.", icon: svgIcon2("#ec4899", "<rect x='4' y='4' width='7' height='7' rx='1.4'/><rect x='13' y='4' width='7' height='7' rx='1.4'/><rect x='4' y='13' width='7' height='7' rx='1.4'/><rect x='13' y='13' width='7' height='7' rx='1.4'/>") },
2308
+ { id: "s5", color: "#f5b301", title: "INFORMACI\xD3N", desc: "Detalles importantes sobre el curso.", icon: svgIcon2("#f5b301", "<path d='M9 18h6'/><path d='M10 21h4'/><path d='M12 3a6 6 0 0 1 4 10.5c-.7.7-1 1.2-1 2.5H9c0-1.3-.3-1.8-1-2.5A6 6 0 0 1 12 3z'/>") }
2309
+ ];
2310
+
2311
+ // ../packages/we-boosters/src/canvas/arenas/ArenaMenuSparta.datos.ts
2312
+ function svgIcon3(color2, inner) {
2313
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${color2}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'>${inner}</svg>`;
2314
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
2315
+ }
2316
+ var GOLD = "#c9a24b";
2317
+ var SPARTA_DEFAULT_ITEMS = [
2318
+ // Casco
2319
+ { id: "s1", color: GOLD, title: "CONTENIDO", desc: "Consulta los temas y subtemas del curso.", icon: svgIcon3(GOLD, "<path d='M5 13a7 7 0 0 1 13-3.5c1.6.4 2.4 1.3 2.4 2.2 0 1-.9 1.6-2 1.6'/><path d='M5 13c0 2.5 1.5 4 3.5 4H12v3'/><path d='M5 13H3.5'/><path d='M12 6V3.5'/>") },
2320
+ // Lanzas cruzadas
2321
+ { id: "s2", color: GOLD, title: "OBJETIVOS", desc: "Conoce los objetivos de aprendizaje.", icon: svgIcon3(GOLD, "<path d='M4 20L20 4'/><path d='M20 4l-3 .4L18.6 7z'/><path d='M20 20L4 4'/><path d='M4 4l3 .4L5.4 7z'/>") },
2322
+ // Búho de Atenea
2323
+ { id: "s3", color: GOLD, title: "EVALUACI\xD3N", desc: "Pon a prueba tus conocimientos.", icon: svgIcon3(GOLD, "<path d='M7 9a5 5 0 0 1 10 0v4a5 5 0 0 1-10 0z'/><circle cx='9.5' cy='10' r='1.6'/><circle cx='14.5' cy='10' r='1.6'/><path d='M12 12.5l-1 1.5h2z'/><path d='M8 5l1.5 2M16 5l-1.5 2'/><path d='M9 18l-1.5 2M15 18l1.5 2'/>") },
2324
+ // Corona de laurel
2325
+ { id: "s4", color: GOLD, title: "RECURSOS", desc: "Accede a materiales de apoyo adicionales.", icon: svgIcon3(GOLD, "<path d='M12 4v15'/><path d='M12 8c-2-2-5-2-6 0 1 2 4 2 6 0z'/><path d='M12 12c-2-2-5-2-6 0 1 2 4 2 6 0z'/><path d='M12 8c2-2 5-2 6 0-1 2-4 2-6 0z'/><path d='M12 12c2-2 5-2 6 0-1 2-4 2-6 0z'/>") },
2326
+ // Templo / Partenón
2327
+ { id: "s5", color: GOLD, title: "INFORMACI\xD3N", desc: "Detalles importantes sobre el curso.", icon: svgIcon3(GOLD, "<path d='M3 9l9-5 9 5z'/><path d='M5 9v8M9.3 9v8M14.7 9v8M19 9v8'/><path d='M3 20h18'/><path d='M4 17h16'/>") }
2328
+ ];
2329
+
2330
+ // ../packages/we-boosters/src/canvas/arenas/ArenaDuckShooting.datos.ts
2331
+ var DEFAULT_QUESTIONS = [
2332
+ { q: 'How do you say "perro" in English?', options: ["Cat", "Dog", "Fish", "Bird"], answer: 1 },
2333
+ { q: 'The opposite of "big" is ___.', options: ["small", "tall", "fast", "old"], answer: 0 },
2334
+ { q: "Which one is a color?", options: ["Table", "Purple", "Run", "Loud"], answer: 1 },
2335
+ { q: "A baby dog is called a ___.", options: ["kitten", "puppy", "calf", "chick"], answer: 1 },
2336
+ { q: "The sun rises in the ___.", options: ["west", "north", "east", "south"], answer: 2 },
2337
+ { q: 'The plural of "child" is ___.', options: ["childs", "children", "childes", "child"], answer: 1 },
2338
+ { q: "How many days are in a week?", options: ["five", "six", "seven", "eight"], answer: 2 },
2339
+ { q: "Which animal can fly?", options: ["Dog", "Bird", "Cat", "Fish"], answer: 1 }
2340
+ ];
2341
+
2342
+ // ../packages/we-boosters/src/canvas/boosters/BoosterRevealer.datos.ts
2343
+ var REVEALER_THEMES = [
2344
+ { id: "aurora", label: "Aurora", swatch: "linear-gradient(135deg,#7c3aed,#4f46e5 60%,#2563eb)" },
2345
+ { id: "sunset", label: "Sunset", swatch: "linear-gradient(135deg,#f97316,#e11d48)" },
2346
+ { id: "ocean", label: "Ocean", swatch: "linear-gradient(135deg,#06b6d4,#1e40af)" },
2347
+ { id: "candy", label: "Candy", swatch: "linear-gradient(135deg,#f472b6,#8b5cf6)" },
2348
+ { id: "galaxy", label: "Galaxy", swatch: "linear-gradient(135deg,#1e1b4b,#312e81)" },
2349
+ { id: "gold", label: "Gold", swatch: "linear-gradient(135deg,#fde68a,#d97706)" }
2350
+ ];
2351
+
2352
+ // ../packages/we-boosters/src/fichas.ts
2353
+ var courseMenu = {
2354
+ type: "course-menu",
2355
+ emoji: "\u{1F9ED}",
2356
+ label: "Men\xFA de curso",
2357
+ blurb: "Pantalla de inicio: bienvenida + tarjetas reemplazables. Edita logo, colores, textos y secciones (a\xF1ade/quita).",
2358
+ premium: false,
2359
+ ready: true,
2360
+ defaultAcfg: {},
2361
+ // sin datos → usa los valores por defecto del template (5 secciones)
2362
+ // Destinos navegables = una por tarjeta. Usa el `id` estable de cada sección (o índice si
2363
+ // falta), igual que lo que envía ArenaMenu por onNavigate. Sin datos → las 5 por defecto.
2364
+ navSlots: (acfg) => {
2365
+ const its = acfg?.items?.length ? acfg.items : DEFAULT_ITEMS;
2366
+ return its.map((it, i) => ({ key: it.id ?? String(i), label: it.title || `Secci\xF3n ${i + 1}` }));
2367
+ }
2368
+ };
2369
+ var spellingJenga = {
2370
+ type: "we-spelling-jenga",
2371
+ emoji: "\u{1F9F1}",
2372
+ label: "Spelling Jenga (we-boosters)",
2373
+ blurb: "Deletreo por orden de letras para 4 jugadores: ordena la palabra o tu torre se derrumba.",
2374
+ premium: false,
2375
+ ready: true,
2376
+ defaultAcfg: {
2377
+ words: ["HOUSE", "WATER", "GREEN", "MUSIC", "HAPPY", "TABLE", "LIGHT", "BREAD", "PHONE", "SMILE", "OCEAN", "APPLE"],
2378
+ playerNames: ["Player 1", "Player 2", "Player 3", "Player 4"]
2379
+ }
2380
+ };
2381
+ var courseMenuKids = {
2382
+ type: "course-menu-kids",
2383
+ emoji: "\u{1F9D2}",
2384
+ label: "Men\xFA de curso (infantil)",
2385
+ blurb: "Pantalla de inicio infantil: cielo, personajes y tarjetas circulares. Edita fondo, im\xE1genes, colores, textos y secciones.",
2386
+ premium: false,
2387
+ ready: true,
2388
+ defaultAcfg: {}
2389
+ // sin datos → usa los valores por defecto del template (5 tarjetas)
2390
+ };
2391
+ var courseMenuNeon = {
2392
+ type: "course-menu-neon",
2393
+ emoji: "\u{1F30C}",
2394
+ label: "Men\xFA de curso (ne\xF3n)",
2395
+ blurb: 'Pantalla de inicio teen sobre fondo oscuro: "\xA1BIENVENIDO!" ne\xF3n, estudiantes y tarjetas de vidrio con borde que respira. Edita fondo, im\xE1genes, colores, textos y secciones.',
2396
+ premium: false,
2397
+ ready: true,
2398
+ defaultAcfg: {},
2399
+ // sin datos → usa los valores por defecto del template (5 tarjetas)
2400
+ // Destinos navegables = una por tarjeta (clave estable `id`, o índice si falta).
2401
+ navSlots: (acfg) => {
2402
+ const its = acfg?.items?.length ? acfg.items : NEON_DEFAULT_ITEMS;
2403
+ return its.map((it, i) => ({ key: it.id ?? String(i), label: it.title || `Secci\xF3n ${i + 1}` }));
2404
+ }
2405
+ };
2406
+ var courseMenuSparta = {
2407
+ type: "course-menu-sparta",
2408
+ emoji: "\u{1F3DB}\uFE0F",
2409
+ label: "Men\xFA de curso (espartano)",
2410
+ blurb: "Pantalla de inicio \xE9pica: est\xE9tica espartana/griega con medallones de bronce. Edita fondo, im\xE1genes, colores, textos y secciones.",
2411
+ premium: false,
2412
+ ready: true,
2413
+ defaultAcfg: {},
2414
+ // sin datos → usa los valores por defecto del template (5 medallones)
2415
+ // Destinos navegables = uno por medallón (clave estable `id`, o índice si falta).
2416
+ navSlots: (acfg) => {
2417
+ const its = acfg?.items?.length ? acfg.items : SPARTA_DEFAULT_ITEMS;
2418
+ return its.map((it, i) => ({ key: it.id ?? String(i), label: it.title || `Secci\xF3n ${i + 1}` }));
2419
+ }
2420
+ };
2421
+ var duckShooting = {
2422
+ type: "duck-shooting",
2423
+ emoji: "\u{1F986}",
2424
+ label: "Duck Shooting",
2425
+ blurb: "Caseta de feria: dispara a los patos (uno por pregunta) y responde la pregunta que aparece. Suma puntos y consigue 3 estrellas. Edita las preguntas.",
2426
+ premium: false,
2427
+ ready: true,
2428
+ defaultAcfg: { questions: DEFAULT_QUESTIONS }
2429
+ // sin datos → preguntas de ejemplo
2430
+ };
2431
+ var jenga = {
2432
+ type: "jenga",
2433
+ emoji: "\u{1F9F1}",
2434
+ label: "Jenga",
2435
+ blurb: "Quiz con torre Jenga 3D real: acierta y la torre aguanta, falla y se saca una pieza hasta que se derrumba. Suma puntos. Edita las preguntas.",
2436
+ premium: false,
2437
+ ready: true,
2438
+ defaultAcfg: {}
2439
+ // sin datos → la arena usa sus preguntas de ejemplo internas
2440
+ };
2441
+ var cellphoneChallenge = {
2442
+ type: "cellphone-challenge",
2443
+ emoji: "\u{1F4F1}",
2444
+ label: "Cellphone Challenge",
2445
+ blurb: "Tel\xE9fono cl\xE1sico de los 2000: aprende y practica la escritura normal (multi-tap) y predictiva, decodifica n\xFAmeros y responde chats estilo SMS. Edita las palabras y las conversaciones.",
2446
+ premium: false,
2447
+ ready: true,
2448
+ defaultAcfg: {}
2449
+ // sin datos → la arena usa sus palabras y conversaciones de ejemplo internas
2450
+ };
2451
+ var boosterEscape = {
2452
+ type: "booster-escape",
2453
+ emoji: "\u{1F9ED}",
2454
+ label: "Booster Escape",
2455
+ blurb: "Laberinto de la selva con dos modos: Individual (corre por los pasillos, esquiva a los enemigos que te persiguen y llega a la respuesta correcta; 3 vidas, dificultad creciente) y Versus 1v1 (dos jugadores compiten: el primero en llegar a la respuesta correcta gana la ronda). Edita las preguntas (con imagen/audio).",
2456
+ premium: false,
2457
+ ready: true,
2458
+ defaultAcfg: {}
2459
+ // sin datos → la arena usa sus preguntas de ejemplo internas
2460
+ };
2461
+ var revealer = {
2462
+ type: "revealer",
2463
+ emoji: "\u{1FA84}",
2464
+ label: "Revelador",
2465
+ blurb: "Tapa m\xE1gica: col\xF3cala sobre una respuesta y, al hacer clic, explota en confeti y revela lo de atr\xE1s.",
2466
+ premium: false,
2467
+ ready: true,
2468
+ defaultWcfg: { label: "?", hint: "Tap to reveal", theme: "aurora" },
2469
+ defaultSize: { w: 22, h: 30 },
2470
+ freeResize: true,
2471
+ hint: "\u{1FA84} Revelador. Col\xF3calo sobre una respuesta y est\xEDralo para cubrirla. En el \u25B6 Player, al hacer clic explota en confeti y deja ver lo de atr\xE1s.",
2472
+ fields: [
2473
+ { key: "label", kind: "text", label: "S\xEDmbolo de la tapa", placeholder: "?", maxLength: 6 },
2474
+ { key: "hint", kind: "text", label: "Texto peque\xF1o", placeholder: "Tap to reveal" },
2475
+ { key: "theme", kind: "theme", label: "Dise\xF1o", options: REVEALER_THEMES }
2476
+ ]
2477
+ };
2478
+ var instructions = {
2479
+ type: "instructions",
2480
+ emoji: "\u2753",
2481
+ label: "Instructions",
2482
+ blurb: 'Moneda "?" que al hacer clic abre un panel de vidrio con las instrucciones de la clase.',
2483
+ premium: false,
2484
+ ready: true,
2485
+ defaultWcfg: {
2486
+ title: "Instructions",
2487
+ icon: "\u{1F4A1}",
2488
+ body: "Read the text on the board.\nDiscuss it with your partner.\nWrite three sentences in your notebook."
2489
+ },
2490
+ defaultSize: { w: 8, h: 11 },
2491
+ freeResize: true,
2492
+ hint: "\u2753 Instructions. Coloca la moneda donde quieras y est\xEDrala (puede ser peque\xF1a). En el \u25B6 Player, al hacer clic se abre un panel de vidrio al 80% del slide con los pasos.",
2493
+ fields: [
2494
+ { key: "title", kind: "text", label: "T\xEDtulo", placeholder: "Instructions", maxLength: 40 },
2495
+ { key: "body", kind: "text", label: "Instrucciones (una por l\xEDnea)", placeholder: "Read the text on the board." },
2496
+ { key: "icon", kind: "text", label: "Emoji del t\xEDtulo", placeholder: "\u{1F4A1}", maxLength: 4 }
2497
+ ]
2498
+ };
2499
+ var FICHAS_ARENAS = [courseMenu, courseMenuKids, courseMenuNeon, courseMenuSparta, duckShooting, jenga, spellingJenga, cellphoneChallenge, boosterEscape];
2500
+ var FICHAS_BOOSTERS = [revealer, instructions];
2501
+
2502
+ // ../src/registro/capacidades.ts
2503
+ var WIDGETS = [
2504
+ { type: "dice", emoji: "\u{1F3B2}", label: "Dado 3D", blurb: "Lanza dados con f\xEDsica real: ruedan, rebotan y se asientan solos.", premium: false, ready: true },
2505
+ { type: "wheel", emoji: "\u{1F3A1}", label: "Ruleta de nombres", blurb: "Gira y elige un nombre al azar. Avatares + lista de nombres configurable.", premium: false, ready: true },
2506
+ { type: "timer", emoji: "\u23F3", label: "Temporizador", blurb: "Cuenta regresiva con anillo de progreso, color y alarma final.", premium: false, ready: true },
2507
+ { type: "score", emoji: "\u{1F3C6}", label: "Marcador de equipos", blurb: "Puntos por equipo (A vs B\u2026). Toca para sumar o restar.", premium: false, ready: true },
2508
+ { type: "groups", emoji: "\u{1F465}", label: "Formar equipos", blurb: "Reparte a los participantes en equipos al azar, con animaci\xF3n.", premium: false, ready: true },
2509
+ { type: "soundboard", emoji: "\u{1F50A}", label: "Soundboard", blurb: "Botonera de efectos de sonido: aplausos, ding, buzzer, redoble.", premium: false, ready: true },
2510
+ { type: "mystery", emoji: "\u{1F381}", label: "Caja misteriosa", blurb: "Toca y revela una palabra/pregunta al azar, sin repetir hasta agotar.", premium: false, ready: true },
2511
+ { type: "buzzer", emoji: "\u{1F514}", label: "Buzzer", blurb: "\xA1Qui\xE9n responde primero! Bot\xF3n con sonido y destello.", premium: false, ready: true },
2512
+ // Instructions es APP-NATIVE (moneda ❓ → panel de vidrio con editor rico "hoja en blanco":
2513
+ // formato, imágenes, emojis, espacios en blanco, enlaces). Reemplaza al de we-boosters.
2514
+ { type: "instructions", emoji: "\u2753", label: "Instructions", blurb: "Moneda \u2753 que al hacer clic abre un panel con las instrucciones del slide. Editor tipo hoja: texto con formato, im\xE1genes, emojis, espacios en blanco y enlaces.", premium: false, ready: true },
2515
+ // Varita Mágica (app-native): saca imágenes una a una y, al cerrarlas, las PLANTA en el slide
2516
+ // como objetos reales (efecto genio). Reemplaza el truco de variables+estados de Articulate.
2517
+ { type: "magicwand", emoji: "\u{1FA84}", label: "Varita M\xE1gica", blurb: 'Carga varias im\xE1genes y, con cada clic en el \u25B6 Player, "saca" una en grande; al cerrarla se planta en el slide como objeto real y movible (animaci\xF3n genio).', premium: false, ready: true },
2518
+ // Sombrero Mágico (app-native): NO se planta como elemento. Es una HERRAMIENTA — al elegirlo,
2519
+ // el 🎩 sigue al cursor y al tocar una imagen la vuelve SILUETA negra (magic:true). En el ▶ Player,
2520
+ // al hacer clic sobre esa imagen se "revela" a color con lluvia de estrellas (se re-tapa al re-entrar).
2521
+ // Quita fondos: la MISMA mecánica del Sombrero (moneda que se arrastra sobre una imagen),
2522
+ // porque el maestro ya la conoce y dice sola sobre qué imagen va a actuar.
2523
+ // Premium de verdad: cada recorte gasta GPU, y el servidor lo exige (no es solo el candado
2524
+ // de la interfaz). El precio del recorte malo es peor que el de la GPU: por eso NO hay carril
2525
+ // barato de reserva — ver la cabecera de `quitarFondo.ts`.
2526
+ { type: "quitafondo", emoji: "\u{1F98E}", label: "Quita Fondos", blurb: "Arr\xE1stralo sobre una imagen y el camale\xF3n se come el fondo. Recorta con el modelo grande: separa mechones de pelo, hojas y patas.", premium: true, ready: true },
2527
+ { type: "transformar", emoji: "\u{1F500}", label: "Transformador", blurb: "Arr\xE1stralo sobre un objeto y elige en qu\xE9 se convierte. En el \u25B6 Player cada clic lo transforma en la siguiente imagen \u2014de la oruga a la mariposa\u2014 y la \xFAltima lo devuelve a como estaba.", premium: false, ready: true },
2528
+ { type: "magichat", emoji: "\u{1F3A9}", label: "Sombrero M\xE1gico", blurb: "Arr\xE1stralo sobre una imagen (o PNG) para volverla una silueta negra. En el \u25B6 Player, al hacer clic se revela a color con una lluvia de estrellas m\xE1gicas.", premium: false, ready: true },
2529
+ // Tómbola: bombo de bingo que gira y suelta una bola (número/palabra/imagen), sin repetir.
2530
+ { type: "tombola", emoji: "\u{1F3B0}", label: "T\xF3mbola", blurb: "Bombo de bingo: gira y suelta una bola al azar con un n\xFAmero, palabra o imagen. Sortea sin repetir hasta vaciarse. Ideal para rifas y elegir al azar.", premium: false, ready: true },
2531
+ // Semáforo de comportamiento: 3 luces que el maestro toca en el Player para dar feedback a la clase.
2532
+ { type: "trafficlight", emoji: "\u{1F6A6}", label: "Sem\xE1foro", blurb: "Sem\xE1foro de comportamiento: en el Player, toca una luz (verde/\xE1mbar/rojo) para dar feedback a la clase. El rojo dispara una alerta sonora y visual.", premium: false, ready: true },
2533
+ // Graficador de ecuaciones (app-native, rama de MATEMÁTICA): orbe → estudio 2D/3D.
2534
+ // three.js entra por lazy() solo si el maestro pulsa 3D.
2535
+ // Label It (app-native, rama de CIENCIAS): la lámina del maestro se vuelve actividad de etiquetar.
2536
+ // Sirve a biología (célula, flor, corazón), pero también a geografía, anatomía o vocabulario.
2537
+ { type: "labelit", emoji: "\u{1F3F7}\uFE0F", label: "Label It \u2014 etiquetar", blurb: "Sube cualquier l\xE1mina (c\xE9lula, flor, coraz\xF3n, mapa, una cara\u2026), toca la imagen para clavar los puntos y escribe el nombre de cada parte. En el \u25B6 Player se juega de tres formas: revelar al tocar, arrastrar las palabras a su punto, o escribirlas.", premium: false, ready: true },
2538
+ // Ciclo (app-native, rama de CIENCIAS): una rueda de etapas reemplaza cinco slides sueltas.
2539
+ // La rueda NO gira (gira el FOCO), así las imágenes quedan siempre derechas en la proyección.
2540
+ // 🍕 «Fracciones manipulables» (regletas que se cortan) SE RETIRÓ del catálogo el 2026-08-12:
2541
+ // Antonio se quedó con el Graficador de fracciones de más abajo, que es lo que él quería para
2542
+ // armar sus láminas. El booster NO se borra —solo su tarjeta—: los slides que ya lo tengan
2543
+ // puesto tienen que seguir abriendo y funcionando (su render, su editor y su config siguen
2544
+ // cableados más abajo). Para devolverlo al catálogo, basta con volver a poner su entrada aquí.
2545
+ { type: "cycle", emoji: "\u{1F504}", label: "Ciclo \u2014 rueda de etapas", blurb: "Rueda con las etapas de un proceso (metamorfosis, mitosis, ciclo del agua, fotos\xEDntesis\u2026), cada una con su imagen y su texto. En el \u25B6 Player, cada clic mueve el foco a la siguiente y el centro cuenta qu\xE9 pasa ah\xED. Puede cerrar en c\xEDrculo o ser una cadena, y los nombres pueden salir ocultos para que la clase adivine.", premium: false, ready: true },
2546
+ // Graficador de fracciones (app-native, rama de MATEMÁTICA): HERRAMIENTA de autoría. No es una
2547
+ // actividad para la clase: es la moneda con la que el maestro DIBUJA las fracciones de su
2548
+ // lámina, una tras otra, sin salir a buscar imágenes fuera.
2549
+ { type: "fracplot", emoji: "\u{1F355}", label: "Graficador de fracciones", blurb: "Escribe una fracci\xF3n en sus dos casillas \u2014abajo en cu\xE1ntos pedazos se corta, arriba cu\xE1ntos se pintan\u2014 y la dibuja al instante. Cada \xABInsertar\xBB la estampa en el slide como una figura que se mueve y se estira como cualquier imagen, y el panel se queda abierto para pedir la siguiente: 3/4, luego 2/3, luego 5/8\u2026 Pizza o barra, con la fracci\xF3n escrita debajo. Una impropia como 4/3 sale como dos pizzas.", premium: false, ready: true },
2550
+ // QR: el puente entre el slide proyectado y el celular del alumno. Estático a
2551
+ // propósito (el enlace va grabado en el dibujo): cero infraestructura, cero costo
2552
+ // mensual y sigue funcionando dentro del .zip exportado a LearnPress.
2553
+ { type: "qr", emoji: "\u{1F533}", label: "QR \u2014 el que se come el enlace", blurb: "Pon un enlace y sale un c\xF3digo QR que la clase escanea con el celular: la tarea, el formulario, un video, la sala de Booster Live. Al pulsar \xABGenerar\xBB, el c\xF3digo se COME el enlace: abre la boca y se traga a bocados unos c\xF3digos QR peque\xF1os; al cerrarla, los m\xF3dulos que faltaban caen en su sitio. Color, forma de los puntos y r\xF3tulo a tu gusto.", premium: false, ready: true },
2554
+ { type: "plotter", emoji: "\u{1F4C8}", label: "Graficador 2D \xB7 3D", blurb: "Orbe que abre un graficador de ecuaciones. En 2D: funciones, polares, param\xE9tricas, c\xEDrculos y elipses, inecuaciones, y los CORTES entre curvas marcados con sus coordenadas. En 3D: superficies z = f(x,y) que se giran con el dedo. Cada letra suelta (a, b, c) se vuelve un deslizador.", premium: true, ready: true }
2555
+ ];
2556
+ var APP_NATIVE_BOOSTERS = ["instructions", "magicwand", "magichat", "quitafondo", "transformar", "tombola", "trafficlight", "plotter", "labelit", "cycle", "fractions", "fracplot", "qr"];
2557
+ var ARENAS_APP = [
2558
+ { type: "classification", emoji: "\u{1F6F8}", label: "Clasificaci\xF3n \xB7 Espacio", blurb: "Arrastra cada palabra a su categor\xEDa correcta. Tema espacial con OVNIs y aliens.", premium: false, ready: true },
2559
+ { type: "classification-jungle", emoji: "\u{1F435}", label: "Clasificaci\xF3n \xB7 Selva", blurb: "Arrastra cada monito a su bungal\xF3 correcto. Tema selva con caba\xF1as de bamb\xFA.", premium: false, ready: true },
2560
+ { type: "gameshow", emoji: "\u{1F3AD}", label: "Game Show", blurb: "Concurso de 2 estudiantes: tel\xF3n, ruleta, preguntas con tiempo y podio.", premium: true, ready: true },
2561
+ { type: "race", emoji: "\u{1F3CE}\uFE0F", label: "Carrera de Karts", blurb: "Carrera estilo F\xF3rmula 1: hasta 4 jugadores eligen corredor, responden preguntas y ganan nitro.", premium: true, ready: true },
2562
+ { type: "pirate", emoji: "\u{1F3F4}\u200D\u2620\uFE0F", label: "Batalla Pirata", blurb: "Concurso de preguntas para TODA la clase desde sus celulares: gana oro por contestar r\xE1pido, y el que acierta abre un cofre que puede quitarle oro a un compa\xF1ero.", premium: true, ready: true },
2563
+ { type: "mole", emoji: "\u{1F573}\uFE0F", label: "Mole Smash", blurb: "Aplasta los topos que llevan la respuesta correcta. Cada cartel admite texto e imagen; el maestro pone la consigna y las opciones.", premium: false, ready: true },
2564
+ { type: "jigsaw", emoji: "\u{1F9E9}", label: "Rompecabezas", blurb: "Sube UNA imagen y la arena la corta sola en piezas de rompecabezas. El alumno las arrastra desde la bandeja hasta rehacer el dibujo. Sirve para cualquier materia: un mapa, una c\xE9lula, un cuadro.", premium: false, ready: true },
2565
+ { type: "memory", emoji: "\u{1F0CF}", label: "Memory Game", blurb: "Encuentra las parejas ocultas. Cada carta acepta imagen, audio y texto: imagen\u2194imagen con audio, imagen\u2194palabra o palabra\u2194definici\xF3n. Individual o por equipos.", premium: false, ready: true },
2566
+ // Word Storm nació en we-boosters y se trajo app-native (ver APP_NATIVE_ARENAS): su ficha
2567
+ // ya no viene del puente, así que vive aquí como cualquier arena de la app.
2568
+ { type: "booster-escape", emoji: "\u{1F9ED}", label: "Booster Escape", blurb: "Laberinto a contrarreloj con dos modos: Individual (corre por los pasillos, esquiva a los enemigos y llega a la respuesta correcta; 3 vidas) y Versus 1v1. Un toque y el explorador corre solo hasta topar. Edita las preguntas (con imagen/audio).", premium: false, ready: true },
2569
+ { type: "las-vegas", emoji: "\u{1F3B0}", label: "The Fabulous Las Vegas", blurb: "Tragamonedas para construir frases: jala la palanca, los rodillos frenan uno a uno y sale una combinaci\xF3n \u2014SHE \xB7 WRITTEN \xB7 A LETTER\u2014 que la clase dice en voz alta. De 1 a 4 rodillos, y cada casilla puede ser una palabra o un dibujo.", premium: false, ready: true },
2570
+ { type: "word-storm", emoji: "\u2328\uFE0F", label: "Word Storm", blurb: "Lluvia de palabras: escr\xEDbelas con el teclado para destruirlas antes de que toquen la l\xEDnea de peligro. La dificultad sube solita. Edita la lista de palabras.", premium: false, ready: true }
2571
+ ];
2572
+ var soloFicha = (e) => ({ type: e.type, emoji: e.emoji, label: e.label, blurb: e.blurb, premium: e.premium, ready: e.ready });
2573
+ var ALL_WIDGETS = [
2574
+ ...WIDGETS,
2575
+ ...FICHAS_BOOSTERS.filter((e) => !APP_NATIVE_BOOSTERS.includes(e.type)).map(soloFicha)
2576
+ ];
2577
+ var ARENAS = [
2578
+ ...ARENAS_APP,
2579
+ ...FICHAS_ARENAS.filter((e) => !APP_NATIVE_ARENAS.includes(e.type)).map(soloFicha)
2580
+ ];
2581
+
2582
+ // ../src/nucleo/fabricas.ts
2583
+ function construirWidget(type, manos) {
2584
+ const ext = FICHAS_BOOSTERS.find((e) => e.type === type && !APP_NATIVE_BOOSTERS.includes(e.type));
2585
+ const meta = ext ?? WIDGETS.find((w) => w.type === type);
2586
+ const etiqueta = meta?.label ?? type;
2587
+ if (!meta) return { ok: false, motivo: "desconocido", tipo: type, etiqueta };
2588
+ if (meta.premium && !manos.premium) return { ok: false, motivo: "premium", tipo: type, etiqueta };
2589
+ if (!meta.ready) return { ok: false, motivo: "no-listo", tipo: type, etiqueta };
2590
+ const p = manos.nextPos(46, 42);
2591
+ const namesDefault = ["Ana", "Luis", "Mar\xEDa", "Carlos", "Sof\xEDa", "Diego", "Valeria", "Mateo"];
2592
+ const wcfg = ext ? { ext: { ...ext.defaultWcfg } } : type === "instructions" ? { ext: { title: "Instructions", icon: "\u{1F4A1}", body: "Read the text on the board.\nDiscuss it with your partner.\nWrite three sentences in your notebook." } } : type === "magicwand" ? { ext: { images: [] } } : type === "magichat" ? { ext: {} } : type === "fracplot" ? { ext: { shape: "pie", label: true, color: 0, sound: true } } : type === "tombola" ? { ext: { kind: "numbers", from: 1, to: 30 } } : type === "trafficlight" ? { ext: { sound: true, start: "green" } } : type === "plotter" ? { ext: defaultPlotterCfg() } : type === "labelit" ? { ext: { mode: "reveal", points: [], shuffle: true, sound: true } } : type === "cycle" ? { ext: {
2593
+ closed: true,
2594
+ guess: false,
2595
+ sound: true,
2596
+ stages: Array.from({ length: 4 }, () => ({ id: cycleUid(), text: "" }))
2597
+ } } : type === "fractions" ? { ext: { mode: "add", title: "", a: "1/2", b: "1/3", denoms: FRAC_DENOMS, labels: true, sound: true } } : type === "qr" ? { ext: { ...QR_DEFAULTS, gen: 0 } } : type === "wheel" ? { names: namesDefault } : type === "timer" ? { seconds: 60 } : type === "score" ? { teams: ["Equipo A", "Equipo B"] } : type === "groups" ? { names: namesDefault, teamCount: 2 } : type === "mystery" ? { items: ["apple", "happy", "run", "book", "blue", "friend", "school", "water"] } : { dice: 1 };
2598
+ const sz = ext ? ext.defaultSize : type === "trafficlight" ? { w: 16, h: 42 } : type === "labelit" ? { w: 52, h: 68 } : type === "cycle" ? { w: 66, h: 64 } : type === "fractions" ? { w: 74, h: 50 } : type === "qr" ? { w: 16, h: 33 } : { w: 7, h: 12.4 };
2599
+ const pos = sz.h > 55 ? { x: (100 - sz.w) / 2, y: (100 - sz.h) / 2 } : p;
2600
+ return { ok: true, el: { id: manos.uid("wdg"), kind: "widget", widget: type, x: pos.x, y: pos.y, w: sz.w, h: sz.h, wcfg } };
2601
+ }
2602
+ function construirArena(type, manos) {
2603
+ const ext = FICHAS_ARENAS.find((e) => e.type === type && !APP_NATIVE_ARENAS.includes(e.type));
2604
+ const metaArena = ARENAS.find((a) => a.type === type);
2605
+ const etiquetaArena = metaArena?.label ?? String(type);
2606
+ if (!metaArena) return { ok: false, motivo: "desconocido", tipo: String(type), etiqueta: etiquetaArena };
2607
+ if (metaArena.premium && !manos.premium) return { ok: false, motivo: "premium", tipo: String(type), etiqueta: etiquetaArena };
2608
+ if (!metaArena.ready) return { ok: false, motivo: "no-listo", tipo: String(type), etiqueta: etiquetaArena };
2609
+ const acfg = ext ? ext.defaultAcfg : type === "classification" ? { categories: ["Animals", "Plants"], items: [
2610
+ { text: "cat", cat: 0 },
2611
+ { text: "hamster", cat: 0 },
2612
+ { text: "dog", cat: 0 },
2613
+ { text: "daisy", cat: 1 },
2614
+ { text: "oak tree", cat: 1 },
2615
+ { text: "tulip", cat: 1 }
2616
+ ] } : type === "classification-jungle" ? { categories: ["verbs", "nouns"], items: [
2617
+ { text: "run", cat: 0 },
2618
+ { text: "jump", cat: 0 },
2619
+ { text: "sit", cat: 0 },
2620
+ { text: "eat", cat: 0 },
2621
+ { text: "cat", cat: 1 },
2622
+ { text: "house", cat: 1 },
2623
+ { text: "tree", cat: 1 }
2624
+ ] } : type === "gameshow" ? { questions: [
2625
+ { q: "The winter solstice occurs in December.", options: ["True", "False"], answer: 0 },
2626
+ { q: "Which planet is the Red Planet?", options: ["Venus", "Mars", "Jupiter", "Saturn"], answer: 1 },
2627
+ { q: "A group of wolves is called a ___.", options: ["pack", "herd", "school", "flock"], answer: 0 },
2628
+ { q: "How many continents are there?", options: ["5", "6", "7", "8"], answer: 2 }
2629
+ ] } : type === "race" ? { track: 0, questions: [
2630
+ { q: "Which planet is the Red Planet?", options: ["Venus", "Mars", "Jupiter", "Saturn"], answer: 1 },
2631
+ { q: "A baby dog is called a ___.", options: ["kitten", "puppy", "cub", "foal"], answer: 1 },
2632
+ { q: "How many continents are there?", options: ["5", "6", "7", "8"], answer: 2 },
2633
+ { q: "Water boils at 100 degrees ___.", options: ["Fahrenheit", "Celsius", "Kelvin", "Newton"], answer: 1 },
2634
+ { q: 'The opposite of "fast" is ___.', options: ["slow", "quick", "rapid", "swift"], answer: 0 }
2635
+ ] } : type === "memory" ? {
2636
+ pairs: [
2637
+ { a: { text: "apple" }, b: { text: "a round red or green fruit" } },
2638
+ { a: { text: "run" }, b: { text: "to move fast on your feet" } },
2639
+ { a: { text: "teacher" }, b: { text: "a person who helps you learn" } },
2640
+ { a: { text: "winter" }, b: { text: "the coldest season of the year" } }
2641
+ ],
2642
+ back: "booster",
2643
+ mode: "solo",
2644
+ teams: 2
2645
+ } : type === "pirate" ? { secs: 20, chests: true, pirateQs: [
2646
+ { q: "Which sentence is correct?", answer: 0, options: [
2647
+ { text: "She goes to school." },
2648
+ { text: "She go to school." },
2649
+ { text: "She going to school." },
2650
+ { text: "She gone to school." }
2651
+ ] },
2652
+ { q: 'The opposite of "expensive" is\u2026', answer: 0, options: [
2653
+ { text: "cheap" },
2654
+ { text: "rich" },
2655
+ { text: "costly" },
2656
+ { text: "heavy" }
2657
+ ] },
2658
+ { q: "Yesterday I ___ to the market.", answer: 1, options: [
2659
+ { text: "go" },
2660
+ { text: "went" },
2661
+ { text: "gone" },
2662
+ { text: "going" }
2663
+ ] }
2664
+ ] } : type === "mole" ? { prompt: "\xA1Aplasta los topos con la oraci\xF3n correcta!", secs: 60, sets: [
2665
+ { options: [{ text: "The cat is on the bed." }, { text: "The cat are on the bed." }], answer: 0 },
2666
+ { options: [{ text: "She goes to school." }, { text: "She go to school." }], answer: 0 },
2667
+ { options: [{ text: "My name is Sara." }, { text: "My name are Sara." }], answer: 0 },
2668
+ { options: [{ text: "They are playing." }, { text: "They is playing." }], answer: 0 }
2669
+ ] } : type === "jigsaw" ? { prompt: "Arma el rompecabezas", level: "normal", imgs: [] } : type === "booster-escape" ? {} : type === "las-vegas" ? {} : type === "word-storm" ? { words: [...WORDSTORM_SAMPLE] } : {};
2670
+ return { ok: true, el: { id: manos.uid("arena"), kind: "arena", arena: type, x: 0, y: 0, w: 100, h: 100, acfg } };
2671
+ }
2672
+ export {
2673
+ ALL_WIDGETS,
2674
+ ARENAS,
2675
+ ESQUEMAS,
2676
+ FORMA_DEF,
2677
+ buscarDibujo,
2678
+ buscarFondo,
2679
+ cargarBiblioteca,
2680
+ construirArena,
2681
+ construirCatalogo,
2682
+ construirWidget,
2683
+ describirTipo,
2684
+ ejecutar,
2685
+ fallosEnTexto,
2686
+ nuevoBorrador,
2687
+ validarCfgDe
2688
+ };