import 'dart:math' as math; import 'package:confetti/confetti.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:provider/provider.dart'; import '../estado/estado_juego.dart'; import '../modelos/gamificacion_usuario.dart'; import '../modelos/jugador.dart'; import '../modelos/palabra.dart'; import '../modelos/partida.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/tema_app.dart'; import 'pantalla_principal.dart'; import 'pantalla_ver_palabra.dart'; class PantallaFinPartida extends StatefulWidget { const PantallaFinPartida({super.key}); @override State createState() => _PantallaFinPartidaState(); } class _PantallaFinPartidaState extends State { bool _guardada = false; ProgresoGamificacionUsuario? _progreso; late final ConfettiController _confetti; @override void initState() { super.initState(); _confetti = ConfettiController(duration: const Duration(seconds: 3)); } @override void dispose() { _confetti.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final estado = context.watch(); final partida = estado.partida; if (partida == null) return const SizedBox.shrink(); final ganaronJugadores = partida.ganador == 'jugadores'; final impostores = partida.jugadores.where((j) => j.esImpostor).toList(); _registrarResultadoSiHaceFalta(context, partida, ganaronJugadores); return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( title: Text(l10n.gameOver), automaticallyImplyLeading: false, backgroundColor: Colors.transparent, elevation: 0, ), body: FondoFarolero( intenso: true, child: Stack( children: [ Positioned.fill( child: IgnorePointer( child: CustomPaint( painter: _RecompensaFondoPainter( color: ganaronJugadores ? TemaApp.colorVerde : TemaApp.colorAcento, ), ), ), ), Align( alignment: Alignment.topCenter, child: ConfettiWidget( confettiController: _confetti, blastDirectionality: BlastDirectionality.explosive, emissionFrequency: 0.06, numberOfParticles: 18, gravity: 0.22, colors: const [ TemaApp.colorDorado, TemaApp.colorNaranja, TemaApp.colorAcento, Color(0xFFFFECBE), ], ), ), SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), child: Column( children: [ _HeroResultado( titulo: ganaronJugadores ? l10n.playersWin : l10n.impostorsWin, icono: ganaronJugadores ? Icons.emoji_events : Icons.theater_comedy, color: ganaronJugadores ? TemaApp.colorVerde : TemaApp.colorAcento, ), const SizedBox(height: 18), if (_progreso == null) const _TarjetaRecompensaCargando() else _TarjetaProgresoGamificacion(progreso: _progreso!), const SizedBox(height: 16), _TarjetaSecreto( palabra: partida.palabraSecreta, categoria: BancoPalabras.nombreBonitoCategoria( partida.categoriaReal, l10n, ), ), const SizedBox(height: 16), _TarjetaImpostores( titulo: impostores.length == 1 ? l10n.theImpostorWas : l10n.theImpostorsWere, impostores: impostores, ), const SizedBox(height: 16), if (partida.historialVotaciones.isNotEmpty) _TarjetaHistorialVotos(partida: partida, l10n: l10n), const SizedBox(height: 24), _BotonesFinPartida( estado: estado, onPrincipal: () async { await context.read().desconectar(); estado.limpiar(); if (!context.mounted) return; Navigator.pushAndRemoveUntil( context, MaterialPageRoute( builder: (_) => const PantallaPrincipal(), ), (route) => false, ); }, ), const SizedBox(height: 16), ], ), ), ), ], ), ), ); } void _registrarResultadoSiHaceFalta( BuildContext context, Partida partida, bool ganaronJugadores, ) { if (_guardada || partida.ganador == null) return; _guardada = true; WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) return; final historial = context.read(); final perfil = context.read(); await historial.guardarPartida(partida); final progreso = await perfil.registrarPartidaCompletada( victoria: ganaronJugadores, ); if (!mounted) return; setState(() => _progreso = progreso); if (progreso.nuevasMedallas.isNotEmpty || progreso.incrementoFuego > 0) { _confetti.play(); } }); } } class _HeroResultado extends StatelessWidget { final String titulo; final IconData icono; final Color color; const _HeroResultado({ required this.titulo, required this.icono, required this.color, }); @override Widget build(BuildContext context) { return Stack( alignment: Alignment.center, children: [ SizedBox( height: 230, width: double.infinity, child: CustomPaint(painter: _RayosRecompensaPainter(color: color)), ), Column( children: [ Text( 'RESULTADOS', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: TemaApp.colorDorado, fontWeight: FontWeight.w900, letterSpacing: 4, ), ).animate().fadeIn(duration: 350.ms).slideY(begin: -0.25), const SizedBox(height: 12), Container( width: 116, height: 116, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ color.withValues(alpha: 0.55), TemaApp.colorSuperficie, Colors.black.withValues(alpha: 0.72), ], ), border: Border.all(color: TemaApp.colorDorado, width: 3), boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.72), blurRadius: 38, spreadRadius: 4, ), BoxShadow( color: TemaApp.colorDorado.withValues(alpha: 0.32), blurRadius: 22, spreadRadius: 1, ), ], ), child: Icon(icono, size: 64, color: TemaApp.colorDorado), ) .animate() .scale( begin: const Offset(0.55, 0.55), duration: 520.ms, curve: Curves.elasticOut, ) .shimmer(delay: 700.ms, duration: 1500.ms), const SizedBox(height: 14), Text( titulo.toUpperCase(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: color, fontWeight: FontWeight.w900, letterSpacing: 1.2, shadows: [ Shadow(color: color.withValues(alpha: 0.85), blurRadius: 22), ], ), ).animate().fadeIn(delay: 180.ms).slideY(begin: 0.25), ], ), ], ); } } class _TarjetaProgresoGamificacion extends StatelessWidget { final ProgresoGamificacionUsuario progreso; const _TarjetaProgresoGamificacion({required this.progreso}); @override Widget build(BuildContext context) { final nuevas = progreso.nuevasMedallas; final antes = progreso.antes.fuego.clamp(0, 100); final despues = progreso.despues.fuego.clamp(0, 100); return _PanelRecompensa( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.local_fire_department, color: TemaApp.colorNaranja), const SizedBox(width: 8), Expanded( child: Text( 'RECOMPENSAS DE PARTIDA', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: TemaApp.colorDorado, fontWeight: FontWeight.w900, letterSpacing: 1, ), ), ), _DeltaFuego(valor: progreso.incrementoFuego), ], ), const SizedBox(height: 16), _BarraFuegoPremium(antes: antes, despues: despues), const SizedBox(height: 16), if (nuevas.isEmpty) Text( 'Sin medallas nuevas esta vez. Segu? acumulando fuego.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: TemaApp.colorTextoSecundario, ), ) else ...[ Text( 'NUEVAS MEDALLAS', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: TemaApp.colorDorado, fontWeight: FontWeight.w800, letterSpacing: 1.5, ), ), const SizedBox(height: 12), Wrap( spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, children: [for (final id in nuevas) _MedallaDesbloqueada(id: id)], ), ], ], ), ).animate().fadeIn(delay: 250.ms).slideY(begin: 0.12); } } class _TarjetaRecompensaCargando extends StatelessWidget { const _TarjetaRecompensaCargando(); @override Widget build(BuildContext context) { return _PanelRecompensa( child: Row( children: [ const SizedBox( width: 28, height: 28, child: CircularProgressIndicator( strokeWidth: 3, color: TemaApp.colorNaranja, ), ), const SizedBox(width: 12), Expanded( child: Text( 'Calculando recompensas...', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: TemaApp.colorDorado, ), ), ), ], ), ); } } class _PanelRecompensa extends StatelessWidget { final Widget child; const _PanelRecompensa({required this.child}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFF111B28).withValues(alpha: 0.96), const Color(0xFF180D22).withValues(alpha: 0.94), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(22), border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.52)), boxShadow: [ BoxShadow( color: TemaApp.colorNaranja.withValues(alpha: 0.18), blurRadius: 32, offset: const Offset(0, 16), ), BoxShadow( color: Colors.black.withValues(alpha: 0.42), blurRadius: 18, offset: const Offset(0, 8), ), ], ), child: child, ); } } class _DeltaFuego extends StatelessWidget { final int valor; const _DeltaFuego({required this.valor}); @override Widget build(BuildContext context) { final texto = valor >= 0 ? '+$valor' : '$valor'; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: TemaApp.colorNaranja.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(999), border: Border.all(color: TemaApp.colorNaranja), boxShadow: [ BoxShadow( color: TemaApp.colorNaranja.withValues(alpha: 0.28), blurRadius: 18, ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.local_fire_department, color: TemaApp.colorNaranja, size: 18), const SizedBox(width: 4), Text( texto, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: TemaApp.colorDorado, fontWeight: FontWeight.w900, ), ), ], ), ).animate().scale(delay: 520.ms, duration: 420.ms, curve: Curves.elasticOut); } } class _BarraFuegoPremium extends StatelessWidget { final int antes; final int despues; const _BarraFuegoPremium({required this.antes, required this.despues}); @override Widget build(BuildContext context) { return TweenAnimationBuilder( tween: Tween(begin: antes / 100, end: despues / 100), duration: const Duration(milliseconds: 1300), curve: Curves.easeOutCubic, builder: (context, value, _) { final normalizado = value.clamp(0.0, 1.0).toDouble(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Fuego', style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const Spacer(), Text( '${(normalizado * 100).round()}%', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: TemaApp.colorDorado, fontWeight: FontWeight.w900, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(999), child: Container( height: 22, decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.42), border: Border.all( color: TemaApp.colorDorado.withValues(alpha: 0.34), ), ), child: Align( alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: normalizado, child: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFFE53935), TemaApp.colorNaranja, TemaApp.colorDorado, Color(0xFFFFECBE), ], ), ), ), ), ), ), ), ], ); }, ); } } class _MedallaDesbloqueada extends StatelessWidget { final String id; const _MedallaDesbloqueada({required this.id}); @override Widget build(BuildContext context) { final medalla = EstadisticasPerfilUsuario.catalogoMedallas[id]; if (medalla == null) return const SizedBox.shrink(); return Container( width: 94, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.28), borderRadius: BorderRadius.circular(18), border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.48)), ), child: Column( children: [ Stack( alignment: Alignment.center, children: [ SizedBox( width: 70, height: 70, child: CustomPaint( painter: _MiniBurstPainter(color: TemaApp.colorDorado), ), ), Image.asset( medalla.assetPath, width: 58, height: 58, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Text(medalla.emoji, style: const TextStyle(fontSize: 32)), ), ], ), const SizedBox(height: 6), Text( medalla.nombre, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: TemaApp.colorTexto, fontWeight: FontWeight.w700, ), ), ], ), ) .animate() .scale(duration: 520.ms, curve: Curves.elasticOut) .shimmer(delay: 650.ms, duration: 1200.ms); } } class _TarjetaSecreto extends StatelessWidget { final String palabra; final String categoria; const _TarjetaSecreto({required this.palabra, required this.categoria}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return _PanelRecompensa( child: Column( children: [ Text(l10n.theSecretWordWas, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Text( palabra.toUpperCase(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineLarge?.copyWith( color: TemaApp.colorNaranja, fontSize: 36, fontWeight: FontWeight.w900, shadows: [ Shadow( color: TemaApp.colorNaranja.withValues(alpha: 0.55), blurRadius: 18, ), ], ), ), const SizedBox(height: 4), Text( l10n.categoryLabel(categoria), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: TemaApp.colorTextoSecundario, ), ), ], ), ).animate().fadeIn(delay: 380.ms).slideY(begin: 0.1); } } class _TarjetaImpostores extends StatelessWidget { final String titulo; final List impostores; const _TarjetaImpostores({required this.titulo, required this.impostores}); @override Widget build(BuildContext context) { return _PanelRecompensa( child: Column( children: [ Text(titulo, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 10), ...impostores.map( (j) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.theater_comedy, size: 20, color: TemaApp.colorAcento), const SizedBox(width: 8), Text( j.nombre, style: Theme.of(context) .textTheme .titleLarge ?.copyWith(color: TemaApp.colorAcento), ), if (j.eliminado) ...[ const SizedBox(width: 8), const Icon(Icons.close, size: 16, color: TemaApp.colorTextoSecundario), ], ], ), ), ), ], ), ).animate().fadeIn(delay: 450.ms); } } class _TarjetaHistorialVotos extends StatelessWidget { final Partida partida; final AppLocalizations l10n; const _TarjetaHistorialVotos({required this.partida, required this.l10n}); @override Widget build(BuildContext context) { return _PanelRecompensa( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.votingHistory, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 12), ...partida.historialVotaciones.asMap().entries.map((entrada) { final ronda = entrada.key + 1; final resultado = entrada.value; return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.roundElimination(ronda, resultado.eliminadoNombre), style: TextStyle( fontWeight: FontWeight.bold, color: resultado.eraImpostor ? TemaApp.colorVerde : TemaApp.colorAcento, ), ), ...resultado.votos.entries.map((v) { final votante = partida.jugadores.firstWhere((j) => j.id == v.key); final votado = partida.jugadores.firstWhere((j) => j.id == v.value); return Text( ' ${votante.nombre} -> ${votado.nombre}', style: Theme.of(context).textTheme.bodyMedium, ); }), ], ), ); }), ], ), ); } } class _BotonesFinPartida extends StatelessWidget { final EstadoJuego estado; final VoidCallback onPrincipal; const _BotonesFinPartida({required this.estado, required this.onPrincipal}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Column( children: [ SizedBox( width: double.infinity, height: 56, child: ElevatedButton.icon( onPressed: () { estado.revancha(); Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const PantallaVerPalabra()), ); }, icon: const Icon(Icons.replay), label: Text(l10n.rematch), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, height: 56, child: OutlinedButton.icon( onPressed: onPrincipal, icon: const Icon(Icons.home), label: Text(l10n.mainMenu), ), ), ], ); } } class _RecompensaFondoPainter extends CustomPainter { final Color color; const _RecompensaFondoPainter({required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint()..isAntiAlias = true; paint.shader = RadialGradient( colors: [color.withValues(alpha: 0.28), Colors.transparent], ).createShader( Rect.fromCircle( center: Offset(size.width * 0.5, size.height * 0.22), radius: size.width * 0.72, ), ); canvas.drawCircle( Offset(size.width * 0.5, size.height * 0.22), size.width * 0.72, paint, ); paint.shader = null; paint.color = TemaApp.colorDorado.withValues(alpha: 0.10); for (var i = 0; i < 34; i++) { final x = (i * 83 % math.max(size.width, 1)).toDouble(); final y = (i * 137 % math.max(size.height, 1)).toDouble(); canvas.drawCircle(Offset(x, y), 1.2 + (i % 4), paint); } } @override bool shouldRepaint(covariant _RecompensaFondoPainter oldDelegate) { return oldDelegate.color != color; } } class _RayosRecompensaPainter extends CustomPainter { final Color color; const _RayosRecompensaPainter({required this.color}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height * 0.5); final paint = Paint()..isAntiAlias = true; for (var i = 0; i < 28; i++) { final angle = (math.pi * 2 / 28) * i; final inner = Offset( center.dx + math.cos(angle - 0.035) * 34, center.dy + math.sin(angle - 0.035) * 34, ); final outer = Offset( center.dx + math.cos(angle) * size.width * 0.46, center.dy + math.sin(angle) * size.width * 0.46, ); final inner2 = Offset( center.dx + math.cos(angle + 0.035) * 34, center.dy + math.sin(angle + 0.035) * 34, ); paint.color = (i.isEven ? TemaApp.colorDorado : color) .withValues(alpha: i.isEven ? 0.16 : 0.09); canvas.drawPath( Path() ..moveTo(inner.dx, inner.dy) ..lineTo(outer.dx, outer.dy) ..lineTo(inner2.dx, inner2.dy) ..close(), paint, ); } } @override bool shouldRepaint(covariant _RayosRecompensaPainter oldDelegate) { return oldDelegate.color != color; } } class _MiniBurstPainter extends CustomPainter { final Color color; const _MiniBurstPainter({required this.color}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final paint = Paint() ..isAntiAlias = true ..color = color.withValues(alpha: 0.22) ..strokeCap = StrokeCap.round; for (var i = 0; i < 16; i++) { final angle = math.pi * 2 * i / 16; paint.strokeWidth = i.isEven ? 3 : 1.4; canvas.drawLine( center, Offset( center.dx + math.cos(angle) * size.width * 0.48, center.dy + math.sin(angle) * size.height * 0.48, ), paint, ); } paint ..style = PaintingStyle.fill ..shader = RadialGradient( colors: [color.withValues(alpha: 0.38), Colors.transparent], ).createShader(Rect.fromCircle(center: center, radius: size.width * 0.42)); canvas.drawCircle(center, size.width * 0.42, paint); } @override bool shouldRepaint(covariant _MiniBurstPainter oldDelegate) { return oldDelegate.color != color; } }