diff --git a/assets/images/components/background.png b/assets/images/components/background.png index 023814e2..77a8542c 100644 Binary files a/assets/images/components/background.png and b/assets/images/components/background.png differ diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 521d575e..2780b608 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -11,8 +11,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/landing/landing.dart'; +import 'package:pinball/theme/theme.dart'; import 'package:pinball_audio/pinball_audio.dart'; class App extends StatelessWidget { @@ -34,20 +35,17 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], - child: MaterialApp( - title: 'I/O Pinball', - theme: ThemeData( - appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)), - colorScheme: ColorScheme.fromSwatch( - accentColor: const Color(0xFF13B9FF), - ), + child: BlocProvider( + create: (context) => ThemeCubit(), + child: const MaterialApp( + title: 'I/O Pinball', + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: PinballGamePage(), ), - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - ], - supportedLocales: AppLocalizations.supportedLocales, - home: const LandingPage(), ), ); } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 26a844f8..f28a0bf6 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -5,6 +5,7 @@ import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -28,6 +29,9 @@ class PinballGame extends Forge2DGame /// Identifier of the play button overlay static const playButtonOverlay = 'play_button'; + @override + Color backgroundColor() => Colors.transparent; + final PinballTheme theme; final PinballAudio audio; diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index f6b7ee81..38ae0144 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,45 +5,25 @@ 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/start_game/start_game.dart'; +import 'package:pinball/theme/theme.dart'; import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_theme/pinball_theme.dart'; class PinballGamePage extends StatelessWidget { const PinballGamePage({ Key? key, - required this.theme, - required this.game, + this.isDebugMode = kDebugMode, }) : super(key: key); - final PinballTheme theme; - final PinballGame game; + final bool isDebugMode; static Route route({ - required PinballTheme theme, bool isDebugMode = kDebugMode, }) { return MaterialPageRoute( builder: (context) { - final audio = context.read(); - - final game = isDebugMode - ? DebugPinballGame(theme: theme, audio: audio) - : PinballGame(theme: theme, audio: audio); - - final pinballAudio = context.read(); - final loadables = [ - ...game.preLoadAssets(), - pinballAudio.load(), - ]; - - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => GameBloc()), - BlocProvider( - create: (_) => AssetsManagerCubit(loadables)..load(), - ), - ], - child: PinballGamePage(theme: theme, game: game), + return PinballGamePage( + isDebugMode: isDebugMode, ); }, ); @@ -51,7 +31,29 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - return PinballGameView(game: game); + final theme = context.read().state.theme; + final audio = context.read(); + final pinballAudio = context.read(); + + final game = isDebugMode + ? DebugPinballGame(theme: theme, audio: audio) + : PinballGame(theme: theme, audio: audio); + + final loadables = [ + ...game.preLoadAssets(), + pinballAudio.load(), + ]; + + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => StartGameBloc(game: game)), + BlocProvider(create: (_) => GameBloc()), + BlocProvider( + create: (_) => AssetsManagerCubit(loadables)..load(), + ), + ], + child: PinballGameView(game: game), + ); } } @@ -65,18 +67,51 @@ class PinballGameView extends StatelessWidget { @override Widget build(BuildContext context) { - final loadingProgress = context.watch().state.progress; + final isLoading = context.select( + (AssetsManagerCubit bloc) => bloc.state.progress != 1, + ); - if (loadingProgress != 1) { - return Scaffold( - body: Center( - child: Text( - loadingProgress.toString(), - ), + return Scaffold( + backgroundColor: Colors.blue, + body: isLoading + ? const _PinballGameLoadingView() + : PinballGameLoadedView(game: game), + ); + } +} + +class _PinballGameLoadingView extends StatelessWidget { + const _PinballGameLoadingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final loadingProgress = context.select( + (AssetsManagerCubit bloc) => bloc.state.progress, + ); + + return Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: LinearProgressIndicator( + color: Colors.white, + value: loadingProgress, ), - ); - } + ), + ); + } +} +@visibleForTesting +class PinballGameLoadedView extends StatelessWidget { + const PinballGameLoadedView({ + Key? key, + required this.game, + }) : super(key: key); + + final PinballGame game; + + @override + Widget build(BuildContext context) { return Stack( children: [ Positioned.fill( diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 6f039124..ce5dce4b 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; /// {@template play_button_overlay} /// [Widget] that renders the button responsible to starting the game @@ -18,9 +19,27 @@ class PlayButtonOverlay extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + return Center( child: ElevatedButton( - onPressed: _game.gameFlowController.start, + onPressed: () { + _game.gameFlowController.start(); + showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + final height = MediaQuery.of(context).size.height * 0.5; + + return Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: const CharacterSelectionDialog(), + ), + ); + }, + ); + }, child: Text(l10n.play), ), ); diff --git a/lib/landing/landing.dart b/lib/landing/landing.dart deleted file mode 100644 index b7da30c3..00000000 --- a/lib/landing/landing.dart +++ /dev/null @@ -1 +0,0 @@ -export 'view/landing_page.dart'; diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart index 54b364e9..61e63d75 100644 --- a/lib/leaderboard/view/leaderboard_page.dart +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -69,7 +69,7 @@ class LeaderboardView extends StatelessWidget { const SizedBox(height: 20), TextButton( onPressed: () => Navigator.of(context).push( - CharacterSelectionPage.route(), + CharacterSelectionDialog.route(), ), child: Text(l10n.retry), ), diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart index 7171c66d..1556b533 100644 --- a/lib/start_game/start_game.dart +++ b/lib/start_game/start_game.dart @@ -1 +1,2 @@ export 'bloc/start_game_bloc.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/landing/view/landing_page.dart b/lib/start_game/widgets/how_to_play_dialog.dart similarity index 81% rename from lib/landing/view/landing_page.dart rename to lib/start_game/widgets/how_to_play_dialog.dart index 5b0474b6..aed7a3e3 100644 --- a/lib/landing/view/landing_page.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -2,42 +2,9 @@ import 'package:flutter/material.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; -class LandingPage extends StatelessWidget { - const LandingPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () => Navigator.of(context).push( - CharacterSelectionPage.route(), - ), - child: Text(l10n.play), - ), - TextButton( - onPressed: () => showDialog( - context: context, - builder: (_) => const _HowToPlayDialog(), - ), - child: Text(l10n.howToPlay), - ), - ], - ), - ), - ); - } -} - -class _HowToPlayDialog extends StatelessWidget { - const _HowToPlayDialog({Key? key}) : super(key: key); +class HowToPlayDialog extends StatelessWidget { + const HowToPlayDialog({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/start_game/widgets/widgets.dart b/lib/start_game/widgets/widgets.dart new file mode 100644 index 00000000..bad2c6b5 --- /dev/null +++ b/lib/start_game/widgets/widgets.dart @@ -0,0 +1 @@ +export 'how_to_play_dialog.dart'; diff --git a/lib/theme/view/character_selection_page.dart b/lib/theme/view/character_selection_page.dart index 119a897e..22aaee22 100644 --- a/lib/theme/view/character_selection_page.dart +++ b/lib/theme/view/character_selection_page.dart @@ -2,17 +2,17 @@ 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/start_game/start_game.dart'; import 'package:pinball/theme/theme.dart'; import 'package:pinball_theme/pinball_theme.dart'; -class CharacterSelectionPage extends StatelessWidget { - const CharacterSelectionPage({Key? key}) : super(key: key); +class CharacterSelectionDialog extends StatelessWidget { + const CharacterSelectionDialog({Key? key}) : super(key: key); static Route route() { return MaterialPageRoute( - builder: (_) => const CharacterSelectionPage(), + builder: (_) => const CharacterSelectionDialog(), ); } @@ -46,11 +46,13 @@ class CharacterSelectionView extends StatelessWidget { const _CharacterSelectionGridView(), const SizedBox(height: 20), TextButton( - onPressed: () => Navigator.of(context).push( - PinballGamePage.route( - theme: context.read().state.theme, - ), - ), + onPressed: () { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: (_) => const HowToPlayDialog(), + ); + }, child: Text(l10n.start), ), ], diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 01b5fea6..9fc79b5d 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -7,8 +7,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/app/app.dart'; -import 'package:pinball/landing/landing.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import '../../helpers/mocks.dart'; @@ -21,16 +22,18 @@ void main() { setUp(() { leaderboardRepository = MockLeaderboardRepository(); pinballAudio = MockPinballAudio(); + + when(pinballAudio.load).thenAnswer((_) => Future.value()); }); - testWidgets('renders LandingPage', (tester) async { + testWidgets('renders PinballGamePage', (tester) async { await tester.pumpWidget( App( leaderboardRepository: leaderboardRepository, pinballAudio: pinballAudio, ), ); - expect(find.byType(LandingPage), findsOneWidget); + expect(find.byType(PinballGamePage), findsOneWidget); }); }); } diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 85f9cfc3..59a0ef88 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -5,40 +5,46 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball/theme/theme.dart'; import '../../helpers/helpers.dart'; void main() { - const theme = PinballTheme(characterTheme: DashTheme()); final game = PinballTestGame(); group('PinballGamePage', () { - testWidgets('renders PinballGameView', (tester) async { - final gameBloc = MockGameBloc(); + late ThemeCubit themeCubit; + late GameBloc gameBloc; + + setUp(() { + themeCubit = MockThemeCubit(); + gameBloc = MockGameBloc(); + + whenListen( + themeCubit, + const Stream.empty(), + initialState: const ThemeState.initial(), + ); + whenListen( gameBloc, Stream.value(const GameState.initial()), initialState: const GameState.initial(), ); + }); + testWidgets('renders PinballGameView', (tester) async { await tester.pumpApp( - PinballGamePage(theme: theme, game: game), - gameBloc: gameBloc, + PinballGamePage(), + themeCubit: themeCubit, ); + expect(find.byType(PinballGameView), findsOneWidget); }); testWidgets( 'renders the loading indicator while the assets load', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - final assetsManagerCubit = MockAssetsManagerCubit(); final initialAssetsState = AssetsManagerState( loadables: [Future.value()], @@ -51,27 +57,52 @@ void main() { ); await tester.pumpApp( - PinballGamePage(theme: theme, game: game), - gameBloc: gameBloc, + PinballGameView( + game: game, + ), assetsManagerCubit: assetsManagerCubit, + themeCubit: themeCubit, ); - expect(find.text('0.0'), findsOneWidget); - final loadedAssetsState = AssetsManagerState( - loadables: [Future.value()], - loaded: [Future.value()], - ); - whenListen( - assetsManagerCubit, - Stream.value(loadedAssetsState), - initialState: loadedAssetsState, + expect( + find.byWidgetPredicate( + (widget) => + widget is LinearProgressIndicator && widget.value == 0.0, + ), + findsOneWidget, ); - - await tester.pump(); - expect(find.byType(PinballGameView), findsOneWidget); }, ); + testWidgets( + 'renders PinballGameLoadedView after resources have been loaded', + (tester) async { + final assetsManagerCubit = MockAssetsManagerCubit(); + + final loadedAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: [Future.value()], + ); + whenListen( + assetsManagerCubit, + Stream.value(loadedAssetsState), + initialState: loadedAssetsState, + ); + + await tester.pumpApp( + PinballGameView( + game: game, + ), + assetsManagerCubit: assetsManagerCubit, + themeCubit: themeCubit, + gameBloc: gameBloc, + ); + + await tester.pump(); + + expect(find.byType(PinballGameLoadedView), findsOneWidget); + }); + group('route', () { Future pumpRoute({ required WidgetTester tester, @@ -85,7 +116,6 @@ void main() { onPressed: () { Navigator.of(context).push( PinballGamePage.route( - theme: theme, isDebugMode: isDebugMode, ), ); @@ -95,6 +125,7 @@ void main() { }, ), ), + themeCubit: themeCubit, ); await tester.tap(find.text('Tap me')); diff --git a/test/game/view/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart similarity index 71% rename from test/game/view/play_button_overlay_test.dart rename to test/game/view/widgets/play_button_overlay_test.dart index 020998d4..210cc347 100644 --- a/test/game/view/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,8 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/theme.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { group('PlayButtonOverlay', () { @@ -31,5 +32,15 @@ void main() { verify(gameFlowController.start).called(1); }); + + testWidgets('displays CharacterSelectionDialog when tapped', + (tester) async { + await tester.pumpApp(PlayButtonOverlay(game: game)); + + await tester.tap(find.text('Play')); + await tester.pump(); + + expect(find.byType(CharacterSelectionDialog), findsOneWidget); + }); }); } diff --git a/test/landing/view/landing_page_test.dart b/test/landing/view/landing_page_test.dart deleted file mode 100644 index 369f8cab..00000000 --- a/test/landing/view/landing_page_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/landing/landing.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LandingPage', () { - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - await tester.pumpApp(LandingPage()); - - expect(find.byType(TextButton), findsNWidgets(2)); - expect(find.text(l10n.play), findsOneWidget); - expect(find.text(l10n.howToPlay), findsOneWidget); - }); - - testWidgets('tapping on play button navigates to CharacterSelectionPage', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - - await tester.pumpApp( - LandingPage(), - navigator: navigator, - ); - - await tester.tap(find.widgetWithText(TextButton, l10n.play)); - - verify(() => navigator.push(any())).called(1); - }); - - testWidgets('tapping on how to play button displays dialog with controls', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - await tester.pumpApp(LandingPage()); - - await tester.tap(find.widgetWithText(TextButton, l10n.howToPlay)); - await tester.pump(); - - expect(find.byType(Dialog), findsOneWidget); - }); - }); - - group('KeyIndicator', () { - testWidgets('fromKeyName renders correctly', (tester) async { - const keyName = 'A'; - - await tester.pumpApp( - KeyIndicator.fromKeyName(keyName: keyName), - ); - - expect(find.text(keyName), findsOneWidget); - }); - - testWidgets('fromIcon renders correctly', (tester) async { - const keyIcon = Icons.keyboard_arrow_down; - - await tester.pumpApp( - KeyIndicator.fromIcon(keyIcon: keyIcon), - ); - - expect(find.byIcon(keyIcon), findsOneWidget); - }); - }); -} diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart new file mode 100644 index 00000000..082f102e --- /dev/null +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/start_game/start_game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('HowToPlayDialog', () { + testWidgets('displays dialog', (tester) async { + await tester.pumpApp(HowToPlayDialog()); + + expect(find.byType(Dialog), findsOneWidget); + }); + }); + + group('KeyIndicator', () { + testWidgets('fromKeyName renders correctly', (tester) async { + const keyName = 'A'; + + await tester.pumpApp( + KeyIndicator.fromKeyName(keyName: keyName), + ); + + expect(find.text(keyName), findsOneWidget); + }); + + testWidgets('fromIcon renders correctly', (tester) async { + const keyIcon = Icons.keyboard_arrow_down; + + await tester.pumpApp( + KeyIndicator.fromIcon(keyIcon: keyIcon), + ); + + expect(find.byIcon(keyIcon), findsOneWidget); + }); + }); +} diff --git a/test/theme/view/character_selection_page_test.dart b/test/theme/view/character_selection_page_test.dart index eeac690f..dcf54a13 100644 --- a/test/theme/view/character_selection_page_test.dart +++ b/test/theme/view/character_selection_page_test.dart @@ -4,6 +4,7 @@ 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/start_game/start_game.dart'; import 'package:pinball/theme/theme.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -24,7 +25,7 @@ void main() { group('CharacterSelectionPage', () { testWidgets('renders CharacterSelectionView', (tester) async { await tester.pumpApp( - CharacterSelectionPage(), + CharacterSelectionDialog(), themeCubit: themeCubit, ); expect(find.byType(CharacterSelectionView), findsOneWidget); @@ -38,7 +39,7 @@ void main() { return ElevatedButton( onPressed: () { Navigator.of(context) - .push(CharacterSelectionPage.route()); + .push(CharacterSelectionDialog.route()); }, child: Text('Tap me'), ); @@ -51,7 +52,7 @@ void main() { await tester.tap(find.text('Tap me')); await tester.pumpAndSettle(); - expect(find.byType(CharacterSelectionPage), findsOneWidget); + expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); @@ -82,20 +83,17 @@ void main() { verify(() => themeCubit.characterSelected(SparkyTheme())).called(1); }); - testWidgets('navigates to PinballGamePage when start is tapped', + testWidgets('displays how to play dialog when start is tapped', (tester) async { - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - await tester.pumpApp( CharacterSelectionView(), themeCubit: themeCubit, - navigator: navigator, ); await tester.ensureVisible(find.byType(TextButton)); await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); - verify(() => navigator.push(any())).called(1); + expect(find.byType(HowToPlayDialog), findsOneWidget); }); });