7bc26447f7
SaveRepository (versioned JSON over prefs) with best-result merging and unlock walking; ContentRepository loads the bundled pack; SeasonFlow/ Progress notifiers orchestrate stage start -> win record -> advance. Season map grid with stars/locks, Home -> Map -> Game navigation, close button, next-stage action on the win overlay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
94 lines
2.8 KiB
Dart
94 lines
2.8 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class StageProgress {
|
|
const StageProgress({required this.stars, required this.bestScore});
|
|
|
|
final int stars;
|
|
final int bestScore;
|
|
}
|
|
|
|
/// Versioned JSON-over-prefs persistence for progress. Total payload stays
|
|
/// tiny (<100 KB), so a key-value blob beats a database here.
|
|
class SaveRepository {
|
|
SaveRepository(this._prefs) {
|
|
final raw = _prefs.getString(_key);
|
|
if (raw != null) {
|
|
final json = jsonDecode(raw) as Map<String, dynamic>;
|
|
final progress = json['progress'] as Map<String, dynamic>? ?? {};
|
|
for (final entry in progress.entries) {
|
|
final value = entry.value as Map<String, dynamic>;
|
|
_progress[entry.key] = StageProgress(
|
|
stars: value['stars'] as int,
|
|
bestScore: value['bestScore'] as int,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
static const _key = 'save_v1';
|
|
|
|
static Future<SaveRepository> open() async =>
|
|
SaveRepository(await SharedPreferences.getInstance());
|
|
|
|
final SharedPreferences _prefs;
|
|
final Map<String, StageProgress> _progress = {};
|
|
|
|
static String _id(String seasonId, String stageId) => '$seasonId/$stageId';
|
|
|
|
/// Immutable copy of all progress, keyed `seasonId/stageId`.
|
|
Map<String, StageProgress> snapshot() => Map.unmodifiable(_progress);
|
|
|
|
StageProgress? progressFor(String seasonId, String stageId) =>
|
|
_progress[_id(seasonId, stageId)];
|
|
|
|
Future<void> recordResult({
|
|
required String seasonId,
|
|
required String stageId,
|
|
required int stars,
|
|
required int score,
|
|
}) async {
|
|
final id = _id(seasonId, stageId);
|
|
final old = _progress[id];
|
|
_progress[id] = StageProgress(
|
|
stars: old == null || stars > old.stars ? stars : old.stars,
|
|
bestScore:
|
|
old == null || score > old.bestScore ? score : old.bestScore,
|
|
);
|
|
await _flush();
|
|
}
|
|
|
|
int totalStars(String seasonId) {
|
|
var total = 0;
|
|
for (final entry in _progress.entries) {
|
|
if (entry.key.startsWith('$seasonId/')) total += entry.value.stars;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// Index of the furthest playable stage: the first without a star, or the
|
|
/// last stage when everything is starred.
|
|
int highestUnlockedIndex(String seasonId, List<String> stageIds) {
|
|
for (var i = 0; i < stageIds.length; i++) {
|
|
final progress = progressFor(seasonId, stageIds[i]);
|
|
if (progress == null || progress.stars == 0) return i;
|
|
}
|
|
return stageIds.length - 1;
|
|
}
|
|
|
|
Future<void> _flush() => _prefs.setString(
|
|
_key,
|
|
jsonEncode({
|
|
'saveVersion': 1,
|
|
'progress': {
|
|
for (final entry in _progress.entries)
|
|
entry.key: {
|
|
'stars': entry.value.stars,
|
|
'bestScore': entry.value.bestScore,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
}
|