From 56e248edfbb10c5b5f14b025c72f6c992a099a11 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 23:52:30 +0200 Subject: [PATCH] draft --- lib/game/view/pinball_game_page.dart | 92 ++++++++-- lib/leaderboard/view/leaderboard_page.dart | 7 +- .../character_selection_dialog.dart | 171 ++++++++++++++++++ lib/select_character/select_character.dart | 2 +- .../widgets/selected_character.dart | 6 +- .../widgets/start_game_listener.dart | 17 +- .../character_selection_dialog_test.dart | 86 +++++++++ 7 files changed, 351 insertions(+), 30 deletions(-) create mode 100644 lib/select_character/character_selection_dialog.dart create mode 100644 test/select_character/character_selection_dialog_test.dart diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 2fd10424..cf145ece 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,8 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball/theme/theme.dart'; import 'package:pinball_audio/pinball_audio.dart'; class PinballGamePage extends StatelessWidget { @@ -44,6 +46,8 @@ class PinballGamePage extends StatelessWidget { ...game.preLoadAssets(), pinballAudio.load(), ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(context), + ...StarAnimation.loadAssets(), ]; return MultiBlocProvider( @@ -113,17 +117,13 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { - final isPlaying = context.select( - (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, - ); - final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; - final screenWidth = MediaQuery.of(context).size.width; - final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); - return StartGameListener( game: game, child: Stack( children: [ + const Positioned.fill( + child: _PinballBackground(), + ), Positioned.fill( child: GameWidget( game: game, @@ -134,22 +134,82 @@ class PinballGameLoadedView extends StatelessWidget { bottom: 20, right: 0, left: 0, - child: PlayButtonOverlay(), + child: _StartGameButton(), ); }, }, ), ), - Positioned( - top: 16, - left: leftMargin, - child: Visibility( - visible: isPlaying, - child: const GameHud(), - ), - ), + const _PinballGameHud(), ], ), ); } } + +class _PinballBackground extends StatelessWidget { + const _PinballBackground({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final characterTheme = context.select( + (CharacterThemeCubit bloc) => bloc.state.characterTheme, + ); + final isStarted = context.select( + (StartGameBloc bloc) => bloc.state.status != StartGameStatus.initial, + ); + + return Visibility( + visible: isStarted, + child: characterTheme.background.image(fit: BoxFit.fill), + ); + } +} + +class _StartGameButton extends StatelessWidget { + const _StartGameButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final isStarted = context.select( + (StartGameBloc bloc) => bloc.state.status != StartGameStatus.initial, + ); + + return isStarted + ? const SizedBox.shrink() + : PinballButton( + child: Text( + l10n.start, + style: AppTextStyle.headline3, + ), + onPressed: () { + context.read().add(const PlayTapped()); + }, + ); + } +} + +class _PinballGameHud extends StatelessWidget { + const _PinballGameHud({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isPlaying = context.select( + (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, + ); + + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + final screenWidth = MediaQuery.of(context).size.width; + final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); + + return Positioned( + top: 16, + left: leftMargin, + child: Visibility( + visible: isPlaying, + child: const GameHud(), + ), + ); + } +} diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart index b9866111..b13c18a5 100644 --- a/lib/leaderboard/view/leaderboard_page.dart +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; class LeaderboardPage extends StatelessWidget { @@ -68,9 +68,8 @@ class LeaderboardView extends StatelessWidget { ), const SizedBox(height: 20), TextButton( - onPressed: () => Navigator.of(context).push( - CharacterSelectionDialog.route(), - ), + onPressed: () => + context.read().add(const PlayTapped()), child: Text(l10n.retry), ), ], diff --git a/lib/select_character/character_selection_dialog.dart b/lib/select_character/character_selection_dialog.dart new file mode 100644 index 00000000..383b204a --- /dev/null +++ b/lib/select_character/character_selection_dialog.dart @@ -0,0 +1,171 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_theme/pinball_theme.dart' hide Assets; +import 'package:pinball_ui/pinball_ui.dart'; + +class CharacterSelectionDialog extends StatelessWidget { + const CharacterSelectionDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const CharacterSelectionView(); + } +} + +class CharacterSelectionView extends StatelessWidget { + const CharacterSelectionView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return PixelatedDecoration( + header: const _CharacterSelectionTitle(), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + SizedBox(height: 20), + Expanded(child: _CharacterSelectionBody()), + _SelectCharacter(), + SizedBox(height: 20), + ], + ), + ); + } +} + +class _CharacterSelectionTitle extends StatelessWidget { + const _CharacterSelectionTitle({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.characterSelectionTitle, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: AppTextStyle.headline2.copyWith( + fontWeight: FontWeight.bold, + color: AppColors.darkBlue, + ), + ), + Text( + l10n.characterSelectionSubtitle, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: AppTextStyle.headline3.copyWith( + color: AppColors.darkBlue, + ), + ), + ], + ); + } +} + +class _CharacterSelectionBody extends StatelessWidget { + const _CharacterSelectionBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(horizontal: 32); + + return Row( + children: [ + const Expanded( + child: Padding( + padding: padding, + child: Center(child: SelectedCharacter()), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StarAnimation.starA(), + const Padding( + padding: padding, + child: _CharacterSelection(), + ), + ], + ), + ), + ], + ); + } +} + +class _CharacterSelection extends StatelessWidget { + const _CharacterSelection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GridView.count( + shrinkWrap: true, + crossAxisCount: 2, + children: const [ + CharacterIcon( + DashTheme(), + key: Key('characterSelectionPage_dashButton'), + ), + CharacterIcon( + SparkyTheme(), + key: Key('characterSelectionPage_sparkyButton'), + ), + CharacterIcon( + AndroidTheme(), + key: Key('characterSelectionPage_androidButton'), + ), + CharacterIcon( + DinoTheme(), + key: Key('characterSelectionPage_dinoButton'), + ), + ], + ); + } +} + +class _SelectCharacter extends StatelessWidget { + const _SelectCharacter({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const Spacer(flex: 5), + const _SelectCharacterButton(), + const Spacer(flex: 2), + StarAnimation.starA(), + const Spacer(flex: 2), + ], + ); + } +} + +class _SelectCharacterButton extends StatelessWidget { + const _SelectCharacterButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return PinballButton( + child: Text( + l10n.select, + style: AppTextStyle.headline5, + ), + onPressed: () { + context.read().add(const CharacterSelected()); + Navigator.of(context).pop(); + }, + ); + } +} diff --git a/lib/select_character/select_character.dart b/lib/select_character/select_character.dart index 827be100..8eb4c1ed 100644 --- a/lib/select_character/select_character.dart +++ b/lib/select_character/select_character.dart @@ -1,3 +1,3 @@ +export 'character_selection_dialog.dart'; export 'cubit/character_theme_cubit.dart'; -export 'view/view.dart'; export 'widgets/widgets.dart'; diff --git a/lib/select_character/widgets/selected_character.dart b/lib/select_character/widgets/selected_character.dart index b668c21e..72dffd6c 100644 --- a/lib/select_character/widgets/selected_character.dart +++ b/lib/select_character/widgets/selected_character.dart @@ -24,7 +24,7 @@ class SelectedCharacter extends StatefulWidget { State createState() => _SelectedCharacterState(); /// Returns a list of assets to be loaded. - static List loadAssets() { + static List loadAssets(BuildContext context) { Flame.images.prefix = ''; const dashTheme = DashTheme(); @@ -41,6 +41,10 @@ class SelectedCharacter extends StatefulWidget { Flame.images.load(androidTheme.background.keyName), Flame.images.load(dinoTheme.background.keyName), Flame.images.load(sparkyTheme.background.keyName), + precacheImage(AssetImage(dashTheme.background.keyName), context), + precacheImage(AssetImage(androidTheme.background.keyName), context), + precacheImage(AssetImage(dinoTheme.background.keyName), context), + precacheImage(AssetImage(sparkyTheme.background.keyName), context), ]; } } diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index fd3097cd..da8a0b5a 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -27,13 +27,19 @@ class StartGameListener extends StatelessWidget { case StartGameStatus.initial: break; case StartGameStatus.selectCharacter: - _onSelectCharacter(context); + _game.gameFlowController.start(); + // We need to add a delay between starting the game and showing + // the dialog. + Future.delayed( + const Duration(milliseconds: 1300), + () => _onSelectCharacter(context), + ); + break; case StartGameStatus.howToPlay: _onHowToPlay(context); break; case StartGameStatus.play: - _game.gameFlowController.start(); break; } }, @@ -55,12 +61,7 @@ Future _onHowToPlay(BuildContext context) async { context: context, child: HowToPlayDialog( onDismissCallback: () { - // We need to add a delay between closing the dialog and starting the - // game. - Future.delayed( - kThemeAnimationDuration, - () => context.read().add(const HowToPlayFinished()), - ); + context.read().add(const HowToPlayFinished()); }, ), ); diff --git a/test/select_character/character_selection_dialog_test.dart b/test/select_character/character_selection_dialog_test.dart new file mode 100644 index 00000000..a0ada832 --- /dev/null +++ b/test/select_character/character_selection_dialog_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +import '../helpers/helpers.dart'; + +void main() { + late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; + + setUp(() { + characterThemeCubit = MockCharacterThemeCubit(); + startGameBloc = MockStartGameBloc(); + + whenListen( + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), + ); + }); + + group('CharacterSelectionPage', () { + testWidgets('renders CharacterSelectionView', (tester) async { + await tester.pumpApp( + CharacterSelectionDialog(), + characterThemeCubit: characterThemeCubit, + ); + expect(find.byType(CharacterSelectionView), findsOneWidget); + }); + }); + + group('CharacterSelectionView', () { + testWidgets('renders correctly', (tester) async { + const titleText = 'Choose your character!'; + await tester.pumpApp( + CharacterSelectionView(), + characterThemeCubit: characterThemeCubit, + ); + + expect(find.text(titleText), findsOneWidget); + expect(find.byType(SelectedCharacter), findsOneWidget); + expect(find.byType(TextButton), findsOneWidget); + }); + + testWidgets('calls characterSelected when a character image is tapped', + (tester) async { + const sparkyButtonKey = Key('characterSelectionPage_sparkyButton'); + + await tester.pumpApp( + CharacterSelectionView(), + characterThemeCubit: characterThemeCubit, + ); + + await tester.tap(find.byKey(sparkyButtonKey)); + + verify(() => characterThemeCubit.characterSelected(SparkyTheme())) + .called(1); + }); + + testWidgets('adds CharacterSelected event when start is tapped', + (tester) async { + whenListen( + startGameBloc, + Stream.value(const StartGameState.initial()), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + CharacterSelectionView(), + characterThemeCubit: characterThemeCubit, + startGameBloc: startGameBloc, + ); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + + verify(() => startGameBloc.add(CharacterSelected())).called(1); + }); + }); +}