mnemo_cards/mnemo_cards_backend/lib/statistics/achievement_manager.dart
Dmitry 306b2fdca7
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
fixes
2025-12-13 17:48:00 +03:00

189 lines
No EOL
6 KiB
Dart

import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
@lazySingleton
class AchievementManager {
final AppDatabase _db;
AchievementManager(this._db);
/// Get all available achievements with their definitions
List<AchievementDto> get allAchievementDefinitions => AchievementDefinitions.allAchievements;
/// Check and update achievements for a user
Future<List<AchievementDto>> checkAndUnlockAchievements(
int userId,
UserDataModel userData,
) async {
final unlockedAchievements = <AchievementDto>[];
// Get existing user achievements
final userAchievements = await getUserAchievements(userId);
final existingIds = userAchievements.map((a) => a.id).toSet();
// Check each achievement
for (final achievement in AchievementDefinitions.allAchievements) {
if (existingIds.contains(achievement.id)) continue;
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);
}
}
}
return unlockedAchievements;
}
/// 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,
));
}
}
// 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));
}
}
return achievements;
}
/// Check if user has specific achievement
Future<bool> hasAchievement(int userId, String achievementId) async {
return await _db.achievementDao.hasAchievement(userId, achievementId);
}
/// Unlock achievement for user
Future<AchievementDto?> unlockAchievement(
int userId,
String achievementId,
) async {
final definition = AchievementDefinitions.getById(achievementId);
if (definition == null) return null;
final companion = UserAchievementsCompanion.insert(
userId: userId,
achievementId: achievementId,
unlockedAt: DateTime.now(),
progress: 1.0,
);
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;
}
}
/// Get achievement progress for user
Future<Map<String, double>> getAchievementProgress(int userId) async {
return await _db.achievementDao.getAchievementProgress(userId);
}
/// Check if achievement condition is met
Future<bool> _checkAchievementCondition(
int userId,
UserDataModel userData,
AchievementDto achievement,
) async {
switch (achievement.type) {
case AchievementType.firstWordLearned:
return userData.totalCards > 0;
case AchievementType.firstTestCompleted:
return userData.totalTests > 0;
case AchievementType.firstPackCompleted:
final userPacks = await _db.userDao.getUserPacks(userId);
return userPacks.isNotEmpty;
case AchievementType.words10Learned:
return userData.totalCards >= 10;
case AchievementType.words50Learned:
return userData.totalCards >= 50;
case AchievementType.words100Learned:
return userData.totalCards >= 100;
case AchievementType.words500Learned:
return userData.totalCards >= 500;
case AchievementType.words1000Learned:
return userData.totalCards >= 1000;
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;
default:
return false;
}
}
/// 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;
}
}
}