Capítulo salas de la guía Neblla — salas, lobby, gathering y emparejamiento. El contrato base está en core.txt. # Salas y emparejamiento ## El lobby Todo usuario conectado aterriza en la sala `lobby`. Quién hay y quién soy: - `neblla.room.users` — array de usuarios de la sala actual. - `neblla.me` — el usuario de esta sesión. `memberJoinedRoom` se dispara una vez por cada usuario que ya estaba al llegar y otra vez por cada entrada nueva; el flag `alreadyPresent` los distingue: ```javascript neblla.on('memberJoinedRoom', event => { if (event.alreadyPresent) { // ya estaba — añadirlo a la lista en silencio } else { // acaba de entrar — sonido, animación } }); ``` ```javascript neblla.on('memberLeftRoom', event => { // event.user se ha ido — quitarlo de la lista }); ``` El lobby es buen momento para publicar el perfil visible del usuario: ```javascript neblla.setPublicInfo({ name: 'John', color: '#ff8800' }); ``` Queda en `user.publicInfo` para todos los de la misma sala y se restaura en reconexiones. Cuando alguien lo actualiza, todos reciben el evento `publicInfo`: ```javascript neblla.on('publicInfo', event => { // event.user.publicInfo tiene los nuevos datos }); ``` Render de lista de jugadores: ```javascript const renderPlayers = () => { neblla.room.users.forEach(user => { console.log(user.publicInfo?.name || 'Guest', user.isPresenter ? '(host)' : '', user.isMe ? '(you)' : ''); }); }; neblla.on('memberJoinedRoom', renderPlayers); neblla.on('memberLeftRoom', renderPlayers); ``` ## Salas con nombre (estilo chat) Mismo nombre de sala = mismo grupo. Un usuario solo puede estar en una sala a la vez. ```javascript neblla.joinRoom('eurovision'); neblla.sendMessage({ type: 'msg', text: '¡me encanta esta canción!' }); ``` Los mensajes solo llegan a quien esté dentro. Cambiar de sala: `joinRoom('otra')`. Volver al lobby: `leaveRoom()`. Por defecto cualquier usuario crea salas con solo hacer `joinRoom('nombre-nuevo')`. Para limitar la app a una lista fija de salas válidas: `developers.neblla.com → Apps → Mi app → Settings → Rooms`. ## Tipos de sala La política se elige por app desde el dashboard: - **`ephemeral`** (default) — existe solo mientras hay alguien conectado; desaparece cuando se va el último usuario. Sin DB, sin historial: quien entra después no ve lo escrito antes. - **`public`** — persistida en MongoDB. Cualquier usuario de la app puede entrar. Historial de chat guardado (cifrado en reposo). Sobrevive a reinicios. Foros, lounges, lobbies públicos. - **`private`** — persistida con whitelist de personas. Solo miembros entran. Los *owners* (rol intra-sala) invitan, expulsan, promueven a otros owners y borran mensajes. Si el owner abandona, el rol pasa al miembro más antiguo. Si la whitelist queda vacía, la sala se borra. Guilds, salas privadas, DMs grupales. Una misma app puede mezclar tipos: las salas nacen `ephemeral` y se promueven con `persistRoom`. ## Partidas: bookRoom → gathering → startRoom Una partida necesita reservar espacio, fijar reglas, esperar jugadores y empezar: ```javascript const roomId = await neblla.bookRoom({ config: { tiempoMax: 180, modo: 'blitz' } }); ``` - `config` es un JSON abierto del desarrollador. Neblla no lo interpreta. Se consulta con `neblla.room.config` (también dentro de `server/gathering.js` y `server/room.js`). - La sala nace en estado `gathering` y es visible desde el lobby vía `neblla.gathering`: array de salas en formación, cada una con `{ name, config, users }`, refrescado por el evento `gathering`. ```javascript // Desde el lobby, listar las partidas abiertas: neblla.gathering.forEach(room => { console.log(room.name, room.users.length, room.config); }); ``` - Los jugadores entran con `joinRoom(roomId)`. Cuando están todos, alguien llama a `neblla.startRoom()`: la sala pasa a `running`, el server arranca y funciona `tellServer` / `messageFromServer`. - Desde `Settings → Rooms` se limita el máximo de jugadores por sala y si se permite entrar como espectador a la partida ya empezada. Flujo correcto (no uses `createRoom`, es @legacy y se salta el estado `gathering`): ```javascript // El nombre de la sala lo genera el servidor — viene en el ack del bookRoom. const roomName = await neblla.bookRoom({ config: { maxPlayers: 4 } }); await neblla.joinRoom(roomName); // usuarios entran, esperan, se llena neblla.startRoom(); ``` Ni `bookRoom` ni `createRoom` aceptan un nombre de cliente — el id de sala siempre lo elige el servidor para evitar colisiones. Sí puedes pedir su longitud: `bookRoom({ length: 3 })` da un código de 3 caracteres (3–12, por defecto 5; alfabeto sin 0/O/1/I/L). ## Gathering configurado vs custom `server/gathering.js` decide *cuándo* empieza la partida. Para los casos comunes no hace falta código: desde `Settings → Rooms` se configuran reglas declarativas — empezar con un mínimo de jugadores, cuando pasen X segundos desde la última entrada, cuando llegue un mensaje específico (botón "Empezar" del anfitrión), o combinaciones. Escribe `gathering.js` solo si la lógica de arranque es más compleja. `server/lobby.js` igual: solo si hay decisiones reales en el lobby (matchmaking inteligente, cola de espera, agrupación por nivel). Patrón de matchmaking: ```javascript // lobby.js const queue = []; neblla.on('messageToPresenter', event => { if (event.data.type === 'ready') { queue.push(event.user.id); if (queue.length >= 4) { neblla.bookRoom().then(roomId => { const players = queue.splice(0, 4); neblla.sendMessage({ type: 'match-found', roomId, players }); }); } } }); // client.js neblla.on('message', event => { if (event.data.type === 'match-found' && event.data.players.includes(neblla.me.id)) { neblla.joinRoom(event.data.roomId); } }); ``` ## API de salas persistentes `neblla.persistRoom({ name, type, whitelist? })` **(CLIENT)** — sube una sala recién reservada de `ephemeral` a `public` o `private`. Hay que llamarlo entre `bookRoom` y el primer `joinRoom` — una vez alguien entra, el tipo queda fijado. La whitelist (para `private`) bootstrappea miembros iniciales; el llamador se añade automáticamente como primer owner. Los ids de la whitelist son **per-app userIds** (los `id` de `neblla.room.users[i]`), no personIds — solo se resuelven contra usuarios online en esta app en ese momento. Los offline se añaden después vía `inviteToRoom`. `neblla.listRooms({ type? })` **(CLIENT)** — lista salas persistentes accesibles. Cada entrada: `{ name, type, creator, memberCount, activeUsers, lastActivityAt }`. `neblla.getRoom(name?)` **(CLIENT)** — inspecciona una sala por nombre (por defecto, la actual). `neblla.getMembers(name?)` **(CLIENT)** — solo `private`. Devuelve `{ members[], invites[] }`. `neblla.deleteRoom(name?)` **(CLIENT)** — borra una sala persistente. Moderadores (cualquier sala) y owners de privadas (la suya). Echa a todos al lobby y emite `roomDeleted`. `neblla.setRoomType(name, type)` **(CLIENT)** — promociona/baja entre `public` y `private`. Solo moderadores. Membresía de salas privadas: - `neblla.acceptInvite(roomName)` / `neblla.rejectInvite(roomName)` — responde a una invitación `memberInvited`. - `neblla.kickFromRoom(targetUserId)` — expulsa de la sala privada actual. Solo owner o moderador. - `neblla.abandonRoom(roomName?)` — renuncia a la membresía. Distinto de `leaveRoom`: leave = "me desconecto ahora", abandon = "quítame de la whitelist". - `neblla.promoteOwner(targetUserId)` / `neblla.demoteOwner(targetUserId)` — gestión de owners. Solo owner. No puedes auto-demotearte si eres el último. - `neblla.inviteToRoom(target)` — añade a la whitelist de tu sala privada. ## room.data `neblla.setRoomData(data)` **(CLIENT, presenter o server peer)** — sustituye `neblla.room.data` (persistente en salas `public`/`private`). Todos los miembros reciben `'roomDataUpdated'`. Sujeto al cap `maxDataSize`; un exceso emite `'roomDataError'`. ## Historial de chat (salas persistentes) `neblla.getRoomHistory({ name?, before?, limit? })` **(CLIENT)** — paginación del chat persistido. Newest-first. Default limit 50, max 200. Los mensajes borrados vienen con `body: null` y `deleted: true`. `neblla.deleteMessage(messageId, name?)` **(CLIENT)** — soft-delete. Autor, owner o moderador. Emite `messageDeleted` a todos. ## Eventos de salas Ciclo de vida general: - `'joinRoom'` **(CLIENT)** — entraste a una sala. `{ room, isLobby }` - `'leaveRoom'` **(CLIENT)** — `{ room, user }` - `'memberJoinedRoom'` **(BOTH)** — `{ user, room, isLobby, alreadyPresent }`. - `'memberLeftRoom'` **(BOTH)** — `{ user, room }` - `'memberJoined'` / `'memberLeft'` **(BOTH)** — alias compactos, payload `{ room: , user }`. NO confundir con `memberJoinedRoom` (objeto `room` completo + `isLobby` + `alreadyPresent`). - `'gathering'` **(CLIENT, lobby)** — refresh del listado de salas en formación. `{ rooms: [{ name, config, users }] }`. Reflejado en `neblla.gathering`. - `'roomStarted'` **(BOTH)** — el presentador llamó a `startRoom()`. `{ room, isMe }` - `'roomDataUpdated'` **(BOTH)** — `{ room, data }`. - `'roomDataError'` **(CLIENT)** — tu `setRoomData(...)` fue rechazado (size cap u otro). - `'roomJoinError'` **(CLIENT)** — fallo entrando a una sala (whitelist, kick, etc). Salas persistentes: - `'memberInvited'` **(CLIENT)** — owner añadió a invitados pendientes. - `'inviteAccepted'` / `'inviteRejected'` **(CLIENT)** - `'memberKicked'` **(CLIENT)** — quitado de la whitelist por owner/moderador. - `'kickedFromRoom'` **(CLIENT)** — push directo al expulsado. - `'memberAbandoned'` **(CLIENT)** — se auto-quitó. - `'ownerAdded'` / `'ownerRemoved'` **(CLIENT)** - `'roomDeleted'` **(CLIENT)** — `{ room, reason, by? }`. - `'roomTypeChanged'` **(CLIENT)** Chat persistido: - `'messagePersisted'` **(CLIENT)** — eco con el id necesario para borrar. - `'messageDeleted'` **(CLIENT)** — render tombstone. - `'messageDropped'` **(CLIENT)** — solo al autor, mensaje no persistido (too_large o rate_limited).