fix: multidispositivo - Random seguro + gestor host + reacción clientes
Some checks failed
Build & Deploy Farolero / Análisis de código (push) Has been cancelled
Build & Deploy Farolero / Build APK + AAB release (push) Has been cancelled

- Random.secure() para selección de impostores (no predecible)
- Random.secure() también en desempate de votación
- Nueva PantallaGestorHost para coordinación multi-device
- Navegación: host va a gestor tras iniciar, no a pantalla de palabra
- PantallaPalabraCliente: cada jugador ve su palabra en su móvil
- PantallaDebateCliente: debate con timer y botón solicitar votación
- PantallaVotacionCliente: voto desde el móvil del cliente
- PantallaUnirse: listener que reacciona a partidaInicio y cambia de fase
- Protocolo: listo/voto/solicitoVotacion via Nearby hacia el host
- Nuevas cadenas l10n ES
This commit is contained in:
ShanaiaBot
2026-04-15 02:09:05 +02:00
parent 302cdf6f1a
commit eb2662f561
27 changed files with 2282 additions and 60 deletions

View File

@@ -2,9 +2,13 @@ import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:provider/provider.dart';
import 'package:farolero/l10n/generated/app_localizations.dart';
import '../modelos/jugador.dart';
import '../servicios/servicio_nearby.dart';
import '../servicios/servicio_permisos.dart';
import '../tema/tema_app.dart';
import 'pantalla_palabra_cliente.dart';
import 'pantalla_debate_cliente.dart';
import 'pantalla_votacion_cliente.dart';
/// Pantalla para unirse a una partida multidispositivo.
/// Flujo: nombre → discovery automático (lista de salas) → fallback QR
@@ -26,6 +30,110 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
String? _error;
String? _salaSeleccionada;
// Estado del juego recibido del host
String? _palabraRecibida;
bool _esImpostor = false;
String? _pistaCategoria;
final List<Jugador> _jugadores = [];
@override
void initState() {
super.initState();
// Registrar listener ANTES del primer build
WidgetsBinding.instance.addPostFrameCallback((_) {
_registrarListenerPartida();
});
}
void _registrarListenerPartida() {
final nearby = context.read<ServicioNearby>();
nearby.onMensaje((endpointId, mensaje) {
if (mensaje.tipo == TipoMensaje.partidaInicio) {
// El host ha iniciado la partida — nos ha enviado nuestra palabra
setState(() {
_palabraRecibida = mensaje.datos['palabra'] as String?;
_esImpostor = mensaje.datos['esImpostor'] as bool? ?? false;
_pistaCategoria = mensaje.datos['categoria'] as String?;
});
// Navegar a pantalla de palabra del cliente
if (mounted && _palabraRecibida != null) {
_navegarAPalabra();
}
} else if (mensaje.tipo == TipoMensaje.fase) {
// El host cambia de fase — navegar a la pantalla correspondiente
final fase = mensaje.datos['fase'] as String?;
if (mounted && fase != null) {
_navegarSegunFase(fase);
}
}
});
}
void _navegarAPalabra() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => PantallaPalabraCliente(
palabra: _palabraRecibida ?? '',
esImpostor: _esImpostor,
pistaCategoria: _pistaCategoria,
onVisto: () {
// Enviar "listo" al host y volver a la espera
final nearby = context.read<ServicioNearby>();
if (nearby.hostEndpointId != null) {
nearby.enviarMensaje(
nearby.hostEndpointId!,
MensajeP2P(tipo: TipoMensaje.listo, datos: {}),
);
}
Navigator.of(context).pop();
},
),
),
);
}
void _navegarSegunFase(String fase) {
switch (fase) {
case 'debate':
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => PantallaDebateCliente(
tiempoDebateSegundos: null,
onSolicitarVotacion: () {
final nearby = context.read<ServicioNearby>();
if (nearby.hostEndpointId != null) {
nearby.enviarMensaje(
nearby.hostEndpointId!,
MensajeP2P(tipo: TipoMensaje.ping, datos: {'solicitoVotacion': true}),
);
}
},
),
),
);
break;
case 'votacion':
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => PantallaVotacionCliente(
jugadores: _jugadores,
onVoto: (votoporId) {
final nearby = context.read<ServicioNearby>();
if (nearby.hostEndpointId != null) {
nearby.enviarMensaje(
nearby.hostEndpointId!,
MensajeP2P(tipo: TipoMensaje.voto, datos: {'votoporId': votoporId}),
);
}
Navigator.of(context).pop();
},
),
),
);
break;
}
}
@override
void dispose() {
_nombreController.dispose();