Apps de mesa

Escribe una pequeña página —un puzzle, una consola, una votación— y preséntala a tus jugadores: las cuatro plantillas, cómo hablar con la mesa, por qué la respuesta no puede ir en el código, los recursos y lo que dentro de la caja no existe.

Una app de mesa es una pequeña página que escribes tú (HTML, CSS y JavaScript) y que enseñas a tus jugadores como una ventana flotante más. Por ejemplo, un puzzle que tocan todos a la vez, una consola de nave, una votación o una cuenta atrás.

No hace falta saber programar para usarla: hay cuatro plantillas que funcionan tal cual, cambiando solo un par de cosas.


Dónde está

Modo Inmersión → Mostrar a la mesa → pestaña App.

Ahí tienes las plantillas, tus apps guardadas y el botón Ampliar, que abre el editor a pantalla completa con resaltado, avisos en línea, autocompletado y la subida de recursos. Para escribir código largo, usa el grande.


El camino corto

  1. Pulsa Cerradura de runas. Se rellena todo, incluida la respuesta.
  2. Pulsa Probar: la ves tal y como la verán ellos.
  3. Pulsa Presentar a la mesa.

La app aparece como ventana flotante para todos los miembros de la campaña, se mueve, se redimensiona, se minimiza y se retira con la papelera (que la quita para todos).


Cómo funciona por debajo

Tu código se ejecuta en el navegador de cada jugador, dentro de una caja aislada. Gracias a esa caja, tu app no puede ver la sesión de nadie, ni sus datos, ni mandar nada fuera.

A cambio, hay cosas que fuera funcionan y aquí no, porque cada una abriría un agujero de seguridad. Están todas en la pestaña Ayuda del editor, y el linter te avisa mientras escribes.

No existeQué pasaEn su lugar
localStorage, sessionStorageLanzan un errorUna variable; y roliando.guardar() para lo que deba durar
alert, confirm, promptBloqueadosPinta el mensaje en la página
fetch, XMLHttpRequestNo hay redroliando.enviar() para hablar con la mesa
window.openBloqueado—
import, requireNo hay módulosPega lo que necesites

El sonido sí funciona, pero no arranca hasta que alguien toca la ventana. Crea o reanuda tu AudioContext dentro de un clic, o llama a roliando.sonar() desde uno.


Hablar con la mesa

Dentro de tu app tienes un objeto roliando, que reúne todo lo que puedes pedirle a Roliando:

// ── QUIÉN Y QUÉ HAY ────────────────────────────────────────────────────────
roliando.yo                    // { nombre, esMaster } — quién está mirando
roliando.assets                // { nombre: url } de lo que hayas subido
roliando.jugadores()           // [{ id, nombre, esMaster }] — quién está sentado AHORA
roliando.alJugadores(fn)       // fn(lista) cuando alguien entra o sale

// ── LO QUE SE GUARDA ───────────────────────────────────────────────────────
roliando.estado                // lo guardado cuando arrancó tu app (o null)
roliando.partida()             // lo guardado AHORA mismo (función, no valor)
roliando.versionPartida()      // su versión, por si depuras
roliando.guardar(datos)        // que sobreviva a recargar
roliando.alEstado(fn)          // fn(estado, quien) cuando la partida cambia
roliando.alAviso(fn)           // fn(motivo) si algo NO se pudo guardar

// ── HABLAR EN VIVO ─────────────────────────────────────────────────────────
roliando.enviar(datos)         // mándalo a los demás, ahora (efímero)
roliando.alRecibir(fn)         // fn(datos, quien) — la otra mitad de `enviar`

// ── PUZZLES ────────────────────────────────────────────────────────────────
await roliando.comprobar(intento)  // → { ok, intentos, agotado }
await roliando.miParte()           // → { hay, master, mia, partes } — tu trozo del reparto
roliando.alAbrir(fn)               // el máster lo ha abierto a mano: se acabó el puzzle
roliando.progreso()                // quién va por dónde (null si no diriges)
roliando.alProgreso(fn)            // fn(lista) cuando cambia (solo si diriges)

// ── HACIA FUERA ────────────────────────────────────────────────────────────
roliando.resuelto('ha abierto la cerradura.')  // al chat de la campaña
roliando.sonar('gong')                         // por el volumen de cada uno
await roliando.tirar('2d6+3', { etiqueta: 'Ataque' })  // dados 3D de verdad → { ok, total, dados }

roliando.partida() y roliando.progreso() son funciones y no valores porque un valor tendría que viajar dentro del documento, y cambiar el documento recarga el marco y reinicia tu app.

La ficha de quien está mirando

Una app puede leer unos pocos números de la ficha de quien la tiene delante, y sólo los que declare de antemano:

// En la app, junto al HTML/CSS/JS: qué campos necesitas.
ficha: ['fuerza', 'pg']
const f = await roliando.miFicha();
// → { hay: true, nombre: 'Kira', campos: { fuerza: 14, pg: 22 }, faltan: [] }
if (f.hay) pintar(f.nombre, f.campos.fuerza);

Hay cuatro reglas, y las aplica el motor:

  1. Sólo la tuya. Cada navegador contesta con lo suyo; no hay forma de pedir la de otro.
  2. Sólo lo declarado. Lo que no esté en ficha no llega, y lo declarado se enseña en la ventana antes de que nadie la toque.
  3. Nada de texto libre. Sólo números. Ni trasfondo, ni notas, ni historia, ni contactos, ni inventario, que es lo que alguien puede querer guardarse ante su propia mesa.
  4. Nunca se escribe. Una app no toca la ficha de nadie.

Lo que esto no impide. La caja impide que tu app mande algo a internet; no impide que se lo cuente a la mesa. Si tu app lee la Fuerza de cada uno y la enseña con enviar(), la ven todos. Es lo normal en un juego de mesa, pero lo decides tú.

Si la montas en Creaciones, dile de qué sistema es. Ahí no hay mesa delante, así que la pestaña Ficha te pide elegir entre tus sistemas y D&D, o «ninguno» si prefieres rellenarlo a mano. Eso no la bloquea: la app se puede presentar en cualquier mesa, y al hacerlo te avisa si sus campos son de otro sitio.

No hace falta saberse las claves de memoria. En el editor grande hay una pestaña Ficha: te enseña los campos numéricos del sistema de tu mesa con su clave y su rótulo, los eliges con un clic, te escribe el código y tiene un botón Probar con mi ficha que te dice qué devolvería miFicha() ahora mismo, usando la función real.

La misma app en otra mesa

fuerza y pg son los campos de un sistema. La misma app en una mesa que juega a otra cosa no los encuentra, así que la respuesta trae faltan con lo que no ha sabido dar:

const f = await roliando.miFicha();
if (f.faltan.length) pedirlosAMano(f.faltan);   // y la app sigue funcionando

Al presentarla, a quien le falte algún número le sale una tira dentro de la ventana («esta app necesita unos números que tu ficha no sabe darle») para ponerlos una vez. Entran por el mismo sitio que lo leído y mandan sobre ello, así que para la app no hay diferencia. Así funcionan también los minijuegos cuyos números no están en ninguna ficha, como la Fuerza de la bestia que monta un equipo.

Guarda también para qué sistema la escribiste (sistema), para poder avisarte antes de presentarla en una mesa distinta.

Leer la mesa y pedirle cosas

Una app puede mirar un trozo de la mesa si lo declara, y pedirle una acción concreta si también la declara. En los dos casos manda lo que ve quien mira: un jugador no recibe nada que el máster tenga oculto.

// La app declara: lee: ['combate', 'puertas'], puede: ['abrir_puerta:id-de-la-puerta']
const mesa = await roliando.mesa();          // { hay, lee, combate, puertas }
if (mesa.combate.activo) pintarRonda(mesa.combate.ronda);

const r = await roliando.pedir({ tipo: 'abrir_puerta', muro: 'id-de-la-puerta' });
if (!r.ok) console.log(r.motivo);            // 'no_declarada', 'solo_master', 'demasiado_rapido'

lee admite escena, combate, puertas, luces, zonas, jugadores y tokens (los tokens a la vista, con su estado, etiquetas, altura, dueño y bando; la vida y la defensa de un PNJ solo las ve el máster). puede admite mensaje, abrir_puerta, cerrar_puerta, cerrar_con_llave, luz, oscuridad, siguiente_turno y texto_flotante, con o sin id (abrir_puerta:ID solo vale para esa puerta). La app no toca el mundo: entrega la acción y la ejecuta la mesa del máster con sus permisos, una cada 800 ms.

En D&D las claves son fijas

Una ficha de D&D no tiene campos que ponga nadie, así que el vocabulario es siempre el mismo:

QuéClaves
Característicasstr dex con int wis cha, y su modificador, str_mod…
Habilidadesathletics, animal_handling, sleight_of_hand, stealth… (con guion bajo)
Derivadasac hp hp_max initiative speed level prof passive_perception

Salen efectivas: si llevas un Cinturón de Fuerza, str es la que tienes puesta, no la de la hoja. La pestaña Ficha del editor te las lista igual que las de un sistema del creador.

Ojo: en D&D esos números se leen de un espejo que tu ficha deja al abrirse (igual que la CA y las salvaciones que ya usa el combate). Una ficha que su dueño no ha abierto desde hace tres niveles tiene el espejo viejo. Si ves un número que no cuadra, basta con que abra su ficha una vez.

Lo que sigue sin alcanzarse

La ficha de otro jugador, el texto libre de cualquiera y escribir en ninguna ficha. No hay ninguna forma de hacerlo.

Que todos vean lo mismo

enviar y alRecibir son las dos mitades de lo mismo. Si tu app solo manda, cada uno ve lo suyo:

// Al girar una runa: que los demás la vean girar
roliando.enviar({ giros: giros });

// Y al revés
roliando.alRecibir(function (datos) {
  if (datos.giros) { giros = datos.giros; pintar(); }
});

enviar es rápido y efímero: para lo que se toca en vivo. Lo que debe seguir ahí después de un F5 va en guardar, que persiste y va más espaciado. Lo normal es llamar a los dos.

Las dos caras de una app

El mismo código recibe roliando.yo.esMaster. Los jugadores ven la cerradura; tú ves el tablero:

if (roliando.yo.esMaster) {
  roliando.alProgreso(function (lista) {
    // [{ nombre: 'Cerbrick', intentos: 3, resuelto: false }, ...]
    pintarTablero(lista);
  });
}

La respuesta de un puzzle

Esto es lo más importante de la guía. Tu código viaja al navegador de tus jugadores para ejecutarse allí, así que cualquier cosa escrita dentro se puede leer, incluida una solución.

Por eso hay un campo La respuesta, abajo a la derecha en el editor grande. Lo que pongas ahí se queda en el servidor y no viaja a nadie. Tu app solo puede preguntar:

roliando.comprobar(giros).then(function (r) {
  if (r.ok) { /* abierta */ }
  else if (r.agotado) { /* se acabaron los intentos */ }
  else { /* falló: r.intentos dice cuántas veces lleva */ }
});

La respuesta puede ser una lista (["ᛁ","ᚦ","ᚠ","ᛚ"]), un número, un objeto o una palabra suelta: se compara con lo que tu app manda a comprobar.

El tope de intentos

Con la respuesta a salvo, lo único que puede hacer alguien que quiera saltárselo es probar combinaciones. Cuatro runas con seis símbolos son 1.296 combinaciones, que un bucle prueba enseguida.

El campo intentos pone un tope por persona. Vacío = sin tope, suficiente para un acertijo de ambiente. Esa misma cuenta es la que alimenta tu tablero.


Imágenes y sonidos

Pestaña Recursos del editor grande. Lo que subas ahí son las únicas direcciones que tu app puede cargar: cualquier otra se bloquea, y lo hace en silencio (no falla, simplemente no aparece). El linter te avisa si escribes una dirección de fuera.

En el código los pides por su nombre:

<img id="mapa">
document.getElementById('mapa').src = roliando.assets.mapa_estelar;
roliando.sonar('gong');

sonar es mejor que un <audio> suelto: suena fuera de la app, por el volumen de efectos que cada jugador ya tiene puesto, así que respeta su slider y no se pelea con la llamada de voz.

Tirar dados

Tu app puede pedir una tirada y que la vea toda la mesa: los dados 3D ruedan en la pantalla de cada uno, la tirada queda en el registro y en el chat, y sale en la emisión. La tira Roliando, no tu app: tu código no puede decidir el resultado, solo recibirlo.

const r = await roliando.tirar('1d20+5', { etiqueta: 'Percepción' });
if (r && r.ok) mostrar(r.total);        // r.dados = los valores de cada dado
  • Fórmula: lo que entiende la bandeja (2d6+3, 1d20, 3d6+1d4-1), hasta 50 dados.
  • Opciones: etiqueta (qué se tira), quien (el nombre que sale; por defecto, el personaje con el que juega quien la lanza), tipoDano (fuego, frio… tiñe los dados) y secreta (solo cuenta si la lanza el máster: la tirada no la ve la mesa).
  • Respuesta: { ok, total, dados, formula } cuando los dados se paran, o { ok: false, error } si la fórmula no vale. Si había otra en marcha o pides muy seguido: { ok: false, ocupado: true } (una a la vez y una cada segundo y medio).

El editor de código

El editor grande trae lo que se espera de uno de verdad, y todo lo que sabe de Roliando sale de la misma lista que la pestaña Ayuda, así que no se contradicen:

  • Autocompletado de todo lo invocable. Escribe roliando. y salen todas las funciones con su firma al lado; con una seleccionada se lee qué hace y se ve un ejemplo. Ctrl+Espacio lo abre en cualquier sitio, y con él salen también los fragmentos (escuchar, partidaViva, tirarDados…) con huecos que se recorren con Tab.
  • La firma mientras escribes. Al abrir el paréntesis de roliando.tirar( se ve la función con el parámetro en el que estás marcado. Pasar el puntero por un nombre conocido enseña lo mismo.
  • Errores con su línea. Un fallo de sintaxis dice en qué línea está; los avisos del linter (localStorage, fetch, alert…) también. Pulsar un aviso de la lista de debajo lleva el cursor a su línea.
  • Consola de la vista previa. Lo que tu app escribe con console.log, sus errores (con la línea de tu JS, enlazada) y lo que le pide a la mesa (guardar, enviar, tirar…) salen bajo la vista previa. En la previa no hay mesa: mesa, miFicha, pedir y tirar contestan «no hay» al momento.
  • Ayuda con buscador. Busca por nombre o por lo que quieres hacer y pulsa Insertar el ejemplo: se añade al final del JS.
  • Ordenar (Mayús+Alt+F), Buscar y reemplazar (Ctrl+F), plegado de bloques, minimapa opcional y una lista de atajos en la barra de arriba del editor.

La caja no cambia por nada de esto: la consola es solo un mensaje más hacia fuera, y la app sigue sin red, sin almacenamiento y sin poder tocar la página que la rodea.


Guardar y reutilizar

Guardar deja la app en tu biblioteca con su respuesta y su tope. Aparece en Mis apps y vuelve al editor de un clic.


Las cuatro plantillas

Qué esQué tocar
Cerradura de runasCuatro runas que giran; la combinación la comprueba el servidorLos cuatro índices de la respuesta
Votación de la mesaUna pregunta, unas opciones, recuento en vivoLa pregunta y las opciones, primera línea del JS
Cuenta atrásEl reloj corre para todos; lo lleva el másterSEGUNDOS
Pizarra compartidaUn lienzo donde dibujan todos a la vezNada

La cuenta atrás la lleva uno solo y los demás la siguen, porque con dos relojes corriendo a la vez las cuentas compartidas se descuadran.

Y tres de inicio para el editor de código, para quien prefiere escribir desde casi cero: En blanco (un título y una nota en cada parte), Contador compartido (un número que sube y baja para toda la mesa y sobrevive a recargar) y Tabla editable (filas que se escriben en vivo y se guardan). Enseñan el ciclo entero: leer lo guardado, cambiar, guardar y mandar, escuchar.


Límites

  • Tres apps a la vez por mesa. Cada una es una página web con su JavaScript corriendo en el navegador de todos.
  • 72.000 caracteres entre HTML, CSS y JS (24.000 de HTML, 20.000 de CSS y 26.000 de JS), y hasta 24 recursos subidos. El documento entero viaja por la red a toda la mesa cada vez que se presenta, de ahí el tope.
  • Cuenta con una o dos décimas de segundo de ida y vuelta. Para puzzles, diales, palancas y revelados no se nota; para algo que dependa de reflejos, no sirve.
  • El sonido suena en todos, pero cada navegador arranca cuando le llega el mensaje. Para ambiente y golpes de tensión sobra; para algo sincronizado al milisegundo, no.
  • En modo emisión la app se ve, pero ahí es una foto: nadie la toca.
  • En la sala de espera todavía no se pueden presentar.

Si algo no va

«No se ve nada». Mira el contador de errores arriba: un fallo de sintaxis deja la app muda: se pinta el HTML y no pasa nada más.

«La imagen no carga». Comprueba que la subiste en Recursos: si es una dirección de fuera, se bloquea sin avisar.

«Dice que no aunque acierto». Revisa que esté puesta La respuesta: sin ella, comprobar dice que no a todo.

«Cada uno ve una cosa». Te falta roliando.alRecibir: estás mandando pero no escuchando.

«No suena». El navegador no deja sonar hasta que alguien toca la ventana. Arranca el audio desde un clic.

Todas las guías de Roliando