feat: character selection page

pull/20/head
Allison Ryan 4 years ago
parent fd111611ee
commit 159cf4850e

@ -6,29 +6,34 @@
// https://opensource.org/licenses/MIT.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart';
import 'package:pinball/theme/theme.dart';
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'I/O Pinball',
theme: ThemeData(
appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)),
colorScheme: ColorScheme.fromSwatch(
accentColor: const Color(0xFF13B9FF),
return BlocProvider(
create: (_) => ThemeCubit(),
child: MaterialApp(
title: 'I/O Pinball',
theme: ThemeData(
appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)),
colorScheme: ColorScheme.fromSwatch(
accentColor: const Color(0xFF13B9FF),
),
),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: const LandingPage(),
),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: const LandingPage(),
);
}
}

@ -3,20 +3,21 @@ import 'package:flame_forge2d/body_component.dart';
import 'package:flutter/material.dart';
import 'package:forge2d/forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
class Ball extends BodyComponent<PinballGame>
with BlocComponent<GameBloc, GameState> {
with BlocComponent<ThemeCubit, ThemeState> {
Ball({
required Vector2 position,
}) : _position = position {
// TODO(alestiago): Use asset instead of color when provided.
paint = Paint()..color = const Color(0xFFFFFFFF);
}
}) : _position = position;
final Vector2 _position;
@override
Body createBody() {
paint = Paint()
..color = gameRef.read<ThemeCubit>().state.theme.characterTheme.ballColor;
final shape = CircleShape()..radius = 2;
final fixtureDef = FixtureDef(shape)..density = 1;

@ -1,4 +1,15 @@
{
"@@locale": "en",
"play": "Play"
"play": "Play",
"@play": {
"description": "Text displayed on the landing page play button"
},
"start": "Start",
"@start": {
"description": "Text displayed on the character seleciton page start button"
},
"characterSelectionTitle": "Choose your character!",
"@characterSelectionTitle": {
"description": "Title text displayed on the character seleciton page"
}
}

@ -1,4 +1,15 @@
{
"@@locale": "es",
"play": "Jugar"
"play": "Jugar",
"@play": {
"description": "Text displayed on the landing page play button"
},
"start": "Comienzo",
"@start": {
"description": "Text displayed on the character seleciton page start button"
},
"characterSelectionTitle": "¡Elige a tu personaje!",
"@characterSelectionTitle": {
"description": "Title text displayed on the character seleciton page"
}
}

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
class LandingPage extends StatelessWidget {
const LandingPage({Key? key}) : super(key: key);
@ -8,11 +8,12 @@ class LandingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
body: Center(
child: TextButton(
onPressed: () =>
Navigator.of(context).push<void>(PinballGamePage.route()),
Navigator.of(context).push<void>(CharacterSelectionPage.route()),
child: Text(l10n.play),
),
),

@ -1 +1,2 @@
export 'cubit/theme_cubit.dart';
export 'view/view.dart';

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
import 'package:provider/provider.dart';
class CharacterSelectionPage extends StatelessWidget {
const CharacterSelectionPage({Key? key}) : super(key: key);
static Route route() {
return MaterialPageRoute<void>(
builder: (_) => const CharacterSelectionPage(),
);
}
@override
Widget build(BuildContext context) {
return const CharacterSelectionView();
}
}
class CharacterSelectionView extends StatelessWidget {
const CharacterSelectionView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
Text(
l10n.characterSelectionTitle,
style: Theme.of(context).textTheme.headline3,
),
const SizedBox(height: 80),
const _CharacterSelectionGridView(),
const SizedBox(height: 20),
TextButton(
onPressed: () =>
Navigator.of(context).push<void>(PinballGamePage.route()),
child: Text(l10n.start),
),
],
),
),
);
}
}
class _CharacterSelectionGridView extends StatelessWidget {
const _CharacterSelectionGridView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20),
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
mainAxisSpacing: 20,
crossAxisSpacing: 20,
children: const [
_CharacterImageButton(
DashTheme(),
key: Key('characterSelectionPage_dashButton'),
),
_CharacterImageButton(
SparkyTheme(),
key: Key('characterSelectionPage_sparkyButton'),
),
_CharacterImageButton(
AndroidTheme(),
key: Key('characterSelectionPage_androidButton'),
),
_CharacterImageButton(
DinoTheme(),
key: Key('characterSelectionPage_dinoButton'),
),
],
),
);
}
}
class _CharacterImageButton extends StatelessWidget {
const _CharacterImageButton(
this.characterTheme, {
Key? key,
}) : super(key: key);
final CharacterTheme characterTheme;
@override
Widget build(BuildContext context) {
final currentCharacterTheme =
context.select((ThemeCubit cubit) => cubit.state.theme.characterTheme);
return GestureDetector(
onTap: () => context.read<ThemeCubit>().characterSelected(characterTheme),
child: DecoratedBox(
decoration: BoxDecoration(
color: (currentCharacterTheme == characterTheme)
? Colors.blue.withOpacity(0.5)
: null,
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: characterTheme.characterAsset.image(),
),
),
);
}
}

@ -0,0 +1 @@
export 'character_selection_page.dart';

@ -1 +1,4 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml
include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer:
exclude:
- lib/**/*.gen.dart

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

@ -1,4 +1,5 @@
library pinball_theme;
export 'src/generated/generated.dart';
export 'src/pinball_theme.dart';
export 'src/themes/themes.dart';

@ -0,0 +1,71 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
AssetGenImage get android => const AssetGenImage('assets/images/android.png');
AssetGenImage get dash => const AssetGenImage('assets/images/dash.png');
AssetGenImage get dino => const AssetGenImage('assets/images/dino.png');
AssetGenImage get sparky => const AssetGenImage('assets/images/sparky.png');
}
class Assets {
Assets._();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
class AssetGenImage extends AssetImage {
const AssetGenImage(String assetName)
: super(assetName, package: 'pinball_theme');
Image image({
Key? key,
ImageFrameBuilder? frameBuilder,
ImageLoadingBuilder? loadingBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? width,
double? height,
Color? color,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
bool isAntiAlias = false,
FilterQuality filterQuality = FilterQuality.low,
}) {
return Image(
key: key,
image: this,
frameBuilder: frameBuilder,
loadingBuilder: loadingBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
filterQuality: filterQuality,
);
}
String get path => assetName;
}

@ -10,4 +10,7 @@ class AndroidTheme extends CharacterTheme {
@override
Color get ballColor => Colors.green;
@override
AssetGenImage get characterAsset => Assets.images.android;
}

@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template character_theme}
/// Base class for creating character themes.
@ -14,6 +15,9 @@ abstract class CharacterTheme extends Equatable {
/// Ball color for this theme.
Color get ballColor;
/// Asset for the theme character.
AssetGenImage get characterAsset;
@override
List<Object?> get props => [ballColor];
}

@ -10,4 +10,7 @@ class DashTheme extends CharacterTheme {
@override
Color get ballColor => Colors.blue;
@override
AssetGenImage get characterAsset => Assets.images.dash;
}

@ -10,4 +10,7 @@ class DinoTheme extends CharacterTheme {
@override
Color get ballColor => Colors.grey;
@override
AssetGenImage get characterAsset => Assets.images.dino;
}

@ -10,4 +10,7 @@ class SparkyTheme extends CharacterTheme {
@override
Color get ballColor => Colors.orange;
@override
AssetGenImage get characterAsset => Assets.images.sparky;
}

@ -14,4 +14,16 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
very_good_analysis: ^2.4.0
very_good_analysis: ^2.4.0
flutter:
uses-material-design: true
generate: true
assets:
- assets/images/
flutter_gen:
assets:
package_parameter_enabled: true
output: lib/src/generated/
line_length: 80

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(AndroidTheme().ballColor, equals(Colors.green));
});
test('characterAsset is correct', () {
expect(AndroidTheme().characterAsset, equals(Assets.images.android));
});
});
}

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(DashTheme().ballColor, equals(Colors.blue));
});
test('characterAsset is correct', () {
expect(DashTheme().characterAsset, equals(Assets.images.dash));
});
});
}

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(DinoTheme().ballColor, equals(Colors.grey));
});
test('characterAsset is correct', () {
expect(DinoTheme().characterAsset, equals(Assets.images.dino));
});
});
}

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(SparkyTheme().ballColor, equals(Colors.orange));
});
test('characterAsset is correct', () {
expect(SparkyTheme().characterAsset, equals(Assets.images.sparky));
});
});
}

@ -324,6 +324,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
pinball_theme:
dependency: "direct main"
description:
path: "packages/pinball_theme"
relative: true
source: path
version: "1.0.0+1"
pool:
dependency: transitive
description:

@ -6,6 +6,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/theme/cubit/theme_cubit.dart';
import '../../helpers/helpers.dart';
@ -13,11 +14,30 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Ball', () {
final flameTester = FlameTester(PinballGame.new);
final gameBloc = MockGameBloc();
final themeCubit = MockThemeCubit();
setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
});
final tester = flameBlocTester(
gameBloc: gameBloc,
themeCubit: themeCubit,
);
flameTester.test(
tester.widgetTest(
'loads correctly',
(game) async {
(game, tester) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
@ -26,9 +46,9 @@ void main() {
);
group('body', () {
flameTester.test(
tester.widgetTest(
'positions correctly',
(game) async {
(game, tester) async {
final position = Vector2.all(10);
final ball = Ball(position: position);
await game.ensureAdd(ball);
@ -38,9 +58,9 @@ void main() {
},
);
flameTester.test(
tester.widgetTest(
'is dynamic',
(game) async {
(game, tester) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
@ -50,9 +70,9 @@ void main() {
});
group('first fixture', () {
flameTester.test(
tester.widgetTest(
'exists',
(game) async {
(game, tester) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
@ -60,9 +80,9 @@ void main() {
},
);
flameTester.test(
tester.widgetTest(
'is dense',
(game) async {
(game, tester) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
@ -71,9 +91,9 @@ void main() {
},
);
flameTester.test(
tester.widgetTest(
'shape is circular',
(game) async {
(game, tester) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
@ -85,23 +105,6 @@ void main() {
});
group('resetting a ball', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final tester = flameBlocTester(
gameBlocBuilder: () {
return gameBloc;
},
);
tester.widgetTest(
'adds BallLost to GameBloc',
(game, tester) async {

@ -5,6 +5,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
import '../../helpers/helpers.dart';
@ -126,25 +127,30 @@ void main() {
});
group('PlungerAnchorPrismaticJointDef', () {
late GameBloc gameBloc;
late Plunger plunger;
late Anchor anchor;
final gameBloc = MockGameBloc();
final themeCubit = MockThemeCubit();
setUp(() {
gameBloc = MockGameBloc();
plunger = Plunger(position: Vector2.zero());
anchor = Anchor(position: Vector2(0, -1));
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
plunger = Plunger(position: Vector2.zero());
anchor = Anchor(position: Vector2(0, -1));
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
});
final flameTester = flameBlocTester(
gameBlocBuilder: () {
return gameBloc;
},
gameBloc: gameBloc,
themeCubit: themeCubit,
);
flameTester.test(

@ -3,6 +3,7 @@ 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/theme.dart';
import '../../helpers/helpers.dart';
@ -21,6 +22,13 @@ void main() {
});
testWidgets('route returns a valid navigation route', (tester) async {
final themeCubit = MockThemeCubit();
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
await tester.pumpApp(
Scaffold(
body: Builder(
@ -34,6 +42,7 @@ void main() {
},
),
),
themeCubit: themeCubit,
);
await tester.tap(find.text('Tap me'));
@ -67,14 +76,25 @@ void main() {
'renders a game over dialog when the user has lost',
(tester) async {
final gameBloc = MockGameBloc();
const state = GameState(score: 0, balls: 0);
const gameState = GameState(score: 0, balls: 0);
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
final themeCubit = MockThemeCubit();
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
await tester.pumpApp(
const PinballGameView(),
gameBloc: gameBloc,
themeCubit: themeCubit,
);
await tester.pump();
expect(

@ -1,16 +1,27 @@
import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
import 'mocks.dart';
FlameTester<PinballGame> flameBlocTester({
required GameBloc Function() gameBlocBuilder,
GameBloc? gameBloc,
ThemeCubit? themeCubit,
}) {
return FlameTester<PinballGame>(
PinballGame.new,
pumpWidget: (gameWidget, tester) async {
await tester.pumpWidget(
BlocProvider.value(
value: gameBlocBuilder(),
MultiBlocProvider(
providers: [
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
BlocProvider.value(
value: themeCubit ?? MockThemeCubit(),
),
],
child: gameWidget,
),
);

@ -1,6 +1,7 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
class MockPinballGame extends Mock implements PinballGame {}
@ -13,3 +14,5 @@ class MockBall extends Mock implements Ball {}
class MockContact extends Mock implements Contact {}
class MockGameBloc extends Mock implements GameBloc {}
class MockThemeCubit extends Mock implements ThemeCubit {}

@ -12,6 +12,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'helpers.dart';
@ -20,17 +21,25 @@ extension PumpApp on WidgetTester {
Widget widget, {
MockNavigator? navigator,
GameBloc? gameBloc,
ThemeCubit? themeCubit,
}) {
return pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
MultiBlocProvider(
providers: [
BlocProvider.value(
value: themeCubit ?? MockThemeCubit(),
),
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
],
supportedLocales: AppLocalizations.supportedLocales,
home: BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
child: navigator != null
child: MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
),

@ -12,7 +12,7 @@ void main() {
expect(find.byType(TextButton), findsOneWidget);
});
testWidgets('tapping on TextButton navigates to PinballGamePage',
testWidgets('tapping on TextButton navigates to CharacterSelectionPage',
(tester) async {
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});

@ -0,0 +1,101 @@
// 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/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
late ThemeCubit themeCubit;
setUp(() {
themeCubit = MockThemeCubit();
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
});
group('CharacterSelectionPage', () {
testWidgets('renders CharacterSelectionView', (tester) async {
await tester.pumpApp(
const CharacterSelectionPage(),
themeCubit: themeCubit,
);
expect(find.byType(CharacterSelectionView), findsOneWidget);
});
testWidgets('route returns a valid navigation route', (tester) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context)
.push<void>(CharacterSelectionPage.route());
},
child: const Text('Tap me'),
);
},
),
),
themeCubit: themeCubit,
);
await tester.tap(find.text('Tap me'));
await tester.pumpAndSettle();
expect(find.byType(CharacterSelectionPage), findsOneWidget);
});
});
group('CharacterSelectionView', () {
testWidgets('renders correctly', (tester) async {
const titleText = 'Choose your character!';
await tester.pumpApp(
CharacterSelectionView(),
themeCubit: themeCubit,
);
expect(find.text(titleText), findsOneWidget);
expect(find.byType(Image), findsNWidgets(4));
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(),
themeCubit: themeCubit,
);
await tester.tap(find.byKey(sparkyButtonKey));
verify(() => themeCubit.characterSelected(SparkyTheme())).called(1);
});
testWidgets('navigates to PinballGamePage when start is tapped',
(tester) async {
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp(
CharacterSelectionView(),
themeCubit: themeCubit,
navigator: navigator,
);
await tester.ensureVisible(find.byType(TextButton));
await tester.tap(find.byType(TextButton));
verify(() => navigator.push<void>(any())).called(1);
});
});
}
Loading…
Cancel
Save