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 medallas; const ResumenGamificacionUsuario({ required this.fuego, required this.medallas, }); Map toJson() => { 'fuego': fuego, 'medallas': medallas, }; factory ResumenGamificacionUsuario.fromJson(Map json) { return ResumenGamificacionUsuario( fuego: (json['fuego'] as num?)?.toInt() ?? 0, medallas: (json['medallas'] as List? ?? const []) .map((valor) => valor.toString()) .toList(), ); } } class ProgresoGamificacionUsuario { final EstadisticasPerfilUsuario antes; final EstadisticasPerfilUsuario despues; final List 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 = { '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 get medallas { final resultado = []; 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 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 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 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, ); } }