Wire season flow: map screen, progress save, win recording, next stage

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>
This commit is contained in:
2026-06-11 17:04:45 +09:00
parent 41c18c8bdd
commit 7bc26447f7
16 changed files with 667 additions and 44 deletions
+28
View File
@@ -0,0 +1,28 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/save_repository.dart';
import 'providers.dart';
/// Exposes saved progress as immutable state so screens rebuild when stars
/// land; persistence goes through [SaveRepository].
class ProgressNotifier extends Notifier<Map<String, StageProgress>> {
@override
Map<String, StageProgress> build() =>
ref.read(saveRepositoryProvider).snapshot();
Future<void> record({
required String seasonId,
required String stageId,
required int stars,
required int score,
}) async {
final repo = ref.read(saveRepositoryProvider);
await repo.recordResult(
seasonId: seasonId,
stageId: stageId,
stars: stars,
score: score,
);
state = repo.snapshot();
}
}
+26
View File
@@ -1,7 +1,12 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/content_repository.dart';
import '../data/save_repository.dart';
import '../game/models/season.dart';
import '../services/audio_service.dart';
import 'game_session_notifier.dart';
import 'progress_notifier.dart';
import 'season_flow_notifier.dart';
final gameSessionProvider =
NotifierProvider<GameSessionNotifier, GameViewState?>(
@@ -13,3 +18,24 @@ final audioServiceProvider = Provider<AudioService>((ref) {
ref.onDispose(service.dispose);
return service;
});
/// Overridden with the opened repository in main() (and in tests).
final saveRepositoryProvider = Provider<SaveRepository>(
(ref) => throw UnimplementedError('override with an opened SaveRepository'),
);
final progressProvider =
NotifierProvider<ProgressNotifier, Map<String, StageProgress>>(
ProgressNotifier.new,
);
final seasonFlowProvider = NotifierProvider<SeasonFlowNotifier, SeasonFlow?>(
SeasonFlowNotifier.new,
);
final contentRepositoryProvider =
Provider<ContentRepository>((ref) => ContentRepository());
final seasonsProvider = FutureProvider<List<SeasonPack>>(
(ref) => ref.read(contentRepositoryProvider).availableSeasons(),
);
+44
View File
@@ -0,0 +1,44 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../game/models/season.dart';
import '../game/models/stage.dart';
import 'providers.dart';
class SeasonFlow {
const SeasonFlow({required this.pack, required this.index});
final SeasonPack pack;
final int index;
StageConfig get stage => pack.stages[index];
bool get hasNext => index + 1 < pack.stages.length;
}
/// Orchestrates which season stage is being played: starting stages,
/// recording wins, advancing to the next stage.
class SeasonFlowNotifier extends Notifier<SeasonFlow?> {
@override
SeasonFlow? build() => null;
void startSeasonStage(SeasonPack pack, int index) {
state = SeasonFlow(pack: pack, index: index);
ref.read(gameSessionProvider.notifier).startStage(pack.stages[index]);
}
Future<void> recordWin({required int stars, required int score}) async {
final flow = state;
if (flow == null) return;
await ref.read(progressProvider.notifier).record(
seasonId: flow.pack.seasonId,
stageId: flow.stage.id,
stars: stars,
score: score,
);
}
void nextStage() {
final flow = state;
if (flow == null || !flow.hasNext) return;
startSeasonStage(flow.pack, flow.index + 1);
}
}