Merge branch 'chore/start-game-listener' into feat/final-select-character-dialog

pull/254/head
arturplaczek 3 years ago
commit 570bc4773a

@ -14,6 +14,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
@ -35,8 +36,11 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio), RepositoryProvider.value(value: _pinballAudio),
], ],
child: BlocProvider( child: MultiBlocProvider(
create: (context) => CharacterThemeCubit(), providers: [
BlocProvider(create: (context) => CharacterThemeCubit()),
BlocProvider(create: (context) => StartGameBloc()),
],
child: const MaterialApp( child: const MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',
localizationsDelegates: [ localizationsDelegates: [

@ -48,7 +48,6 @@ class PinballGamePage extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => GameBloc()),
BlocProvider( BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(), create: (_) => AssetsManagerCubit(loadables)..load(),
@ -114,36 +113,43 @@ class PinballGameLoadedView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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 gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
return Stack( return StartGameListener(
children: [ game: game,
Positioned.fill( child: Stack(
child: GameWidget<PinballGame>( children: [
game: game, Positioned.fill(
initialActiveOverlays: const [PinballGame.playButtonOverlay], child: GameWidget<PinballGame>(
overlayBuilderMap: { game: game,
PinballGame.playButtonOverlay: (context, game) { initialActiveOverlays: const [PinballGame.playButtonOverlay],
return Positioned( overlayBuilderMap: {
bottom: 20, PinballGame.playButtonOverlay: (context, game) {
right: 0, return const Positioned(
left: 0, bottom: 20,
child: PlayButtonOverlay(game: game), right: 0,
); left: 0,
child: PlayButtonOverlay(),
);
},
}, },
}, ),
), ),
), Positioned(
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc top: 16,
// status left: leftMargin,
Positioned( child: Visibility(
top: 16, visible: isPlaying,
left: leftMargin, child: const GameHud(),
child: const GameHud(), ),
), ),
], ],
),
); );
} }
} }

@ -1,20 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart';
/// {@template play_button_overlay} /// {@template play_button_overlay}
/// [Widget] that renders the button responsible to starting the game /// [Widget] that renders the button responsible to starting the game
/// {@endtemplate} /// {@endtemplate}
class PlayButtonOverlay extends StatelessWidget { class PlayButtonOverlay extends StatelessWidget {
/// {@macro play_button_overlay} /// {@macro play_button_overlay}
const PlayButtonOverlay({ const PlayButtonOverlay({Key? key}) : super(key: key);
Key? key,
required PinballGame game,
}) : _game = game,
super(key: key);
final PinballGame _game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,23 +17,7 @@ class PlayButtonOverlay extends StatelessWidget {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
_game.gameFlowController.start(); context.read<StartGameBloc>().add(const PlayTapped());
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) {
// TODO(arturplaczek): remove after merge StarBlocListener
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), child: Text(l10n.play),
), ),

@ -47,19 +47,7 @@ class CharacterSelectionView extends StatelessWidget {
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
// TODO(arturplaczek): remove after merge StarBlocListener context.read<StartGameBloc>().add(const CharacterSelected());
final height = MediaQuery.of(context).size.height * 0.5;
showDialog<void>(
context: context,
builder: (_) => Center(
child: SizedBox(
height: height,
width: height * 1.4,
child: const HowToPlayDialog(),
),
),
);
}, },
child: Text(l10n.start), child: Text(l10n.start),
), ),

@ -1,6 +1,5 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
part 'start_game_event.dart'; part 'start_game_event.dart';
part 'start_game_state.dart'; part 'start_game_state.dart';
@ -10,23 +9,16 @@ part 'start_game_state.dart';
/// {@endtemplate} /// {@endtemplate}
class StartGameBloc extends Bloc<StartGameEvent, StartGameState> { class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
/// {@macro start_game_bloc} /// {@macro start_game_bloc}
StartGameBloc({ StartGameBloc() : super(const StartGameState.initial()) {
required PinballGame game,
}) : _game = game,
super(const StartGameState.initial()) {
on<PlayTapped>(_onPlayTapped); on<PlayTapped>(_onPlayTapped);
on<CharacterSelected>(_onCharacterSelected); on<CharacterSelected>(_onCharacterSelected);
on<HowToPlayFinished>(_onHowToPlayFinished); on<HowToPlayFinished>(_onHowToPlayFinished);
} }
final PinballGame _game;
void _onPlayTapped( void _onPlayTapped(
PlayTapped event, PlayTapped event,
Emitter<StartGameState> emit, Emitter<StartGameState> emit,
) { ) {
_game.gameFlowController.start();
emit( emit(
state.copyWith( state.copyWith(
status: StartGameStatus.selectCharacter, status: StartGameStatus.selectCharacter,

@ -5,22 +5,33 @@ import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
class HowToPlayDialog extends StatelessWidget { class HowToPlayDialog extends StatelessWidget {
const HowToPlayDialog({Key? key}) : super(key: key); const HowToPlayDialog({
Key? key,
required this.onDismissCallback,
}) : super(key: key);
final VoidCallback onDismissCallback;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
const spacing = SizedBox(height: 16); const spacing = SizedBox(height: 16);
return PixelatedDecoration( return WillPopScope(
header: Text(l10n.howToPlay), onWillPop: () {
body: ListView( onDismissCallback.call();
children: const [ return Future.value(true);
spacing, },
_LaunchControls(), child: PixelatedDecoration(
spacing, header: Text(l10n.howToPlay),
_FlipperControls(), body: ListView(
], children: const [
spacing,
_LaunchControls(),
spacing,
_FlipperControls(),
],
),
), ),
); );
} }
@ -66,9 +77,7 @@ class _FlipperControls extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
Column( Column(
children: [ children: [
Row( Wrap(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left),
rowSpacing, rowSpacing,

@ -0,0 +1,90 @@
// 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/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball/theme/theme.dart';
class StartGameListener extends StatelessWidget {
const StartGameListener({
Key? key,
required Widget child,
required PinballGame game,
}) : _child = child,
_game = game,
super(key: key);
final Widget _child;
final PinballGame _game;
@override
Widget build(BuildContext context) {
return BlocListener<StartGameBloc, StartGameState>(
listener: (context, state) {
switch (state.status) {
case StartGameStatus.initial:
break;
case StartGameStatus.selectCharacter:
_onSelectCharacter(context);
break;
case StartGameStatus.howToPlay:
_onHowToPlay(context);
break;
case StartGameStatus.play:
_game.gameFlowController.start();
break;
}
},
child: _child,
);
}
void _onSelectCharacter(BuildContext context) {
_showPinballDialog(
context: context,
child: const CharacterSelectionDialog(),
barrierDismissible: false,
);
}
}
Future<void> _onHowToPlay(BuildContext context) async {
_showPinballDialog(
context: context,
child: HowToPlayDialog(
onDismissCallback: () {
// We need to add a delay between closing the dialog and starting the
// game.
Future.delayed(
kThemeAnimationDuration,
() => context.read<StartGameBloc>().add(const HowToPlayFinished()),
);
},
),
);
}
void _showPinballDialog({
required BuildContext context,
required Widget child,
bool barrierDismissible = true,
}) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
showDialog<void>(
context: context,
barrierColor: AppColors.transparent,
barrierDismissible: barrierDismissible,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth,
width: gameWidgetWidth,
child: child,
),
);
},
);
}

@ -1 +1,2 @@
export 'how_to_play_dialog.dart'; export 'how_to_play_dialog.dart';
export 'start_game_listener.dart';

@ -198,12 +198,11 @@ void main() {
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>), find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget, findsOneWidget,
); );
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc
// status expect(
// expect( find.byType(GameHud),
// find.byType(GameHud), findsNothing,
// findsNothing, );
// );
}); });
testWidgets('renders a hud on play state', (tester) async { testWidgets('renders a hud on play state', (tester) async {

@ -1,46 +1,42 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/bloc/start_game_bloc.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
group('PlayButtonOverlay', () { group('PlayButtonOverlay', () {
late PinballGame game; late StartGameBloc startGameBloc;
late GameFlowController gameFlowController;
setUp(() { setUp(() {
game = MockPinballGame(); startGameBloc = MockStartGameBloc();
gameFlowController = MockGameFlowController();
when(() => game.gameFlowController).thenReturn(gameFlowController); whenListen(
when(gameFlowController.start).thenAnswer((_) {}); startGameBloc,
Stream.value(const StartGameState.initial()),
initialState: const StartGameState.initial(),
);
}); });
testWidgets('renders correctly', (tester) async { testWidgets('renders correctly', (tester) async {
await tester.pumpApp(PlayButtonOverlay(game: game)); await tester.pumpApp(const PlayButtonOverlay());
expect(find.text('Play'), findsOneWidget); expect(find.text('Play'), findsOneWidget);
}); });
testWidgets('calls gameFlowController.start when taped', (tester) async { testWidgets('adds PlayTapped event to StartGameBloc when taped',
await tester.pumpApp(PlayButtonOverlay(game: game));
await tester.tap(find.text('Play'));
await tester.pump();
verify(gameFlowController.start).called(1);
});
testWidgets('displays CharacterSelectionDialog when tapped',
(tester) async { (tester) async {
await tester.pumpApp(PlayButtonOverlay(game: game)); await tester.pumpApp(
const PlayButtonOverlay(),
startGameBloc: startGameBloc,
);
await tester.tap(find.text('Play')); await tester.tap(find.text('Play'));
await tester.pump(); await tester.pump();
expect(find.byType(CharacterSelectionDialog), findsOneWidget); verify(() => startGameBloc.add(const PlayTapped())).called(1);
}); });
}); });
} }

@ -12,9 +12,12 @@ import '../../helpers/helpers.dart';
void main() { void main() {
late CharacterThemeCubit characterThemeCubit; late CharacterThemeCubit characterThemeCubit;
late StartGameBloc startGameBloc;
setUp(() { setUp(() {
characterThemeCubit = MockCharacterThemeCubit(); characterThemeCubit = MockCharacterThemeCubit();
startGameBloc = MockStartGameBloc();
whenListen( whenListen(
characterThemeCubit, characterThemeCubit,
const Stream<CharacterThemeState>.empty(), const Stream<CharacterThemeState>.empty(),
@ -84,17 +87,24 @@ void main() {
.called(1); .called(1);
}); });
testWidgets('displays how to play dialog when start is tapped', testWidgets('adds CharacterSelected event when start is tapped',
(tester) async { (tester) async {
whenListen(
startGameBloc,
Stream.value(const StartGameState.initial()),
initialState: const StartGameState.initial(),
);
await tester.pumpApp( await tester.pumpApp(
CharacterSelectionView(), CharacterSelectionView(),
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
startGameBloc: startGameBloc,
); );
await tester.ensureVisible(find.byType(TextButton)); await tester.ensureVisible(find.byType(TextButton));
await tester.tap(find.byType(TextButton)); await tester.tap(find.byType(TextButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(HowToPlayDialog), findsOneWidget); verify(() => startGameBloc.add(CharacterSelected())).called(1);
}); });
}); });

@ -22,9 +22,7 @@ void main() {
group('StartGameBloc', () { group('StartGameBloc', () {
blocTest<StartGameBloc, StartGameState>( blocTest<StartGameBloc, StartGameState>(
'on PlayTapped changes status to selectCharacter', 'on PlayTapped changes status to selectCharacter',
build: () => StartGameBloc( build: StartGameBloc.new,
game: pinballGame,
),
act: (bloc) => bloc.add(const PlayTapped()), act: (bloc) => bloc.add(const PlayTapped()),
expect: () => [ expect: () => [
const StartGameState( const StartGameState(
@ -35,9 +33,7 @@ void main() {
blocTest<StartGameBloc, StartGameState>( blocTest<StartGameBloc, StartGameState>(
'on CharacterSelected changes status to howToPlay', 'on CharacterSelected changes status to howToPlay',
build: () => StartGameBloc( build: StartGameBloc.new,
game: pinballGame,
),
act: (bloc) => bloc.add(const CharacterSelected()), act: (bloc) => bloc.add(const CharacterSelected()),
expect: () => [ expect: () => [
const StartGameState( const StartGameState(
@ -48,9 +44,7 @@ void main() {
blocTest<StartGameBloc, StartGameState>( blocTest<StartGameBloc, StartGameState>(
'on HowToPlayFinished changes status to play', 'on HowToPlayFinished changes status to play',
build: () => StartGameBloc( build: StartGameBloc.new,
game: pinballGame,
),
act: (bloc) => bloc.add(const HowToPlayFinished()), act: (bloc) => bloc.add(const HowToPlayFinished()),
expect: () => [ expect: () => [
const StartGameState( const StartGameState(

@ -12,7 +12,11 @@ void main() {
testWidgets('displays content', (tester) async { testWidgets('displays content', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en')); final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(HowToPlayDialog()); await tester.pumpApp(
HowToPlayDialog(
onDismissCallback: () {},
),
);
expect(find.text(l10n.launchControls), findsOneWidget); expect(find.text(l10n.launchControls), findsOneWidget);
}); });

@ -0,0 +1,178 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import '../../helpers/helpers.dart';
void main() {
late StartGameBloc startGameBloc;
late PinballGame pinballGame;
group('StartGameListener', () {
setUp(() {
startGameBloc = MockStartGameBloc();
pinballGame = MockPinballGame();
});
testWidgets(
'on selectCharacter status shows SelectCharacter dialog',
(tester) async {
whenListen(
startGameBloc,
Stream.value(
const StartGameState(status: StartGameStatus.selectCharacter),
),
initialState: const StartGameState.initial(),
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
await tester.pumpAndSettle();
expect(
find.byType(CharacterSelectionDialog),
findsOneWidget,
);
},
);
testWidgets(
'on howToPlay status shows HowToPlay dialog',
(tester) async {
whenListen(
startGameBloc,
Stream.value(
const StartGameState(status: StartGameStatus.howToPlay),
),
initialState: const StartGameState.initial(),
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
await tester.pumpAndSettle();
expect(
find.byType(HowToPlayDialog),
findsOneWidget,
);
},
);
testWidgets(
'on play status call start on game controller',
(tester) async {
whenListen(
startGameBloc,
Stream.value(
const StartGameState(status: StartGameStatus.play),
),
initialState: const StartGameState.initial(),
);
final gameController = MockGameFlowController();
when(() => pinballGame.gameFlowController)
.thenAnswer((invocation) => gameController);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
await tester.pumpAndSettle(kThemeAnimationDuration);
await tester.pumpAndSettle(kThemeAnimationDuration);
verify(gameController.start).called(1);
},
);
testWidgets(
'do nothing on initial status',
(tester) async {
whenListen(
startGameBloc,
Stream.value(
const StartGameState(status: StartGameStatus.initial),
),
initialState: const StartGameState.initial(),
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
await tester.pumpAndSettle();
expect(
find.byType(HowToPlayDialog),
findsNothing,
);
expect(
find.byType(CharacterSelectionDialog),
findsNothing,
);
},
);
testWidgets(
'adds HowToPlayFinished event after closing HowToPlayDialog',
(tester) async {
whenListen(
startGameBloc,
Stream.value(
const StartGameState(status: StartGameStatus.howToPlay),
),
initialState: const StartGameState.initial(),
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
await tester.pumpAndSettle();
expect(
find.byType(HowToPlayDialog),
findsOneWidget,
);
await tester.tapAt(const Offset(1, 1));
await tester.pumpAndSettle();
expect(
find.byType(HowToPlayDialog),
findsNothing,
);
await tester.pumpAndSettle();
verify(
() => startGameBloc.add(const HowToPlayFinished()),
).called(1);
},
);
});
}
Loading…
Cancel
Save