Gamificación
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,228 @@
|
|||||||
|
class MedallaUsuario {
|
||||||
|
final String id;
|
||||||
|
final String emoji;
|
||||||
|
final String assetPath;
|
||||||
|
final String nombre;
|
||||||
|
final String descripcion;
|
||||||
|
|
||||||
|
const MedallaUsuario({
|
||||||
|
required this.id,
|
||||||
|
required this.emoji,
|
||||||
|
required this.assetPath,
|
||||||
|
required this.nombre,
|
||||||
|
required this.descripcion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResumenGamificacionUsuario {
|
||||||
|
final int fuego;
|
||||||
|
final List<String> medallas;
|
||||||
|
|
||||||
|
const ResumenGamificacionUsuario({
|
||||||
|
required this.fuego,
|
||||||
|
required this.medallas,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'fuego': fuego,
|
||||||
|
'medallas': medallas,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory ResumenGamificacionUsuario.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ResumenGamificacionUsuario(
|
||||||
|
fuego: (json['fuego'] as num?)?.toInt() ?? 0,
|
||||||
|
medallas: (json['medallas'] as List<dynamic>? ?? const [])
|
||||||
|
.map((valor) => valor.toString())
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProgresoGamificacionUsuario {
|
||||||
|
final EstadisticasPerfilUsuario antes;
|
||||||
|
final EstadisticasPerfilUsuario despues;
|
||||||
|
final List<String> nuevasMedallas;
|
||||||
|
|
||||||
|
const ProgresoGamificacionUsuario({
|
||||||
|
required this.antes,
|
||||||
|
required this.despues,
|
||||||
|
required this.nuevasMedallas,
|
||||||
|
});
|
||||||
|
|
||||||
|
int get incrementoFuego => despues.fuego - antes.fuego;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EstadisticasPerfilUsuario {
|
||||||
|
final int partidasJugadas;
|
||||||
|
final int partidasGanadas;
|
||||||
|
final int partidasPerdidas;
|
||||||
|
final int partidasComoImpostor;
|
||||||
|
final int victoriasComoImpostor;
|
||||||
|
final String? fechaUltimaPartidaIso;
|
||||||
|
final int partidasHoy;
|
||||||
|
final int subidaFuegoHoy;
|
||||||
|
final int fuego;
|
||||||
|
|
||||||
|
const EstadisticasPerfilUsuario({
|
||||||
|
this.partidasJugadas = 0,
|
||||||
|
this.partidasGanadas = 0,
|
||||||
|
this.partidasPerdidas = 0,
|
||||||
|
this.partidasComoImpostor = 0,
|
||||||
|
this.victoriasComoImpostor = 0,
|
||||||
|
this.fechaUltimaPartidaIso,
|
||||||
|
this.partidasHoy = 0,
|
||||||
|
this.subidaFuegoHoy = 0,
|
||||||
|
this.fuego = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const catalogoMedallas = <String, MedallaUsuario>{
|
||||||
|
'novato': MedallaUsuario(id: 'novato', emoji: '🎲', assetPath: 'assets/medals/novato.png', nombre: 'Novato', descripcion: 'Jugó su primera partida.'),
|
||||||
|
'habitual': MedallaUsuario(id: 'habitual', emoji: '🧭', assetPath: 'assets/medals/habitual.png', nombre: 'Habitual', descripcion: 'Jugó 10 partidas.'),
|
||||||
|
'veterano': MedallaUsuario(id: 'veterano', emoji: '🏛️', assetPath: 'assets/medals/veterano.png', nombre: 'Veterano', descripcion: 'Jugó 50 partidas.'),
|
||||||
|
'leyenda': MedallaUsuario(id: 'leyenda', emoji: '👑', assetPath: 'assets/medals/leyenda.png', nombre: 'Leyenda', descripcion: 'Jugó 100 partidas.'),
|
||||||
|
'primera_victoria': MedallaUsuario(id: 'primera_victoria', emoji: '🥉', assetPath: 'assets/medals/primera_victoria.png', nombre: 'Primera victoria', descripcion: 'Ganó una partida.'),
|
||||||
|
'diez_victorias': MedallaUsuario(id: 'diez_victorias', emoji: '🥈', assetPath: 'assets/medals/diez_victorias.png', nombre: 'Diez victorias', descripcion: 'Ganó 10 partidas.'),
|
||||||
|
'veinticinco_victorias': MedallaUsuario(id: 'veinticinco_victorias', emoji: '🥇', assetPath: 'assets/medals/veinticinco_victorias.png', nombre: 'Veinticinco victorias', descripcion: 'Ganó 25 partidas.'),
|
||||||
|
'cincuenta_victorias': MedallaUsuario(id: 'cincuenta_victorias', emoji: '💎', assetPath: 'assets/medals/cincuenta_victorias.png', nombre: 'Cincuenta victorias', descripcion: 'Ganó 50 partidas.'),
|
||||||
|
'primer_engano': MedallaUsuario(id: 'primer_engano', emoji: '🎭', assetPath: 'assets/medals/primer_engano.png', nombre: 'Primer engaño', descripcion: 'Ganó como impostor.'),
|
||||||
|
'impostor_habitual': MedallaUsuario(id: 'impostor_habitual', emoji: '🃏', assetPath: 'assets/medals/impostor_habitual.png', nombre: 'Impostor habitual', descripcion: 'Ganó 5 partidas como impostor.'),
|
||||||
|
'lobo_faroles': MedallaUsuario(id: 'lobo_faroles', emoji: '🐺', assetPath: 'assets/medals/lobo_faroles.png', nombre: 'Lobo entre faroles', descripcion: 'Ganó 15 partidas como impostor.'),
|
||||||
|
'brasa': MedallaUsuario(id: 'brasa', emoji: '♨️', assetPath: 'assets/medals/brasa.png', nombre: 'Brasa', descripcion: 'Mantiene algo de fuego reciente.'),
|
||||||
|
'llama_suave': MedallaUsuario(id: 'llama_suave', emoji: '🔥', assetPath: 'assets/medals/llama_suave.png', nombre: 'Llama suave', descripcion: 'Está jugando con cierta asiduidad.'),
|
||||||
|
'llama_fuerte': MedallaUsuario(id: 'llama_fuerte', emoji: '🔥', assetPath: 'assets/medals/llama_fuerte.png', nombre: 'Llama fuerte', descripcion: 'Tiene una asiduidad alta.'),
|
||||||
|
'incandescente': MedallaUsuario(id: 'incandescente', emoji: '🌋', assetPath: 'assets/medals/incandescente.png', nombre: 'Incandescente', descripcion: 'Tiene el fuego al máximo.'),
|
||||||
|
};
|
||||||
|
|
||||||
|
EstadisticasPerfilUsuario copiar({
|
||||||
|
int? partidasJugadas,
|
||||||
|
int? partidasGanadas,
|
||||||
|
int? partidasPerdidas,
|
||||||
|
int? partidasComoImpostor,
|
||||||
|
int? victoriasComoImpostor,
|
||||||
|
String? fechaUltimaPartidaIso,
|
||||||
|
bool limpiarFechaUltimaPartida = false,
|
||||||
|
int? partidasHoy,
|
||||||
|
int? subidaFuegoHoy,
|
||||||
|
int? fuego,
|
||||||
|
}) {
|
||||||
|
return EstadisticasPerfilUsuario(
|
||||||
|
partidasJugadas: partidasJugadas ?? this.partidasJugadas,
|
||||||
|
partidasGanadas: partidasGanadas ?? this.partidasGanadas,
|
||||||
|
partidasPerdidas: partidasPerdidas ?? this.partidasPerdidas,
|
||||||
|
partidasComoImpostor: partidasComoImpostor ?? this.partidasComoImpostor,
|
||||||
|
victoriasComoImpostor: victoriasComoImpostor ?? this.victoriasComoImpostor,
|
||||||
|
fechaUltimaPartidaIso: limpiarFechaUltimaPartida ? null : (fechaUltimaPartidaIso ?? this.fechaUltimaPartidaIso),
|
||||||
|
partidasHoy: partidasHoy ?? this.partidasHoy,
|
||||||
|
subidaFuegoHoy: subidaFuegoHoy ?? this.subidaFuegoHoy,
|
||||||
|
fuego: (fuego ?? this.fuego).clamp(0, 100).toInt(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EstadisticasPerfilUsuario registrarPartida({
|
||||||
|
required bool victoria,
|
||||||
|
bool comoImpostor = false,
|
||||||
|
bool victoriaComoImpostor = false,
|
||||||
|
DateTime? fecha,
|
||||||
|
}) {
|
||||||
|
final momento = fecha ?? DateTime.now();
|
||||||
|
final normalizada = DateTime(momento.year, momento.month, momento.day);
|
||||||
|
final anterior = _aplicarPasoDeDias(normalizada);
|
||||||
|
final ganancia = anterior._gananciaFuegoSiguientePartida();
|
||||||
|
return anterior.copiar(
|
||||||
|
partidasJugadas: anterior.partidasJugadas + 1,
|
||||||
|
partidasGanadas: anterior.partidasGanadas + (victoria ? 1 : 0),
|
||||||
|
partidasPerdidas: anterior.partidasPerdidas + (victoria ? 0 : 1),
|
||||||
|
partidasComoImpostor: anterior.partidasComoImpostor + (comoImpostor ? 1 : 0),
|
||||||
|
victoriasComoImpostor: anterior.victoriasComoImpostor + (victoriaComoImpostor ? 1 : 0),
|
||||||
|
fechaUltimaPartidaIso: normalizada.toIso8601String(),
|
||||||
|
partidasHoy: anterior.partidasHoy + 1,
|
||||||
|
subidaFuegoHoy: anterior.subidaFuegoHoy + ganancia,
|
||||||
|
fuego: anterior.fuego + ganancia,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EstadisticasPerfilUsuario _aplicarPasoDeDias(DateTime hoy) {
|
||||||
|
final ultima = fechaUltimaPartidaIso == null ? null : DateTime.tryParse(fechaUltimaPartidaIso!);
|
||||||
|
if (ultima == null) return copiar(partidasHoy: 0, subidaFuegoHoy: 0);
|
||||||
|
final ultimoDia = DateTime(ultima.year, ultima.month, ultima.day);
|
||||||
|
final diferenciaDias = hoy.difference(ultimoDia).inDays;
|
||||||
|
if (diferenciaDias <= 0) return this;
|
||||||
|
final diasSinJugar = diferenciaDias - 1;
|
||||||
|
var fuegoActual = fuego;
|
||||||
|
for (var i = 1; i <= diasSinJugar; i++) {
|
||||||
|
fuegoActual -= i == 1 ? 3 : (i == 2 ? 5 : 7);
|
||||||
|
}
|
||||||
|
return copiar(partidasHoy: 0, subidaFuegoHoy: 0, fuego: fuegoActual);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _gananciaFuegoSiguientePartida() {
|
||||||
|
final numeroPartidaDelDia = partidasHoy + 1;
|
||||||
|
final base = switch (numeroPartidaDelDia) { 1 => 6, 2 => 5, 3 => 4, 4 => 3, 5 => 2, _ => 1 };
|
||||||
|
final restanteHoy = (25 - subidaFuegoHoy).clamp(0, 25).toInt();
|
||||||
|
return base.clamp(0, restanteHoy).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get medallas {
|
||||||
|
final resultado = <String>[];
|
||||||
|
if (partidasJugadas >= 1) resultado.add('novato');
|
||||||
|
if (partidasJugadas >= 10) resultado.add('habitual');
|
||||||
|
if (partidasJugadas >= 50) resultado.add('veterano');
|
||||||
|
if (partidasJugadas >= 100) resultado.add('leyenda');
|
||||||
|
if (partidasGanadas >= 1) resultado.add('primera_victoria');
|
||||||
|
if (partidasGanadas >= 10) resultado.add('diez_victorias');
|
||||||
|
if (partidasGanadas >= 25) resultado.add('veinticinco_victorias');
|
||||||
|
if (partidasGanadas >= 50) resultado.add('cincuenta_victorias');
|
||||||
|
if (victoriasComoImpostor >= 1) resultado.add('primer_engano');
|
||||||
|
if (victoriasComoImpostor >= 5) resultado.add('impostor_habitual');
|
||||||
|
if (victoriasComoImpostor >= 15) resultado.add('lobo_faroles');
|
||||||
|
if (fuego >= 100) {
|
||||||
|
resultado.add('incandescente');
|
||||||
|
} else if (fuego >= 50) {
|
||||||
|
resultado.add('llama_fuerte');
|
||||||
|
} else if (fuego >= 25) {
|
||||||
|
resultado.add('llama_suave');
|
||||||
|
} else if (fuego > 0) {
|
||||||
|
resultado.add('brasa');
|
||||||
|
}
|
||||||
|
return resultado;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get medallasPrincipales {
|
||||||
|
final ids = medallas;
|
||||||
|
const prioridad = [
|
||||||
|
'incandescente', 'llama_fuerte', 'llama_suave', 'brasa',
|
||||||
|
'leyenda', 'veterano', 'habitual', 'novato',
|
||||||
|
'cincuenta_victorias', 'veinticinco_victorias', 'diez_victorias', 'primera_victoria',
|
||||||
|
'lobo_faroles', 'impostor_habitual', 'primer_engano',
|
||||||
|
];
|
||||||
|
return prioridad.where(ids.contains).take(3).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ResumenGamificacionUsuario get resumen => ResumenGamificacionUsuario(fuego: fuego, medallas: medallasPrincipales);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'partidasJugadas': partidasJugadas,
|
||||||
|
'partidasGanadas': partidasGanadas,
|
||||||
|
'partidasPerdidas': partidasPerdidas,
|
||||||
|
'partidasComoImpostor': partidasComoImpostor,
|
||||||
|
'victoriasComoImpostor': victoriasComoImpostor,
|
||||||
|
if (fechaUltimaPartidaIso != null) 'fechaUltimaPartidaIso': fechaUltimaPartidaIso,
|
||||||
|
'partidasHoy': partidasHoy,
|
||||||
|
'subidaFuegoHoy': subidaFuegoHoy,
|
||||||
|
'fuego': fuego,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory EstadisticasPerfilUsuario.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EstadisticasPerfilUsuario(
|
||||||
|
partidasJugadas: (json['partidasJugadas'] as num?)?.toInt() ?? 0,
|
||||||
|
partidasGanadas: (json['partidasGanadas'] as num?)?.toInt() ?? 0,
|
||||||
|
partidasPerdidas: (json['partidasPerdidas'] as num?)?.toInt() ?? 0,
|
||||||
|
partidasComoImpostor: (json['partidasComoImpostor'] as num?)?.toInt() ?? 0,
|
||||||
|
victoriasComoImpostor: (json['victoriasComoImpostor'] as num?)?.toInt() ?? 0,
|
||||||
|
fechaUltimaPartidaIso: json['fechaUltimaPartidaIso'] as String?,
|
||||||
|
partidasHoy: (json['partidasHoy'] as num?)?.toInt() ?? 0,
|
||||||
|
subidaFuegoHoy: (json['subidaFuegoHoy'] as num?)?.toInt() ?? 0,
|
||||||
|
fuego: (json['fuego'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ class Usuario {
|
|||||||
final String? foto;
|
final String? foto;
|
||||||
final String? creadoPorClienteId;
|
final String? creadoPorClienteId;
|
||||||
final String? clienteIdSeleccionado;
|
final String? clienteIdSeleccionado;
|
||||||
|
final int fuego;
|
||||||
|
final List<String> medallas;
|
||||||
|
|
||||||
Usuario({
|
Usuario({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -16,6 +18,8 @@ class Usuario {
|
|||||||
this.foto,
|
this.foto,
|
||||||
this.creadoPorClienteId,
|
this.creadoPorClienteId,
|
||||||
this.clienteIdSeleccionado,
|
this.clienteIdSeleccionado,
|
||||||
|
this.fuego = 0,
|
||||||
|
this.medallas = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get estaSeleccionado => clienteIdSeleccionado != null;
|
bool get estaSeleccionado => clienteIdSeleccionado != null;
|
||||||
@@ -29,6 +33,8 @@ class Usuario {
|
|||||||
String? foto,
|
String? foto,
|
||||||
String? creadoPorClienteId,
|
String? creadoPorClienteId,
|
||||||
String? clienteIdSeleccionado,
|
String? clienteIdSeleccionado,
|
||||||
|
int? fuego,
|
||||||
|
List<String>? medallas,
|
||||||
bool liberarSeleccion = false,
|
bool liberarSeleccion = false,
|
||||||
}) {
|
}) {
|
||||||
return Usuario(
|
return Usuario(
|
||||||
@@ -41,6 +47,8 @@ class Usuario {
|
|||||||
clienteIdSeleccionado: liberarSeleccion
|
clienteIdSeleccionado: liberarSeleccion
|
||||||
? null
|
? null
|
||||||
: (clienteIdSeleccionado ?? this.clienteIdSeleccionado),
|
: (clienteIdSeleccionado ?? this.clienteIdSeleccionado),
|
||||||
|
fuego: fuego ?? this.fuego,
|
||||||
|
medallas: medallas ?? this.medallas,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +61,8 @@ class Usuario {
|
|||||||
if (creadoPorClienteId != null) 'creadoPorClienteId': creadoPorClienteId,
|
if (creadoPorClienteId != null) 'creadoPorClienteId': creadoPorClienteId,
|
||||||
if (clienteIdSeleccionado != null)
|
if (clienteIdSeleccionado != null)
|
||||||
'clienteIdSeleccionado': clienteIdSeleccionado,
|
'clienteIdSeleccionado': clienteIdSeleccionado,
|
||||||
|
if (fuego > 0) 'fuego': fuego,
|
||||||
|
if (medallas.isNotEmpty) 'medallas': medallas,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory Usuario.fromJson(Map<String, dynamic> json) => Usuario(
|
factory Usuario.fromJson(Map<String, dynamic> json) => Usuario(
|
||||||
@@ -63,5 +73,9 @@ class Usuario {
|
|||||||
foto: json['foto'] as String?,
|
foto: json['foto'] as String?,
|
||||||
creadoPorClienteId: json['creadoPorClienteId'] as String?,
|
creadoPorClienteId: json['creadoPorClienteId'] as String?,
|
||||||
clienteIdSeleccionado: json['clienteIdSeleccionado'] as String?,
|
clienteIdSeleccionado: json['clienteIdSeleccionado'] as String?,
|
||||||
|
fuego: (json['fuego'] as num?)?.toInt() ?? 0,
|
||||||
|
medallas: (json['medallas'] as List<dynamic>? ?? const [])
|
||||||
|
.map((valor) => valor.toString())
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,13 +145,17 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|||||||
// 3. Iniciar host en Nearby
|
// 3. Iniciar host en Nearby
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final nearby = context.read<ServicioNearby>();
|
final nearby = context.read<ServicioNearby>();
|
||||||
final perfil = context.read<ServicioPerfilUsuario>().perfil;
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
||||||
|
final perfil = servicioPerfil.perfil;
|
||||||
|
final gamificacion = servicioPerfil.resumenGamificacion;
|
||||||
final nombreSala = '${nombre.trim()} - Farolero';
|
final nombreSala = '${nombre.trim()} - Farolero';
|
||||||
final ok = await nearby.iniciarHost(
|
final ok = await nearby.iniciarHost(
|
||||||
nombreSala,
|
nombreSala,
|
||||||
nombre.trim(),
|
nombre.trim(),
|
||||||
miNick: perfil.nick,
|
miNick: perfil.nick,
|
||||||
miAvatar: perfil.avatarAsset,
|
miAvatar: perfil.avatarAsset,
|
||||||
|
miFuego: gamificacion.fuego,
|
||||||
|
miMedallas: gamificacion.medallas,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import 'package:farolero/l10n/generated/app_localizations.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../estado/estado_juego.dart';
|
import '../estado/estado_juego.dart';
|
||||||
import '../modelos/palabra.dart';
|
import '../modelos/palabra.dart';
|
||||||
|
import '../modelos/gamificacion_usuario.dart';
|
||||||
import '../servicios/servicio_historial_partidas.dart';
|
import '../servicios/servicio_historial_partidas.dart';
|
||||||
import '../servicios/servicio_nearby.dart';
|
import '../servicios/servicio_nearby.dart';
|
||||||
|
import '../servicios/servicio_perfil_usuario.dart';
|
||||||
|
import '../tema/componentes_farolero.dart';
|
||||||
import '../tema/tema_app.dart';
|
import '../tema/tema_app.dart';
|
||||||
import 'pantalla_principal.dart';
|
import 'pantalla_principal.dart';
|
||||||
import 'pantalla_ver_palabra.dart';
|
import 'pantalla_ver_palabra.dart';
|
||||||
@@ -18,6 +21,7 @@ class PantallaFinPartida extends StatefulWidget {
|
|||||||
|
|
||||||
class _PantallaFinPartidaState extends State<PantallaFinPartida> {
|
class _PantallaFinPartidaState extends State<PantallaFinPartida> {
|
||||||
bool _guardada = false;
|
bool _guardada = false;
|
||||||
|
ProgresoGamificacionUsuario? _progreso;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -31,9 +35,15 @@ class _PantallaFinPartidaState extends State<PantallaFinPartida> {
|
|||||||
partida.jugadores.where((j) => j.esImpostor).toList();
|
partida.jugadores.where((j) => j.esImpostor).toList();
|
||||||
if (!_guardada && partida.ganador != null) {
|
if (!_guardada && partida.ganador != null) {
|
||||||
_guardada = true;
|
_guardada = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
context.read<ServicioHistorialPartidas>().guardarPartida(partida);
|
final historial = context.read<ServicioHistorialPartidas>();
|
||||||
|
final perfil = context.read<ServicioPerfilUsuario>();
|
||||||
|
await historial.guardarPartida(partida);
|
||||||
|
final progreso = await perfil.registrarPartidaCompletada(
|
||||||
|
victoria: ganaronJugadores,
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _progreso = progreso);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,6 +101,10 @@ class _PantallaFinPartidaState extends State<PantallaFinPartida> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
if (_progreso != null) ...[
|
||||||
|
_TarjetaProgresoGamificacion(progreso: _progreso!),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
// Palabra secreta
|
// Palabra secreta
|
||||||
Card(
|
Card(
|
||||||
@@ -260,3 +274,59 @@ class _PantallaFinPartidaState extends State<PantallaFinPartida> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _TarjetaProgresoGamificacion extends StatelessWidget {
|
||||||
|
final ProgresoGamificacionUsuario progreso;
|
||||||
|
|
||||||
|
const _TarjetaProgresoGamificacion({required this.progreso});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final nuevas = progreso.nuevasMedallas;
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Tu progreso', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('🔥', style: TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progreso.despues.fuego / 100,
|
||||||
|
minHeight: 8,
|
||||||
|
color: TemaApp.colorNaranja,
|
||||||
|
backgroundColor: Colors.black.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text('${progreso.despues.fuego}%'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
progreso.incrementoFuego >= 0
|
||||||
|
? '+${progreso.incrementoFuego}% de fuego'
|
||||||
|
: '${progreso.incrementoFuego}% de fuego',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
if (nuevas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Nuevas medallas',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
MedallasCompactasFarolero(ids: nuevas, max: nuevas.length),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:farolero/l10n/generated/app_localizations.dart';
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../modelos/inicio_partida_multijugador.dart';
|
import '../modelos/inicio_partida_multijugador.dart';
|
||||||
|
import '../modelos/gamificacion_usuario.dart';
|
||||||
import '../modelos/palabra.dart';
|
import '../modelos/palabra.dart';
|
||||||
import '../modelos/snapshot_partida_online.dart';
|
import '../modelos/snapshot_partida_online.dart';
|
||||||
import '../servicios/servicio_historial_partidas.dart';
|
import '../servicios/servicio_historial_partidas.dart';
|
||||||
import '../servicios/servicio_nearby.dart';
|
import '../servicios/servicio_nearby.dart';
|
||||||
|
import '../servicios/servicio_perfil_usuario.dart';
|
||||||
|
import '../tema/componentes_farolero.dart';
|
||||||
import '../tema/tema_app.dart';
|
import '../tema/tema_app.dart';
|
||||||
import 'pantalla_notas_online.dart';
|
import 'pantalla_notas_online.dart';
|
||||||
import 'pantalla_principal.dart';
|
import 'pantalla_principal.dart';
|
||||||
@@ -30,6 +33,7 @@ class PantallaFinPartidaOnline extends StatefulWidget {
|
|||||||
|
|
||||||
class _PantallaFinPartidaOnlineState extends State<PantallaFinPartidaOnline> {
|
class _PantallaFinPartidaOnlineState extends State<PantallaFinPartidaOnline> {
|
||||||
bool _guardada = false;
|
bool _guardada = false;
|
||||||
|
ProgresoGamificacionUsuario? _progreso;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -41,11 +45,18 @@ class _PantallaFinPartidaOnlineState extends State<PantallaFinPartidaOnline> {
|
|||||||
|
|
||||||
if (!_guardada && snapshot.ganador != null) {
|
if (!_guardada && snapshot.ganador != null) {
|
||||||
_guardada = true;
|
_guardada = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
context
|
final historial = context.read<ServicioHistorialPartidas>();
|
||||||
.read<ServicioHistorialPartidas>()
|
final perfil = context.read<ServicioPerfilUsuario>();
|
||||||
.guardarSnapshotOnline(snapshot);
|
await historial.guardarSnapshotOnline(snapshot);
|
||||||
|
final progreso = await perfil.registrarPartidaCompletada(
|
||||||
|
victoria: _perfilLocalGano(snapshot, jugadoresControlados),
|
||||||
|
comoImpostor: jugadoresControlados.any((j) => j.esImpostor),
|
||||||
|
victoriaComoImpostor: snapshot.ganador == 'impostores' &&
|
||||||
|
jugadoresControlados.any((j) => j.esImpostor),
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _progreso = progreso);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,6 +143,10 @@ class _PantallaFinPartidaOnlineState extends State<PantallaFinPartidaOnline> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
if (_progreso != null) ...[
|
||||||
|
_TarjetaProgresoGamificacion(progreso: _progreso!),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
Card(
|
Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@@ -254,4 +269,71 @@ class _PantallaFinPartidaOnlineState extends State<PantallaFinPartidaOnline> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _perfilLocalGano(
|
||||||
|
SnapshotPartidaOnline snapshot,
|
||||||
|
List<JugadorInicioPartida> jugadoresControlados,
|
||||||
|
) {
|
||||||
|
if (jugadoresControlados.isEmpty) return snapshot.ganador == 'jugadores';
|
||||||
|
final ganaronImpostores = snapshot.ganador == 'impostores';
|
||||||
|
return jugadoresControlados.any(
|
||||||
|
(jugador) => jugador.esImpostor ? ganaronImpostores : !ganaronImpostores,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TarjetaProgresoGamificacion extends StatelessWidget {
|
||||||
|
final ProgresoGamificacionUsuario progreso;
|
||||||
|
|
||||||
|
const _TarjetaProgresoGamificacion({required this.progreso});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final nuevas = progreso.nuevasMedallas;
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Tu progreso', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('🔥', style: TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progreso.despues.fuego / 100,
|
||||||
|
minHeight: 8,
|
||||||
|
color: TemaApp.colorNaranja,
|
||||||
|
backgroundColor: Colors.black.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text('${progreso.despues.fuego}%'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
progreso.incrementoFuego >= 0
|
||||||
|
? '+${progreso.incrementoFuego}% de fuego'
|
||||||
|
: '${progreso.incrementoFuego}% de fuego',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
if (nuevas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Nuevas medallas',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
MedallasCompactasFarolero(ids: nuevas, max: nuevas.length),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:farolero/l10n/generated/app_localizations.dart';
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../estado/estado_juego.dart';
|
import '../estado/estado_juego.dart';
|
||||||
|
import '../modelos/gamificacion_usuario.dart';
|
||||||
import '../modelos/inicio_partida_multijugador.dart';
|
import '../modelos/inicio_partida_multijugador.dart';
|
||||||
import '../modelos/jugador.dart';
|
import '../modelos/jugador.dart';
|
||||||
import '../modelos/partida.dart';
|
import '../modelos/partida.dart';
|
||||||
import '../modelos/snapshot_partida_online.dart';
|
import '../modelos/snapshot_partida_online.dart';
|
||||||
import '../servicios/servicio_historial_partidas.dart';
|
import '../servicios/servicio_historial_partidas.dart';
|
||||||
import '../servicios/servicio_nearby.dart';
|
import '../servicios/servicio_nearby.dart';
|
||||||
|
import '../servicios/servicio_perfil_usuario.dart';
|
||||||
import '../tema/componentes_farolero.dart';
|
import '../tema/componentes_farolero.dart';
|
||||||
import '../tema/tema_app.dart';
|
import '../tema/tema_app.dart';
|
||||||
import 'pantalla_notas_online.dart';
|
import 'pantalla_notas_online.dart';
|
||||||
@@ -31,6 +33,7 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
|
|||||||
int _segundosRestantes = 0;
|
int _segundosRestantes = 0;
|
||||||
bool _hostListo = false;
|
bool _hostListo = false;
|
||||||
bool _partidaOnlineGuardada = false;
|
bool _partidaOnlineGuardada = false;
|
||||||
|
ProgresoGamificacionUsuario? _progresoGamificacion;
|
||||||
String? _primerTurnoId;
|
String? _primerTurnoId;
|
||||||
String? _primerTurnoNombre;
|
String? _primerTurnoNombre;
|
||||||
final Map<String, bool> _clientesListos = {};
|
final Map<String, bool> _clientesListos = {};
|
||||||
@@ -695,7 +698,7 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
|
|||||||
label: Text(
|
label: Text(
|
||||||
_hostYaVoto(context)
|
_hostYaVoto(context)
|
||||||
? 'Votos del host registrados'
|
? 'Votos del host registrados'
|
||||||
: 'Votar por los jugadores de este m?vil',
|
: 'Votar por los jugadores de este móvil',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -934,12 +937,57 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
|
|||||||
partida == null ? '' : l10n.theWordWas(partida.palabraSecreta),
|
partida == null ? '' : l10n.theWordWas(partida.palabraSecreta),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
if (_progresoGamificacion != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildProgresoGamificacion(context, _progresoGamificacion!),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildProgresoGamificacion(
|
||||||
|
BuildContext context,
|
||||||
|
ProgresoGamificacionUsuario progreso,
|
||||||
|
) {
|
||||||
|
final nuevas = progreso.nuevasMedallas;
|
||||||
|
return PanelFarolero(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
color: TemaApp.colorSuperficie.withValues(alpha: 0.82),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Tu progreso', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('🔥', style: TextStyle(fontSize: 22)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progreso.despues.fuego / 100,
|
||||||
|
minHeight: 8,
|
||||||
|
color: TemaApp.colorNaranja,
|
||||||
|
backgroundColor: Colors.black.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('${progreso.despues.fuego}%'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (nuevas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
MedallasCompactasFarolero(ids: nuevas, max: nuevas.length),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool _hostYaVoto(BuildContext context) {
|
bool _hostYaVoto(BuildContext context) {
|
||||||
final estado = context.read<EstadoJuego>();
|
final estado = context.read<EstadoJuego>();
|
||||||
final sala = context.read<ServicioNearby>().estadoSala;
|
final sala = context.read<ServicioNearby>().estadoSala;
|
||||||
@@ -1182,7 +1230,28 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
|
|||||||
final partida = context.read<EstadoJuego>().partida;
|
final partida = context.read<EstadoJuego>().partida;
|
||||||
if (partida?.ganador == null) return;
|
if (partida?.ganador == null) return;
|
||||||
_partidaOnlineGuardada = true;
|
_partidaOnlineGuardada = true;
|
||||||
await context.read<ServicioHistorialPartidas>().guardarPartida(partida!);
|
final historial = context.read<ServicioHistorialPartidas>();
|
||||||
|
final nearby = context.read<ServicioNearby>();
|
||||||
|
final perfil = context.read<ServicioPerfilUsuario>();
|
||||||
|
await historial.guardarPartida(partida!);
|
||||||
|
final jugadoresHost = _jugadoresHostControlados(
|
||||||
|
partida,
|
||||||
|
nearby,
|
||||||
|
);
|
||||||
|
final ganaronImpostores = partida.ganador == 'impostores';
|
||||||
|
final victoria = jugadoresHost.isEmpty
|
||||||
|
? partida.ganador == 'jugadores'
|
||||||
|
: jugadoresHost.any(
|
||||||
|
(jugador) =>
|
||||||
|
jugador.esImpostor ? ganaronImpostores : !ganaronImpostores,
|
||||||
|
);
|
||||||
|
final progreso = await perfil.registrarPartidaCompletada(
|
||||||
|
victoria: victoria,
|
||||||
|
comoImpostor: jugadoresHost.any((j) => j.esImpostor),
|
||||||
|
victoriaComoImpostor:
|
||||||
|
ganaronImpostores && jugadoresHost.any((j) => j.esImpostor),
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _progresoGamificacion = progreso);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _iniciarAdivinanzaOnline(BuildContext context) async {
|
Future<void> _iniciarAdivinanzaOnline(BuildContext context) async {
|
||||||
|
|||||||
@@ -217,15 +217,26 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
? TemaApp.colorNaranja
|
? TemaApp.colorNaranja
|
||||||
: TemaApp.colorAzul,
|
: TemaApp.colorAzul,
|
||||||
size: 38,
|
size: 38,
|
||||||
|
fuego: usuario.fuego,
|
||||||
|
medallas: usuario.medallas,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(usuario.nombre),
|
title: Text(usuario.nombre),
|
||||||
subtitle: Text(
|
subtitle: Column(
|
||||||
seleccionadoPorMi
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? 'Seleccionado por este móvil'
|
children: [
|
||||||
: seleccionadoPorOtro
|
Text(
|
||||||
? 'Seleccionado por otro cliente'
|
seleccionadoPorMi
|
||||||
: 'Disponible',
|
? 'Seleccionado por este móvil'
|
||||||
|
: seleccionadoPorOtro
|
||||||
|
? 'Seleccionado por otro cliente'
|
||||||
|
: 'Disponible',
|
||||||
|
),
|
||||||
|
if (usuario.medallas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
MedallasCompactasFarolero(ids: usuario.medallas),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -300,11 +311,15 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (nombre != null && nombre.trim().isNotEmpty) {
|
if (nombre != null && nombre.trim().isNotEmpty) {
|
||||||
|
final gamificacion =
|
||||||
|
context.read<ServicioPerfilUsuario>().resumenGamificacion;
|
||||||
await nearby.crearUsuarioSala(
|
await nearby.crearUsuarioSala(
|
||||||
nombre.trim(),
|
nombre.trim(),
|
||||||
seleccionar: true,
|
seleccionar: true,
|
||||||
nick: perfil.nick,
|
nick: perfil.nick,
|
||||||
avatar: perfil.avatarAsset,
|
avatar: perfil.avatarAsset,
|
||||||
|
fuego: gamificacion.fuego,
|
||||||
|
medallas: gamificacion.medallas,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class PantallaPrincipal extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final perfil = context.watch<ServicioPerfilUsuario>().perfil;
|
final servicioPerfil = context.watch<ServicioPerfilUsuario>();
|
||||||
|
final perfil = servicioPerfil.perfil;
|
||||||
|
final gamificacion = servicioPerfil.resumenGamificacion;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: FondoFarolero(
|
body: FondoFarolero(
|
||||||
@@ -35,6 +37,8 @@ class PantallaPrincipal extends StatelessWidget {
|
|||||||
texto: perfil.nombre.substring(0, 1).toUpperCase(),
|
texto: perfil.nombre.substring(0, 1).toUpperCase(),
|
||||||
assetPath: perfil.avatarAsset,
|
assetPath: perfil.avatarAsset,
|
||||||
size: 48,
|
size: 48,
|
||||||
|
fuego: gamificacion.fuego,
|
||||||
|
medallas: gamificacion.medallas,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -53,9 +57,9 @@ class PantallaPrincipal extends StatelessWidget {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: 0.68,
|
value: gamificacion.fuego / 100,
|
||||||
minHeight: 4,
|
minHeight: 4,
|
||||||
color: TemaApp.colorPurpura,
|
color: TemaApp.colorNaranja,
|
||||||
backgroundColor: Colors.black.withValues(alpha: 0.45),
|
backgroundColor: Colors.black.withValues(alpha: 0.45),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -314,11 +314,15 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final nearby = context.read<ServicioNearby>();
|
final nearby = context.read<ServicioNearby>();
|
||||||
final perfil = context.read<ServicioPerfilUsuario>().perfil;
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
||||||
|
final perfil = servicioPerfil.perfil;
|
||||||
|
final gamificacion = servicioPerfil.resumenGamificacion;
|
||||||
final ok = await nearby.buscarHosts(
|
final ok = await nearby.buscarHosts(
|
||||||
_nombreController.text.trim(),
|
_nombreController.text.trim(),
|
||||||
miNick: perfil.nick,
|
miNick: perfil.nick,
|
||||||
miAvatar: perfil.avatarAsset,
|
miAvatar: perfil.avatarAsset,
|
||||||
|
miFuego: gamificacion.fuego,
|
||||||
|
miMedallas: gamificacion.medallas,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -342,7 +346,9 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final nearby = context.read<ServicioNearby>();
|
final nearby = context.read<ServicioNearby>();
|
||||||
final perfil = context.read<ServicioPerfilUsuario>().perfil;
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
||||||
|
final perfil = servicioPerfil.perfil;
|
||||||
|
final gamificacion = servicioPerfil.resumenGamificacion;
|
||||||
// Parar discovery antes de conectar
|
// Parar discovery antes de conectar
|
||||||
await nearby.pararBusqueda();
|
await nearby.pararBusqueda();
|
||||||
final ok = await nearby.conectarAHost(
|
final ok = await nearby.conectarAHost(
|
||||||
@@ -350,6 +356,8 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
_nombreController.text.trim(),
|
_nombreController.text.trim(),
|
||||||
miNick: perfil.nick,
|
miNick: perfil.nick,
|
||||||
miAvatar: perfil.avatarAsset,
|
miAvatar: perfil.avatarAsset,
|
||||||
|
miFuego: gamificacion.fuego,
|
||||||
|
miMedallas: gamificacion.medallas,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!ok && mounted) {
|
if (!ok && mounted) {
|
||||||
@@ -389,11 +397,15 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
// Iniciar búsqueda para que Nearby encuentre al host
|
// Iniciar búsqueda para que Nearby encuentre al host
|
||||||
final nearby = context.read<ServicioNearby>();
|
final nearby = context.read<ServicioNearby>();
|
||||||
if (!nearby.buscando) {
|
if (!nearby.buscando) {
|
||||||
final perfil = context.read<ServicioPerfilUsuario>().perfil;
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
||||||
|
final perfil = servicioPerfil.perfil;
|
||||||
|
final gamificacion = servicioPerfil.resumenGamificacion;
|
||||||
await nearby.buscarHosts(
|
await nearby.buscarHosts(
|
||||||
_nombreController.text.trim(),
|
_nombreController.text.trim(),
|
||||||
miNick: perfil.nick,
|
miNick: perfil.nick,
|
||||||
miAvatar: perfil.avatarAsset,
|
miAvatar: perfil.avatarAsset,
|
||||||
|
miFuego: gamificacion.fuego,
|
||||||
|
miMedallas: gamificacion.medallas,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -835,11 +847,15 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (nombre != null && nombre.trim().isNotEmpty) {
|
if (nombre != null && nombre.trim().isNotEmpty) {
|
||||||
|
final gamificacion =
|
||||||
|
context.read<ServicioPerfilUsuario>().resumenGamificacion;
|
||||||
await nearby.crearUsuarioSala(
|
await nearby.crearUsuarioSala(
|
||||||
nombre.trim(),
|
nombre.trim(),
|
||||||
seleccionar: true,
|
seleccionar: true,
|
||||||
nick: perfil.nick,
|
nick: perfil.nick,
|
||||||
avatar: perfil.avatarAsset,
|
avatar: perfil.avatarAsset,
|
||||||
|
fuego: gamificacion.fuego,
|
||||||
|
medallas: gamificacion.medallas,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -861,17 +877,29 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
usuario.estaSeleccionado && usuario.clienteIdSeleccionado != miClientId;
|
usuario.estaSeleccionado && usuario.clienteIdSeleccionado != miClientId;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Text(
|
leading: AvatarFarolero(
|
||||||
usuario.avatar ?? '??',
|
texto: usuario.nombre.isEmpty ? '?' : usuario.nombre[0],
|
||||||
style: const TextStyle(fontSize: 24),
|
assetPath: usuario.avatar,
|
||||||
|
size: 38,
|
||||||
|
fuego: usuario.fuego,
|
||||||
|
medallas: usuario.medallas,
|
||||||
),
|
),
|
||||||
title: Text(usuario.nombre),
|
title: Text(usuario.nombre),
|
||||||
subtitle: Text(
|
subtitle: Column(
|
||||||
seleccionadoPorMi
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? 'Seleccionado por este m?vil'
|
children: [
|
||||||
: seleccionadoPorOtro
|
Text(
|
||||||
? 'No disponible'
|
seleccionadoPorMi
|
||||||
: 'Disponible',
|
? 'Seleccionado por este móvil'
|
||||||
|
: seleccionadoPorOtro
|
||||||
|
? 'No disponible'
|
||||||
|
: 'Disponible',
|
||||||
|
),
|
||||||
|
if (usuario.medallas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
MedallasCompactasFarolero(ids: usuario.medallas),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
trailing: seleccionadoPorMi
|
trailing: seleccionadoPorMi
|
||||||
? IconButton(
|
? IconButton(
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
String? _miNombre;
|
String? _miNombre;
|
||||||
String? _miNick;
|
String? _miNick;
|
||||||
String? _miAvatar;
|
String? _miAvatar;
|
||||||
|
int _miFuego = 0;
|
||||||
|
List<String> _miMedallas = const [];
|
||||||
|
|
||||||
final Map<String, JugadorConectado> _jugadores = {};
|
final Map<String, JugadorConectado> _jugadores = {};
|
||||||
final List<OnMensajeCallback> _listeners = [];
|
final List<OnMensajeCallback> _listeners = [];
|
||||||
@@ -213,6 +215,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
String miNombre, {
|
String miNombre, {
|
||||||
String? miNick,
|
String? miNick,
|
||||||
String? miAvatar,
|
String? miAvatar,
|
||||||
|
int miFuego = 0,
|
||||||
|
List<String> miMedallas = const [],
|
||||||
}) async {
|
}) async {
|
||||||
if (_conectado ||
|
if (_conectado ||
|
||||||
_anunciando ||
|
_anunciando ||
|
||||||
@@ -245,6 +249,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
foto: miAvatar,
|
foto: miAvatar,
|
||||||
creadoPorClienteId: _hostClientId,
|
creadoPorClienteId: _hostClientId,
|
||||||
clienteIdSeleccionado: _hostClientId,
|
clienteIdSeleccionado: _hostClientId,
|
||||||
|
fuego: miFuego,
|
||||||
|
medallas: miMedallas,
|
||||||
);
|
);
|
||||||
_estadoSala!.crearUsuario(usuarioHost);
|
_estadoSala!.crearUsuario(usuarioHost);
|
||||||
_sincronizarPoolDesdeSala();
|
_sincronizarPoolDesdeSala();
|
||||||
@@ -281,10 +287,14 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
String miNombre, {
|
String miNombre, {
|
||||||
String? miNick,
|
String? miNick,
|
||||||
String? miAvatar,
|
String? miAvatar,
|
||||||
|
int miFuego = 0,
|
||||||
|
List<String> miMedallas = const [],
|
||||||
}) async {
|
}) async {
|
||||||
_miNombre = miNombre;
|
_miNombre = miNombre;
|
||||||
_miNick = miNick;
|
_miNick = miNick;
|
||||||
_miAvatar = miAvatar;
|
_miAvatar = miAvatar;
|
||||||
|
_miFuego = miFuego;
|
||||||
|
_miMedallas = miMedallas;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final resultado = await Nearby().startDiscovery(
|
final resultado = await Nearby().startDiscovery(
|
||||||
@@ -312,10 +322,14 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
String miNombre, {
|
String miNombre, {
|
||||||
String? miNick,
|
String? miNick,
|
||||||
String? miAvatar,
|
String? miAvatar,
|
||||||
|
int miFuego = 0,
|
||||||
|
List<String> miMedallas = const [],
|
||||||
}) async {
|
}) async {
|
||||||
_miNombre = miNombre;
|
_miNombre = miNombre;
|
||||||
_miNick = miNick;
|
_miNick = miNick;
|
||||||
_miAvatar = miAvatar;
|
_miAvatar = miAvatar;
|
||||||
|
_miFuego = miFuego;
|
||||||
|
_miMedallas = miMedallas;
|
||||||
try {
|
try {
|
||||||
await Nearby().requestConnection(
|
await Nearby().requestConnection(
|
||||||
miNombre,
|
miNombre,
|
||||||
@@ -359,6 +373,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
'nombre': _miNombre ?? 'Jugador',
|
'nombre': _miNombre ?? 'Jugador',
|
||||||
if (_miNick != null) 'nick': _miNick,
|
if (_miNick != null) 'nick': _miNick,
|
||||||
if (_miAvatar != null) 'avatar': _miAvatar,
|
if (_miAvatar != null) 'avatar': _miAvatar,
|
||||||
|
'fuego': _miFuego,
|
||||||
|
'medallas': _miMedallas,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -515,6 +531,10 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
final nombre = mensaje.datos['nombre'] as String? ?? 'Jugador';
|
final nombre = mensaje.datos['nombre'] as String? ?? 'Jugador';
|
||||||
final nick = mensaje.datos['nick'] as String?;
|
final nick = mensaje.datos['nick'] as String?;
|
||||||
final avatar = mensaje.datos['avatar'] as String?;
|
final avatar = mensaje.datos['avatar'] as String?;
|
||||||
|
final fuego = (mensaje.datos['fuego'] as num?)?.toInt() ?? 0;
|
||||||
|
final medallas = (mensaje.datos['medallas'] as List<dynamic>? ?? const [])
|
||||||
|
.map((valor) => valor.toString())
|
||||||
|
.toList();
|
||||||
final clientId = endpointId;
|
final clientId = endpointId;
|
||||||
_jugadores[endpointId] = JugadorConectado(
|
_jugadores[endpointId] = JugadorConectado(
|
||||||
endpointId: endpointId,
|
endpointId: endpointId,
|
||||||
@@ -528,6 +548,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
nombre: nombre,
|
nombre: nombre,
|
||||||
nick: nick,
|
nick: nick,
|
||||||
avatar: avatar,
|
avatar: avatar,
|
||||||
|
fuego: fuego,
|
||||||
|
medallas: medallas,
|
||||||
);
|
);
|
||||||
|
|
||||||
enviarMensaje(
|
enviarMensaje(
|
||||||
@@ -555,6 +577,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
required String nombre,
|
required String nombre,
|
||||||
String? nick,
|
String? nick,
|
||||||
String? avatar,
|
String? avatar,
|
||||||
|
int fuego = 0,
|
||||||
|
List<String> medallas = const [],
|
||||||
}) {
|
}) {
|
||||||
final sala = _estadoSala;
|
final sala = _estadoSala;
|
||||||
if (sala == null || sala.fase != FaseSalaMultijugador.lobby) return;
|
if (sala == null || sala.fase != FaseSalaMultijugador.lobby) return;
|
||||||
@@ -579,6 +603,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
avatar: avatar,
|
avatar: avatar,
|
||||||
foto: avatar,
|
foto: avatar,
|
||||||
creadoPorClienteId: clientId,
|
creadoPorClienteId: clientId,
|
||||||
|
fuego: fuego,
|
||||||
|
medallas: medallas,
|
||||||
);
|
);
|
||||||
final resultadoCrear = sala.crearUsuario(usuario);
|
final resultadoCrear = sala.crearUsuario(usuario);
|
||||||
if (!resultadoCrear.exitoso) return;
|
if (!resultadoCrear.exitoso) return;
|
||||||
@@ -769,6 +795,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
bool seleccionar = true,
|
bool seleccionar = true,
|
||||||
String? nick,
|
String? nick,
|
||||||
String? avatar,
|
String? avatar,
|
||||||
|
int fuego = 0,
|
||||||
|
List<String> medallas = const [],
|
||||||
}) async {
|
}) async {
|
||||||
final nombreLimpio = nombre.trim();
|
final nombreLimpio = nombre.trim();
|
||||||
if (nombreLimpio.isEmpty) return;
|
if (nombreLimpio.isEmpty) return;
|
||||||
@@ -780,6 +808,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
avatar: avatar,
|
avatar: avatar,
|
||||||
foto: avatar,
|
foto: avatar,
|
||||||
creadoPorClienteId: clientId,
|
creadoPorClienteId: clientId,
|
||||||
|
fuego: fuego,
|
||||||
|
medallas: medallas,
|
||||||
);
|
);
|
||||||
if (_esHost && _estadoSala != null && clientId != null) {
|
if (_esHost && _estadoSala != null && clientId != null) {
|
||||||
final resultado = _estadoSala!.crearUsuario(usuario);
|
final resultado = _estadoSala!.crearUsuario(usuario);
|
||||||
@@ -979,6 +1009,8 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
_miNombre = null;
|
_miNombre = null;
|
||||||
_miNick = null;
|
_miNick = null;
|
||||||
_miAvatar = null;
|
_miAvatar = null;
|
||||||
|
_miFuego = 0;
|
||||||
|
_miMedallas = const [];
|
||||||
_palabraRecibida = null;
|
_palabraRecibida = null;
|
||||||
_soyImpostor = null;
|
_soyImpostor = null;
|
||||||
_faseActual = null;
|
_faseActual = null;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../modelos/gamificacion_usuario.dart';
|
||||||
|
|
||||||
class PerfilUsuario {
|
class PerfilUsuario {
|
||||||
final String nombre;
|
final String nombre;
|
||||||
final String nick;
|
final String nick;
|
||||||
@@ -25,6 +29,7 @@ class ServicioPerfilUsuario extends ChangeNotifier {
|
|||||||
static const _claveNombre = 'perfil.nombre';
|
static const _claveNombre = 'perfil.nombre';
|
||||||
static const _claveNick = 'perfil.nick';
|
static const _claveNick = 'perfil.nick';
|
||||||
static const _claveAvatar = 'perfil.avatar';
|
static const _claveAvatar = 'perfil.avatar';
|
||||||
|
static const _claveEstadisticas = 'perfil.estadisticas';
|
||||||
|
|
||||||
static const avatares = [
|
static const avatares = [
|
||||||
'assets/avatars/avatar_01.png',
|
'assets/avatars/avatar_01.png',
|
||||||
@@ -57,6 +62,18 @@ class ServicioPerfilUsuario extends ChangeNotifier {
|
|||||||
'assets/avatars/avatar_28.png',
|
'assets/avatars/avatar_28.png',
|
||||||
'assets/avatars/avatar_29.png',
|
'assets/avatars/avatar_29.png',
|
||||||
'assets/avatars/avatar_30.png',
|
'assets/avatars/avatar_30.png',
|
||||||
|
'assets/avatars/capybara_01.png',
|
||||||
|
'assets/avatars/capybara_02.png',
|
||||||
|
'assets/avatars/capybara_03.png',
|
||||||
|
'assets/avatars/capybara_04.png',
|
||||||
|
'assets/avatars/capybara_05.png',
|
||||||
|
'assets/avatars/capybara_06.png',
|
||||||
|
'assets/avatars/capybara_07.png',
|
||||||
|
'assets/avatars/capybara_08.png',
|
||||||
|
'assets/avatars/capybara_09.png',
|
||||||
|
'assets/avatars/capybara_10.png',
|
||||||
|
'assets/avatars/capybara_11.png',
|
||||||
|
'assets/avatars/capybara_12.png',
|
||||||
];
|
];
|
||||||
|
|
||||||
PerfilUsuario _perfil = const PerfilUsuario(
|
PerfilUsuario _perfil = const PerfilUsuario(
|
||||||
@@ -64,9 +81,13 @@ class ServicioPerfilUsuario extends ChangeNotifier {
|
|||||||
nick: 'farolero',
|
nick: 'farolero',
|
||||||
avatarAsset: 'assets/avatars/avatar_01.png',
|
avatarAsset: 'assets/avatars/avatar_01.png',
|
||||||
);
|
);
|
||||||
|
EstadisticasPerfilUsuario _estadisticas =
|
||||||
|
const EstadisticasPerfilUsuario();
|
||||||
bool _cargado = false;
|
bool _cargado = false;
|
||||||
|
|
||||||
PerfilUsuario get perfil => _perfil;
|
PerfilUsuario get perfil => _perfil;
|
||||||
|
EstadisticasPerfilUsuario get estadisticas => _estadisticas;
|
||||||
|
ResumenGamificacionUsuario get resumenGamificacion => _estadisticas.resumen;
|
||||||
bool get cargado => _cargado;
|
bool get cargado => _cargado;
|
||||||
|
|
||||||
Future<void> cargar() async {
|
Future<void> cargar() async {
|
||||||
@@ -76,6 +97,16 @@ class ServicioPerfilUsuario extends ChangeNotifier {
|
|||||||
nick: prefs.getString(_claveNick) ?? _perfil.nick,
|
nick: prefs.getString(_claveNick) ?? _perfil.nick,
|
||||||
avatarAsset: prefs.getString(_claveAvatar) ?? _perfil.avatarAsset,
|
avatarAsset: prefs.getString(_claveAvatar) ?? _perfil.avatarAsset,
|
||||||
);
|
);
|
||||||
|
final estadisticasJson = prefs.getString(_claveEstadisticas);
|
||||||
|
if (estadisticasJson != null) {
|
||||||
|
try {
|
||||||
|
_estadisticas = EstadisticasPerfilUsuario.fromJson(
|
||||||
|
json.decode(estadisticasJson) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
_estadisticas = const EstadisticasPerfilUsuario();
|
||||||
|
}
|
||||||
|
}
|
||||||
_cargado = true;
|
_cargado = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -101,4 +132,34 @@ class ServicioPerfilUsuario extends ChangeNotifier {
|
|||||||
await prefs.setString(_claveAvatar, _perfil.avatarAsset);
|
await prefs.setString(_claveAvatar, _perfil.avatarAsset);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ProgresoGamificacionUsuario> registrarPartidaCompletada({
|
||||||
|
required bool victoria,
|
||||||
|
bool comoImpostor = false,
|
||||||
|
bool victoriaComoImpostor = false,
|
||||||
|
DateTime? fecha,
|
||||||
|
}) async {
|
||||||
|
final antes = _estadisticas;
|
||||||
|
final despues = antes.registrarPartida(
|
||||||
|
victoria: victoria,
|
||||||
|
comoImpostor: comoImpostor,
|
||||||
|
victoriaComoImpostor: victoriaComoImpostor,
|
||||||
|
fecha: fecha,
|
||||||
|
);
|
||||||
|
final medallasPrevias = antes.medallas.toSet();
|
||||||
|
final nuevasMedallas = despues.medallas
|
||||||
|
.where((id) => !medallasPrevias.contains(id))
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
_estadisticas = despues;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_claveEstadisticas, json.encode(despues.toJson()));
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
return ProgresoGamificacionUsuario(
|
||||||
|
antes: antes,
|
||||||
|
despues: despues,
|
||||||
|
nuevasMedallas: nuevasMedallas,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../modelos/gamificacion_usuario.dart';
|
||||||
import 'tema_app.dart';
|
import 'tema_app.dart';
|
||||||
|
|
||||||
class FondoFarolero extends StatelessWidget {
|
class FondoFarolero extends StatelessWidget {
|
||||||
@@ -272,6 +273,8 @@ class AvatarFarolero extends StatelessWidget {
|
|||||||
final String? assetPath;
|
final String? assetPath;
|
||||||
final Color color;
|
final Color color;
|
||||||
final double size;
|
final double size;
|
||||||
|
final int fuego;
|
||||||
|
final List<String> medallas;
|
||||||
|
|
||||||
const AvatarFarolero({
|
const AvatarFarolero({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -279,43 +282,172 @@ class AvatarFarolero extends StatelessWidget {
|
|||||||
this.assetPath,
|
this.assetPath,
|
||||||
this.color = TemaApp.colorNaranja,
|
this.color = TemaApp.colorNaranja,
|
||||||
this.size = 40,
|
this.size = 40,
|
||||||
|
this.fuego = 0,
|
||||||
|
this.medallas = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
final lienzo = size + 10;
|
||||||
width: size,
|
return SizedBox(
|
||||||
height: size,
|
width: lienzo,
|
||||||
decoration: BoxDecoration(
|
height: lienzo,
|
||||||
shape: BoxShape.circle,
|
child: Stack(
|
||||||
gradient: RadialGradient(
|
clipBehavior: Clip.none,
|
||||||
colors: [color.withValues(alpha: 0.9), TemaApp.colorSuperficie],
|
alignment: Alignment.center,
|
||||||
),
|
children: [
|
||||||
border: Border.all(color: TemaApp.colorDorado, width: 2),
|
CustomPaint(
|
||||||
),
|
size: Size(lienzo, lienzo),
|
||||||
child: Center(
|
painter: _FuegoAvatarPainter(fuego: fuego),
|
||||||
child: assetPath == null
|
|
||||||
? Text(
|
|
||||||
texto,
|
|
||||||
style: TextStyle(
|
|
||||||
color: TemaApp.colorTexto,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: size * 0.36,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ClipOval(
|
|
||||||
child: Image.asset(
|
|
||||||
assetPath!,
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: RadialGradient(
|
||||||
|
colors: [color.withValues(alpha: 0.9), TemaApp.colorSuperficie],
|
||||||
|
),
|
||||||
|
border: Border.all(color: TemaApp.colorDorado, width: 2),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: assetPath == null
|
||||||
|
? Text(
|
||||||
|
texto,
|
||||||
|
style: TextStyle(
|
||||||
|
color: TemaApp.colorTexto,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: size * 0.36,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
assetPath!,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (medallas.isNotEmpty)
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -4,
|
||||||
|
child: _MiniMedalla(id: medallas.first),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MedallasCompactasFarolero extends StatelessWidget {
|
||||||
|
final List<String> ids;
|
||||||
|
final int max;
|
||||||
|
|
||||||
|
const MedallasCompactasFarolero({
|
||||||
|
super.key,
|
||||||
|
required this.ids,
|
||||||
|
this.max = 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final visibles = ids.take(max).toList();
|
||||||
|
if (visibles.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
for (final id in visibles) _MiniMedalla(id: id),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MiniMedalla extends StatelessWidget {
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
const _MiniMedalla({required this.id});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final medalla = EstadisticasPerfilUsuario.catalogoMedallas[id];
|
||||||
|
if (medalla == null) return const SizedBox.shrink();
|
||||||
|
return Tooltip(
|
||||||
|
message: '${medalla.nombre}: ${medalla.descripcion}',
|
||||||
|
child: Container(
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: TemaApp.colorSuperficie.withValues(alpha: 0.92),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.32),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Image.asset(
|
||||||
|
medalla.assetPath,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
Text(medalla.emoji, style: const TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FuegoAvatarPainter extends CustomPainter {
|
||||||
|
final int fuego;
|
||||||
|
|
||||||
|
const _FuegoAvatarPainter({required this.fuego});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final porcentaje = fuego.clamp(0, 100) / 100;
|
||||||
|
final rect = Offset.zero & size;
|
||||||
|
final centro = rect.center;
|
||||||
|
final radio = math.min(size.width, size.height) / 2 - 3;
|
||||||
|
final base = Paint()
|
||||||
|
..isAntiAlias = true
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..color = Colors.black.withValues(alpha: 0.28);
|
||||||
|
canvas.drawCircle(centro, radio, base);
|
||||||
|
if (porcentaje <= 0) return;
|
||||||
|
|
||||||
|
final fuegoPaint = Paint()
|
||||||
|
..isAntiAlias = true
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..shader = const SweepGradient(
|
||||||
|
colors: [Color(0xFFFFC247), Color(0xFFFF7A1A), Color(0xFFE53935)],
|
||||||
|
).createShader(Rect.fromCircle(center: centro, radius: radio));
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: centro, radius: radio),
|
||||||
|
-math.pi / 2,
|
||||||
|
math.pi * 2 * porcentaje,
|
||||||
|
false,
|
||||||
|
fuegoPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _FuegoAvatarPainter oldDelegate) {
|
||||||
|
return oldDelegate.fuego != fuego;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FondoFaroleroPainter extends CustomPainter {
|
class _FondoFaroleroPainter extends CustomPainter {
|
||||||
final bool intenso;
|
final bool intenso;
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,4 @@ flutter:
|
|||||||
- assets/palabras_fr.json
|
- assets/palabras_fr.json
|
||||||
- assets/words/
|
- assets/words/
|
||||||
- assets/avatars/
|
- assets/avatars/
|
||||||
|
- assets/medals/
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:farolero/modelos/gamificacion_usuario.dart';
|
||||||
|
import 'package:farolero/modelos/usuario.dart';
|
||||||
|
import 'package:farolero/servicios/servicio_perfil_usuario.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('EstadisticasPerfilUsuario', () {
|
||||||
|
test('sube fuego con rendimientos decrecientes y tope diario', () {
|
||||||
|
var stats = const EstadisticasPerfilUsuario();
|
||||||
|
final fecha = DateTime(2026, 5, 9);
|
||||||
|
|
||||||
|
for (var i = 0; i < 20; i++) {
|
||||||
|
stats = stats.registrarPartida(victoria: false, fecha: fecha);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(stats.fuego, 25);
|
||||||
|
expect(stats.subidaFuegoHoy, 25);
|
||||||
|
expect(stats.partidasHoy, 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('permite descansar un dia sin penalizar y penaliza dias extra', () {
|
||||||
|
final inicial = EstadisticasPerfilUsuario(
|
||||||
|
fuego: 50,
|
||||||
|
fechaUltimaPartidaIso: DateTime(2026, 5, 1).toIso8601String(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final unDiaDespues = inicial.registrarPartida(
|
||||||
|
victoria: false,
|
||||||
|
fecha: DateTime(2026, 5, 2),
|
||||||
|
);
|
||||||
|
final tresDiasDespues = inicial.registrarPartida(
|
||||||
|
victoria: false,
|
||||||
|
fecha: DateTime(2026, 5, 4),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(unDiaDespues.fuego, 56);
|
||||||
|
expect(tresDiasDespues.fuego, 48);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calcula medallas principales sin usar llama para partidas jugadas', () {
|
||||||
|
final stats = const EstadisticasPerfilUsuario(
|
||||||
|
partidasJugadas: 10,
|
||||||
|
partidasGanadas: 1,
|
||||||
|
fuego: 20,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stats.medallas, contains('habitual'));
|
||||||
|
expect(stats.medallas, contains('brasa'));
|
||||||
|
expect(stats.medallas, isNot(contains('llama_suave')));
|
||||||
|
expect(stats.medallasPrincipales, contains('habitual'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Usuario serializa fuego y medallas', () {
|
||||||
|
final usuario = Usuario(
|
||||||
|
id: 'u1',
|
||||||
|
nombre: 'León',
|
||||||
|
fuego: 42,
|
||||||
|
medallas: const ['brasa', 'habitual'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final restaurado = Usuario.fromJson(usuario.toJson());
|
||||||
|
|
||||||
|
expect(restaurado.nombre, 'León');
|
||||||
|
expect(restaurado.fuego, 42);
|
||||||
|
expect(restaurado.medallas, ['brasa', 'habitual']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('los avatares de capibara están disponibles en el perfil', () {
|
||||||
|
expect(
|
||||||
|
ServicioPerfilUsuario.avatares,
|
||||||
|
contains('assets/avatars/capybara_01.png'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ServicioPerfilUsuario.avatares,
|
||||||
|
contains('assets/avatars/capybara_12.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||