fix: journey map season-complete styling, scroll callback guard, palette constant

- Guard addPostFrameCallback with !_autoScrolled so it only fires once per
  widget lifetime instead of on every LayoutBuilder rebuild.
- Derive seasonComplete flag; pass it into _node so the last stage uses
  gold "done" styling (not "current/next") when the season is fully 3-starred.
- Extract const Color(0xFF232B4A) to GamePalette.lockedNode.
- Remove warnIfMissed: false from tap call in season_map_screen_test; ensureVisible
  already guarantees hit-testing succeeds (confirmed: test still passes cleanly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:30:22 +09:00
parent 78eb5c0639
commit 2b44dcd812
3 changed files with 13 additions and 10 deletions
+11 -6
View File
@@ -65,6 +65,8 @@ class _JourneyMapState extends ConsumerState<_JourneyMap> {
final ids = [for (final stage in pack.stages) stage.id]; final ids = [for (final stage in pack.stages) stage.id];
final unlocked = repo.highestUnlockedIndex(pack.seasonId, ids); final unlocked = repo.highestUnlockedIndex(pack.seasonId, ids);
final totalStars = repo.totalStars(pack.seasonId); final totalStars = repo.totalStars(pack.seasonId);
final seasonComplete = totalStars == pack.stages.length * 3 &&
pack.stages.isNotEmpty;
final locale = Localizations.localeOf(context).languageCode; final locale = Localizations.localeOf(context).languageCode;
final colors = ThemeColors(pack.theme); final colors = ThemeColors(pack.theme);
@@ -78,9 +80,11 @@ class _JourneyMapState extends ConsumerState<_JourneyMap> {
builder: (context, constraints) { builder: (context, constraints) {
final layout = MapLayout(width: constraints.maxWidth); final layout = MapLayout(width: constraints.maxWidth);
final count = pack.stages.length; final count = pack.stages.length;
WidgetsBinding.instance.addPostFrameCallback((_) => if (!_autoScrolled) {
_autoScrollTo( WidgetsBinding.instance.addPostFrameCallback((_) =>
layout, unlocked, count, constraints.maxHeight)); _autoScrollTo(
layout, unlocked, count, constraints.maxHeight));
}
return SingleChildScrollView( return SingleChildScrollView(
controller: _scroll, controller: _scroll,
reverse: true, reverse: true,
@@ -104,6 +108,7 @@ class _JourneyMapState extends ConsumerState<_JourneyMap> {
unlocked, unlocked,
repo.progressFor(pack.seasonId, ids[i])?.stars ?? 0, repo.progressFor(pack.seasonId, ids[i])?.stars ?? 0,
colors, colors,
seasonComplete,
), ),
], ],
), ),
@@ -166,9 +171,9 @@ class _JourneyMapState extends ConsumerState<_JourneyMap> {
} }
Widget _node(BuildContext context, MapLayout layout, int i, int count, Widget _node(BuildContext context, MapLayout layout, int i, int count,
int unlocked, int stars, ThemeColors colors) { int unlocked, int stars, ThemeColors colors, bool seasonComplete) {
final center = layout.nodeCenter(i, count); final center = layout.nodeCenter(i, count);
final isCurrent = i == unlocked; final isCurrent = i == unlocked && !seasonComplete;
final isUnlocked = i <= unlocked; final isUnlocked = i <= unlocked;
final size = isCurrent ? 64.0 : 52.0; final size = isCurrent ? 64.0 : 52.0;
@@ -213,7 +218,7 @@ class _JourneyMapState extends ConsumerState<_JourneyMap> {
], ],
) )
: null, : null,
color: isUnlocked ? null : const Color(0xFF232B4A), color: isUnlocked ? null : GamePalette.lockedNode,
boxShadow: isCurrent boxShadow: isCurrent
? [ ? [
BoxShadow( BoxShadow(
+1
View File
@@ -22,6 +22,7 @@ class GamePalette {
static Color tile(int colorId) => tileColors[colorId % tileColors.length]; static Color tile(int colorId) => tileColors[colorId % tileColors.length];
static const lockedNode = Color(0xFF232B4A);
static const gem = Color(0xFF7CF5FF); static const gem = Color(0xFF7CF5FF);
static const ghostLegal = Color(0x66FFFFFF); static const ghostLegal = Color(0x66FFFFFF);
static const ghostIllegal = Color(0x55FF5252); static const ghostIllegal = Color(0x55FF5252);
+1 -4
View File
@@ -89,10 +89,7 @@ void main() {
// Stage 1 is starred, so stage 2 (index 1) is unlocked and playable. // Stage 1 is starred, so stage 2 (index 1) is unlocked and playable.
// Ensure the node is visible before tapping. // Ensure the node is visible before tapping.
await tester.ensureVisible(find.byKey(const Key('stage_node_1'))); await tester.ensureVisible(find.byKey(const Key('stage_node_1')));
await tester.tap( await tester.tap(find.byKey(const Key('stage_node_1')));
find.byKey(const Key('stage_node_1')),
warnIfMissed: false,
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(GameScreen), findsOneWidget); expect(find.byType(GameScreen), findsOneWidget);