6e5e423ab4
No se puede marcar “vista” sin revelar la palabra antes. Se puede volver a ver la palabra durante debate/votación/resultado. Notas online privadas por partida y jugador. Tests añadidos para notas scoped. Ajusté roomId en el payload de inicio para que las notas no se mezclen entre partidas.
1075 lines
36 KiB
Dart
1075 lines
36 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/inicio_partida_multijugador.dart';
|
|
import '../modelos/jugador.dart';
|
|
import '../modelos/partida.dart';
|
|
import '../servicios/servicio_nearby.dart';
|
|
import '../tema/componentes_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;
|
|
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>();
|
|
|
|
switch (fase) {
|
|
case FaseJuego.debate:
|
|
estado.iniciarDebate();
|
|
final primero = _elegirPrimerTurno();
|
|
nearby.enviarCambioFase('debate', {
|
|
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');
|
|
_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,
|
|
});
|
|
}
|
|
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: const Center(child: Text('Error: Sin partida')),
|
|
);
|
|
}
|
|
|
|
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(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
_buildFaseIndicator(context, partida.fase, l10n),
|
|
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 _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),
|
|
];
|
|
|
|
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);
|
|
default:
|
|
return const Center(child: Text('Fin de la partida'));
|
|
}
|
|
}
|
|
|
|
Widget _buildFaseVerPalabra(
|
|
BuildContext context,
|
|
AppLocalizations l10n,
|
|
bool todosListos,
|
|
ServicioNearby nearby,
|
|
) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.waitingPlayersSeeWord,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
l10n.connectedPlayers,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildJugadorTile(nearby.miNombre ?? 'Host', true, _hostListo),
|
|
...nearby.jugadores.map(
|
|
(j) => _buildJugadorTile(
|
|
j.nombre,
|
|
false,
|
|
_clientesListos[j.endpointId] ?? false,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
// Botón para que el host vea su palabra
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _mostrarPalabraHost(context),
|
|
icon: const Icon(Icons.visibility),
|
|
label: Text(l10n.seeYourWord),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: TemaApp.colorNaranja,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (todosListos)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: TemaApp.colorVerde.withValues(alpha: 0.2),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.check_circle, color: TemaApp.colorVerde),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
l10n.allSeenStartDebate,
|
|
style: const TextStyle(color: TemaApp.colorVerde),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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 Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (tiempo != null) ...[
|
|
Text(l10n.debate, style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: _segundosRestantes == 0
|
|
? TemaApp.colorAcento.withValues(alpha: 0.3)
|
|
: TemaApp.colorTarjeta,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
_segundosRestantes == 0
|
|
? l10n.timeUp
|
|
: l10n.timeRemaining,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
Text(
|
|
_formatearTiempo(_segundosRestantes),
|
|
style: Theme.of(context).textTheme.headlineLarge,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
_buildPrimerTurno(context),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
l10n.activePlayers,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: nearby.jugadores.length + 1,
|
|
itemBuilder: (context, index) {
|
|
if (index == 0) {
|
|
return _buildJugadorTile(
|
|
nearby.miNombre ?? 'Host',
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
final j = nearby.jugadores[index - 1];
|
|
return _buildJugadorTile(j.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(
|
|
'Empieza $nombre diciendo su palabra.',
|
|
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 Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(l10n.voting, style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: TemaApp.colorTarjeta,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(l10n.votesProgress(votosEmitidos, totalVotos)),
|
|
const SizedBox(height: 8),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: progreso.clamp(0.0, 1.0).toDouble(),
|
|
backgroundColor: TemaApp.colorSuperficie,
|
|
valueColor: const AlwaysStoppedAnimation(
|
|
TemaApp.colorAcento,
|
|
),
|
|
minHeight: 8,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
onPressed: _hostYaVoto(context) ? null : () => _abrirVotacionHost(context),
|
|
icon: const Icon(Icons.how_to_vote),
|
|
label: Text(
|
|
_hostYaVoto(context)
|
|
? 'Votos del host registrados'
|
|
: 'Votar por los jugadores de este m?vil',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
l10n.playersVoted,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: partida.jugadoresActivos.length,
|
|
itemBuilder: (context, index) {
|
|
final jugador = partida.jugadoresActivos[index];
|
|
final haVotado = estado.votos.containsKey(jugador.id);
|
|
return _buildJugadorTile(jugador.nombre, false, haVotado);
|
|
},
|
|
),
|
|
),
|
|
if (todosVotaron)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: TemaApp.colorVerde.withValues(alpha: 0.2),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.check_circle, color: TemaApp.colorVerde),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
l10n.allVoted,
|
|
style: const TextStyle(color: TemaApp.colorVerde),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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 const Center(child: Text('Sin resultado'));
|
|
}
|
|
|
|
final conteo = <String, int>{};
|
|
for (final votadoId in resultado.votos.values) {
|
|
conteo[votadoId] = (conteo[votadoId] ?? 0) + 1;
|
|
}
|
|
final maxVotos = conteo.values.isEmpty
|
|
? 1
|
|
: conteo.values.reduce((a, b) => a > b ? a : b);
|
|
final ranking = conteo.entries.toList()
|
|
..sort((a, b) => b.value.compareTo(a.value));
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(l10n.result, style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: TemaApp.decoracionPanel(
|
|
color: resultado.eraImpostor
|
|
? TemaApp.colorVerde.withValues(alpha: 0.18)
|
|
: TemaApp.colorAcento.withValues(alpha: 0.18),
|
|
borderColor: resultado.eraImpostor
|
|
? TemaApp.colorVerde
|
|
: TemaApp.colorAcento,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
resultado.eliminadoNombre,
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
resultado.eraImpostor
|
|
? l10n.wasImpostor
|
|
: l10n.wasInnocent,
|
|
style: TextStyle(
|
|
color: resultado.eraImpostor
|
|
? TemaApp.colorVerde
|
|
: TemaApp.colorAcento,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(l10n.votesThisRound,
|
|
style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 12),
|
|
Expanded(
|
|
child: ListView(
|
|
children: [
|
|
...ranking.map((entry) {
|
|
final jugador = partida.jugadores.firstWhere(
|
|
(j) => j.id == entry.key,
|
|
orElse: () => partida.jugadores.first,
|
|
);
|
|
return _buildBarraResultado(
|
|
context,
|
|
nombre: jugador.nombre,
|
|
votos: entry.value,
|
|
maxVotos: maxVotos,
|
|
destacado: entry.key == resultado.eliminadoId,
|
|
);
|
|
}),
|
|
const Divider(height: 24),
|
|
...resultado.votos.entries.map((entry) {
|
|
final votante = partida.jugadores.firstWhere(
|
|
(j) => j.id == entry.key,
|
|
orElse: () => partida.jugadores.first,
|
|
);
|
|
final votado = partida.jugadores.firstWhere(
|
|
(j) => j.id == entry.value,
|
|
orElse: () => partida.jugadores.first,
|
|
);
|
|
return ListTile(
|
|
dense: true,
|
|
leading: const Icon(Icons.how_to_vote),
|
|
title: Text('${votante.nombre} → ${votado.nombre}'),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBarraResultado(
|
|
BuildContext context, {
|
|
required String nombre,
|
|
required int votos,
|
|
required int maxVotos,
|
|
required bool destacado,
|
|
}) {
|
|
final color = destacado ? TemaApp.colorAcento : TemaApp.colorNaranja;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(child: Text(nombre)),
|
|
Text('$votos',
|
|
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: LinearProgressIndicator(
|
|
value: (votos / maxVotos).clamp(0.0, 1.0).toDouble(),
|
|
minHeight: 10,
|
|
backgroundColor: TemaApp.colorSuperficie,
|
|
valueColor: AlwaysStoppedAnimation(color),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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 Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: listo
|
|
? TemaApp.colorVerde.withValues(alpha: 0.2)
|
|
: TemaApp.colorTarjeta,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
esHost ? 'Host' : 'Cliente',
|
|
style: const TextStyle(fontSize: 18),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text(nombre)),
|
|
if (listo)
|
|
const Icon(Icons.check_circle, color: TemaApp.colorVerde, size: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBotonAccion(
|
|
BuildContext context,
|
|
FaseJuego fase,
|
|
AppLocalizations l10n,
|
|
bool todosListos,
|
|
bool todosVotaron,
|
|
) {
|
|
switch (fase) {
|
|
case FaseJuego.verPalabra:
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 56,
|
|
child: ElevatedButton.icon(
|
|
onPressed: todosListos
|
|
? () => _avanzarAFase(FaseJuego.debate)
|
|
: null,
|
|
icon: const Icon(Icons.forum),
|
|
label: Text(
|
|
todosListos
|
|
? l10n.allSeenStartDebate
|
|
: l10n.waitingPlayersSeeWord,
|
|
),
|
|
),
|
|
);
|
|
case FaseJuego.debate:
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 56,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _avanzarAFase(FaseJuego.votacion),
|
|
icon: const Icon(Icons.how_to_vote),
|
|
label: Text(l10n.goToVoting),
|
|
),
|
|
);
|
|
case FaseJuego.votacion:
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 56,
|
|
child: ElevatedButton.icon(
|
|
onPressed: todosVotaron
|
|
? () => _avanzarAFase(FaseJuego.resultado)
|
|
: null,
|
|
icon: const Icon(Icons.visibility),
|
|
label: Text(todosVotaron ? l10n.revealResult : l10n.waitingVoting),
|
|
),
|
|
);
|
|
default:
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
}
|
|
|
|
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: [
|
|
Text(
|
|
widget.esImpostor ? 'Impostor' : 'Ciudadano',
|
|
style: const TextStyle(fontSize: 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(
|
|
'Categoría: ${widget.categoria}',
|
|
style: Theme.of(context).textTheme.bodyLarge
|
|
?.copyWith(color: TemaApp.colorNaranja),
|
|
),
|
|
],
|
|
],
|
|
)
|
|
: Column(
|
|
children: [
|
|
const Text('🔒', style: TextStyle(fontSize: 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),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 56,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _haRevelado
|
|
? () {
|
|
widget.onVisto();
|
|
Navigator.of(context).pop();
|
|
}
|
|
: null,
|
|
icon: const Icon(Icons.check),
|
|
label: Text(
|
|
_haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|