Files
farolero/lib/pantallas/pantalla_gestor_host.dart
T

1206 lines
40 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../modelos/gamificacion_usuario.dart';
import '../modelos/inicio_partida_multijugador.dart';
import '../modelos/jugador.dart';
import '../modelos/palabra.dart';
import '../modelos/partida.dart';
import '../modelos/snapshot_partida_online.dart';
import '../servicios/servicio_historial_partidas.dart';
import '../servicios/servicio_nearby.dart';
import '../servicios/servicio_perfil_usuario.dart';
import '../tema/componentes_farolero.dart';
import '../tema/componentes_resultado_farolero.dart';
import '../tema/tema_app.dart';
import 'pantalla_notas_online.dart';
import 'pantalla_revision_palabra.dart';
import 'pantalla_votacion_cliente.dart';
import 'pantalla_palabras_cliente.dart';
class PantallaGestorHost extends StatefulWidget {
final VoidCallback onPartidaFin;
const PantallaGestorHost({super.key, required this.onPartidaFin});
@override
State<PantallaGestorHost> createState() => _PantallaGestorHostState();
}
class _PantallaGestorHostState extends State<PantallaGestorHost> {
Timer? _timer;
int _segundosRestantes = 0;
bool _hostListo = false;
bool _partidaOnlineGuardada = false;
ProgresoGamificacionUsuario? _progresoGamificacion;
String? _primerTurnoId;
String? _primerTurnoNombre;
final Map<String, bool> _clientesListos = {};
final Map<String, String> _votosRecibidos = {};
@override
void initState() {
super.initState();
_iniciarTemporizador();
_registrarListeners();
}
void _iniciarTemporizador() {
final estado = context.read<EstadoJuego>();
final tiempo = estado.partida?.config.tiempoDebateSegundos;
if (tiempo != null) {
_segundosRestantes = tiempo;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_segundosRestantes > 0) {
setState(() => _segundosRestantes--);
} else {
timer.cancel();
}
});
}
}
void _registrarListeners() {
final nearby = context.read<ServicioNearby>();
nearby.onMensaje((endpointId, mensaje) {
if (mensaje.tipo == TipoMensaje.listo) {
setState(() => _clientesListos[endpointId] = true);
} else if (mensaje.tipo == TipoMensaje.voto) {
final votanteId = mensaje.datos['votanteId'] as String?;
final votoId =
mensaje.datos['votadoId'] as String? ??
mensaje.datos['votoporId'] as String?;
if (votanteId != null && votoId != null) {
context.read<EstadoJuego>().registrarVoto(votanteId, votoId);
setState(() => _votosRecibidos[votanteId] = votoId);
}
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
String _formatearTiempo(int segundos) {
final min = segundos ~/ 60;
final seg = segundos % 60;
return "${min.toString().padLeft(2, '0')}:${seg.toString().padLeft(2, '0')}";
}
void _avanzarAFase(FaseJuego fase) {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
final l10n = AppLocalizations.of(context)!;
switch (fase) {
case FaseJuego.debate:
estado.iniciarDebate();
final primero = _elegirPrimerTurno();
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,
});
_iniciarTemporizador();
break;
case FaseJuego.votacion:
estado.iniciarVotacion();
nearby.enviarCambioFase('votacion', _snapshot(fase: 'votacion').toJson());
_votosRecibidos.clear();
break;
case FaseJuego.resultado:
final resultado = estado.procesarVotacion();
if (resultado != null) {
nearby.enviarResultadoVotacion(
_snapshot(
fase: 'resultado',
resultadoActual: resultado,
mensaje: _mensajeSiguienteAccion(estado, resultado, l10n),
).toJson(),
);
}
break;
default:
break;
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final nearby = context.watch<ServicioNearby>();
final partida = estado.partida;
if (partida == null) {
return Scaffold(
appBar: AppBar(title: Text(l10n.hostGame)),
body: FondoFarolero(
intenso: true,
child: Center(child: Text(l10n.errorNoGame)),
),
);
}
final todosListos =
_hostListo && _clientesListos.length >= nearby.jugadores.length;
final todosVotaron = estado.todosHanVotado();
return Scaffold(
appBar: AppBar(
title: Text(l10n.hostGame),
automaticallyImplyLeading: false,
actions: [
IconButton(
tooltip: l10n.seeYourWord,
icon: const Icon(Icons.visibility),
onPressed: partida.fase.index <= FaseJuego.verPalabra.index
? null
: () => mostrarRevisionPalabraOnline(
context: context,
jugadoresControlados: _jugadoresHostControlados(
partida,
nearby,
),
pistaCategoria: partida.config.pistaImpostor
? partida.categoriaReal
: null,
),
),
IconButton(
tooltip: l10n.notesTitle,
icon: const Icon(Icons.edit_note),
onPressed: partida.fase.index < FaseJuego.debate.index ||
nearby.roomId == null
? null
: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PantallaNotasOnline(
partidaId: nearby.roomId!,
jugadores: partida.jugadoresActivos,
autoresControlados: _jugadoresHostControlados(
partida,
nearby,
),
),
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
await nearby.desconectar();
widget.onPartidaFin();
},
),
],
),
body: FondoFarolero(
intenso: true,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildAvisoClientesDesconectados(context, nearby),
_buildFaseIndicator(context, partida.fase, l10n),
const SizedBox(height: 8),
const ArteGameplayFarolero.fase(height: 92),
const SizedBox(height: 16),
Expanded(
child: _buildContenidoFase(
context,
partida.fase,
l10n,
todosListos,
todosVotaron,
),
),
const SizedBox(height: 16),
_buildBotonAccion(
context,
partida.fase,
l10n,
todosListos,
todosVotaron,
),
],
),
),
),
);
}
Widget _buildAvisoClientesDesconectados(
BuildContext context,
ServicioNearby nearby,
) {
final sala = nearby.estadoSala;
final usuariosAfectados = sala?.usuariosDeClientesDesconectados ?? const [];
if (usuariosAfectados.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: TemaApp.decoracionPanel(
color: TemaApp.colorAcento.withValues(alpha: 0.16),
borderColor: TemaApp.colorAcento.withValues(alpha: 0.65),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.link_off, color: TemaApp.colorAcento),
const SizedBox(width: 8),
Expanded(
child: Text(
AppLocalizations.of(context)!.disconnectedPlayersWarning,
style: Theme.of(context).textTheme.titleSmall,
),
),
],
),
const SizedBox(height: 6),
Text(
usuariosAfectados.map((usuario) => usuario.nombre).join(', '),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 260,
child: BotonFarolero.oscuro(
texto: AppLocalizations.of(context)!.assumeOnThisPhone,
icono: Icons.person_add_alt_1,
assetIconPath: 'assets/ui/generated/actions/action_add_player.webp',
onPressed: () => nearby.asumirUsuariosDesconectados(),
),
),
),
],
),
),
);
}
Widget _buildFaseIndicator(
BuildContext context,
FaseJuego fase,
AppLocalizations l10n,
) {
final fases = [
(FaseJuego.verPalabra, l10n.seeYourWord),
(FaseJuego.debate, l10n.debate),
(FaseJuego.votacion, l10n.voting),
(FaseJuego.resultado, l10n.result),
(FaseJuego.adivinanza, l10n.guess),
(FaseJuego.finPartida, l10n.gameOver),
];
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: fases.map((e) {
final esActiva = fase == e.$1 || fase.index > e.$1.index;
return Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: esActiva ? TemaApp.colorAcento : TemaApp.colorSuperficie,
borderRadius: BorderRadius.circular(20),
),
child: Text(
e.$2,
style: TextStyle(
color: esActiva ? Colors.white : TemaApp.colorTextoSecundario,
fontWeight: esActiva ? FontWeight.bold : FontWeight.normal,
),
),
);
}).toList(),
),
);
}
Widget _buildContenidoFase(
BuildContext context,
FaseJuego fase,
AppLocalizations l10n,
bool todosListos,
bool todosVotaron,
) {
final nearby = context.watch<ServicioNearby>();
switch (fase) {
case FaseJuego.verPalabra:
return _buildFaseVerPalabra(context, l10n, todosListos, nearby);
case FaseJuego.debate:
return _buildFaseDebate(context, l10n, nearby);
case FaseJuego.votacion:
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 Center(child: Text(l10n.gameOver));
}
}
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,
AppLocalizations l10n,
) {
final partida = estado.partida;
if (partida != null && _hayFinTrasVotacion(partida)) {
return l10n.gameOver;
}
if (resultado.eraImpostor) {
return l10n.impostorCanGuess.replaceAll('\n', ' ');
}
return l10n.gameContinues;
}
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,
bool todosListos,
ServicioNearby nearby,
) {
return TarjetaFaseFarolero(
icono: Icons.visibility,
assetIconPath: 'assets/ui/generated/actions/action_reveal_word.webp',
titulo: l10n.waitingPlayersSeeWord,
subtitulo: l10n.connectedPlayers,
child: Column(
children: [
_buildJugadorTile(nearby.miNombre ?? 'Host', true, _hostListo),
...nearby.jugadores.map(
(jugador) => _buildJugadorTile(
jugador.nombre,
false,
_clientesListos[jugador.endpointId] ?? false,
),
),
const SizedBox(height: 12),
BotonFarolero(
texto: l10n.seeYourWord,
icono: Icons.visibility,
assetIconPath: 'assets/ui/generated/actions/action_reveal_word.webp',
onPressed: () => _mostrarPalabraHost(context),
),
if (todosListos) ...[
const SizedBox(height: 12),
EstadoJugadorFarolero(
nombre: l10n.allSeenStartDebate,
completado: true,
icono: Icons.check_circle,
),
],
],
),
);
}
List<JugadorInicioPartida> _jugadoresHostControlados(
Partida partida,
ServicioNearby nearby,
) {
final sala = nearby.estadoSala;
if (sala == null) return const [];
return sala
.usuariosPorCliente(sala.hostClientId)
.where((usuario) => partida.jugadores.any((j) => j.id == usuario.id))
.map((usuario) {
final jugador = partida.jugadores.firstWhere(
(j) => j.id == usuario.id,
);
return JugadorInicioPartida(
jugadorId: jugador.id,
nombre: jugador.nombre,
esImpostor: jugador.esImpostor,
palabra: jugador.palabra ?? partida.palabraSecreta,
);
})
.toList();
}
void _mostrarPalabraHost(BuildContext context) {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
final partida = estado.partida;
if (partida == null) return;
final jugadoresHost = _jugadoresHostControlados(partida, nearby);
if (jugadoresHost.length > 1) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PantallaPalabrasCliente(
jugadores: jugadoresHost,
pistaCategoria: partida.config.pistaImpostor
? partida.categoriaReal
: null,
onTodosVistos: () {
setState(() => _hostListo = true);
Navigator.of(context).pop();
},
),
),
);
return;
}
final hostLocal = jugadoresHost.isNotEmpty
? partida.jugadores.firstWhere((j) => j.id == jugadoresHost.first.jugadorId)
: partida.jugadores.first;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => _PantallaRevelarPalabraHost(
nombre: hostLocal.nombre,
esImpostor: hostLocal.esImpostor,
palabra: partida.palabraSecreta,
pistaActiva: partida.config.pistaImpostor,
categoria: partida.categoriaReal,
onVisto: () => setState(() => _hostListo = true),
),
),
);
}
Jugador? _elegirPrimerTurno() {
final partida = context.read<EstadoJuego>().partida;
if (partida == null || partida.jugadoresActivos.isEmpty) return null;
if (_primerTurnoId != null) {
return partida.jugadoresActivos.firstWhere(
(j) => j.id == _primerTurnoId,
orElse: () => partida.jugadoresActivos.first,
);
}
final elegido = partida.jugadoresActivos[
Random.secure().nextInt(partida.jugadoresActivos.length)];
_primerTurnoId = elegido.id;
_primerTurnoNombre = elegido.nombre;
return elegido;
}
Widget _buildFaseDebate(
BuildContext context,
AppLocalizations l10n,
ServicioNearby nearby,
) {
final estado = context.read<EstadoJuego>();
final tiempo = estado.partida?.config.tiempoDebateSegundos;
return TarjetaFaseFarolero(
icono: Icons.forum,
assetIconPath: 'assets/ui/generated/actions/action_rules_book.webp',
titulo: l10n.debate,
subtitulo: l10n.debateInstructions,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tiempo != null) ...[
TemporizadorFarolero(
etiqueta: _segundosRestantes == 0
? l10n.timeUp
: l10n.timeRemaining,
tiempo: _formatearTiempo(_segundosRestantes),
agotado: _segundosRestantes == 0,
),
const SizedBox(height: 16),
],
_buildPrimerTurno(context),
const SizedBox(height: 16),
Text(l10n.activePlayers, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
_buildJugadorTile(nearby.miNombre ?? 'Host', true, true),
...nearby.jugadores.map(
(jugador) => _buildJugadorTile(jugador.nombre, false, true),
),
],
),
);
}
Widget _buildPrimerTurno(BuildContext context) {
final primero = _elegirPrimerTurno();
final nombre = _primerTurnoNombre ?? primero?.nombre;
if (nombre == null) return const SizedBox.shrink();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: TemaApp.decoracionPanel(
color: TemaApp.colorNaranja.withValues(alpha: 0.16),
borderColor: TemaApp.colorNaranja.withValues(alpha: 0.7),
),
child: Row(
children: [
const Icon(Icons.record_voice_over, color: TemaApp.colorNaranja),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(context)!.firstTurnInstruction(nombre),
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
);
}
Widget _buildFaseVotacion(
BuildContext context,
AppLocalizations l10n,
bool todosVotaron,
ServicioNearby nearby,
) {
final estado = context.watch<EstadoJuego>();
final partida = estado.partida!;
final totalVotos = partida.jugadoresActivos.length;
final votosEmitidos = estado.votos.length;
final progreso = totalVotos == 0 ? 0.0 : votosEmitidos / totalVotos;
return TarjetaFaseFarolero(
icono: Icons.how_to_vote,
assetIconPath: 'assets/ui/generated/actions/action_vote_mask.webp',
titulo: l10n.voting,
subtitulo: l10n.votesProgress(votosEmitidos, totalVotos),
color: TemaApp.colorAcento,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
value: progreso.clamp(0.0, 1.0).toDouble(),
minHeight: 14,
backgroundColor: Colors.black.withValues(alpha: 0.35),
valueColor: const AlwaysStoppedAnimation(TemaApp.colorAcento),
),
),
const SizedBox(height: 16),
if (!_hostYaVoto(context))
BotonFarolero.secundario(
texto: l10n.votar,
icono: Icons.how_to_vote,
onPressed: () => _abrirVotacionHost(context),
),
if (!_hostYaVoto(context)) const SizedBox(height: 16),
...partida.jugadoresActivos.map((jugador) {
final haVotado = estado.votos.containsKey(jugador.id);
return _buildJugadorTile(jugador.nombre, false, haVotado);
}),
if (todosVotaron)
EstadoJugadorFarolero(
nombre: l10n.allVoted,
completado: true,
icono: Icons.check_circle,
),
],
),
);
}
Widget _buildFaseResultado(BuildContext context, AppLocalizations l10n) {
final partida = context.watch<EstadoJuego>().partida;
final resultado = partida?.historialVotaciones.isNotEmpty == true
? partida!.historialVotaciones.last
: null;
if (partida == null || resultado == null) {
return Center(child: Text(l10n.noResult));
}
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 8),
child: ResultadoRondaFarolero(
resultado: resultado,
jugadores: partida.jugadores,
),
);
}
Widget _buildFaseAdivinanza(BuildContext context, AppLocalizations l10n) {
final partida = context.watch<EstadoJuego>().partida;
final ultimo = partida?.historialVotaciones.isNotEmpty == true
? partida!.historialVotaciones.last
: null;
return TarjetaFaseFarolero(
icono: Icons.psychology,
assetIconPath: 'assets/ui/generated/actions/action_impostor_mask.webp',
titulo: l10n.impostorGuessTitle,
subtitulo: ultimo == null
? l10n.impostorCanGuess
: '${ultimo.eliminadoNombre}: ${l10n.impostorCanGuess}',
color: TemaApp.colorNaranja,
child: const ArteGameplayFarolero.resultado(height: 132),
);
}
Widget _buildFaseFinOnline(BuildContext context, AppLocalizations l10n) {
final partida = context.watch<EstadoJuego>().partida;
if (partida == null) return Center(child: Text(l10n.noResult));
final ganaronJugadores = partida.ganador == 'jugadores';
final color = ganaronJugadores ? TemaApp.colorVerde : TemaApp.colorAcento;
final impostores = partida.jugadores.where((j) => j.esImpostor).toList();
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
children: [
HeroFinalPartidaFarolero(
encabezado: l10n.gameOver,
titulo: ganaronJugadores ? l10n.playersWin : l10n.impostorsWin,
icono: ganaronJugadores ? Icons.emoji_events : Icons.theater_comedy,
color: color,
),
const SizedBox(height: 12),
if (_progresoGamificacion == null)
const TarjetaRecompensaCargandoPremium()
else
TarjetaProgresoGamificacionPremium(
progreso: _progresoGamificacion!,
),
const SizedBox(height: 18),
TarjetaSecretoPremium(
palabra: partida.palabraSecreta,
categoria: BancoPalabras.nombreBonitoCategoria(
partida.categoriaReal,
l10n,
),
),
const SizedBox(height: 18),
TarjetaImpostoresPremium(
titulo: impostores.length == 1
? l10n.theImpostorWas
: l10n.theImpostorsWere,
impostores: impostores,
),
const SizedBox(height: 18),
if (partida.historialVotaciones.isNotEmpty)
TarjetaHistorialVotosPremium(
historial: partida.historialVotaciones,
jugadores: partida.jugadores,
),
],
),
);
}
bool _hostYaVoto(BuildContext context) {
final estado = context.read<EstadoJuego>();
final sala = context.read<ServicioNearby>().estadoSala;
if (sala == null || estado.partida == null) return false;
final hostIds = sala.usuariosPorCliente(sala.hostClientId).map((u) => u.id);
return hostIds.every((id) => estado.votos.containsKey(id));
}
void _abrirVotacionHost(BuildContext context) {
final estado = context.read<EstadoJuego>();
final sala = context.read<ServicioNearby>().estadoSala;
final partida = estado.partida;
if (sala == null || partida == null) return;
final jugadoresHost = sala.usuariosPorCliente(sala.hostClientId)
.where((usuario) => partida.jugadoresActivos.any((j) => j.id == usuario.id))
.map(
(usuario) => JugadorInicioPartida(
jugadorId: usuario.id,
nombre: usuario.nombre,
esImpostor: partida.jugadores.firstWhere((j) => j.id == usuario.id).esImpostor,
palabra: partida.jugadores.firstWhere((j) => j.id == usuario.id).palabra,
),
)
.toList();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PantallaVotacionCliente(
jugadores: partida.jugadoresActivos,
jugadoresControlados: jugadoresHost,
partidaId: context.read<ServicioNearby>().roomId,
pistaCategoria: partida.config.pistaImpostor
? partida.categoriaReal
: null,
onVotos: (votos) {
for (final entry in votos.entries) {
estado.registrarVoto(entry.key, entry.value);
_votosRecibidos[entry.key] = entry.value;
}
if (mounted) setState(() {});
Navigator.of(context).pop();
},
),
),
);
}
Widget _buildJugadorTile(String nombre, bool esHost, bool listo) {
return EstadoJugadorFarolero(
nombre: nombre,
destacado: esHost,
completado: listo,
icono: esHost ? Icons.phone_android : Icons.devices,
assetIconPath: esHost ? 'assets/ui/generated/actions/action_mobile_device.webp' : 'assets/ui/generated/actions/action_multidevice_signal.webp',
);
}
Widget _buildBotonAccion(
BuildContext context,
FaseJuego fase,
AppLocalizations l10n,
bool todosListos,
bool todosVotaron,
) {
switch (fase) {
case FaseJuego.verPalabra:
return BotonFarolero(
texto: todosListos ? l10n.allSeenStartDebate : l10n.waitingPlayersSeeWord,
icono: Icons.forum,
assetIconPath: 'assets/ui/generated/actions/action_rules_book.webp',
onPressed: todosListos ? () => _avanzarAFase(FaseJuego.debate) : null,
);
case FaseJuego.debate:
return BotonFarolero.secundario(
texto: l10n.goToVoting,
icono: Icons.how_to_vote,
assetIconPath: 'assets/ui/generated/actions/action_vote_mask.webp',
onPressed: () => _avanzarAFase(FaseJuego.votacion),
);
case FaseJuego.votacion:
return BotonFarolero(
texto: todosVotaron ? l10n.revealResult : l10n.waitingVoting,
icono: Icons.visibility,
assetIconPath: 'assets/ui/generated/actions/action_result_trophy.webp',
onPressed: todosVotaron ? () => _avanzarAFase(FaseJuego.resultado) : null,
);
case FaseJuego.resultado:
return _buildAccionesResultado(context, l10n);
case FaseJuego.adivinanza:
return _buildAccionesAdivinanza(context, l10n);
case FaseJuego.finPartida:
return BotonFarolero.oscuro(
texto: l10n.mainMenu,
icono: Icons.home,
onPressed: () async {
final nearby = context.read<ServicioNearby>();
await nearby.desconectar();
widget.onPartidaFin();
},
);
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 BotonFarolero(
texto: l10n.seeEndResult,
icono: Icons.emoji_events,
assetIconPath: 'assets/ui/generated/actions/action_result_trophy.webp',
onPressed: () => _finalizarPartidaOnline(context),
);
}
if (resultado.eraImpostor) {
return Column(
children: [
BotonFarolero.oscuro(
texto: l10n.impostorGuessWord,
icono: Icons.psychology,
assetIconPath: 'assets/ui/generated/actions/action_impostor_mask.webp',
onPressed: () => _iniciarAdivinanzaOnline(context),
),
const SizedBox(height: 12),
BotonFarolero(
texto: l10n.nextRound,
icono: Icons.skip_next,
onPressed: () => _siguienteRondaOnline(context),
),
],
);
}
return BotonFarolero(
texto: l10n.nextRound,
icono: Icons.skip_next,
onPressed: () => _siguienteRondaOnline(context),
);
}
Widget _buildAccionesAdivinanza(BuildContext context, AppLocalizations l10n) {
return Column(
children: [
BotonFarolero(
texto: l10n.guess,
icono: Icons.check_circle,
assetIconPath: 'assets/ui/generated/actions/action_impostor_mask.webp',
onPressed: () => _resolverAdivinanzaOnline(context),
),
const SizedBox(height: 12),
BotonFarolero.oscuro(
texto: l10n.dontGuess,
icono: Icons.skip_next,
onPressed: () => _siguienteRondaOnline(context),
),
],
);
}
Future<void> _finalizarPartidaOnline(BuildContext context) async {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
estado.comprobarFinPartida();
final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true)
.toJson();
await _guardarHistorialOnlineHost(context);
await nearby.enviarFinPartida(snapshotFinal);
if (mounted) setState(() {});
}
Future<void> _guardarHistorialOnlineHost(BuildContext context) async {
if (_partidaOnlineGuardada) return;
final partida = context.read<EstadoJuego>().partida;
if (partida?.ganador == null) return;
_partidaOnlineGuardada = true;
final historial = context.read<ServicioHistorialPartidas>();
final nearby = context.read<ServicioNearby>();
final perfil = context.read<ServicioPerfilUsuario>();
await historial.guardarPartida(partida!);
final jugadoresHost = _jugadoresHostControlados(
partida,
nearby,
);
final ganaronImpostores = partida.ganador == 'impostores';
final victoria = jugadoresHost.isEmpty
? partida.ganador == 'jugadores'
: jugadoresHost.any(
(jugador) =>
jugador.esImpostor ? ganaronImpostores : !ganaronImpostores,
);
final progreso = await perfil.registrarPartidaCompletada(
victoria: victoria,
comoImpostor: jugadoresHost.any((j) => j.esImpostor),
victoriaComoImpostor:
ganaronImpostores && jugadoresHost.any((j) => j.esImpostor),
);
if (mounted) setState(() => _progresoGamificacion = progreso);
}
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>();
final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true)
.toJson();
await _guardarHistorialOnlineHost(context);
await nearby.enviarFinPartida(snapshotFinal);
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 {
final String nombre;
final bool esImpostor;
final String palabra;
final bool pistaActiva;
final String categoria;
final VoidCallback onVisto;
const _PantallaRevelarPalabraHost({
required this.nombre,
required this.esImpostor,
required this.palabra,
required this.pistaActiva,
required this.categoria,
required this.onVisto,
});
@override
State<_PantallaRevelarPalabraHost> createState() =>
_PantallaRevelarPalabraHostState();
}
class _PantallaRevelarPalabraHostState
extends State<_PantallaRevelarPalabraHost> {
bool _manteniendo = false;
bool _haRevelado = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(widget.nombre)),
body: FondoFarolero(
intenso: true,
child: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.nombre,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 32),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: _manteniendo
? (widget.esImpostor
? TemaApp.colorAcento.withValues(alpha: 0.3)
: TemaApp.colorVerde.withValues(alpha: 0.3))
: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _manteniendo
? (widget.esImpostor
? TemaApp.colorAcento
: TemaApp.colorVerde)
: Colors.transparent,
width: 2,
),
),
child: _manteniendo
? Column(
children: [
Icon(
widget.esImpostor
? Icons.theater_comedy
: Icons.key,
color: widget.esImpostor
? TemaApp.colorAcento
: TemaApp.colorVerde,
size: 48,
),
const SizedBox(height: 16),
Text(
widget.esImpostor
? l10n.youAreImpostor
: l10n.yourWordIs,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(
color: widget.esImpostor
? TemaApp.colorAcento
: TemaApp.colorVerde,
),
),
if (!widget.esImpostor) ...[
const SizedBox(height: 12),
TarjetaPalabraFarolero(palabra: widget.palabra),
],
if (widget.esImpostor && widget.pistaActiva) ...[
const SizedBox(height: 12),
Text(
l10n.clueCategory(
BancoPalabras.nombreBonitoCategoria(
widget.categoria,
l10n,
),
),
style: Theme.of(context).textTheme.bodyLarge
?.copyWith(color: TemaApp.colorNaranja),
),
],
],
)
: Column(
children: [
const Icon(
Icons.lock,
color: TemaApp.colorDorado,
size: 48,
),
const SizedBox(height: 16),
Text(
l10n.holdToSeeWord,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.makeSureNoOneLooks,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 24),
GestureDetector(
onLongPressStart: (_) => setState(() {
_manteniendo = true;
_haRevelado = true;
}),
onLongPressEnd: (_) => setState(() => _manteniendo = false),
child: Container(
width: double.infinity,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _manteniendo
? [TemaApp.colorNaranja, TemaApp.colorAcento]
: [TemaApp.colorAcento, TemaApp.colorAcento],
),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
_manteniendo ? l10n.showingWord : l10n.holdToSee,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
),
const SizedBox(height: 16),
BotonFarolero(
texto: _haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
icono: Icons.check,
onPressed: _haRevelado
? () {
widget.onVisto();
Navigator.of(context).pop();
}
: null,
),
],
),
),
),
),
);
}
}