mnemo_cards/mnemo_cards_backend/lib/statistics/achievement_manager.dart

189 lines
6 KiB
Dart
Raw Normal View History

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-13 13:27:05 +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(
int userId,
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-13 13:27:05 +00:00
final isUnlocked = await _checkAchievementCondition(userId, userData, achievement);
if (isUnlocked) {
final unlocked = await unlockAchievement(userId, achievement.id);
if (unlocked != null) {
unlockedAchievements.add(unlocked);
}
} else {
// Update progress if applicable
final progress = await _calculateAchievementProgress(userId, userData, achievement);
if (progress > 0.0) {
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
Future<List<AchievementDto>> getUserAchievements(int userId) async {
final userAchievements = await _db.achievementDao.getUserAchievements(userId);
final progressMap = await _db.achievementDao.getAchievementProgress(userId);
final achievements = <AchievementDto>[];
for (final userAchievement in userAchievements) {
final definition = AchievementDefinitions.getById(userAchievement.achievementId);
if (definition != null) {
achievements.add(definition.copyWith(
unlockedAt: userAchievement.unlockedAt,
progress: userAchievement.progress,
));
}
}
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
Future<bool> hasAchievement(int userId, String achievementId) async {
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-11-16 11:25:27 +00:00
int userId,
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,
unlockedAt: DateTime.now(),
progress: 1.0,
);
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
final existing = await _db.achievementDao.getUserAchievement(userId, achievementId);
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
Future<Map<String, double>> getAchievementProgress(int userId) async {
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(
int 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) {
case AchievementType.firstWordLearned:
return userData.totalCards > 0;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +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 13:27:05 +00:00
case AchievementType.words10Learned:
return userData.totalCards >= 10;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
case AchievementType.words50Learned:
return userData.totalCards >= 50;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
case AchievementType.words100Learned:
return userData.totalCards >= 100;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +00:00
case AchievementType.words500Learned:
return userData.totalCards >= 500;
2025-11-16 11:25:27 +00:00
2025-12-13 13:27:05 +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;
case AchievementType.perfectTest:
return userData.totalTests > 0;
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(
int userId,
UserDataModel userData,
AchievementDto achievement,
) async {
switch (achievement.type) {
case AchievementType.words10Learned:
return (userData.totalCards / 10.0).clamp(0.0, 1.0);
case AchievementType.words50Learned:
return (userData.totalCards / 50.0).clamp(0.0, 1.0);
case AchievementType.words100Learned:
return (userData.totalCards / 100.0).clamp(0.0, 1.0);
case AchievementType.words500Learned:
return (userData.totalCards / 500.0).clamp(0.0, 1.0);
case AchievementType.words1000Learned:
return (userData.totalCards / 1000.0).clamp(0.0, 1.0);
default:
return 0.0;
}
2025-11-16 11:25:27 +00:00
}
2025-12-13 13:27:05 +00:00
}