mnemo_cards/mnemo_cards_backend/lib/statistics/achievement_manager.dart
Dmitry 1ab5071345
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (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
f
2025-12-14 02:35:14 +03:00

191 lines
No EOL
6.2 KiB
Dart

import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.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(
String 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(String 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.dateTime,
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(String userId, String achievementId) async {
return await _db.achievementDao.hasAchievement(userId, achievementId);
}
/// Unlock achievement for user
Future<AchievementDto?> unlockAchievement(
String userId,
String achievementId,
) async {
final definition = AchievementDefinitions.getById(achievementId);
if (definition == null) return null;
final companion = UserAchievementsCompanion.insert(
userId: userId,
achievementId: achievementId,
unlockedAt: Value(PgDateTime(DateTime.now())),
progress: Value(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(String userId) async {
return await _db.achievementDao.getAchievementProgress(userId);
}
/// Check if achievement condition is met
Future<bool> _checkAchievementCondition(
String 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(
String 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;
}
}
}