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

@ -48,7 +48,6 @@ class PinballGamePage extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()),
BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
@ -114,11 +113,16 @@ 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 Stack(
return StartGameListener(
game: game,
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(
@ -126,24 +130,26 @@ class PinballGameLoadedView extends StatelessWidget {
initialActiveOverlays: const [PinballGame.playButtonOverlay],
overlayBuilderMap: {
PinballGame.playButtonOverlay: (context, game) {
return Positioned(
return const Positioned(
bottom: 20,
right: 0,
left: 0,
child: PlayButtonOverlay(game: game),
child: PlayButtonOverlay(),
);
},
},
),
),
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc
// status
Positioned(
top: 16,
left: leftMargin,
child: Visibility(
visible: isPlaying,
child: const GameHud(),
),
),
],
),
);
}
}

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

@ -47,19 +47,7 @@ class CharacterSelectionView extends StatelessWidget {
TextButton(
onPressed: () {
Navigator.of(context).pop();
// TODO(arturplaczek): remove after merge StarBlocListener
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(),
),
),
);
context.read<StartGameBloc>().add(const CharacterSelected());
},
child: Text(l10n.start),
),

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

@ -5,14 +5,24 @@ import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
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
Widget build(BuildContext context) {
final l10n = context.l10n;
const spacing = SizedBox(height: 16);
return PixelatedDecoration(
return WillPopScope(
onWillPop: () {
onDismissCallback.call();
return Future.value(true);
},
child: PixelatedDecoration(
header: Text(l10n.howToPlay),
body: ListView(
children: const [
@ -22,6 +32,7 @@ class HowToPlayDialog extends StatelessWidget {
_FlipperControls(),
],
),
),
);
}
}
@ -66,9 +77,7 @@ class _FlipperControls extends StatelessWidget {
const SizedBox(height: 10),
Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
Wrap(
children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left),
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 'start_game_listener.dart';

@ -198,12 +198,11 @@ void main() {
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget,
);
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc
// status
// expect(
// find.byType(GameHud),
// findsNothing,
// );
expect(
find.byType(GameHud),
findsNothing,
);
});
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:mocktail/mocktail.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';
void main() {
group('PlayButtonOverlay', () {
late PinballGame game;
late GameFlowController gameFlowController;
late StartGameBloc startGameBloc;
setUp(() {
game = MockPinballGame();
gameFlowController = MockGameFlowController();
startGameBloc = MockStartGameBloc();
when(() => game.gameFlowController).thenReturn(gameFlowController);
when(gameFlowController.start).thenAnswer((_) {});
whenListen(
startGameBloc,
Stream.value(const StartGameState.initial()),
initialState: const StartGameState.initial(),
);
});
testWidgets('renders correctly', (tester) async {
await tester.pumpApp(PlayButtonOverlay(game: game));
await tester.pumpApp(const PlayButtonOverlay());
expect(find.text('Play'), findsOneWidget);
});
testWidgets('calls gameFlowController.start when taped', (tester) async {
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',
testWidgets('adds PlayTapped event to StartGameBloc when taped',
(tester) async {
await tester.pumpApp(PlayButtonOverlay(game: game));
await tester.pumpApp(
const PlayButtonOverlay(),
startGameBloc: startGameBloc,
);
await tester.tap(find.text('Play'));
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() {
late CharacterThemeCubit characterThemeCubit;
late StartGameBloc startGameBloc;
setUp(() {
characterThemeCubit = MockCharacterThemeCubit();
startGameBloc = MockStartGameBloc();
whenListen(
characterThemeCubit,
const Stream<CharacterThemeState>.empty(),
@ -84,17 +87,24 @@ void main() {
.called(1);
});
testWidgets('displays how to play dialog when start is tapped',
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();
expect(find.byType(HowToPlayDialog), findsOneWidget);
verify(() => startGameBloc.add(CharacterSelected())).called(1);
});
});

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

@ -12,7 +12,11 @@ void main() {
testWidgets('displays content', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(HowToPlayDialog());
await tester.pumpApp(
HowToPlayDialog(
onDismissCallback: () {},
),
);
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