Files
farolero/lib/tema/componentes_resultado_farolero.dart
T
FreeTLab 90ada9099f
Build & Deploy Farolero / Análisis de código (push) Successful in 14s
Build & Deploy Farolero / Build APK + AAB release (push) Successful in 1m46s
Multitud de iconos más
2026-05-12 01:36:41 +02:00

1217 lines
37 KiB
Dart

import 'dart:math' as math;
import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../modelos/gamificacion_usuario.dart';
import '../modelos/jugador.dart';
import '../modelos/partida.dart';
import 'tema_app.dart';
import 'componentes_farolero.dart';
class ResultadoRondaFarolero extends StatelessWidget {
final ResultadoVotacion resultado;
final List<Jugador> jugadores;
final String? mensaje;
final Widget? acciones;
const ResultadoRondaFarolero({
super.key,
required this.resultado,
required this.jugadores,
this.mensaje,
this.acciones,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final color =
resultado.eraImpostor ? TemaApp.colorVerde : TemaApp.colorAcento;
return Column(
children: [
const SizedBox(height: 8),
Stack(
alignment: Alignment.center,
children: [
SizedBox(
height: 188,
width: double.infinity,
child: CustomPaint(
painter: HeroCinematicoResultadoPainter(color: color),
),
),
Image.asset(
'assets/ui/generated/meta/result_verdict_art.webp',
height: 150,
fit: BoxFit.contain,
).animate().scale(
duration: 520.ms,
curve: Curves.elasticOut,
begin: const Offset(0.78, 0.78),
),
],
),
const SizedBox(height: 10),
Text(
resultado.eliminadoNombre,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontSize: 36,
fontWeight: FontWeight.w900,
shadows: [
Shadow(color: color.withValues(alpha: 0.55), blurRadius: 18),
],
),
),
const SizedBox(height: 12),
_BadgeResultadoRonda(
texto: resultado.eraImpostor ? l10n.wasImpostor : l10n.wasInnocent,
color: color,
),
if (mensaje != null && mensaje!.trim().isNotEmpty) ...[
const SizedBox(height: 14),
PanelResultadoFarolero(
padding: const EdgeInsets.all(16),
child: Text(
mensaje!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
const SizedBox(height: 22),
DetalleVotosFarolero(resultado: resultado, jugadores: jugadores),
if (acciones != null) ...[
const SizedBox(height: 24),
acciones!,
],
],
);
}
}
class DetalleVotosFarolero extends StatelessWidget {
final ResultadoVotacion resultado;
final List<Jugador> jugadores;
const DetalleVotosFarolero({
super.key,
required this.resultado,
required this.jugadores,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final jugadoresPorId = {for (final jugador in jugadores) jugador.id: jugador};
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 PanelResultadoFarolero(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconoFarolero(Icons.bar_chart, color: TemaApp.colorNaranja),
const SizedBox(width: 8),
Expanded(
child: Text(
l10n.votesThisRound,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
],
),
const SizedBox(height: 14),
...ranking.map((entry) {
final jugador = jugadoresPorId[entry.key];
return BarraVotosFarolero(
nombre: jugador?.nombre ?? '?',
votos: entry.value,
total: maxVotos,
destacado: entry.key == resultado.eliminadoId,
);
}),
const Divider(height: 26),
...resultado.votos.entries.map((entry) {
final votante = jugadoresPorId[entry.key]?.nombre ?? '?';
final votado = jugadoresPorId[entry.value]?.nombre ?? '?';
final fueAlEliminado = entry.value == resultado.eliminadoId;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
IconoFarolero(
fueAlEliminado ? Icons.how_to_vote : Icons.arrow_forward,
size: 18,
color: fueAlEliminado
? TemaApp.colorAcento
: TemaApp.colorTextoSecundario,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'$votante$votado',
style: TextStyle(
color: fueAlEliminado
? TemaApp.colorTexto
: TemaApp.colorTextoSecundario,
fontWeight: fueAlEliminado
? FontWeight.bold
: FontWeight.normal,
),
),
),
],
),
);
}),
],
),
).animate().fadeIn(delay: 160.ms).slideY(begin: 0.08);
}
}
class BarraVotosFarolero extends StatelessWidget {
final String nombre;
final int votos;
final int total;
final bool destacado;
const BarraVotosFarolero({
super.key,
required this.nombre,
required this.votos,
required this.total,
required this.destacado,
});
@override
Widget build(BuildContext context) {
final color = destacado ? TemaApp.colorAcento : TemaApp.colorNaranja;
final proporcion = total == 0 ? 0.0 : votos / total;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
nombre,
style: TextStyle(
fontWeight: destacado ? FontWeight.bold : FontWeight.w600,
),
),
),
Text(
'$votos',
style: TextStyle(color: color, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
value: proporcion.clamp(0.0, 1.0).toDouble(),
minHeight: 12,
backgroundColor: Colors.black.withValues(alpha: 0.42),
valueColor: AlwaysStoppedAnimation(color),
),
),
],
),
);
}
}
class HeroFinalPartidaFarolero extends StatelessWidget {
final String encabezado;
final String titulo;
final IconData icono;
final Color color;
const HeroFinalPartidaFarolero({
super.key,
required this.encabezado,
required this.titulo,
required this.icono,
required this.color,
});
@override
Widget build(BuildContext context) {
final apertura = String.fromCharCode(0x00A1);
final tituloLimpio = titulo
.replaceAll(apertura, '')
.replaceAll('!', '')
.trim()
.toUpperCase();
return Stack(
alignment: Alignment.center,
children: [
SizedBox(
height: 520,
width: double.infinity,
child: CustomPaint(painter: HeroCinematicoResultadoPainter(color: color)),
),
Column(
children: [
Text(
encabezado,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: TemaApp.colorDorado,
fontSize: 38,
fontWeight: FontWeight.w900,
shadows: [
Shadow(
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
blurRadius: 18,
),
],
),
).animate().fadeIn(duration: 260.ms).slideY(begin: -0.18),
const SizedBox(height: 58),
Stack(
alignment: Alignment.center,
children: [
Image.asset(
'assets/ui/generated/final_rewards/cinematic_burst.webp',
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: IconoResultadoPremiumFarolero(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: 14),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'$apertura$tituloLimpio!',
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: color,
fontSize: 32,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
shadows: [
Shadow(
color: color.withValues(alpha: 0.90),
blurRadius: 24,
),
],
),
),
).animate().fadeIn(delay: 180.ms).slideY(begin: 0.25),
],
),
],
);
}
}
class IconoResultadoPremiumFarolero extends StatelessWidget {
final IconData icono;
const IconoResultadoPremiumFarolero({super.key, required this.icono});
@override
Widget build(BuildContext context) {
if (icono != Icons.theater_comedy) {
return IconoFarolero(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: IconoFarolero(
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: IconoFarolero(
Icons.sentiment_dissatisfied,
size: 70,
color: TemaApp.colorDorado.withValues(alpha: 0.98),
),
),
),
],
);
}
}
class TarjetaProgresoGamificacionPremium extends StatelessWidget {
final ProgresoGamificacionUsuario progreso;
const TarjetaProgresoGamificacionPremium({
super.key,
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);
final l10n = AppLocalizations.of(context)!;
return PanelResultadoFarolero(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const _IconoFuegoRecompensa(),
const SizedBox(width: 14),
Expanded(
child: Text(
l10n.matchRewards.toUpperCase(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: TemaApp.colorDorado,
fontSize: 20,
fontWeight: FontWeight.w900,
letterSpacing: 1.1,
height: 1.05,
),
),
),
_DeltaFuegoPremium(valor: progreso.incrementoFuego),
],
),
const SizedBox(height: 16),
BarraFuegoPremiumFarolero(antes: antes, despues: despues),
const SizedBox(height: 20),
if (nuevas.isEmpty)
Text(
l10n.noNewMedalsKeepFire,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: TemaApp.colorTextoSecundario,
height: 1.35,
),
)
else ...[
Text(
l10n.newMedals.toUpperCase(),
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) MedallaDesbloqueadaPremium(id: id),
],
),
],
],
),
).animate().fadeIn(delay: 250.ms).slideY(begin: 0.12);
}
}
class TarjetaRecompensaCargandoPremium extends StatelessWidget {
const TarjetaRecompensaCargandoPremium({super.key});
@override
Widget build(BuildContext context) {
return PanelResultadoFarolero(
child: Row(
children: [
const SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
strokeWidth: 3,
color: TemaApp.colorNaranja,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(context)!.calculatingRewards,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: TemaApp.colorDorado,
),
),
),
],
),
);
}
}
class PanelResultadoFarolero extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
const PanelResultadoFarolero({
super.key,
required this.child,
this.padding = const EdgeInsets.all(24),
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(32),
child: Container(
width: double.infinity,
padding: padding,
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 BarraFuegoPremiumFarolero extends StatelessWidget {
final int antes;
final int despues;
const BarraFuegoPremiumFarolero({
super.key,
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, child) {
final normalizado = value.clamp(0.0, 1.0).toDouble();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.asset(
'assets/ui/generated/actions/action_fire_badge.webp',
width: 22,
height: 22,
fit: BoxFit.contain,
filterQuality: FilterQuality.medium,
),
const SizedBox(width: 6),
Text(
'${AppLocalizations.of(context)!.fireLabel} '
'${(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,
),
),
),
),
],
),
),
),
],
);
},
);
}
}
class MedallaDesbloqueadaPremium extends StatelessWidget {
final String id;
const MedallaDesbloqueadaPremium({super.key, 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.webp',
width: 82,
height: 82,
fit: BoxFit.cover,
),
SizedBox(
width: 70,
height: 70,
child: CustomPaint(
painter: MiniBurstFaroleroPainter(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 TarjetaSecretoPremium extends StatelessWidget {
final String palabra;
final String categoria;
const TarjetaSecretoPremium({
super.key,
required this.palabra,
required this.categoria,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return PanelResultadoFarolero(
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 TarjetaImpostoresPremium extends StatelessWidget {
final String titulo;
final List<Jugador> impostores;
const TarjetaImpostoresPremium({
super.key,
required this.titulo,
required this.impostores,
});
@override
Widget build(BuildContext context) {
return PanelResultadoFarolero(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconoFarolero(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(
(jugador) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconoFarolero(
Icons.theater_comedy,
size: 20,
color: TemaApp.colorAcento,
),
const SizedBox(width: 8),
Flexible(
child: Text(
jugador.nombre,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: TemaApp.colorAcento),
),
),
if (jugador.eliminado) ...[
const SizedBox(width: 8),
IconoFarolero(
Icons.close,
size: 16,
color: TemaApp.colorTextoSecundario,
),
],
],
),
),
),
],
),
).animate().fadeIn(delay: 450.ms);
}
}
class TarjetaHistorialVotosPremium extends StatelessWidget {
final List<ResultadoVotacion> historial;
final List<Jugador> jugadores;
const TarjetaHistorialVotosPremium({
super.key,
required this.historial,
required this.jugadores,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final jugadoresPorId = {for (final jugador in jugadores) jugador.id: jugador};
return PanelResultadoFarolero(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.votingHistory,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 12),
...historial.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((voto) {
final votante = jugadoresPorId[voto.key]?.nombre ?? '?';
final votado = jugadoresPorId[voto.value]?.nombre ?? '?';
return Text(
' $votante$votado',
style: Theme.of(context).textTheme.bodyMedium,
);
}),
],
),
);
}),
],
),
);
}
}
class EscenarioFinPartidaFaroleroPainter extends CustomPainter {
final Color color;
const EscenarioFinPartidaFaroleroPainter({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;
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);
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();
}
}
@override
bool shouldRepaint(covariant EscenarioFinPartidaFaroleroPainter oldDelegate) {
return oldDelegate.color != color;
}
}
class HeroCinematicoResultadoPainter extends CustomPainter {
final Color color;
const HeroCinematicoResultadoPainter({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 HeroCinematicoResultadoPainter oldDelegate) {
return oldDelegate.color != color;
}
}
class MiniBurstFaroleroPainter extends CustomPainter {
final Color color;
const MiniBurstFaroleroPainter({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 MiniBurstFaroleroPainter oldDelegate) {
return oldDelegate.color != color;
}
}
class _BadgeResultadoRonda extends StatelessWidget {
final String texto;
final Color color;
const _BadgeResultadoRonda({required this.texto, required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.24),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: color),
boxShadow: [
BoxShadow(color: color.withValues(alpha: 0.28), blurRadius: 18),
],
),
child: Text(
texto,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
).animate().scale(delay: 180.ms, duration: 380.ms);
}
}
class _IconoFuegoRecompensa extends StatelessWidget {
const _IconoFuegoRecompensa();
@override
Widget build(BuildContext context) {
return 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: Padding(
padding: const EdgeInsets.all(7),
child: Image.asset(
'assets/ui/generated/actions/action_fire_badge.webp',
fit: BoxFit.contain,
filterQuality: FilterQuality.medium,
),
),
);
}
}
class _DeltaFuegoPremium extends StatelessWidget {
final int valor;
const _DeltaFuegoPremium({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: [
Image.asset(
'assets/ui/generated/actions/action_fire_badge.webp',
width: 26,
height: 26,
fit: BoxFit.contain,
filterQuality: FilterQuality.medium,
),
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);
}
}