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 createState() => _PantallaGestorHostState(); } class _PantallaGestorHostState extends State { Timer? _timer; int _segundosRestantes = 0; bool _hostListo = false; bool _partidaOnlineGuardada = false; ProgresoGamificacionUsuario? _progresoGamificacion; String? _primerTurnoId; String? _primerTurnoNombre; final Map _clientesListos = {}; final Map _votosRecibidos = {}; @override void initState() { super.initState(); _iniciarTemporizador(); _registrarListeners(); } void _iniciarTemporizador() { final estado = context.read(); 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(); 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().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(); final nearby = context.read(); 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(); final nearby = context.watch(); 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(); 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(); final nearby = context.read(); 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 _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(); final nearby = context.read(); 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().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(); 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(); 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().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().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().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(); final sala = context.read().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(); final sala = context.read().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().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(); await nearby.desconectar(); widget.onPartidaFin(); }, ); default: return const SizedBox.shrink(); } } Widget _buildAccionesResultado(BuildContext context, AppLocalizations l10n) { final estado = context.read(); 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 _finalizarPartidaOnline(BuildContext context) async { final estado = context.read(); final nearby = context.read(); estado.comprobarFinPartida(); final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true) .toJson(); await _guardarHistorialOnlineHost(context); await nearby.enviarFinPartida(snapshotFinal); if (mounted) setState(() {}); } Future _guardarHistorialOnlineHost(BuildContext context) async { if (_partidaOnlineGuardada) return; final partida = context.read().partida; if (partida?.ganador == null) return; _partidaOnlineGuardada = true; final historial = context.read(); final nearby = context.read(); final perfil = context.read(); 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 _iniciarAdivinanzaOnline(BuildContext context) async { final estado = context.read(); final nearby = context.read(); estado.iniciarAdivinanza(); await nearby.enviarCambioFase( 'adivinanza', _snapshot( fase: 'adivinanza', mensaje: AppLocalizations.of(context)!.impostorCanGuess, ).toJson(), ); if (mounted) setState(() {}); } Future _resolverAdivinanzaOnline(BuildContext context) async { final l10n = AppLocalizations.of(context)!; final controller = TextEditingController(); final intento = await showDialog( 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(); final acierto = estado.intentarAdivinar(intento); if (acierto) { final nearby = context.read(); final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true) .toJson(); await _guardarHistorialOnlineHost(context); await nearby.enviarFinPartida(snapshotFinal); if (mounted) setState(() {}); return; } await _siguienteRondaOnline(context); } Future _siguienteRondaOnline(BuildContext context) async { final estado = context.read(); final nearby = context.read(); 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, ), ], ), ), ), ), ); } }