import 'dart:convert'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../game/models/level.dart'; class LevelService { static const String _progressKey = 'level_progress'; static LevelService? _instance; static LevelService get instance => _instance ??= LevelService._(); LevelService._(); List? _levels; Map? _progress; /// Load all levels from JSON configuration Future> loadLevels() async { if (_levels != null) return _levels!; try { final String jsonString = await rootBundle.loadString('assets/levels/levels.json'); final Map jsonData = json.decode(jsonString); _levels = (jsonData['levels'] as List) .map((levelJson) => Level.fromJson(levelJson)) .toList(); return _levels!; } catch (e) { throw Exception('Failed to load levels: $e'); } } /// Get a specific level by ID Future getLevel(int levelId) async { final levels = await loadLevels(); try { return levels.firstWhere((level) => level.id == levelId); } catch (e) { return null; } } /// Load level progress from persistent storage Future> loadProgress() async { if (_progress != null) return _progress!; try { final prefs = await SharedPreferences.getInstance(); final String? progressJson = prefs.getString(_progressKey); if (progressJson != null) { final Map progressData = json.decode(progressJson); _progress = progressData.map( (key, value) => MapEntry( int.parse(key), LevelProgress.fromJson(value), ), ); } else { // Initialize default progress - level 1 is unlocked _progress = {1: const LevelProgress(levelId: 1, unlocked: true)}; await saveProgress(); } return _progress!; } catch (e) { // Fallback to default progress _progress = {1: const LevelProgress(levelId: 1, unlocked: true)}; return _progress!; } } /// Save level progress to persistent storage Future saveProgress() async { if (_progress == null) return; try { final prefs = await SharedPreferences.getInstance(); final Map progressData = _progress!.map( (key, value) => MapEntry(key.toString(), value.toJson()), ); await prefs.setString(_progressKey, json.encode(progressData)); } catch (e) { throw Exception('Failed to save progress: $e'); } } /// Get progress for a specific level Future getLevelProgress(int levelId) async { final progress = await loadProgress(); return progress[levelId] ?? LevelProgress(levelId: levelId); } /// Update progress for a specific level Future updateLevelProgress(LevelProgress newProgress) async { final progress = await loadProgress(); _progress![newProgress.levelId] = newProgress; // Auto-unlock next level if current level is completed if (newProgress.completed) { final nextLevelId = newProgress.levelId + 1; final levels = await loadLevels(); // Check if next level exists if (levels.any((level) => level.id == nextLevelId)) { final nextProgress = progress[nextLevelId] ?? LevelProgress(levelId: nextLevelId); if (!nextProgress.unlocked) { _progress![nextLevelId] = nextProgress.copyWith(unlocked: true); } } } await saveProgress(); } /// Get all levels with their current progress Future> getLevelsWithProgress() async { final levels = await loadLevels(); final progress = await loadProgress(); return levels.map((level) { final levelProgress = progress[level.id] ?? LevelProgress(levelId: level.id); return LevelWithProgress(level: level, progress: levelProgress); }).toList(); } /// Calculate stars earned based on level performance int calculateStars(Level level, int score, int movesUsed, int timeUsed, Map gemsCleared) { final criteria = level.starRating.criteria; final thresholds = level.starRating.thresholds; int value; switch (criteria) { case StarCriteria.SCORE: value = score; break; case StarCriteria.MOVES_REMAINING: value = (level.constraints.maxMoves ?? 0) - movesUsed; break; case StarCriteria.TIME_REMAINING: value = (level.constraints.timeLimit ?? 0) - timeUsed; break; case StarCriteria.EFFICIENCY: // For efficiency, we use score but consider objectives completion value = score; // Bonus for completing objectives if (_areObjectivesCompleted(level.objectives, gemsCleared)) { value += 500; // Bonus points for completing objectives } break; case StarCriteria.COMBINED: // Combined scoring considers multiple factors value = score; if (level.constraints.hasMoveLimit) { final movesRemaining = (level.constraints.maxMoves ?? 0) - movesUsed; value += movesRemaining * 50; // Bonus for efficient moves } if (_areObjectivesCompleted(level.objectives, gemsCleared)) { value += 500; // Bonus for completing objectives } break; } if (value >= thresholds.threeStar) return 3; if (value >= thresholds.twoStar) return 2; if (value >= thresholds.oneStar) return 1; return 0; } /// Check if level objectives are completed bool _areObjectivesCompleted( LevelObjectives objectives, Map gemsCleared) { // If there are no objectives, they are considered completed if (!objectives.hasGemTypeObjectives) { return true; } for (final entry in objectives.clearGemTypes.entries) { final requiredCount = entry.value; final clearedCount = gemsCleared[entry.key] ?? 0; if (clearedCount < requiredCount) { return false; } } return true; } /// Check if level is completed (all constraints and objectives met) bool isLevelCompleted(Level level, int score, int moves, int timeUsed, Map gemsCleared) { // Check score constraint if (level.constraints.hasScoreTarget && score < level.constraints.targetScore!) { return false; } // Check move constraint if (level.constraints.hasMoveLimit && moves > level.constraints.maxMoves!) { return false; } // Check time constraint if (level.constraints.hasTimeLimit && timeUsed > level.constraints.timeLimit!) { return false; } // Check objectives if (!_areObjectivesCompleted(level.objectives, gemsCleared)) { return false; } return true; } /// Check if level is failed (constraints violated but not completed) bool isLevelFailed(Level level, int score, int moves, int timeUsed, Map gemsCleared) { // Failed if move limit exceeded and not completed if (level.constraints.hasMoveLimit && moves >= level.constraints.maxMoves!) { return !isLevelCompleted(level, score, moves, timeUsed, gemsCleared); } // Failed if time limit exceeded and not completed if (level.constraints.hasTimeLimit && timeUsed >= level.constraints.timeLimit!) { return !isLevelCompleted(level, score, moves, timeUsed, gemsCleared); } return false; } /// Reset all progress (for testing or new game) Future resetProgress() async { _progress = {1: const LevelProgress(levelId: 1, unlocked: true)}; await saveProgress(); } } /// Helper class to combine level and progress data class LevelWithProgress { final Level level; final LevelProgress progress; const LevelWithProgress({ required this.level, required this.progress, }); bool get isUnlocked => progress.unlocked || level.unlocked; bool get isCompleted => progress.completed; int get stars => progress.stars; int get bestScore => progress.bestScore; }