Posible mejora en el multidispositivo

This commit is contained in:
2026-05-05 22:45:51 +02:00
parent 016333f6c0
commit cfe5d479ff
8 changed files with 1228 additions and 12 deletions

View File

@@ -7,6 +7,7 @@ import '../estado/estado_juego.dart';
import '../modelos/inicio_partida_multijugador.dart';
import '../modelos/jugador.dart';
import '../modelos/partida.dart';
import '../modelos/snapshot_partida_online.dart';
import '../servicios/servicio_nearby.dart';
import '../tema/componentes_farolero.dart';
import '../tema/tema_app.dart';
@@ -94,6 +95,7 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
estado.iniciarDebate();
final primero = _elegirPrimerTurno();
nearby.enviarCambioFase('debate', {
..._snapshot(fase: 'debate').toJson(),
if (primero != null) ...{
'primerTurnoId': primero.id,
'primerTurnoNombre': primero.nombre,
@@ -105,18 +107,19 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
break;
case FaseJuego.votacion:
estado.iniciarVotacion();
nearby.enviarCambioFase('votacion');
nearby.enviarCambioFase('votacion', _snapshot(fase: 'votacion').toJson());
_votosRecibidos.clear();
break;
case FaseJuego.resultado:
final resultado = estado.procesarVotacion();
if (resultado != null) {
nearby.enviarResultadoVotacion({
'eliminadoId': resultado.eliminadoId,
'eliminadoNombre': resultado.eliminadoNombre,
'eraImpostor': resultado.eraImpostor,
'votos': resultado.votos,
});
nearby.enviarResultadoVotacion(
_snapshot(
fase: 'resultado',
resultadoActual: resultado,
mensaje: _mensajeSiguienteAccion(estado, resultado),
).toJson(),
);
}
break;
default:
@@ -233,6 +236,8 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
(FaseJuego.debate, l10n.debate),
(FaseJuego.votacion, l10n.voting),
(FaseJuego.resultado, l10n.result),
(FaseJuego.adivinanza, l10n.guess),
(FaseJuego.finPartida, l10n.gameOver),
];
return SingleChildScrollView(
@@ -278,11 +283,55 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
return _buildFaseVotacion(context, l10n, todosVotaron, nearby);
case FaseJuego.resultado:
return _buildFaseResultado(context, l10n);
case FaseJuego.adivinanza:
return _buildFaseAdivinanza(context, l10n);
case FaseJuego.finPartida:
return _buildFaseFinOnline(context, l10n);
default:
return const Center(child: Text('Fin de la partida'));
}
}
SnapshotPartidaOnline _snapshot({
required String fase,
ResultadoVotacion? resultadoActual,
String? mensaje,
bool revelarFinal = false,
}) {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
final partida = estado.partida!;
return SnapshotPartidaOnline.desdePartida(
partida,
roomId: nearby.roomId,
fase: fase,
resultadoActual: resultadoActual,
mensaje: mensaje,
revelarPalabra: revelarFinal,
revelarImpostores: revelarFinal,
);
}
String _mensajeSiguienteAccion(
EstadoJuego estado,
ResultadoVotacion resultado,
) {
final partida = estado.partida;
if (partida != null && _hayFinTrasVotacion(partida)) {
return 'La partida ha terminado.';
}
if (resultado.eraImpostor) {
return 'El impostor eliminado puede intentar adivinar la palabra.';
}
return 'La partida continúa en la siguiente ronda.';
}
bool _hayFinTrasVotacion(Partida partida) {
final impostoresVivos = partida.impostoresActivos.length;
final jugadoresVivos = partida.jugadoresNormalesActivos.length;
return impostoresVivos == 0 || impostoresVivos >= jugadoresVivos;
}
Widget _buildFaseVerPalabra(
BuildContext context,
AppLocalizations l10n,
@@ -775,6 +824,67 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
);
}
Widget _buildFaseAdivinanza(BuildContext context, AppLocalizations l10n) {
final partida = context.watch<EstadoJuego>().partida;
final ultimo = partida?.historialVotaciones.isNotEmpty == true
? partida!.historialVotaciones.last
: null;
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.psychology, size: 56, color: TemaApp.colorNaranja),
const SizedBox(height: 16),
Text(
l10n.impostorGuessTitle,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
ultimo == null
? l10n.impostorCanGuess
: '${ultimo.eliminadoNombre}: ${l10n.impostorCanGuess}',
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildFaseFinOnline(BuildContext context, AppLocalizations l10n) {
final partida = context.watch<EstadoJuego>().partida;
final ganaronJugadores = partida?.ganador == 'jugadores';
return Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
ganaronJugadores ? '🎉' : '🎭',
style: const TextStyle(fontSize: 64),
),
const SizedBox(height: 16),
Text(
ganaronJugadores ? l10n.playersWin : l10n.impostorsWin,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 12),
Text(
partida == null ? '' : l10n.theWordWas(partida.palabraSecreta),
textAlign: TextAlign.center,
),
],
),
),
);
}
bool _hostYaVoto(BuildContext context) {
final estado = context.read<EstadoJuego>();
final sala = context.read<ServicioNearby>().estadoSala;
@@ -895,10 +1005,200 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
label: Text(todosVotaron ? l10n.revealResult : l10n.waitingVoting),
),
);
case FaseJuego.resultado:
return _buildAccionesResultado(context, l10n);
case FaseJuego.adivinanza:
return _buildAccionesAdivinanza(context, l10n);
case FaseJuego.finPartida:
return SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () async {
final nearby = context.read<ServicioNearby>();
await nearby.desconectar();
widget.onPartidaFin();
},
icon: const Icon(Icons.home),
label: Text(l10n.mainMenu),
),
);
default:
return const SizedBox.shrink();
}
}
Widget _buildAccionesResultado(BuildContext context, AppLocalizations l10n) {
final estado = context.read<EstadoJuego>();
final partida = estado.partida;
final resultado = partida?.historialVotaciones.isNotEmpty == true
? partida!.historialVotaciones.last
: null;
if (partida == null || resultado == null) return const SizedBox.shrink();
if (_hayFinTrasVotacion(partida)) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => _finalizarPartidaOnline(context),
icon: const Icon(Icons.emoji_events),
label: Text(l10n.seeEndResult),
),
);
}
if (resultado.eraImpostor) {
return Column(
children: [
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _iniciarAdivinanzaOnline(context),
icon: const Icon(Icons.psychology),
label: Text(l10n.impostorGuessWord),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => _siguienteRondaOnline(context),
icon: const Icon(Icons.skip_next),
label: Text(l10n.nextRound),
),
),
],
);
}
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => _siguienteRondaOnline(context),
icon: const Icon(Icons.skip_next),
label: Text(l10n.nextRound),
),
);
}
Widget _buildAccionesAdivinanza(BuildContext context, AppLocalizations l10n) {
return Column(
children: [
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => _resolverAdivinanzaOnline(context),
icon: const Icon(Icons.check_circle),
label: Text(l10n.guess),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _siguienteRondaOnline(context),
icon: const Icon(Icons.skip_next),
label: Text(l10n.dontGuess),
),
),
],
);
}
Future<void> _finalizarPartidaOnline(BuildContext context) async {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
estado.comprobarFinPartida();
await nearby.enviarFinPartida(
_snapshot(fase: 'finPartida', revelarFinal: true).toJson(),
);
if (mounted) setState(() {});
}
Future<void> _iniciarAdivinanzaOnline(BuildContext context) async {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
estado.iniciarAdivinanza();
await nearby.enviarCambioFase(
'adivinanza',
_snapshot(
fase: 'adivinanza',
mensaje: AppLocalizations.of(context)!.impostorCanGuess,
).toJson(),
);
if (mounted) setState(() {});
}
Future<void> _resolverAdivinanzaOnline(BuildContext context) async {
final l10n = AppLocalizations.of(context)!;
final controller = TextEditingController();
final intento = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n.impostorGuessTitle),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(hintText: l10n.guessWordHint),
onSubmitted: (value) => Navigator.pop(ctx, value),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () => Navigator.pop(ctx, controller.text),
child: Text(l10n.accept),
),
],
),
);
controller.dispose();
if (!context.mounted) return;
if (intento == null || intento.trim().isEmpty) {
await _siguienteRondaOnline(context);
return;
}
final estado = context.read<EstadoJuego>();
final acierto = estado.intentarAdivinar(intento);
if (acierto) {
final nearby = context.read<ServicioNearby>();
await nearby.enviarFinPartida(
_snapshot(fase: 'finPartida', revelarFinal: true).toJson(),
);
if (mounted) setState(() {});
return;
}
await _siguienteRondaOnline(context);
}
Future<void> _siguienteRondaOnline(BuildContext context) async {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
estado.siguienteRonda();
_primerTurnoId = null;
_primerTurnoNombre = null;
final primero = _elegirPrimerTurno();
await nearby.enviarCambioFase('debate', {
..._snapshot(fase: 'debate').toJson(),
if (primero != null) ...{
'primerTurnoId': primero.id,
'primerTurnoNombre': primero.nombre,
},
if (estado.partida?.config.tiempoDebateSegundos != null)
'tiempoDebateSegundos': estado.partida!.config.tiempoDebateSegundos,
});
_timer?.cancel();
_iniciarTemporizador();
if (mounted) setState(() {});
}
}
class _PantallaRevelarPalabraHost extends StatefulWidget {