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:
@@ -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 ====================
|
||||
|
||||
Reference in New Issue
Block a user