fix: multidispositivo - Random seguro + gestor host + reacción clientes
- 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:
@@ -7,7 +7,9 @@ import '../modelos/partida.dart';
|
||||
import '../servicios/servicio_nearby.dart';
|
||||
import '../servicios/servicio_permisos.dart';
|
||||
import '../tema/tema_app.dart';
|
||||
import 'pantalla_gestor_host.dart';
|
||||
import 'pantalla_lobby_host.dart';
|
||||
import 'pantalla_principal.dart';
|
||||
import 'pantalla_ver_palabra.dart';
|
||||
|
||||
class PantallaCrearPartida extends StatefulWidget {
|
||||
@@ -30,23 +32,28 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
|
||||
int get _maxImpostores => (_jugadores.length / 3).floor().clamp(1, 4);
|
||||
|
||||
List<String> _etiquetasTiempo(AppLocalizations l10n) =>
|
||||
[l10n.noLimit, l10n.oneMin, l10n.twoMin, l10n.threeMin, l10n.fiveMin];
|
||||
List<String> _etiquetasTiempo(AppLocalizations l10n) => [
|
||||
l10n.noLimit,
|
||||
l10n.oneMin,
|
||||
l10n.twoMin,
|
||||
l10n.threeMin,
|
||||
l10n.fiveMin,
|
||||
];
|
||||
|
||||
void _agregarJugador() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final nombre = _controladorNombre.text.trim();
|
||||
if (nombre.isEmpty) return;
|
||||
if (_jugadores.contains(nombre)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.playerAlreadyExists)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.playerAlreadyExists)));
|
||||
return;
|
||||
}
|
||||
if (_jugadores.length >= 20) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.maxPlayersReached)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.maxPlayersReached)));
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
@@ -76,9 +83,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
}
|
||||
|
||||
if (_jugadores.length < 3) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.minPlayersRequired)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.minPlayersRequired)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,7 +113,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
if (!permisosOk) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Se necesitan permisos de Bluetooth y ubicación')),
|
||||
const SnackBar(
|
||||
content: Text('Se necesitan permisos de Bluetooth y ubicación'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -125,7 +134,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
if (!ok) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No se pudo crear la sala. Verifica Bluetooth.')),
|
||||
const SnackBar(
|
||||
content: Text('No se pudo crear la sala. Verifica Bluetooth.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -163,7 +174,8 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
final jugadorNearby = nearby.jugadores[i];
|
||||
// El jugador [0] es el host, los de nearby son [1..n]
|
||||
final jugadorPartida = partida.jugadores[i + 1];
|
||||
impostores[jugadorNearby.endpointId] = jugadorPartida.esImpostor;
|
||||
impostores[jugadorNearby.endpointId] =
|
||||
jugadorPartida.esImpostor;
|
||||
}
|
||||
|
||||
nearby.enviarInicioPartida(
|
||||
@@ -174,7 +186,19 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const PantallaVerPalabra()),
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PantallaGestorHost(
|
||||
onPartidaFin: () {
|
||||
estado.limpiar();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const PantallaPrincipal(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -241,8 +265,10 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.gameMode,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
l10n.gameMode,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SegmentedButton<bool>(
|
||||
segments: [
|
||||
@@ -275,8 +301,10 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.category,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
l10n.category,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -288,7 +316,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
items: categorias.map((c) {
|
||||
return DropdownMenuItem(
|
||||
value: c,
|
||||
child: Text(BancoPalabras.nombreBonitoCategoria(c, l10n)),
|
||||
child: Text(
|
||||
BancoPalabras.nombreBonitoCategoria(c, l10n),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (v) => setState(() => _categoria = v!),
|
||||
@@ -310,10 +340,14 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(l10n.playersCount(_jugadores.length),
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(l10n.playersRangeHint,
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(
|
||||
l10n.playersCount(_jugadores.length),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
l10n.playersRangeHint,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -342,13 +376,17 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: TemaApp.colorTarjeta,
|
||||
child: Text('${e.key + 1}',
|
||||
style:
|
||||
const TextStyle(color: TemaApp.colorTexto)),
|
||||
child: Text(
|
||||
'${e.key + 1}',
|
||||
style: const TextStyle(color: TemaApp.colorTexto),
|
||||
),
|
||||
),
|
||||
title: Text(e.value),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close, color: TemaApp.colorAcento),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: TemaApp.colorAcento,
|
||||
),
|
||||
onPressed: () => _eliminarJugador(e.key),
|
||||
),
|
||||
dense: true,
|
||||
@@ -367,8 +405,10 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.configuration,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
l10n.configuration,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Número de impostores
|
||||
@@ -384,10 +424,10 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
: null,
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
),
|
||||
Text('$_numImpostores',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge),
|
||||
Text(
|
||||
'$_numImpostores',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _numImpostores < _maxImpostores
|
||||
? () => setState(() => _numImpostores++)
|
||||
@@ -404,8 +444,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
title: Text(l10n.impostorClue),
|
||||
subtitle: Text(l10n.impostorClueDescription),
|
||||
value: _pistaImpostor,
|
||||
onChanged: (v) =>
|
||||
setState(() => _pistaImpostor = v),
|
||||
onChanged: (v) => setState(() => _pistaImpostor = v),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
|
||||
@@ -423,8 +462,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
child: Text(etiquetas[i]),
|
||||
),
|
||||
),
|
||||
onChanged: (v) =>
|
||||
setState(() => _tiempoDebate = v),
|
||||
onChanged: (v) => setState(() => _tiempoDebate = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -439,7 +477,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: (_modoMultimovil || _jugadores.length >= 3) ? _iniciarPartida : null,
|
||||
onPressed: (_modoMultimovil || _jugadores.length >= 3)
|
||||
? _iniciarPartida
|
||||
: null,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: Text(l10n.startGame),
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
Reference in New Issue
Block a user