feat(multi-device): host puede participar como jugador

- Añadido modelo Usuario con pool de usuarios sincronizado
- El host ahora recibe palabra y rol como cualquier jugador
- UI de selección de perfil en pantallas de lobby
- Los clientes pueden ver usuarios del servidor o crear nuevos
- El juego no inicia hasta que el host selecciona perfil
This commit is contained in:
ShanaiaBot
2026-04-24 18:47:56 +02:00
parent 3df3ae1e95
commit d3fc3386f9
31 changed files with 1266 additions and 106 deletions

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:nearby_connections/nearby_connections.dart';
import '../modelos/usuario.dart';
/// Tipos de mensajes en el protocolo P2P
enum TipoMensaje {
@@ -14,6 +15,9 @@ enum TipoMensaje {
listo,
ping,
jugadorDesconectado,
usuarioNuevo,
usuarioEliminado,
usuariosActualizados,
}
/// Mensaje del protocolo P2P entre dispositivos
@@ -23,10 +27,7 @@ class MensajeP2P {
MensajeP2P({required this.tipo, required this.datos});
String toJson() => json.encode({
'tipo': tipo.name,
'datos': datos,
});
String toJson() => json.encode({'tipo': tipo.name, 'datos': datos});
factory MensajeP2P.fromJson(String jsonStr) {
final mapa = json.decode(jsonStr) as Map<String, dynamic>;
@@ -53,7 +54,8 @@ class JugadorConectado {
}
/// Callback para mensajes recibidos
typedef OnMensajeCallback = void Function(String endpointId, MensajeP2P mensaje);
typedef OnMensajeCallback =
void Function(String endpointId, MensajeP2P mensaje);
/// Servicio para conexiones P2P usando Google Nearby Connections API.
class ServicioNearby extends ChangeNotifier {
@@ -80,6 +82,9 @@ class ServicioNearby extends ChangeNotifier {
String? _faseActual;
Map<String, dynamic>? _datosPartida;
// Pool de usuarios para modo multi-dispositivo
final Map<String, Usuario> _usuariosPool = {};
bool get esHost => _esHost;
bool get conectado => _conectado;
bool get buscando => _buscando;
@@ -95,7 +100,11 @@ class ServicioNearby extends ChangeNotifier {
List<JugadorConectado> get jugadores => _jugadores.values.toList();
int get numJugadoresConectados => _jugadores.length;
Map<String, String> get hostsEncontrados => Map.unmodifiable(_hostsEncontrados);
Map<String, String> get hostsEncontrados =>
Map.unmodifiable(_hostsEncontrados);
/// Pool de usuarios disponibles para seleccionar en modo multi-dispositivo
List<Usuario> get usuarios => _usuariosPool.values.toList();
/// Registra un listener de mensajes
void onMensaje(OnMensajeCallback callback) {
@@ -113,6 +122,45 @@ class ServicioNearby extends ChangeNotifier {
}
}
// ==================== USER POOL ====================
/// Agrega un usuario al pool de usuarios
void agregarUsuario(Usuario usuario) {
_usuariosPool[usuario.id] = usuario;
notifyListeners();
}
/// Elimina un usuario del pool
void eliminarUsuario(String usuarioId) {
_usuariosPool.remove(usuarioId);
notifyListeners();
}
/// Obtiene un usuario por su ID
Usuario? getUsuario(String usuarioId) {
return _usuariosPool[usuarioId];
}
/// Sincroniza el pool de usuarios con una lista
void sincronizarUsuarios(List<Usuario> usuarios) {
_usuariosPool.clear();
for (final usuario in usuarios) {
_usuariosPool[usuario.id] = usuario;
}
notifyListeners();
}
/// Obtiene el jugador local del host (él mismo como participante)
/// Retorna un JugadorConectado con endpointId null porque es local
JugadorConectado? getJugadorLocal() {
if (_miNombre == null) return null;
return JugadorConectado(
endpointId: _miEndpointId ?? '', // vacío indica que es el host local
nombre: _miNombre!,
listo: true,
);
}
// ==================== HOST ====================
/// Inicia como host (anunciando el endpoint)
@@ -211,10 +259,13 @@ class ServicioNearby extends ChangeNotifier {
_hostEndpointId = endpointId;
_conectado = true;
// Enviar mensaje de unirse
enviarMensaje(endpointId, MensajeP2P(
tipo: TipoMensaje.unirse,
datos: {'nombre': _miNombre ?? 'Jugador'},
));
enviarMensaje(
endpointId,
MensajeP2P(
tipo: TipoMensaje.unirse,
datos: {'nombre': _miNombre ?? 'Jugador'},
),
);
}
notifyListeners();
} else {
@@ -228,10 +279,12 @@ class ServicioNearby extends ChangeNotifier {
final jugador = _jugadores.remove(endpointId);
if (jugador != null) {
// Notificar a todos que se desconectó
enviarATodos(MensajeP2P(
tipo: TipoMensaje.jugadorDesconectado,
datos: {'nombre': jugador.nombre, 'endpointId': endpointId},
));
enviarATodos(
MensajeP2P(
tipo: TipoMensaje.jugadorDesconectado,
datos: {'nombre': jugador.nombre, 'endpointId': endpointId},
),
);
}
} else {
// Cliente perdió conexión con host
@@ -241,7 +294,11 @@ class ServicioNearby extends ChangeNotifier {
notifyListeners();
}
void _onEndpointEncontrado(String endpointId, String endpointName, String serviceId) {
void _onEndpointEncontrado(
String endpointId,
String endpointName,
String serviceId,
) {
debugPrint('Host encontrado: $endpointName ($endpointId)');
_hostsEncontrados[endpointId] = endpointName;
notifyListeners();
@@ -303,17 +360,20 @@ class ServicioNearby extends ChangeNotifier {
endpointId: endpointId,
nombre: nombre,
);
// Enviar info de sala al nuevo jugador
enviarMensaje(endpointId, MensajeP2P(
tipo: TipoMensaje.salaInfo,
datos: {
'sala': _nombreSala,
'jugadores': _jugadores.values.map((j) => {
'nombre': j.nombre,
'endpointId': j.endpointId,
}).toList(),
},
));
// Enviar info de sala al nuevo jugador (incluye pool de usuarios)
enviarMensaje(
endpointId,
MensajeP2P(
tipo: TipoMensaje.salaInfo,
datos: {
'sala': _nombreSala,
'jugadores': _jugadores.values
.map((j) => {'nombre': j.nombre, 'endpointId': j.endpointId})
.toList(),
'usuarios': _usuariosPool.values.map((u) => u.toJson()).toList(),
},
),
);
notifyListeners();
break;
@@ -330,15 +390,62 @@ class ServicioNearby extends ChangeNotifier {
}
break;
case TipoMensaje.usuarioNuevo:
_handleUsuarioNuevo(mensaje);
break;
case TipoMensaje.usuariosActualizados:
_handleUsuariosActualizados(mensaje);
break;
default:
break;
}
}
void _handleUsuarioNuevo(MensajeP2P mensaje) {
final usuarioJson = mensaje.datos['usuario'] as Map<String, dynamic>?;
if (usuarioJson != null) {
final nuevoUsuario = Usuario.fromJson(usuarioJson);
_usuariosPool[nuevoUsuario.id] = nuevoUsuario;
// Propagar a todos los clientes
if (_esHost) {
enviarATodos(
MensajeP2P(
tipo: TipoMensaje.usuarioNuevo,
datos: {'usuario': usuarioJson},
),
);
}
notifyListeners();
}
}
void _handleUsuariosActualizados(MensajeP2P mensaje) {
final usuariosJson = mensaje.datos['usuarios'] as List<dynamic>?;
if (usuariosJson != null) {
_usuariosPool.clear();
for (final u in usuariosJson) {
final usuario = Usuario.fromJson(u as Map<String, dynamic>);
_usuariosPool[usuario.id] = usuario;
}
notifyListeners();
}
}
void _procesarMensajeCliente(String endpointId, MensajeP2P mensaje) {
switch (mensaje.tipo) {
case TipoMensaje.salaInfo:
_datosPartida = mensaje.datos;
// Sincronizar pool de usuarios si viene en el mensaje
final usuariosData = mensaje.datos['usuarios'] as List<dynamic>?;
if (usuariosData != null) {
_usuariosPool.clear();
for (final u in usuariosData) {
final usuario = Usuario.fromJson(u as Map<String, dynamic>);
_usuariosPool[usuario.id] = usuario;
}
}
notifyListeners();
break;
@@ -402,38 +509,42 @@ class ServicioNearby extends ChangeNotifier {
}) async {
for (final entry in _jugadores.entries) {
final esImpostor = impostores[entry.key] ?? false;
await enviarMensaje(entry.key, MensajeP2P(
tipo: TipoMensaje.partidaInicio,
datos: {
'palabra': esImpostor ? null : palabraSecreta,
'esImpostor': esImpostor,
'categoria': categoria,
'numJugadores': _jugadores.length + 1, // +1 por el host
},
));
await enviarMensaje(
entry.key,
MensajeP2P(
tipo: TipoMensaje.partidaInicio,
datos: {
'palabra': esImpostor ? null : palabraSecreta,
'esImpostor': esImpostor,
'categoria': categoria,
'numJugadores': _jugadores.length + 1, // +1 por el host
},
),
);
}
}
/// Host envía cambio de fase
Future<void> enviarCambioFase(String fase, [Map<String, dynamic>? extra]) async {
Future<void> enviarCambioFase(
String fase, [
Map<String, dynamic>? extra,
]) async {
final datos = {'fase': fase, ...?extra};
await enviarATodos(MensajeP2P(tipo: TipoMensaje.fase, datos: datos));
}
/// Host envía resultado de votación
Future<void> enviarResultadoVotacion(Map<String, dynamic> resultado) async {
await enviarATodos(MensajeP2P(
tipo: TipoMensaje.votacionResultado,
datos: resultado,
));
await enviarATodos(
MensajeP2P(tipo: TipoMensaje.votacionResultado, datos: resultado),
);
}
/// Host envía fin de partida
Future<void> enviarFinPartida(Map<String, dynamic> resultado) async {
await enviarATodos(MensajeP2P(
tipo: TipoMensaje.partidaFin,
datos: resultado,
));
await enviarATodos(
MensajeP2P(tipo: TipoMensaje.partidaFin, datos: resultado),
);
}
// ==================== LIMPIEZA ====================