feat: Implement multiplayer game session management
- Add models for managing player assignments and game session initialization in `inicio_partida_multijugador.dart`. - Create a multiplayer room state management system in `sala_multijugador.dart`, including user registration, selection, and session validation. - Develop a UI screen for displaying player words sequentially in `pantalla_palabras_cliente.dart`. - Implement unit tests for the multiplayer session management and player assignment logic in `inicio_partida_multijugador_test.dart` and `sala_multijugador_test.dart`.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nearby_connections/nearby_connections.dart';
|
||||
import '../modelos/inicio_partida_multijugador.dart';
|
||||
import '../modelos/sala_multijugador.dart';
|
||||
import '../modelos/usuario.dart';
|
||||
|
||||
/// Tipos de mensajes en el protocolo P2P
|
||||
/// Tipos de mensajes en el protocolo P2P.
|
||||
enum TipoMensaje {
|
||||
salaInfo,
|
||||
partidaInicio,
|
||||
@@ -15,12 +17,18 @@ enum TipoMensaje {
|
||||
listo,
|
||||
ping,
|
||||
jugadorDesconectado,
|
||||
clienteRegistrado,
|
||||
estadoSala,
|
||||
crearUsuario,
|
||||
seleccionarUsuario,
|
||||
liberarUsuario,
|
||||
errorOperacion,
|
||||
usuarioNuevo,
|
||||
usuarioEliminado,
|
||||
usuariosActualizados,
|
||||
}
|
||||
|
||||
/// Mensaje del protocolo P2P entre dispositivos
|
||||
/// Mensaje del protocolo P2P entre dispositivos.
|
||||
class MensajeP2P {
|
||||
final TipoMensaje tipo;
|
||||
final Map<String, dynamic> datos;
|
||||
@@ -40,7 +48,8 @@ class MensajeP2P {
|
||||
Uint8List toBytes() => Uint8List.fromList(utf8.encode(toJson()));
|
||||
}
|
||||
|
||||
/// Info de un jugador conectado
|
||||
/// Info de un dispositivo conectado. El nombre identifica al cliente/dispositivo,
|
||||
/// no necesariamente a un jugador de la partida.
|
||||
class JugadorConectado {
|
||||
final String endpointId;
|
||||
final String nombre;
|
||||
@@ -53,13 +62,13 @@ class JugadorConectado {
|
||||
});
|
||||
}
|
||||
|
||||
/// Callback para mensajes recibidos
|
||||
typedef OnMensajeCallback =
|
||||
void Function(String endpointId, MensajeP2P mensaje);
|
||||
|
||||
/// Servicio para conexiones P2P usando Google Nearby Connections API.
|
||||
class ServicioNearby extends ChangeNotifier {
|
||||
static const _serviceId = 'es.freetimelab.farolero';
|
||||
static const _hostClientId = 'host';
|
||||
|
||||
bool _esHost = false;
|
||||
bool _conectado = false;
|
||||
@@ -67,23 +76,21 @@ class ServicioNearby extends ChangeNotifier {
|
||||
bool _anunciando = false;
|
||||
String? _miEndpointId;
|
||||
String? _hostEndpointId;
|
||||
String? _roomId;
|
||||
String? _miClientId;
|
||||
String? _nombreSala;
|
||||
String? _miNombre;
|
||||
|
||||
final Map<String, JugadorConectado> _jugadores = {};
|
||||
final List<OnMensajeCallback> _listeners = [];
|
||||
final Map<String, String> _hostsEncontrados = {};
|
||||
final Map<String, Usuario> _usuariosPool = {};
|
||||
|
||||
// Hosts descubiertos (para discovery automático)
|
||||
final Map<String, String> _hostsEncontrados = {}; // endpointId -> nombre
|
||||
|
||||
// Estado para clientes
|
||||
String? _palabraRecibida;
|
||||
bool? _soyImpostor;
|
||||
String? _faseActual;
|
||||
Map<String, dynamic>? _datosPartida;
|
||||
|
||||
// Pool de usuarios para modo multi-dispositivo
|
||||
final Map<String, Usuario> _usuariosPool = {};
|
||||
EstadoSalaMultijugador? _estadoSala;
|
||||
|
||||
bool get esHost => _esHost;
|
||||
bool get conectado => _conectado;
|
||||
@@ -91,27 +98,35 @@ class ServicioNearby extends ChangeNotifier {
|
||||
bool get anunciando => _anunciando;
|
||||
String? get miEndpointId => _miEndpointId;
|
||||
String? get hostEndpointId => _hostEndpointId;
|
||||
String? get roomId => _roomId;
|
||||
String? get miClientId => _miClientId;
|
||||
String? get nombreSala => _nombreSala;
|
||||
String? get miNombre => _miNombre;
|
||||
String? get palabraRecibida => _palabraRecibida;
|
||||
bool? get soyImpostor => _soyImpostor;
|
||||
String? get faseActual => _faseActual;
|
||||
Map<String, dynamic>? get datosPartida => _datosPartida;
|
||||
EstadoSalaMultijugador? get estadoSala => _estadoSala;
|
||||
|
||||
List<JugadorConectado> get jugadores => _jugadores.values.toList();
|
||||
int get numJugadoresConectados => _jugadores.length;
|
||||
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();
|
||||
List<Usuario> get usuarios =>
|
||||
(_estadoSala?.usuarios.values ?? _usuariosPool.values).toList();
|
||||
|
||||
List<Usuario> get misUsuariosSeleccionados {
|
||||
final clientId = _miClientId;
|
||||
final sala = _estadoSala;
|
||||
if (clientId == null || sala == null) return [];
|
||||
return sala.usuariosPorCliente(clientId);
|
||||
}
|
||||
|
||||
/// Registra un listener de mensajes
|
||||
void onMensaje(OnMensajeCallback callback) {
|
||||
_listeners.add(callback);
|
||||
}
|
||||
|
||||
/// Elimina un listener
|
||||
void removeMensajeListener(OnMensajeCallback callback) {
|
||||
_listeners.remove(callback);
|
||||
}
|
||||
@@ -122,26 +137,24 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== USER POOL ====================
|
||||
// ==================== USER POOL / SALA ====================
|
||||
|
||||
/// Agrega un usuario al pool de usuarios
|
||||
void agregarUsuario(Usuario usuario) {
|
||||
_usuariosPool[usuario.id] = usuario;
|
||||
_estadoSala?.usuarios[usuario.id] = usuario;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Elimina un usuario del pool
|
||||
void eliminarUsuario(String usuarioId) {
|
||||
_usuariosPool.remove(usuarioId);
|
||||
_estadoSala?.usuarios.remove(usuarioId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Obtiene un usuario por su ID
|
||||
Usuario? getUsuario(String usuarioId) {
|
||||
return _usuariosPool[usuarioId];
|
||||
return _estadoSala?.usuarios[usuarioId] ?? _usuariosPool[usuarioId];
|
||||
}
|
||||
|
||||
/// Sincroniza el pool de usuarios con una lista
|
||||
void sincronizarUsuarios(List<Usuario> usuarios) {
|
||||
_usuariosPool.clear();
|
||||
for (final usuario in usuarios) {
|
||||
@@ -150,12 +163,38 @@ class ServicioNearby extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Obtiene el jugador local del host (él mismo como participante)
|
||||
/// Retorna un JugadorConectado con endpointId null porque es local
|
||||
void _sincronizarSala(EstadoSalaMultijugador sala) {
|
||||
_estadoSala = sala;
|
||||
_roomId = sala.roomId;
|
||||
_usuariosPool
|
||||
..clear()
|
||||
..addEntries(sala.usuarios.entries);
|
||||
}
|
||||
|
||||
void _sincronizarPoolDesdeSala() {
|
||||
final sala = _estadoSala;
|
||||
if (sala == null) return;
|
||||
_usuariosPool
|
||||
..clear()
|
||||
..addEntries(sala.usuarios.entries);
|
||||
}
|
||||
|
||||
Future<void> _broadcastEstadoSala() async {
|
||||
final sala = _estadoSala;
|
||||
if (sala == null) return;
|
||||
_sincronizarPoolDesdeSala();
|
||||
if (_esHost) {
|
||||
await enviarATodos(
|
||||
MensajeP2P(tipo: TipoMensaje.estadoSala, datos: sala.toJson()),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
JugadorConectado? getJugadorLocal() {
|
||||
if (_miNombre == null) return null;
|
||||
return JugadorConectado(
|
||||
endpointId: _miEndpointId ?? '', // vacío indica que es el host local
|
||||
endpointId: _miEndpointId ?? '',
|
||||
nombre: _miNombre!,
|
||||
listo: true,
|
||||
);
|
||||
@@ -163,10 +202,29 @@ class ServicioNearby extends ChangeNotifier {
|
||||
|
||||
// ==================== HOST ====================
|
||||
|
||||
/// Inicia como host (anunciando el endpoint)
|
||||
Future<bool> iniciarHost(String nombreSala, String miNombre) async {
|
||||
_nombreSala = nombreSala;
|
||||
_miNombre = miNombre;
|
||||
_roomId = DateTime.now().microsecondsSinceEpoch.toString();
|
||||
_miClientId = _hostClientId;
|
||||
_estadoSala = EstadoSalaMultijugador.crear(
|
||||
roomId: _roomId!,
|
||||
nombreSala: nombreSala,
|
||||
hostClientId: _hostClientId,
|
||||
hostNombre: miNombre,
|
||||
);
|
||||
|
||||
// Compatibilidad con el flujo actual: el nombre con el que se crea la sala
|
||||
// arranca como usuario seleccionado por el host. Luego puede crear/seleccionar
|
||||
// más usuarios en el lobby.
|
||||
final usuarioHost = Usuario(
|
||||
id: 'u-${_roomId!}-host',
|
||||
nombre: miNombre,
|
||||
creadoPorClienteId: _hostClientId,
|
||||
clienteIdSeleccionado: _hostClientId,
|
||||
);
|
||||
_estadoSala!.crearUsuario(usuarioHost);
|
||||
_sincronizarPoolDesdeSala();
|
||||
|
||||
try {
|
||||
final resultado = await Nearby().startAdvertising(
|
||||
@@ -194,7 +252,6 @@ class ServicioNearby extends ChangeNotifier {
|
||||
|
||||
// ==================== CLIENTE ====================
|
||||
|
||||
/// Busca hosts disponibles
|
||||
Future<bool> buscarHosts(String miNombre) async {
|
||||
_miNombre = miNombre;
|
||||
|
||||
@@ -219,7 +276,6 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Conecta a un host específico
|
||||
Future<bool> conectarAHost(String endpointId, String miNombre) async {
|
||||
try {
|
||||
await Nearby().requestConnection(
|
||||
@@ -239,8 +295,7 @@ class ServicioNearby extends ChangeNotifier {
|
||||
// ==================== CALLBACKS NEARBY ====================
|
||||
|
||||
void _onConexionIniciada(String endpointId, ConnectionInfo info) {
|
||||
debugPrint('Conexión iniciada con $endpointId: ${info.endpointName}');
|
||||
// Auto-aceptar conexiones
|
||||
debugPrint('Conexion iniciada con $endpointId: ${info.endpointName}');
|
||||
Nearby().acceptConnection(
|
||||
endpointId,
|
||||
onPayLoadRecieved: _onPayloadRecibido,
|
||||
@@ -249,16 +304,13 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _onResultadoConexion(String endpointId, Status status) {
|
||||
debugPrint('Resultado conexión $endpointId: $status');
|
||||
debugPrint('Resultado conexion $endpointId: $status');
|
||||
if (status == Status.CONNECTED) {
|
||||
if (_esHost) {
|
||||
// Host: esperar mensaje 'unirse' del cliente
|
||||
debugPrint('Cliente conectado: $endpointId');
|
||||
} else {
|
||||
// Cliente: conectado al host
|
||||
_hostEndpointId = endpointId;
|
||||
_conectado = true;
|
||||
// Enviar mensaje de unirse
|
||||
enviarMensaje(
|
||||
endpointId,
|
||||
MensajeP2P(
|
||||
@@ -269,16 +321,20 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
} else {
|
||||
debugPrint('Conexión fallida con $endpointId');
|
||||
debugPrint('Conexion fallida con $endpointId');
|
||||
}
|
||||
}
|
||||
|
||||
void _onDesconexion(String endpointId) {
|
||||
debugPrint('Desconexión: $endpointId');
|
||||
debugPrint('Desconexion: $endpointId');
|
||||
if (_esHost) {
|
||||
final jugador = _jugadores.remove(endpointId);
|
||||
final cliente = _estadoSala?.clientePorEndpoint(endpointId);
|
||||
if (cliente != null) {
|
||||
_estadoSala?.desconectarCliente(cliente.clientId);
|
||||
_broadcastEstadoSala();
|
||||
}
|
||||
if (jugador != null) {
|
||||
// Notificar a todos que se desconectó
|
||||
enviarATodos(
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.jugadorDesconectado,
|
||||
@@ -287,7 +343,6 @@ class ServicioNearby extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Cliente perdió conexión con host
|
||||
_conectado = false;
|
||||
_hostEndpointId = null;
|
||||
}
|
||||
@@ -312,7 +367,6 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Para el discovery sin desconectar
|
||||
Future<void> pararBusqueda() async {
|
||||
try {
|
||||
await Nearby().stopDiscovery();
|
||||
@@ -334,9 +388,7 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _onPayloadUpdate(String endpointId, PayloadTransferUpdate update) {
|
||||
// No necesitamos trackear progreso para bytes pequeños
|
||||
}
|
||||
void _onPayloadUpdate(String endpointId, PayloadTransferUpdate update) {}
|
||||
|
||||
// ==================== PROCESAMIENTO DE MENSAJES ====================
|
||||
|
||||
@@ -355,33 +407,11 @@ class ServicioNearby extends ChangeNotifier {
|
||||
void _procesarMensajeHost(String endpointId, MensajeP2P mensaje) {
|
||||
switch (mensaje.tipo) {
|
||||
case TipoMensaje.unirse:
|
||||
final nombre = mensaje.datos['nombre'] as String? ?? 'Jugador';
|
||||
_jugadores[endpointId] = JugadorConectado(
|
||||
endpointId: endpointId,
|
||||
nombre: nombre,
|
||||
);
|
||||
// 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();
|
||||
_registrarClienteRemoto(endpointId, mensaje);
|
||||
break;
|
||||
|
||||
case TipoMensaje.voto:
|
||||
// Propagar al flujo de juego
|
||||
_notificarMensaje(endpointId, mensaje);
|
||||
break;
|
||||
|
||||
case TipoMensaje.listo:
|
||||
final jugador = _jugadores[endpointId];
|
||||
if (jugador != null) {
|
||||
@@ -389,33 +419,68 @@ class ServicioNearby extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
break;
|
||||
|
||||
case TipoMensaje.crearUsuario:
|
||||
_handleCrearUsuario(endpointId, mensaje);
|
||||
break;
|
||||
case TipoMensaje.seleccionarUsuario:
|
||||
_handleSeleccionarUsuario(endpointId, mensaje);
|
||||
break;
|
||||
case TipoMensaje.liberarUsuario:
|
||||
_handleLiberarUsuario(endpointId, mensaje);
|
||||
break;
|
||||
case TipoMensaje.eliminarUsuario:
|
||||
_handleEliminarUsuario(endpointId, mensaje);
|
||||
break;
|
||||
case TipoMensaje.usuarioNuevo:
|
||||
_handleUsuarioNuevo(mensaje);
|
||||
break;
|
||||
|
||||
case TipoMensaje.usuariosActualizados:
|
||||
_handleUsuariosActualizados(mensaje);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _registrarClienteRemoto(String endpointId, MensajeP2P mensaje) {
|
||||
final nombre = mensaje.datos['nombre'] as String? ?? 'Jugador';
|
||||
final clientId = endpointId;
|
||||
_jugadores[endpointId] = JugadorConectado(
|
||||
endpointId: endpointId,
|
||||
nombre: nombre,
|
||||
);
|
||||
_estadoSala?.registrarCliente(
|
||||
ClienteSala(clientId: clientId, endpointId: endpointId, nombre: nombre),
|
||||
);
|
||||
|
||||
enviarMensaje(
|
||||
endpointId,
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.clienteRegistrado,
|
||||
datos: {
|
||||
'clientId': clientId,
|
||||
'sala': _nombreSala,
|
||||
'roomId': _roomId,
|
||||
'jugadores': _jugadores.values
|
||||
.map((j) => {'nombre': j.nombre, 'endpointId': j.endpointId})
|
||||
.toList(),
|
||||
'usuarios': _usuariosPool.values.map((u) => u.toJson()).toList(),
|
||||
if (_estadoSala != null) 'estadoSala': _estadoSala!.toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
_broadcastEstadoSala();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
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
|
||||
_estadoSala?.usuarios[nuevoUsuario.id] = nuevoUsuario;
|
||||
if (_esHost) {
|
||||
enviarATodos(
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.usuarioNuevo,
|
||||
datos: {'usuario': usuarioJson},
|
||||
),
|
||||
);
|
||||
_broadcastEstadoSala();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -433,11 +498,87 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String _clientIdParaEndpoint(String endpointId, MensajeP2P mensaje) {
|
||||
return mensaje.datos['clientId'] as String? ??
|
||||
_estadoSala?.clientePorEndpoint(endpointId)?.clientId ??
|
||||
endpointId;
|
||||
}
|
||||
|
||||
Future<void> _enviarErrorOperacion(
|
||||
String endpointId,
|
||||
ResultadoOperacionSala resultado,
|
||||
) async {
|
||||
if (resultado.exitoso) return;
|
||||
await enviarMensaje(
|
||||
endpointId,
|
||||
MensajeP2P(tipo: TipoMensaje.errorOperacion, datos: resultado.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleCrearUsuario(String endpointId, MensajeP2P mensaje) {
|
||||
final sala = _estadoSala;
|
||||
final usuarioJson = mensaje.datos['usuario'] as Map<String, dynamic>?;
|
||||
if (sala == null || usuarioJson == null) return;
|
||||
final clientId = _clientIdParaEndpoint(endpointId, mensaje);
|
||||
final usuario = Usuario.fromJson(usuarioJson).copiar(
|
||||
creadoPorClienteId: clientId,
|
||||
liberarSeleccion: true,
|
||||
);
|
||||
final resultadoCrear = sala.crearUsuario(usuario);
|
||||
if (!resultadoCrear.exitoso) {
|
||||
_enviarErrorOperacion(endpointId, resultadoCrear);
|
||||
return;
|
||||
}
|
||||
if (mensaje.datos['seleccionar'] == true) {
|
||||
final resultadoSeleccion = sala.seleccionarUsuario(
|
||||
usuarioId: usuario.id,
|
||||
clienteId: clientId,
|
||||
);
|
||||
_enviarErrorOperacion(endpointId, resultadoSeleccion);
|
||||
}
|
||||
_broadcastEstadoSala();
|
||||
}
|
||||
|
||||
void _handleSeleccionarUsuario(String endpointId, MensajeP2P mensaje) {
|
||||
final sala = _estadoSala;
|
||||
final usuarioId = mensaje.datos['usuarioId'] as String?;
|
||||
if (sala == null || usuarioId == null) return;
|
||||
final resultado = sala.seleccionarUsuario(
|
||||
usuarioId: usuarioId,
|
||||
clienteId: _clientIdParaEndpoint(endpointId, mensaje),
|
||||
);
|
||||
_enviarErrorOperacion(endpointId, resultado);
|
||||
_broadcastEstadoSala();
|
||||
}
|
||||
|
||||
void _handleLiberarUsuario(String endpointId, MensajeP2P mensaje) {
|
||||
final sala = _estadoSala;
|
||||
final usuarioId = mensaje.datos['usuarioId'] as String?;
|
||||
if (sala == null || usuarioId == null) return;
|
||||
final resultado = sala.liberarUsuario(
|
||||
usuarioId: usuarioId,
|
||||
solicitanteClientId: _clientIdParaEndpoint(endpointId, mensaje),
|
||||
);
|
||||
_enviarErrorOperacion(endpointId, resultado);
|
||||
_broadcastEstadoSala();
|
||||
}
|
||||
|
||||
void _handleEliminarUsuario(String endpointId, MensajeP2P mensaje) {
|
||||
final sala = _estadoSala;
|
||||
final usuarioId = mensaje.datos['usuarioId'] as String?;
|
||||
if (sala == null || usuarioId == null) return;
|
||||
final resultado = sala.eliminarUsuario(
|
||||
usuarioId: usuarioId,
|
||||
solicitanteClientId: _clientIdParaEndpoint(endpointId, mensaje),
|
||||
);
|
||||
_enviarErrorOperacion(endpointId, resultado);
|
||||
_broadcastEstadoSala();
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -448,42 +589,54 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
|
||||
case TipoMensaje.clienteRegistrado:
|
||||
_miClientId = mensaje.datos['clientId'] as String?;
|
||||
_datosPartida = mensaje.datos;
|
||||
final estadoSalaJson =
|
||||
mensaje.datos['estadoSala'] as Map<String, dynamic>?;
|
||||
if (estadoSalaJson != null) {
|
||||
_sincronizarSala(EstadoSalaMultijugador.fromJson(estadoSalaJson));
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case TipoMensaje.estadoSala:
|
||||
_sincronizarSala(EstadoSalaMultijugador.fromJson(mensaje.datos));
|
||||
notifyListeners();
|
||||
break;
|
||||
case TipoMensaje.partidaInicio:
|
||||
_palabraRecibida = mensaje.datos['palabra'] as String?;
|
||||
_soyImpostor = mensaje.datos['esImpostor'] as bool? ?? false;
|
||||
final jugadoresInicio = mensaje.datos['jugadores'] as List<dynamic>?;
|
||||
if (jugadoresInicio != null && jugadoresInicio.isNotEmpty) {
|
||||
final primerJugador = jugadoresInicio.first as Map<String, dynamic>;
|
||||
_palabraRecibida = primerJugador['palabra'] as String?;
|
||||
_soyImpostor = primerJugador['esImpostor'] as bool? ?? false;
|
||||
} else {
|
||||
_palabraRecibida = mensaje.datos['palabra'] as String?;
|
||||
_soyImpostor = mensaje.datos['esImpostor'] as bool? ?? false;
|
||||
}
|
||||
_datosPartida = mensaje.datos;
|
||||
notifyListeners();
|
||||
break;
|
||||
|
||||
case TipoMensaje.fase:
|
||||
_faseActual = mensaje.datos['fase'] as String?;
|
||||
_datosPartida = mensaje.datos;
|
||||
notifyListeners();
|
||||
break;
|
||||
|
||||
case TipoMensaje.votacionResultado:
|
||||
_datosPartida = mensaje.datos;
|
||||
notifyListeners();
|
||||
break;
|
||||
|
||||
case TipoMensaje.partidaFin:
|
||||
case TipoMensaje.errorOperacion:
|
||||
_datosPartida = mensaje.datos;
|
||||
notifyListeners();
|
||||
break;
|
||||
|
||||
case TipoMensaje.jugadorDesconectado:
|
||||
notifyListeners();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ENVÍO ====================
|
||||
// ==================== ENVIO ====================
|
||||
|
||||
/// Envía un mensaje a un dispositivo específico
|
||||
Future<void> enviarMensaje(String endpointId, MensajeP2P mensaje) async {
|
||||
try {
|
||||
await Nearby().sendBytesPayload(endpointId, mensaje.toBytes());
|
||||
@@ -492,20 +645,113 @@ class ServicioNearby extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Envía un mensaje a todos los dispositivos conectados (solo host)
|
||||
Future<void> enviarATodos(MensajeP2P mensaje) async {
|
||||
for (final id in _jugadores.keys) {
|
||||
await enviarMensaje(id, mensaje);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> crearUsuarioSala(String nombre, {bool seleccionar = true}) async {
|
||||
final nombreLimpio = nombre.trim();
|
||||
if (nombreLimpio.isEmpty) return;
|
||||
final clientId = _miClientId;
|
||||
final usuario = Usuario(
|
||||
id: 'u-${DateTime.now().microsecondsSinceEpoch}',
|
||||
nombre: nombreLimpio,
|
||||
creadoPorClienteId: clientId,
|
||||
);
|
||||
if (_esHost && _estadoSala != null && clientId != null) {
|
||||
final resultado = _estadoSala!.crearUsuario(usuario);
|
||||
if (resultado.exitoso && seleccionar) {
|
||||
_estadoSala!.seleccionarUsuario(
|
||||
usuarioId: usuario.id,
|
||||
clienteId: clientId,
|
||||
);
|
||||
}
|
||||
await _broadcastEstadoSala();
|
||||
return;
|
||||
}
|
||||
final hostId = _hostEndpointId;
|
||||
if (hostId == null) return;
|
||||
await enviarMensaje(
|
||||
hostId,
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.crearUsuario,
|
||||
datos: {
|
||||
'clientId': clientId,
|
||||
'seleccionar': seleccionar,
|
||||
'usuario': usuario.toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> seleccionarUsuarioSala(String usuarioId) async {
|
||||
final clientId = _miClientId;
|
||||
if (_esHost && _estadoSala != null && clientId != null) {
|
||||
_estadoSala!.seleccionarUsuario(usuarioId: usuarioId, clienteId: clientId);
|
||||
await _broadcastEstadoSala();
|
||||
return;
|
||||
}
|
||||
final hostId = _hostEndpointId;
|
||||
if (hostId == null) return;
|
||||
await enviarMensaje(
|
||||
hostId,
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.seleccionarUsuario,
|
||||
datos: {'clientId': clientId, 'usuarioId': usuarioId},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> liberarUsuarioSala(String usuarioId) async {
|
||||
final clientId = _miClientId;
|
||||
if (_esHost && _estadoSala != null && clientId != null) {
|
||||
_estadoSala!.liberarUsuario(
|
||||
usuarioId: usuarioId,
|
||||
solicitanteClientId: clientId,
|
||||
);
|
||||
await _broadcastEstadoSala();
|
||||
return;
|
||||
}
|
||||
final hostId = _hostEndpointId;
|
||||
if (hostId == null) return;
|
||||
await enviarMensaje(
|
||||
hostId,
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.liberarUsuario,
|
||||
datos: {'clientId': clientId, 'usuarioId': usuarioId},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> eliminarUsuarioSala(String usuarioId) async {
|
||||
final clientId = _miClientId;
|
||||
if (_esHost && _estadoSala != null && clientId != null) {
|
||||
_estadoSala!.eliminarUsuario(
|
||||
usuarioId: usuarioId,
|
||||
solicitanteClientId: clientId,
|
||||
);
|
||||
await _broadcastEstadoSala();
|
||||
return;
|
||||
}
|
||||
final hostId = _hostEndpointId;
|
||||
if (hostId == null) return;
|
||||
await enviarMensaje(
|
||||
hostId,
|
||||
MensajeP2P(
|
||||
tipo: TipoMensaje.eliminarUsuario,
|
||||
datos: {'clientId': clientId, 'usuarioId': usuarioId},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== HOST: ACCIONES DE JUEGO ====================
|
||||
|
||||
/// Host envía inicio de partida con la palabra de cada jugador
|
||||
Future<void> enviarInicioPartida({
|
||||
required String palabraSecreta,
|
||||
required String categoria,
|
||||
required Map<String, bool> impostores, // endpointId -> esImpostor
|
||||
required Map<String, bool> impostores,
|
||||
}) async {
|
||||
for (final entry in _jugadores.entries) {
|
||||
final esImpostor = impostores[entry.key] ?? false;
|
||||
@@ -517,14 +763,39 @@ class ServicioNearby extends ChangeNotifier {
|
||||
'palabra': esImpostor ? null : palabraSecreta,
|
||||
'esImpostor': esImpostor,
|
||||
'categoria': categoria,
|
||||
'numJugadores': _jugadores.length + 1, // +1 por el host
|
||||
'numJugadores': _jugadores.length + 1,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Host envía cambio de fase
|
||||
Future<void> enviarInicioPartidaMulti({
|
||||
required List<AsignacionJugador> asignaciones,
|
||||
required String palabraSecreta,
|
||||
required String categoria,
|
||||
required Map<String, bool> impostoresPorJugadorId,
|
||||
required List<Map<String, dynamic>> jugadoresTodos,
|
||||
}) async {
|
||||
final payloads = InicioPartidaMultijugador.crearPayloadsPorCliente(
|
||||
asignaciones: asignaciones,
|
||||
palabraSecreta: palabraSecreta,
|
||||
categoria: categoria,
|
||||
impostoresPorJugadorId: impostoresPorJugadorId,
|
||||
);
|
||||
|
||||
for (final payload in payloads.values) {
|
||||
final endpointId = payload.endpointId;
|
||||
if (endpointId == null) continue;
|
||||
final datos = payload.toJson();
|
||||
datos['jugadoresTodos'] = jugadoresTodos;
|
||||
await enviarMensaje(
|
||||
endpointId,
|
||||
MensajeP2P(tipo: TipoMensaje.partidaInicio, datos: datos),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enviarCambioFase(
|
||||
String fase, [
|
||||
Map<String, dynamic>? extra,
|
||||
@@ -533,14 +804,12 @@ class ServicioNearby extends ChangeNotifier {
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
/// Host envía fin de partida
|
||||
Future<void> enviarFinPartida(Map<String, dynamic> resultado) async {
|
||||
await enviarATodos(
|
||||
MensajeP2P(tipo: TipoMensaje.partidaFin, datos: resultado),
|
||||
@@ -549,7 +818,6 @@ class ServicioNearby extends ChangeNotifier {
|
||||
|
||||
// ==================== LIMPIEZA ====================
|
||||
|
||||
/// Desconecta y limpia todo
|
||||
Future<void> desconectar() async {
|
||||
try {
|
||||
await Nearby().stopAllEndpoints();
|
||||
@@ -565,27 +833,30 @@ class ServicioNearby extends ChangeNotifier {
|
||||
_anunciando = false;
|
||||
_miEndpointId = null;
|
||||
_hostEndpointId = null;
|
||||
_roomId = null;
|
||||
_miClientId = null;
|
||||
_nombreSala = null;
|
||||
_miNombre = null;
|
||||
_palabraRecibida = null;
|
||||
_soyImpostor = null;
|
||||
_faseActual = null;
|
||||
_datosPartida = null;
|
||||
_estadoSala = null;
|
||||
_jugadores.clear();
|
||||
_hostsEncontrados.clear();
|
||||
_usuariosPool.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Genera los datos para el código QR de conexión
|
||||
String generarDatosQR(String nombreSala) {
|
||||
return json.encode({
|
||||
'app': 'farolero',
|
||||
'sala': nombreSala,
|
||||
'host': _miNombre,
|
||||
'roomId': _roomId,
|
||||
});
|
||||
}
|
||||
|
||||
/// Parsea datos de QR escaneado
|
||||
static Map<String, dynamic>? parsearQR(String datos) {
|
||||
try {
|
||||
final mapa = json.decode(datos) as Map<String, dynamic>;
|
||||
|
||||
Reference in New Issue
Block a user