Files
BlockSeasons/lib/data/save_repository.dart
T
2026-06-13 17:56:22 +09:00

162 lines
4.7 KiB
Dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'streak.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,
);
}
final streak = json['streak'] as Map<String, dynamic>?;
if (streak != null) {
_streak = StreakState(
current: streak['current'] as int,
best: streak['best'] as int,
lastYmd: streak['lastYmd'] as String?,
);
}
_tutorialDone =
(json['flags'] as Map<String, dynamic>?)?['tutorialDone'] as bool? ??
false;
_endlessBest =
(json['endless'] as Map<String, dynamic>?)?['best'] as int? ?? 0;
_adsRemoved =
(json['flags'] as Map<String, dynamic>?)?['adsRemoved'] as bool? ??
false;
_soundEnabled =
(json['flags'] as Map<String, dynamic>?)?['soundEnabled'] as bool? ??
true;
}
}
static const _key = 'save_v1';
static Future<SaveRepository> open() async =>
SaveRepository(await SharedPreferences.getInstance());
final SharedPreferences _prefs;
final Map<String, StageProgress> _progress = {};
StreakState _streak = StreakState.initial;
bool _tutorialDone = false;
int _endlessBest = 0;
bool _adsRemoved = false;
bool _soundEnabled = true;
StreakState get streak => _streak;
bool get tutorialDone => _tutorialDone;
int get endlessBest => _endlessBest;
bool get adsRemoved => _adsRemoved;
bool get soundEnabled => _soundEnabled;
Future<void> markTutorialDone() {
_tutorialDone = true;
return _flush();
}
Future<void> setAdsRemoved(bool value) {
_adsRemoved = value;
return _flush();
}
Future<void> setSoundEnabled(bool value) {
_soundEnabled = value;
return _flush();
}
Future<void> recordEndlessScore(int score) {
if (score > _endlessBest) _endlessBest = score;
return _flush();
}
Future<void> saveStreak(StreakState streak) {
_streak = streak;
return _flush();
}
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,
},
},
'streak': {
'current': _streak.current,
'best': _streak.best,
'lastYmd': _streak.lastYmd,
},
'flags': {
'tutorialDone': _tutorialDone,
'adsRemoved': _adsRemoved,
'soundEnabled': _soundEnabled,
},
'endless': {'best': _endlessBest},
}),
);
}