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:
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user