No se puede marcar “vista” sin revelar la palabra antes. Se puede volver a ver la palabra durante debate/votación/resultado. Notas online privadas por partida y jugador. Tests añadidos para notas scoped. Ajusté roomId en el payload de inicio para que las notas no se mezclen entre partidas.
908 lines
26 KiB
Dart
908 lines
26 KiB
Dart
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.
|
|
enum TipoMensaje {
|
|
salaInfo,
|
|
partidaInicio,
|
|
fase,
|
|
votacionResultado,
|
|
partidaFin,
|
|
unirse,
|
|
voto,
|
|
listo,
|
|
ping,
|
|
jugadorDesconectado,
|
|
clienteRegistrado,
|
|
estadoSala,
|
|
crearUsuario,
|
|
seleccionarUsuario,
|
|
liberarUsuario,
|
|
eliminarUsuario,
|
|
errorOperacion,
|
|
usuarioNuevo,
|
|
// Compatibilidad con versiones previas del protocolo.
|
|
usuarioEliminado,
|
|
usuariosActualizados,
|
|
}
|
|
|
|
/// Mensaje del protocolo P2P entre dispositivos.
|
|
class MensajeP2P {
|
|
final TipoMensaje tipo;
|
|
final Map<String, dynamic> datos;
|
|
|
|
MensajeP2P({required this.tipo, required this.datos});
|
|
|
|
String toJson() => json.encode({'tipo': tipo.name, 'datos': datos});
|
|
|
|
factory MensajeP2P.fromJson(String jsonStr) {
|
|
final mapa = json.decode(jsonStr) as Map<String, dynamic>;
|
|
return MensajeP2P(
|
|
tipo: TipoMensaje.values.firstWhere((t) => t.name == mapa['tipo']),
|
|
datos: mapa['datos'] as Map<String, dynamic>,
|
|
);
|
|
}
|
|
|
|
Uint8List toBytes() => Uint8List.fromList(utf8.encode(toJson()));
|
|
}
|
|
|
|
/// 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;
|
|
bool listo;
|
|
|
|
JugadorConectado({
|
|
required this.endpointId,
|
|
required this.nombre,
|
|
this.listo = false,
|
|
});
|
|
}
|
|
|
|
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;
|
|
bool _buscando = false;
|
|
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 = {};
|
|
|
|
String? _palabraRecibida;
|
|
bool? _soyImpostor;
|
|
String? _faseActual;
|
|
Map<String, dynamic>? _datosPartida;
|
|
EstadoSalaMultijugador? _estadoSala;
|
|
|
|
bool get esHost => _esHost;
|
|
bool get conectado => _conectado;
|
|
bool get buscando => _buscando;
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
void onMensaje(OnMensajeCallback callback) {
|
|
_listeners.add(callback);
|
|
}
|
|
|
|
void removeMensajeListener(OnMensajeCallback callback) {
|
|
_listeners.remove(callback);
|
|
}
|
|
|
|
void _notificarMensaje(String endpointId, MensajeP2P mensaje) {
|
|
for (final listener in _listeners) {
|
|
listener(endpointId, mensaje);
|
|
}
|
|
}
|
|
|
|
// ==================== USER POOL / SALA ====================
|
|
|
|
void agregarUsuario(Usuario usuario) {
|
|
_usuariosPool[usuario.id] = usuario;
|
|
_estadoSala?.usuarios[usuario.id] = usuario;
|
|
notifyListeners();
|
|
}
|
|
|
|
void eliminarUsuario(String usuarioId) {
|
|
_usuariosPool.remove(usuarioId);
|
|
_estadoSala?.usuarios.remove(usuarioId);
|
|
notifyListeners();
|
|
}
|
|
|
|
Usuario? getUsuario(String usuarioId) {
|
|
return _estadoSala?.usuarios[usuarioId] ?? _usuariosPool[usuarioId];
|
|
}
|
|
|
|
void sincronizarUsuarios(List<Usuario> usuarios) {
|
|
_usuariosPool.clear();
|
|
for (final usuario in usuarios) {
|
|
_usuariosPool[usuario.id] = usuario;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
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 ?? '',
|
|
nombre: _miNombre!,
|
|
listo: true,
|
|
);
|
|
}
|
|
|
|
// ==================== HOST ====================
|
|
|
|
Future<bool> iniciarHost(
|
|
String nombreSala,
|
|
String miNombre, {
|
|
String? miNick,
|
|
String? miAvatar,
|
|
}) async {
|
|
if (_conectado ||
|
|
_anunciando ||
|
|
_buscando ||
|
|
_estadoSala != null ||
|
|
_jugadores.isNotEmpty) {
|
|
await desconectar();
|
|
await Future<void>.delayed(const Duration(milliseconds: 250));
|
|
}
|
|
|
|
_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,
|
|
nick: miNick,
|
|
avatar: miAvatar,
|
|
foto: miAvatar,
|
|
creadoPorClienteId: _hostClientId,
|
|
clienteIdSeleccionado: _hostClientId,
|
|
);
|
|
_estadoSala!.crearUsuario(usuarioHost);
|
|
_sincronizarPoolDesdeSala();
|
|
|
|
try {
|
|
final resultado = await Nearby().startAdvertising(
|
|
miNombre,
|
|
Strategy.P2P_STAR,
|
|
onConnectionInitiated: _onConexionIniciada,
|
|
onConnectionResult: _onResultadoConexion,
|
|
onDisconnected: _onDesconexion,
|
|
serviceId: _serviceId,
|
|
);
|
|
|
|
if (resultado) {
|
|
_esHost = true;
|
|
_anunciando = true;
|
|
_conectado = true;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
await desconectar();
|
|
return false;
|
|
} catch (e) {
|
|
debugPrint('Error iniciando host: $e');
|
|
await desconectar();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== CLIENTE ====================
|
|
|
|
Future<bool> buscarHosts(String miNombre) async {
|
|
_miNombre = miNombre;
|
|
|
|
try {
|
|
final resultado = await Nearby().startDiscovery(
|
|
miNombre,
|
|
Strategy.P2P_STAR,
|
|
onEndpointFound: _onEndpointEncontrado,
|
|
onEndpointLost: _onEndpointPerdido,
|
|
serviceId: _serviceId,
|
|
);
|
|
|
|
if (resultado) {
|
|
_buscando = true;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
debugPrint('Error buscando hosts: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> conectarAHost(String endpointId, String miNombre) async {
|
|
try {
|
|
await Nearby().requestConnection(
|
|
miNombre,
|
|
endpointId,
|
|
onConnectionInitiated: _onConexionIniciada,
|
|
onConnectionResult: _onResultadoConexion,
|
|
onDisconnected: _onDesconexion,
|
|
);
|
|
return true;
|
|
} catch (e) {
|
|
debugPrint('Error conectando a host: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== CALLBACKS NEARBY ====================
|
|
|
|
void _onConexionIniciada(String endpointId, ConnectionInfo info) {
|
|
debugPrint('Conexion iniciada con $endpointId: ${info.endpointName}');
|
|
Nearby().acceptConnection(
|
|
endpointId,
|
|
onPayLoadRecieved: _onPayloadRecibido,
|
|
onPayloadTransferUpdate: _onPayloadUpdate,
|
|
);
|
|
}
|
|
|
|
void _onResultadoConexion(String endpointId, Status status) {
|
|
debugPrint('Resultado conexion $endpointId: $status');
|
|
if (status == Status.CONNECTED) {
|
|
if (_esHost) {
|
|
debugPrint('Cliente conectado: $endpointId');
|
|
} else {
|
|
_hostEndpointId = endpointId;
|
|
_conectado = true;
|
|
enviarMensaje(
|
|
endpointId,
|
|
MensajeP2P(
|
|
tipo: TipoMensaje.unirse,
|
|
datos: {'nombre': _miNombre ?? 'Jugador'},
|
|
),
|
|
);
|
|
}
|
|
notifyListeners();
|
|
} else {
|
|
debugPrint('Conexion fallida con $endpointId');
|
|
}
|
|
}
|
|
|
|
void _onDesconexion(String 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) {
|
|
enviarATodos(
|
|
MensajeP2P(
|
|
tipo: TipoMensaje.jugadorDesconectado,
|
|
datos: {'nombre': jugador.nombre, 'endpointId': endpointId},
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
_conectado = false;
|
|
_hostEndpointId = null;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void _onEndpointEncontrado(
|
|
String endpointId,
|
|
String endpointName,
|
|
String serviceId,
|
|
) {
|
|
debugPrint('Host encontrado: $endpointName ($endpointId)');
|
|
_hostsEncontrados[endpointId] = endpointName;
|
|
notifyListeners();
|
|
}
|
|
|
|
void _onEndpointPerdido(String? endpointId) {
|
|
debugPrint('Endpoint perdido: $endpointId');
|
|
if (endpointId != null) {
|
|
_hostsEncontrados.remove(endpointId);
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> pararBusqueda() async {
|
|
try {
|
|
await Nearby().stopDiscovery();
|
|
} catch (_) {}
|
|
_buscando = false;
|
|
_hostsEncontrados.clear();
|
|
notifyListeners();
|
|
}
|
|
|
|
void _onPayloadRecibido(String endpointId, Payload payload) {
|
|
if (payload.type == PayloadType.BYTES && payload.bytes != null) {
|
|
try {
|
|
final jsonStr = utf8.decode(payload.bytes!);
|
|
final mensaje = MensajeP2P.fromJson(jsonStr);
|
|
_procesarMensaje(endpointId, mensaje);
|
|
} catch (e) {
|
|
debugPrint('Error procesando payload: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onPayloadUpdate(String endpointId, PayloadTransferUpdate update) {}
|
|
|
|
// ==================== PROCESAMIENTO DE MENSAJES ====================
|
|
|
|
void _procesarMensaje(String endpointId, MensajeP2P mensaje) {
|
|
debugPrint('Mensaje de $endpointId: ${mensaje.tipo.name}');
|
|
|
|
if (_esHost) {
|
|
_procesarMensajeHost(endpointId, mensaje);
|
|
} else {
|
|
_procesarMensajeCliente(endpointId, mensaje);
|
|
}
|
|
|
|
_notificarMensaje(endpointId, mensaje);
|
|
}
|
|
|
|
void _procesarMensajeHost(String endpointId, MensajeP2P mensaje) {
|
|
switch (mensaje.tipo) {
|
|
case TipoMensaje.unirse:
|
|
_registrarClienteRemoto(endpointId, mensaje);
|
|
break;
|
|
case TipoMensaje.voto:
|
|
_notificarMensaje(endpointId, mensaje);
|
|
break;
|
|
case TipoMensaje.listo:
|
|
final jugador = _jugadores[endpointId];
|
|
if (jugador != null) {
|
|
jugador.listo = true;
|
|
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:
|
|
case TipoMensaje.usuarioEliminado:
|
|
_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;
|
|
_estadoSala?.usuarios[nuevoUsuario.id] = nuevoUsuario;
|
|
if (_esHost) {
|
|
_broadcastEstadoSala();
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
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;
|
|
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;
|
|
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:
|
|
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:
|
|
case TipoMensaje.partidaFin:
|
|
case TipoMensaje.errorOperacion:
|
|
_datosPartida = mensaje.datos;
|
|
notifyListeners();
|
|
break;
|
|
case TipoMensaje.jugadorDesconectado:
|
|
notifyListeners();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ==================== ENVIO ====================
|
|
|
|
Future<void> enviarMensaje(String endpointId, MensajeP2P mensaje) async {
|
|
try {
|
|
await Nearby().sendBytesPayload(endpointId, mensaje.toBytes());
|
|
} catch (e) {
|
|
debugPrint('Error enviando a $endpointId: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> enviarATodos(MensajeP2P mensaje) async {
|
|
for (final id in _jugadores.keys) {
|
|
await enviarMensaje(id, mensaje);
|
|
}
|
|
}
|
|
|
|
Future<void> crearUsuarioSala(
|
|
String nombre, {
|
|
bool seleccionar = true,
|
|
String? nick,
|
|
String? avatar,
|
|
}) async {
|
|
final nombreLimpio = nombre.trim();
|
|
if (nombreLimpio.isEmpty) return;
|
|
final clientId = _miClientId;
|
|
final usuario = Usuario(
|
|
id: 'u-${DateTime.now().microsecondsSinceEpoch}',
|
|
nombre: nombreLimpio,
|
|
nick: nick,
|
|
avatar: avatar,
|
|
foto: avatar,
|
|
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 ====================
|
|
|
|
Future<void> enviarInicioPartida({
|
|
required String palabraSecreta,
|
|
required String categoria,
|
|
required Map<String, bool> impostores,
|
|
}) 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,
|
|
'roomId': _roomId,
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
datos['roomId'] = _roomId;
|
|
await enviarMensaje(
|
|
endpointId,
|
|
MensajeP2P(tipo: TipoMensaje.partidaInicio, datos: datos),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> enviarCambioFase(
|
|
String fase, [
|
|
Map<String, dynamic>? extra,
|
|
]) async {
|
|
final datos = {'fase': fase, ...?extra};
|
|
await enviarATodos(MensajeP2P(tipo: TipoMensaje.fase, datos: datos));
|
|
}
|
|
|
|
Future<void> enviarResultadoVotacion(Map<String, dynamic> resultado) async {
|
|
await enviarATodos(
|
|
MensajeP2P(tipo: TipoMensaje.votacionResultado, datos: resultado),
|
|
);
|
|
}
|
|
|
|
Future<void> enviarFinPartida(Map<String, dynamic> resultado) async {
|
|
await enviarATodos(
|
|
MensajeP2P(tipo: TipoMensaje.partidaFin, datos: resultado),
|
|
);
|
|
}
|
|
|
|
// ==================== LIMPIEZA ====================
|
|
|
|
Future<void> desconectar() async {
|
|
try {
|
|
await Nearby().stopAllEndpoints();
|
|
if (_anunciando) await Nearby().stopAdvertising();
|
|
if (_buscando) await Nearby().stopDiscovery();
|
|
} catch (e) {
|
|
debugPrint('Error desconectando: $e');
|
|
}
|
|
|
|
_esHost = false;
|
|
_conectado = false;
|
|
_buscando = false;
|
|
_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();
|
|
}
|
|
|
|
String generarDatosQR(String nombreSala) {
|
|
return json.encode({
|
|
'app': 'farolero',
|
|
'sala': nombreSala,
|
|
'host': _miNombre,
|
|
'roomId': _roomId,
|
|
});
|
|
}
|
|
|
|
static Map<String, dynamic>? parsearQR(String datos) {
|
|
try {
|
|
final mapa = json.decode(datos) as Map<String, dynamic>;
|
|
if (mapa['app'] == 'farolero') return mapa;
|
|
return null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
desconectar();
|
|
super.dispose();
|
|
}
|
|
}
|