Gamificación

This commit is contained in:
2026-05-09 17:24:46 +02:00
parent dcecee805b
commit e2cebafdbb
29 changed files with 877 additions and 58 deletions

View File

@@ -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,
);
}
}

View File

@@ -7,6 +7,8 @@ class Usuario {
final String? foto;
final String? creadoPorClienteId;
final String? clienteIdSeleccionado;
final int fuego;
final List<String> medallas;
Usuario({
required this.id,
@@ -16,6 +18,8 @@ class Usuario {
this.foto,
this.creadoPorClienteId,
this.clienteIdSeleccionado,
this.fuego = 0,
this.medallas = const [],
});
bool get estaSeleccionado => clienteIdSeleccionado != null;
@@ -29,6 +33,8 @@ class Usuario {
String? foto,
String? creadoPorClienteId,
String? clienteIdSeleccionado,
int? fuego,
List<String>? medallas,
bool liberarSeleccion = false,
}) {
return Usuario(
@@ -41,6 +47,8 @@ class Usuario {
clienteIdSeleccionado: liberarSeleccion
? null
: (clienteIdSeleccionado ?? this.clienteIdSeleccionado),
fuego: fuego ?? this.fuego,
medallas: medallas ?? this.medallas,
);
}
@@ -53,6 +61,8 @@ class Usuario {
if (creadoPorClienteId != null) 'creadoPorClienteId': creadoPorClienteId,
if (clienteIdSeleccionado != null)
'clienteIdSeleccionado': clienteIdSeleccionado,
if (fuego > 0) 'fuego': fuego,
if (medallas.isNotEmpty) 'medallas': medallas,
};
factory Usuario.fromJson(Map<String, dynamic> json) => Usuario(
@@ -63,5 +73,9 @@ class Usuario {
foto: json['foto'] as String?,
creadoPorClienteId: json['creadoPorClienteId'] 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(),
);
}