mnemo_cards/mnemo_cards_backend/lib/statistics/achievement_manager.dart

214 lines
6.4 KiB
Dart
Raw Normal View History

2025-12-13 20:55:50 +00:00
import 'package:drift/drift.dart';
2025-12-13 23:35:14 +00:00
import 'package:drift_postgres/drift_postgres.dart';
2025-11-16 11:25:27 +00:00
import 'package:injectable/injectable.dart';
2025-12-13 13:27:05 +00:00
import 'package:mnemo_cards_backend/database/database.dart';
2025-11-16 11:25:27 +00:00
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
@lazySingleton
class AchievementManager {
2025-12-13 13:27:05 +00:00
final AppDatabase _db;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
AchievementManager(this._db);
2025-11-16 11:25:27 +00:00
/// Get all available achievements with their definitions
2025-12-20 18:26:15 +00:00
List<AchievementDto> get allAchievementDefinitions =>
AchievementDefinitions.allAchievements;
2025-11-16 11:25:27 +00:00
/// Check and update achievements for a user
Future<List<AchievementDto>> checkAndUnlockAchievements(
2025-12-13 20:55:50 +00:00
String userId,
2025-11-16 11:25:27 +00:00
UserDataModel userData,
) async {
2025-12-13 13:27:05 +00:00
final unlockedAchievements = <AchievementDto>[];
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
// Get existing user achievements
final userAchievements = await getUserAchievements(userId);
final existingIds = userAchievements.map((a) => a.id).toSet();
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
// Check each achievement
for (final achievement in AchievementDefinitions.allAchievements) {
if (existingIds.contains(achievement.id)) continue;
2025-11-16 11:25:27 +00:00
2025-12-20 18:26:15 +00:00
final isUnlocked = await _checkAchievementCondition(
userId,
userData,
achievement,
);
2025-12-13 13:27:05 +00:00
if (isUnlocked) {
final unlocked = await unlockAchievement(userId, achievement.id);
if (unlocked != null) {
unlockedAchievements.add(unlocked);
}
} else {
// Update progress if applicable
2025-12-20 18:26:15 +00:00
final progress = await _calculateAchievementProgress(
userId,
userData,
achievement,
);
2025-12-13 13:27:05 +00:00
if (progress > 0.0) {
2025-12-20 18:26:15 +00:00
await _db.achievementDao.updateAchievementProgress(
userId,
achievement.id,
progress,
);
2025-11-16 11:25:27 +00:00
}
}
}
2025-12-13 13:27:05 +00:00
return unlockedAchievements;
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Get achievements for a user
2025-12-13 20:55:50 +00:00
Future<List<AchievementDto>> getUserAchievements(String userId) async {
2025-12-20 18:26:15 +00:00
final userAchievements = await _db.achievementDao.getUserAchievements(
userId,
);
2025-12-13 13:27:05 +00:00
final progressMap = await _db.achievementDao.getAchievementProgress(userId);
final achievements = <AchievementDto>[];
for (final userAchievement in userAchievements) {
2025-12-20 18:26:15 +00:00
final definition = AchievementDefinitions.getById(
userAchievement.achievementId,
);
2025-12-13 13:27:05 +00:00
if (definition != null) {
2025-12-20 18:26:15 +00:00
achievements.add(
definition.copyWith(
unlockedAt: userAchievement.unlockedAt.dateTime,
progress: userAchievement.progress,
),
);
2025-12-13 13:27:05 +00:00
}
}
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
// Add locked achievements with progress
for (final definition in AchievementDefinitions.allAchievements) {
if (!achievements.any((a) => a.id == definition.id)) {
final progress = progressMap[definition.id] ?? 0.0;
achievements.add(definition.updateProgress(progress));
2025-11-16 11:25:27 +00:00
}
}
2025-12-13 13:27:05 +00:00
return achievements;
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Check if user has specific achievement
2025-12-13 20:55:50 +00:00
Future<bool> hasAchievement(String userId, String achievementId) async {
2025-12-13 13:27:05 +00:00
return await _db.achievementDao.hasAchievement(userId, achievementId);
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Unlock achievement for user
Future<AchievementDto?> unlockAchievement(
2025-12-13 20:55:50 +00:00
String userId,
2025-11-16 11:25:27 +00:00
String achievementId,
) async {
2025-12-13 13:27:05 +00:00
final definition = AchievementDefinitions.getById(achievementId);
if (definition == null) return null;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
final companion = UserAchievementsCompanion.insert(
userId: userId,
achievementId: achievementId,
2025-12-13 23:35:14 +00:00
unlockedAt: Value(PgDateTime(DateTime.now())),
2025-12-13 20:55:50 +00:00
progress: Value(1.0),
2025-12-13 13:27:05 +00:00
);
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
try {
await _db.achievementDao.unlockAchievement(companion);
return definition.unlock();
} catch (e) {
// Achievement might already exist, return existing
2025-12-20 18:26:15 +00:00
final existing = await _db.achievementDao.getUserAchievement(
userId,
achievementId,
);
2025-12-13 13:27:05 +00:00
return existing != null ? definition.unlock() : null;
}
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Get achievement progress for user
2025-12-13 20:55:50 +00:00
Future<Map<String, double>> getAchievementProgress(String userId) async {
2025-12-13 13:27:05 +00:00
return await _db.achievementDao.getAchievementProgress(userId);
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Check if achievement condition is met
Future<bool> _checkAchievementCondition(
2025-12-13 20:55:50 +00:00
String userId,
2025-11-16 11:25:27 +00:00
UserDataModel userData,
2025-12-13 13:27:05 +00:00
AchievementDto achievement,
2025-11-16 11:25:27 +00:00
) async {
2025-12-13 13:27:05 +00:00
switch (achievement.type) {
2025-12-13 20:55:50 +00:00
// case AchievementType.firstWordLearned:
// return userData.totalCards > 0;
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.firstTestCompleted:
// return userData.totalTests > 0;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
case AchievementType.firstPackCompleted:
final userPacks = await _db.userDao.getUserPacks(userId);
return userPacks.isNotEmpty;
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words10Learned:
// return userData.totalCards >= 10;
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words50Learned:
// return userData.totalCards >= 50;
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words100Learned:
// return userData.totalCards >= 100;
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words500Learned:
// return userData.totalCards >= 500;
2025-11-16 11:25:27 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words1000Learned:
// return userData.totalCards >= 1000;
2025-11-16 11:25:27 +00:00
2025-12-13 14:48:00 +00:00
case AchievementType.streak7Days:
return userData.currentStreak >= 7;
case AchievementType.streak30Days:
return userData.currentStreak >= 30;
case AchievementType.streak100Days:
return userData.currentStreak >= 100;
2025-12-13 20:55:50 +00:00
// case AchievementType.perfectTest:
// return userData.totalTests > 0;
2025-12-13 14:48:00 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.speedLearner:
// return userData.totalCards >= 100 && userData.totalStudyTimeMinutes < 600;
2025-12-13 13:27:05 +00:00
default:
return false;
}
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
/// Calculate progress towards achievement
Future<double> _calculateAchievementProgress(
2025-12-13 20:55:50 +00:00
String userId,
2025-12-13 13:27:05 +00:00
UserDataModel userData,
AchievementDto achievement,
) async {
switch (achievement.type) {
2025-12-13 20:55:50 +00:00
// case AchievementType.words10Learned:
// return (userData.totalCards / 10.0).clamp(0.0, 1.0);
2025-12-13 13:27:05 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words50Learned:
// return (userData.totalCards / 50.0).clamp(0.0, 1.0);
2025-12-13 13:27:05 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words100Learned:
// return (userData.totalCards / 100.0).clamp(0.0, 1.0);
2025-12-13 13:27:05 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words500Learned:
// return (userData.totalCards / 500.0).clamp(0.0, 1.0);
2025-12-13 13:27:05 +00:00
2025-12-13 20:55:50 +00:00
// case AchievementType.words1000Learned:
// return (userData.totalCards / 1000.0).clamp(0.0, 1.0);
2025-12-13 13:27:05 +00:00
default:
return 0.0;
}
2025-11-16 11:25:27 +00:00
}
2025-12-20 18:26:15 +00:00
}