Remove legacy GamePlaying states and consolidate all gameplay logic to use the GameLevelPlaying state system. This simplifies state management by eliminating duplicate code paths and ensures consistent behavior across all game modes including Free Play.
289 lines
8.9 KiB
Dart
289 lines
8.9 KiB
Dart
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 const int freePlayLevelId = 0; // Special ID for Free Play
|
|
static LevelService? _instance;
|
|
static LevelService get instance => _instance ??= LevelService._();
|
|
|
|
LevelService._();
|
|
|
|
List<Level>? _levels;
|
|
Map<int, LevelProgress>? _progress;
|
|
|
|
/// Get the hardcoded Free Play level
|
|
Level get freePlayLevel => const Level(
|
|
id: freePlayLevelId,
|
|
name: 'Free Play',
|
|
description: 'Play without constraints - match gems and score points!',
|
|
constraints: LevelConstraints(), // No constraints
|
|
objectives: LevelObjectives(), // No objectives
|
|
starRating: StarRating(
|
|
criteria: StarCriteria.SCORE,
|
|
thresholds: StarThresholds(
|
|
oneStar: 1000,
|
|
twoStar: 5000,
|
|
threeStar: 10000,
|
|
),
|
|
),
|
|
availableGemTypes: [0, 1, 2, 3, 4, 5], // All gem types available
|
|
unlocked: true, // Always unlocked
|
|
);
|
|
|
|
/// Load all levels from JSON configuration
|
|
Future<List<Level>> loadLevels() async {
|
|
if (_levels != null) return _levels!;
|
|
|
|
try {
|
|
final String jsonString =
|
|
await rootBundle.loadString('assets/levels/levels.json');
|
|
final Map<String, dynamic> 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<Level?> getLevel(int levelId) async {
|
|
// Return Free Play level for ID 0
|
|
if (levelId == freePlayLevelId) {
|
|
return freePlayLevel;
|
|
}
|
|
|
|
final levels = await loadLevels();
|
|
try {
|
|
return levels.firstWhere((level) => level.id == levelId);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Load level progress from persistent storage
|
|
Future<Map<int, LevelProgress>> loadProgress() async {
|
|
if (_progress != null) return _progress!;
|
|
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final String? progressJson = prefs.getString(_progressKey);
|
|
|
|
if (progressJson != null) {
|
|
final Map<String, dynamic> 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<void> saveProgress() async {
|
|
if (_progress == null) return;
|
|
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final Map<String, dynamic> 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<LevelProgress> getLevelProgress(int levelId) async {
|
|
final progress = await loadProgress();
|
|
return progress[levelId] ?? LevelProgress(levelId: levelId);
|
|
}
|
|
|
|
/// Update progress for a specific level
|
|
Future<void> 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<List<LevelWithProgress>> 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<int, int> 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<int, int> 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<int, int> gemsCleared) {
|
|
// Check score constraint
|
|
if (level.id == 0 ||
|
|
(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<int, int> 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<void> 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;
|
|
}
|