1181 lines
37 KiB
Dart
1181 lines
37 KiB
Dart
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<PantallaFinPartida> createState() => _PantallaFinPartidaState();
|
|
}
|
|
|
|
class _PantallaFinPartidaState extends State<PantallaFinPartida> {
|
|
bool _guardada = false;
|
|
ProgresoGamificacionUsuario? _progreso;
|
|
late final ConfettiController _confetti;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_confetti = ConfettiController(duration: const Duration(seconds: 5));
|
|
Future.delayed(const Duration(milliseconds: 450), () {
|
|
if (mounted) _confetti.play();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_confetti.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final estado = context.watch<EstadoJuego>();
|
|
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,
|
|
backgroundColor: const Color(0xFF05070D),
|
|
body: FondoFarolero(
|
|
intenso: true,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: CustomPaint(
|
|
painter: _EscenarioFinPartidaPainter(
|
|
color: ganaronJugadores
|
|
? TemaApp.colorVerde
|
|
: TemaApp.colorAcento,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.topCenter,
|
|
child: ConfettiWidget(
|
|
confettiController: _confetti,
|
|
blastDirectionality: BlastDirectionality.explosive,
|
|
emissionFrequency: 0.09,
|
|
numberOfParticles: 28,
|
|
gravity: 0.18,
|
|
colors: const [
|
|
TemaApp.colorDorado,
|
|
TemaApp.colorNaranja,
|
|
TemaApp.colorAcento,
|
|
Color(0xFFFFECBE),
|
|
],
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.black.withValues(alpha: 0.10),
|
|
Colors.black.withValues(alpha: 0.52),
|
|
],
|
|
stops: const [0.0, 0.54, 1.0],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(20, 22, 20, 28),
|
|
child: Column(
|
|
children: [
|
|
_HeroResultado(
|
|
encabezado: l10n.gameOver,
|
|
titulo:
|
|
ganaronJugadores ? l10n.playersWin : l10n.impostorsWin,
|
|
icono: ganaronJugadores
|
|
? Icons.emoji_events
|
|
: Icons.theater_comedy,
|
|
color: ganaronJugadores
|
|
? TemaApp.colorVerde
|
|
: TemaApp.colorAcento,
|
|
),
|
|
Transform.translate(
|
|
offset: const Offset(0, -18),
|
|
child: Column(
|
|
children: [
|
|
if (_progreso == null)
|
|
const _TarjetaRecompensaCargando()
|
|
else
|
|
_TarjetaProgresoGamificacion(progreso: _progreso!),
|
|
const SizedBox(height: 18),
|
|
_TarjetaSecreto(
|
|
palabra: partida.palabraSecreta,
|
|
categoria: BancoPalabras.nombreBonitoCategoria(
|
|
partida.categoriaReal,
|
|
l10n,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
_TarjetaImpostores(
|
|
titulo: impostores.length == 1
|
|
? l10n.theImpostorWas
|
|
: l10n.theImpostorsWere,
|
|
impostores: impostores,
|
|
),
|
|
const SizedBox(height: 18),
|
|
if (partida.historialVotaciones.isNotEmpty)
|
|
_TarjetaHistorialVotos(partida: partida, l10n: l10n),
|
|
const SizedBox(height: 24),
|
|
_BotonesFinPartida(
|
|
estado: estado,
|
|
onPrincipal: () async {
|
|
await context.read<ServicioNearby>().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<ServicioHistorialPartidas>();
|
|
final perfil = context.read<ServicioPerfilUsuario>();
|
|
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 encabezado;
|
|
final String titulo;
|
|
final IconData icono;
|
|
final Color color;
|
|
|
|
const _HeroResultado({
|
|
required this.encabezado,
|
|
required this.titulo,
|
|
required this.icono,
|
|
required this.color,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final apertura = String.fromCharCode(0x00A1);
|
|
final tituloLimpio = titulo
|
|
.replaceAll(apertura, '')
|
|
.replaceAll('!', '')
|
|
.trim()
|
|
.toUpperCase();
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
SizedBox(
|
|
height: 420,
|
|
width: double.infinity,
|
|
child: CustomPaint(painter: _HeroCinematicoPainter(color: color)),
|
|
),
|
|
Column(
|
|
children: [
|
|
Text(
|
|
encabezado,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontSize: 38,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: -0.5,
|
|
shadows: [
|
|
Shadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
).animate().fadeIn(duration: 260.ms).slideY(begin: -0.18),
|
|
const SizedBox(height: 62),
|
|
Text(
|
|
l10n.result.toUpperCase(),
|
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 5,
|
|
),
|
|
).animate().fadeIn(duration: 350.ms).slideY(begin: -0.25),
|
|
const SizedBox(height: 20),
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Image.asset(
|
|
'assets/ui/generated/final_rewards/cinematic_burst.png',
|
|
width: 260,
|
|
height: 260,
|
|
fit: BoxFit.contain,
|
|
),
|
|
Container(
|
|
width: 154,
|
|
height: 154,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
color.withValues(alpha: 0.36),
|
|
const Color(0xFF111116),
|
|
Colors.black.withValues(alpha: 0.88),
|
|
],
|
|
),
|
|
border: Border.all(color: TemaApp.colorDorado, width: 4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.75),
|
|
blurRadius: 52,
|
|
spreadRadius: 7,
|
|
),
|
|
BoxShadow(
|
|
color: color.withValues(alpha: 0.62),
|
|
blurRadius: 36,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: _IconoResultadoPremium(icono: icono),
|
|
),
|
|
],
|
|
)
|
|
.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: 12),
|
|
Text(
|
|
'$apertura$tituloLimpio!',
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
color: color,
|
|
fontSize: 34,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.2,
|
|
shadows: [
|
|
Shadow(color: color.withValues(alpha: 0.90), blurRadius: 24),
|
|
],
|
|
),
|
|
).animate().fadeIn(delay: 180.ms).slideY(begin: 0.25),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _IconoResultadoPremium extends StatelessWidget {
|
|
final IconData icono;
|
|
|
|
const _IconoResultadoPremium({required this.icono});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (icono != Icons.theater_comedy) {
|
|
return Icon(icono, size: 82, color: TemaApp.colorDorado);
|
|
}
|
|
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Transform.translate(
|
|
offset: const Offset(-18, 12),
|
|
child: Transform.rotate(
|
|
angle: -0.10,
|
|
child: Icon(
|
|
Icons.mood,
|
|
size: 66,
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.98),
|
|
),
|
|
),
|
|
),
|
|
Transform.translate(
|
|
offset: const Offset(20, -13),
|
|
child: Transform.rotate(
|
|
angle: 0.12,
|
|
child: Icon(
|
|
Icons.sentiment_dissatisfied,
|
|
size: 70,
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.98),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
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: [
|
|
Container(
|
|
width: 52,
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
TemaApp.colorNaranja.withValues(alpha: 0.95),
|
|
TemaApp.colorDorado.withValues(alpha: 0.78),
|
|
],
|
|
begin: Alignment.bottomLeft,
|
|
end: Alignment.topRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.42),
|
|
blurRadius: 22,
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.local_fire_department,
|
|
color: Color(0xFF1B1010),
|
|
size: 30,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Text(
|
|
'RECOMPENSAS DE PARTIDA',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.1,
|
|
height: 1.05,
|
|
),
|
|
),
|
|
),
|
|
_DeltaFuego(valor: progreso.incrementoFuego),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
_BarraFuegoPremium(antes: antes, despues: despues),
|
|
const SizedBox(height: 20),
|
|
if (nuevas.isEmpty)
|
|
Text(
|
|
'Sin medallas nuevas esta vez. Seguí acumulando fuego.',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorTextoSecundario,
|
|
height: 1.35,
|
|
),
|
|
)
|
|
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 ClipRRect(
|
|
borderRadius: BorderRadius.circular(32),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
const Color(0xFF101A27).withValues(alpha: 0.96),
|
|
const Color(0xFF1B0E24).withValues(alpha: 0.94),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(32),
|
|
border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.58)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.25),
|
|
blurRadius: 44,
|
|
offset: const Offset(0, 22),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.50),
|
|
blurRadius: 28,
|
|
offset: const Offset(0, 14),
|
|
),
|
|
],
|
|
),
|
|
foregroundDecoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(32),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.white.withValues(alpha: 0.06),
|
|
Colors.transparent,
|
|
TemaApp.colorDorado.withValues(alpha: 0.04),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
),
|
|
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: 18, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
TemaApp.colorNaranja.withValues(alpha: 0.28),
|
|
TemaApp.colorDorado.withValues(alpha: 0.14),
|
|
],
|
|
),
|
|
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: 24),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
texto,
|
|
style: Theme.of(context).textTheme.headlineSmall?.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<double>(
|
|
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: 12),
|
|
Container(
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.72),
|
|
borderRadius: BorderRadius.circular(999),
|
|
border: Border.all(
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.38),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.24),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: Stack(
|
|
children: [
|
|
FractionallySizedBox(
|
|
widthFactor: normalizado,
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Color(0xFFE53935),
|
|
TemaApp.colorNaranja,
|
|
TemaApp.colorDorado,
|
|
Color(0xFFFFECBE),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.white.withValues(alpha: 0.32),
|
|
Colors.transparent,
|
|
Colors.black.withValues(alpha: 0.18),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 12,
|
|
right: 12,
|
|
top: 5,
|
|
child: Container(
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(999),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.white.withValues(alpha: 0.62),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
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: [
|
|
Image.asset(
|
|
'assets/rewards/medal_unlock_burst.png',
|
|
width: 82,
|
|
height: 82,
|
|
fit: BoxFit.cover,
|
|
),
|
|
SizedBox(
|
|
width: 70,
|
|
height: 70,
|
|
child: CustomPaint(
|
|
painter: _MiniBurstPainter(color: TemaApp.colorDorado),
|
|
),
|
|
),
|
|
Image.asset(
|
|
medalla.assetPath,
|
|
width: 58,
|
|
height: 58,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
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.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
palabra.toUpperCase(),
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
|
color: TemaApp.colorNaranja,
|
|
fontSize: 42,
|
|
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<Jugador> impostores;
|
|
|
|
const _TarjetaImpostores({required this.titulo, required this.impostores});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _PanelRecompensa(
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.theater_comedy, color: TemaApp.colorAcento),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
titulo,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
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 _EscenarioFinPartidaPainter extends CustomPainter {
|
|
final Color color;
|
|
|
|
const _EscenarioFinPartidaPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()..isAntiAlias = true;
|
|
|
|
paint.shader = const LinearGradient(
|
|
colors: [
|
|
Color(0xFF050B14),
|
|
Color(0xFF091322),
|
|
Color(0xFF16091F),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
).createShader(Offset.zero & size);
|
|
canvas.drawRect(Offset.zero & size, paint);
|
|
|
|
paint.shader = RadialGradient(
|
|
colors: [
|
|
color.withValues(alpha: 0.28),
|
|
TemaApp.colorNaranja.withValues(alpha: 0.12),
|
|
Colors.transparent,
|
|
],
|
|
).createShader(
|
|
Rect.fromCircle(
|
|
center: Offset(size.width * 0.5, size.height * 0.30),
|
|
radius: size.width * 0.86,
|
|
),
|
|
);
|
|
canvas.drawCircle(
|
|
Offset(size.width * 0.5, size.height * 0.30),
|
|
size.width * 0.86,
|
|
paint,
|
|
);
|
|
|
|
paint.shader = null;
|
|
_drawSkyline(canvas, size, paint);
|
|
|
|
for (var i = 0; i < 52; i++) {
|
|
final x = (i * 71 % math.max(size.width, 1)).toDouble();
|
|
final y = (i * 137 % math.max(size.height, 1)).toDouble();
|
|
final palette = [
|
|
TemaApp.colorDorado,
|
|
TemaApp.colorNaranja,
|
|
TemaApp.colorAcento,
|
|
const Color(0xFFFFF1C7),
|
|
];
|
|
final confettiPaint = Paint()
|
|
..isAntiAlias = true
|
|
..color = palette[i % palette.length].withValues(alpha: 0.72);
|
|
canvas.save();
|
|
canvas.translate(x, y);
|
|
canvas.rotate((i % 9 - 4) * 0.22);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromCenter(
|
|
center: Offset.zero,
|
|
width: 8 + (i % 4) * 6,
|
|
height: 4 + (i % 3) * 3,
|
|
),
|
|
const Radius.circular(1.5),
|
|
),
|
|
confettiPaint,
|
|
);
|
|
canvas.restore();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void _drawSkyline(Canvas canvas, Size size, Paint paint) {
|
|
paint.color = Colors.black.withValues(alpha: 0.36);
|
|
final base = size.height * 0.78;
|
|
final path = Path()
|
|
..moveTo(0, size.height)
|
|
..lineTo(0, base)
|
|
..lineTo(size.width * 0.16, base - 42)
|
|
..lineTo(size.width * 0.30, base - 12)
|
|
..lineTo(size.width * 0.48, base - 60)
|
|
..lineTo(size.width * 0.66, base - 26)
|
|
..lineTo(size.width * 0.82, base - 72)
|
|
..lineTo(size.width, base - 34)
|
|
..lineTo(size.width, size.height)
|
|
..close();
|
|
canvas.drawPath(path, paint);
|
|
|
|
paint.color = TemaApp.colorNaranja.withValues(alpha: 0.18);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromCenter(
|
|
center: Offset(size.width * 0.52, base - 90),
|
|
width: 28,
|
|
height: 130,
|
|
),
|
|
const Radius.circular(12),
|
|
),
|
|
paint,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _EscenarioFinPartidaPainter oldDelegate) {
|
|
return oldDelegate.color != color;
|
|
}
|
|
}
|
|
|
|
class _HeroCinematicoPainter extends CustomPainter {
|
|
final Color color;
|
|
|
|
const _HeroCinematicoPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height * 0.54);
|
|
final paint = Paint()..isAntiAlias = true;
|
|
|
|
paint.shader = RadialGradient(
|
|
colors: [
|
|
TemaApp.colorNaranja.withValues(alpha: 0.30),
|
|
color.withValues(alpha: 0.14),
|
|
Colors.transparent,
|
|
],
|
|
).createShader(Rect.fromCircle(center: center, radius: size.width * 0.54));
|
|
canvas.drawCircle(center, size.width * 0.54, paint);
|
|
paint.shader = null;
|
|
|
|
for (var i = 0; i < 28; i++) {
|
|
final angle = (math.pi * 2 / 28) * i;
|
|
final inner = Offset(
|
|
center.dx + math.cos(angle - 0.035) * 52,
|
|
center.dy + math.sin(angle - 0.035) * 52,
|
|
);
|
|
final outer = Offset(
|
|
center.dx + math.cos(angle) * size.width * 0.58,
|
|
center.dy + math.sin(angle) * size.width * 0.58,
|
|
);
|
|
final inner2 = Offset(
|
|
center.dx + math.cos(angle + 0.035) * 52,
|
|
center.dy + math.sin(angle + 0.035) * 52,
|
|
);
|
|
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 _HeroCinematicoPainter 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;
|
|
}
|
|
}
|