import 'dart:async'; import 'dart:developer'; import 'package:injectable/injectable.dart'; import 'package:isar/isar.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; /// Service for managing user achievements /// /// Handles achievement checking, unlocking, and progress tracking. /// Automatically evaluates user progress against achievement requirements. @lazySingleton class AchievementManager { final Isar _isar; AchievementManager(this._isar); /// Get all available achievements with their definitions List get allAchievementDefinitions => _achievementDefinitions; /// Check and update achievements for a user /// /// Evaluates all achievements against current user data and unlocks any /// newly achieved accomplishments. Returns list of newly unlocked achievements. Future> checkAndUnlockAchievements( int userId, UserDataModel userData, ) async { final newlyUnlocked = []; try { // Get current user achievements final currentAchievements = await _getUserAchievements(userId); for (final definition in _achievementDefinitions) { // Skip if already unlocked if (currentAchievements .any((a) => a.id == definition.id && a.isUnlocked)) { continue; } // Check if achievement should be unlocked final shouldUnlock = await _evaluateAchievement(definition, userData); if (shouldUnlock) { final unlockedAchievement = definition.unlock(); newlyUnlocked.add(unlockedAchievement); // Save to database await _saveAchievement(userId, unlockedAchievement); log('Achievement unlocked: ${unlockedAchievement.title} for user $userId'); } } } catch (e, s) { log('Error checking achievements for user $userId: $e\n$s'); } return newlyUnlocked; } /// Get achievement progress for a user /// /// Returns progress (0.0 to 1.0) for achievements that support progress tracking Future> getAchievementProgress( int userId, UserDataModel userData, ) async { final progress = {}; for (final definition in _achievementDefinitions) { if (definition.supportsProgress) { final currentProgress = await _calculateProgress(definition, userData); progress[definition.id] = currentProgress; } } return progress; } /// Get all achievements for a user Future> getUserAchievements(int userId) async { return await _getUserAchievements(userId); } /// Force unlock an achievement for a user (admin function) Future forceUnlockAchievement( int userId, String achievementId, ) async { final definition = _achievementDefinitions.firstWhere( (def) => def.id == achievementId, ); final unlockedAchievement = definition.unlock(); await _saveAchievement(userId, unlockedAchievement); return true; } /// Evaluate if an achievement should be unlocked Future _evaluateAchievement( AchievementDefinition definition, UserDataModel userData, ) async { return await definition.evaluate(userData); } /// Calculate progress for an achievement Future _calculateProgress( AchievementDefinition definition, UserDataModel userData, ) async { return await definition.calculateProgress?.call(userData) ?? 0.0; } /// Get user achievements from database Future> _getUserAchievements(int userId) async { final user = await _isar.userModels.get(userId); if (user?.userData.value == null) return []; return user!.userData.value!.achievements .map((model) => model.toDto()) .toList(); } /// Save achievement to user data Future _saveAchievement(int userId, AchievementDto achievement) async { final user = await _isar.userModels.get(userId); if (user?.userData.value == null) return; final userData = user!.userData.value!; final model = AchievementModel.fromDto(achievement); // Update achievements list final existingAchievements = userData.achievements; final updatedAchievements = [ ...existingAchievements.where((a) => a.id != achievement.id), model, ]; final updatedUserData = userData.copyWith(achievements: updatedAchievements); await _isar.writeTxn(() async { await _isar.userDataModels.put(updatedUserData); }); } /// Achievement definitions static final List _achievementDefinitions = [ // First Steps AchievementDefinition( id: 'first_word', title: 'First Word', description: 'Learn your first word', type: AchievementType.firstWordLearned, evaluate: (data) => Future.value(data.words.isNotEmpty), supportsProgress: false, ), AchievementDefinition( id: 'first_test', title: 'First Test', description: 'Complete your first test', type: AchievementType.firstTestCompleted, evaluate: (data) => Future.value(data.testsStatistics.isNotEmpty), supportsProgress: false, ), AchievementDefinition( id: 'first_pack', title: 'First Pack', description: 'Complete your first pack', type: AchievementType.firstPackCompleted, evaluate: (data) => Future.value(data.packProgress.any((p) => p.progress >= 1.0)), supportsProgress: false, ), // Streak Achievements AchievementDefinition( id: 'streak_3', title: '3-Day Streak', description: 'Study for 3 consecutive days', type: AchievementType.streak3Days, evaluate: (data) => Future.value(data.currentStreak >= 3), supportsProgress: true, calculateProgress: (data) => Future.value((data.currentStreak / 3.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'streak_7', title: 'Week Warrior', description: 'Study for 7 consecutive days', type: AchievementType.streak7Days, evaluate: (data) => Future.value(data.currentStreak >= 7), supportsProgress: true, calculateProgress: (data) => Future.value((data.currentStreak / 7.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'streak_30', title: 'Monthly Master', description: 'Study for 30 consecutive days', type: AchievementType.streak30Days, evaluate: (data) => Future.value(data.currentStreak >= 30), supportsProgress: true, calculateProgress: (data) => Future.value((data.currentStreak / 30.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'streak_100', title: 'Century Champion', description: 'Study for 100 consecutive days', type: AchievementType.streak100Days, evaluate: (data) => Future.value(data.currentStreak >= 100), supportsProgress: true, calculateProgress: (data) => Future.value((data.currentStreak / 100.0).clamp(0.0, 1.0)), ), // Words Mastery AchievementDefinition( id: 'words_10', title: 'Word Explorer', description: 'Learn 10 words', type: AchievementType.words10Learned, evaluate: (data) => Future.value(data.words.length >= 10), supportsProgress: true, calculateProgress: (data) => Future.value((data.words.length / 10.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'words_50', title: 'Vocabulary Builder', description: 'Learn 50 words', type: AchievementType.words50Learned, evaluate: (data) => Future.value(data.words.length >= 50), supportsProgress: true, calculateProgress: (data) => Future.value((data.words.length / 50.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'words_100', title: 'Language Learner', description: 'Learn 100 words', type: AchievementType.words100Learned, evaluate: (data) => Future.value(data.words.length >= 100), supportsProgress: true, calculateProgress: (data) => Future.value((data.words.length / 100.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'words_500', title: 'Word Master', description: 'Learn 500 words', type: AchievementType.words500Learned, evaluate: (data) => Future.value(data.words.length >= 500), supportsProgress: true, calculateProgress: (data) => Future.value((data.words.length / 500.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'words_1000', title: 'Vocabulary Expert', description: 'Learn 1000 words', type: AchievementType.words1000Learned, evaluate: (data) => Future.value(data.words.length >= 1000), supportsProgress: true, calculateProgress: (data) => Future.value((data.words.length / 1000.0).clamp(0.0, 1.0)), ), // Performance Achievements AchievementDefinition( id: 'perfect_test', title: 'Perfect Score', description: 'Complete a test with 100% accuracy', type: AchievementType.perfectTestScore, evaluate: (data) => Future.value( data.testsStatistics.any((test) => test.attempts.any((attempt) { final totalWords = attempt.words.length; final correctWords = attempt.words .where((word) => word.correct > word.incorrect) .length; return totalWords > 0 && correctWords == totalWords; })), ), supportsProgress: false, ), AchievementDefinition( id: 'speed_learner', title: 'Speed Learner', description: 'Complete a pack in less than 24 hours', type: AchievementType.speedLearner, evaluate: (data) => Future.value( data.packProgress.any((pack) => pack.progress >= 1.0 && pack.studyTimeMinutes < 24 * 60 // Less than 24 hours ), ), supportsProgress: false, ), // Dedication Achievements AchievementDefinition( id: 'dedicated_learner', title: 'Dedicated Learner', description: 'Study for 100 hours total', type: AchievementType.dedicatedLearner, evaluate: (data) => Future.value(data.totalStudyTimeMinutes >= 100 * 60), supportsProgress: true, calculateProgress: (data) => Future.value( (data.totalStudyTimeMinutes / (100 * 60)).clamp(0.0, 1.0)), ), // Time-based Achievements AchievementDefinition( id: 'early_bird', title: 'Early Bird', description: 'Study before 6 AM', type: AchievementType.earlyBird, evaluate: (data) => Future.value( data.studyDates.any((date) => date.hour < 6), ), supportsProgress: false, ), AchievementDefinition( id: 'night_owl', title: 'Night Owl', description: 'Study after 10 PM', type: AchievementType.nightOwl, evaluate: (data) => Future.value( data.studyDates.any((date) => date.hour >= 22), ), supportsProgress: false, ), // Special Achievements AchievementDefinition( id: 'consistent_learner', title: 'Consistent Learner', description: 'Study every day for a month', type: AchievementType.consistentLearner, evaluate: (data) => Future.value(data.longestStreak >= 30), supportsProgress: true, calculateProgress: (data) => Future.value((data.longestStreak / 30.0).clamp(0.0, 1.0)), ), AchievementDefinition( id: 'language_master', title: 'Language Master', description: 'Complete 5 packs', type: AchievementType.languageMaster, evaluate: (data) => Future.value( data.packProgress.where((p) => p.progress >= 1.0).length >= 5, ), supportsProgress: true, calculateProgress: (data) => Future.value( (data.packProgress.where((p) => p.progress >= 1.0).length / 5.0) .clamp(0.0, 1.0), ), ), ]; } /// Definition of an achievement with evaluation logic class AchievementDefinition { final String id; final String title; final String description; final AchievementType type; final Future Function(UserDataModel data) evaluate; final bool supportsProgress; final Future Function(UserDataModel data)? calculateProgress; AchievementDefinition({ required this.id, required this.title, required this.description, required this.type, required this.evaluate, this.supportsProgress = false, this.calculateProgress, }); /// Create an unlocked achievement DTO AchievementDto unlock() { return AchievementDto( id: id, title: title, description: description, type: type, unlockedAt: DateTime.now(), progress: 1.0, ); } /// Create a locked achievement DTO with progress AchievementDto withProgress(double progress) { return AchievementDto( id: id, title: title, description: description, type: type, progress: progress.clamp(0.0, 1.0), ); } }