From cf92856dc1ba7ae3c34e2b3019ce83c13d0f3421 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 24 Mar 2022 14:36:57 -0300 Subject: [PATCH 1/9] feat: implementing composable blueprints (#92) * feat: implementing composable blueprints * fix: coverage --- lib/flame/blueprint.dart | 22 +++++++++++++++++- test/flame/blueprint_test.dart | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/flame/blueprint.dart b/lib/flame/blueprint.dart index d536d650..57af7d6d 100644 --- a/lib/flame/blueprint.dart +++ b/lib/flame/blueprint.dart @@ -14,6 +14,8 @@ const _attachedErrorMessage = "Can't add to attached Blueprints"; /// the [FlameGame] level. abstract class Blueprint { final List _components = []; + final List _blueprints = []; + bool _isAttached = false; /// Called before the the [Component]s managed @@ -25,7 +27,10 @@ abstract class Blueprint { @mustCallSuper Future attach(T game) async { build(game); - await game.addAll(_components); + await Future.wait([ + game.addAll(_components), + ..._blueprints.map(game.addFromBlueprint).toList(), + ]); _isAttached = true; } @@ -41,8 +46,23 @@ abstract class Blueprint { _components.add(component); } + /// Adds a list of [Blueprint]s to this blueprint. + void addAllBlueprints(List blueprints) { + assert(!_isAttached, _attachedErrorMessage); + _blueprints.addAll(blueprints); + } + + /// Adds a single [Blueprint] to this blueprint. + void addBlueprint(Blueprint blueprint) { + assert(!_isAttached, _attachedErrorMessage); + _blueprints.add(blueprint); + } + /// Returns a copy of the components built by this blueprint List get components => List.unmodifiable(_components); + + /// Returns a copy of the children blueprints + List get blueprints => List.unmodifiable(_blueprints); } /// A [Blueprint] that provides additional diff --git a/test/flame/blueprint_test.dart b/test/flame/blueprint_test.dart index 3a9f5ed3..e5fc2c4f 100644 --- a/test/flame/blueprint_test.dart +++ b/test/flame/blueprint_test.dart @@ -14,6 +14,28 @@ class MyBlueprint extends Blueprint { } } +class MyOtherBlueprint extends Blueprint { + @override + void build(_) { + add(Component()); + } +} + +class YetMyOtherBlueprint extends Blueprint { + @override + void build(_) { + add(Component()); + } +} + +class MyComposedBlueprint extends Blueprint { + @override + void build(_) { + addBlueprint(MyBlueprint()); + addAllBlueprints([MyOtherBlueprint(), YetMyOtherBlueprint()]); + } +} + class MyForge2dBlueprint extends Forge2DBlueprint { @override void build(_) { @@ -24,12 +46,23 @@ class MyForge2dBlueprint extends Forge2DBlueprint { void main() { group('Blueprint', () { + setUpAll(() { + registerFallbackValue(MyBlueprint()); + registerFallbackValue(Component()); + }); + test('components can be added to it', () { final blueprint = MyBlueprint()..build(MockPinballGame()); expect(blueprint.components.length, equals(3)); }); + test('blueprints can be added to it', () { + final blueprint = MyComposedBlueprint()..build(MockPinballGame()); + + expect(blueprint.blueprints.length, equals(3)); + }); + test('adds the components to a game on attach', () { final mockGame = MockPinballGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); @@ -38,6 +71,14 @@ void main() { verify(() => mockGame.addAll(any())).called(1); }); + test('adds components from a child Blueprint the to a game on attach', () { + final mockGame = MockPinballGame(); + when(() => mockGame.addAll(any())).thenAnswer((_) async {}); + MyComposedBlueprint().attach(mockGame); + + verify(() => mockGame.addAll(any())).called(4); + }); + test( 'throws assertion error when adding to an already attached blueprint', () async { From ae9e6453ce60e9e08a0c0776d4051c31fe292172 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Thu, 24 Mar 2022 12:52:29 -0500 Subject: [PATCH 2/9] feat: add launch config for sandbox (#95) * feat: launch config for sandbox * refactor: match existing naming --- .vscode/launch.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index b4e33cec..1b855b10 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,6 +29,12 @@ "type": "dart", "program": "lib/main_production.dart", "args": ["--flavor", "production", "--target", "lib/main_production.dart"] + }, + { + "name": "Launch component sandbox", + "request": "launch", + "type": "dart", + "program": "packages/pinball_components/sandbox/lib/main.dart" } ] } From 1f0a0c2f0472e91d5fa4e948f1c811cc2cdc2159 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Thu, 24 Mar 2022 19:02:45 +0100 Subject: [PATCH 3/9] feat: leaderboard screen (#51) * feat: leader board page * feat: strings for leader board ui * feat: navigate to leaderboard from gameover dialog * feat: set character theme for leaderboard * test: test leaderboard page * chore: removed unused var from tests * chore: leaderboard misspelling, doc and minor fixes * chore: doc * chore: api doc * refactor: pass theme to leaderboard widgets * Update lib/leaderboard/view/leaderboard_page.dart Co-authored-by: Alejandro Santiago * chore: removed ios files * refactor: leaderboard screen now uses leaderboard_repository models * test: added tests for extensions * chore: added todo to move model * feat: added navigator helper method * feat: add Flame compatibility to test navigator helper * chore: removed unused import * test: modify test to avoid time out * chore: test method name changed * refactor: changes from pr * refactor: removed themecubit * feat: provide leaderbloc * feat: added leaderbloc to screen * chore: removed unused imports * chore: strings names * fix: fixed test with model changed * refactor: removed multirepositoryprovider * test: remove unnecessary tests * chore: unused variable * chore: unused imports Co-authored-by: Alejandro Santiago --- lib/game/view/pinball_game_page.dart | 2 +- lib/game/view/widgets/game_over_dialog.dart | 25 +- lib/l10n/arb/app_en.arb | 28 ++ lib/leaderboard/leaderboard.dart | 1 + lib/leaderboard/view/leaderboard_page.dart | 306 ++++++++++++++++++ test/app/view/app_test.dart | 3 +- test/game/components/ball_test.dart | 1 + test/game/view/pinball_game_page_test.dart | 5 +- .../view/widgets/game_over_dialog_test.dart | 44 +++ test/helpers/helpers.dart | 1 + test/helpers/mocks.dart | 5 + test/helpers/navigator.dart | 37 +++ test/helpers/pump_app.dart | 39 ++- .../view/leaderboard_page_test.dart | 150 +++++++++ 14 files changed, 620 insertions(+), 27 deletions(-) create mode 100644 lib/leaderboard/view/leaderboard_page.dart create mode 100644 test/game/view/widgets/game_over_dialog_test.dart create mode 100644 test/helpers/navigator.dart create mode 100644 test/leaderboard/view/leaderboard_page_test.dart diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 21bd4074..579d830b 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -70,7 +70,7 @@ class _PinballGameViewState extends State { showDialog( context: context, builder: (_) { - return const GameOverDialog(); + return GameOverDialog(theme: widget.theme.characterTheme); }, ); } diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart index 9d1c61b0..29164a62 100644 --- a/lib/game/view/widgets/game_over_dialog.dart +++ b/lib/game/view/widgets/game_over_dialog.dart @@ -1,21 +1,40 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; +import 'package:pinball_theme/pinball_theme.dart'; /// {@template game_over_dialog} /// [Dialog] displayed when the [PinballGame] is over. /// {@endtemplate} class GameOverDialog extends StatelessWidget { /// {@macro game_over_dialog} - const GameOverDialog({Key? key}) : super(key: key); + const GameOverDialog({Key? key, required this.theme}) : super(key: key); + + /// Current [CharacterTheme] to customize dialog + final CharacterTheme theme; @override Widget build(BuildContext context) { - return const Dialog( + final l10n = context.l10n; + + return Dialog( child: SizedBox( width: 200, height: 200, child: Center( - child: Text('Game Over'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(l10n.gameOver), + TextButton( + onPressed: () => Navigator.of(context).push( + LeaderboardPage.route(theme: theme), + ), + child: Text(l10n.leaderboard), + ), + ], + ), ), ), ); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a118501e..235c8f2e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -23,5 +23,33 @@ "characterSelectionTitle": "Choose your character!", "@characterSelectionTitle": { "description": "Title text displayed on the character selection page" + }, + "gameOver": "Game Over", + "@gameOver": { + "description": "Text displayed on the ending dialog when game finishes" + }, + "leaderboard": "Leaderboard", + "@leaderboard": { + "description": "Text displayed on the ending dialog leaderboard button" + }, + "rank": "Rank", + "@rank": { + "description": "Text displayed on the leaderboard page header rank column" + }, + "character": "Character", + "@character": { + "description": "Text displayed on the leaderboard page header character column" + }, + "username": "Username", + "@username": { + "description": "Text displayed on the leaderboard page header userName column" + }, + "score": "Score", + "@score": { + "description": "Text displayed on the leaderboard page header score column" + }, + "retry": "Retry", + "@retry": { + "description": "Text displayed on the retry button leaders board page" } } \ No newline at end of file diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart index 156b7f78..08765743 100644 --- a/lib/leaderboard/leaderboard.dart +++ b/lib/leaderboard/leaderboard.dart @@ -1,2 +1,3 @@ export 'bloc/leaderboard_bloc.dart'; export 'models/leader_board_entry.dart'; +export 'view/leaderboard_page.dart'; diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart new file mode 100644 index 00000000..54b364e9 --- /dev/null +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -0,0 +1,306 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +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/theme/theme.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +class LeaderboardPage extends StatelessWidget { + const LeaderboardPage({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + static Route route({required CharacterTheme theme}) { + return MaterialPageRoute( + builder: (_) => LeaderboardPage(theme: theme), + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LeaderboardBloc( + context.read(), + )..add(const Top10Fetched()), + child: LeaderboardView(theme: theme), + ); + } +} + +class LeaderboardView extends StatelessWidget { + const LeaderboardView({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Scaffold( + body: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 80), + Text( + l10n.leaderboard, + style: Theme.of(context).textTheme.headline3, + ), + const SizedBox(height: 80), + BlocBuilder( + builder: (context, state) { + switch (state.status) { + case LeaderboardStatus.loading: + return _LeaderboardLoading(theme: theme); + case LeaderboardStatus.success: + return _LeaderboardRanking( + ranking: state.leaderboard, + theme: theme, + ); + case LeaderboardStatus.error: + return _LeaderboardError(theme: theme); + } + }, + ), + const SizedBox(height: 20), + TextButton( + onPressed: () => Navigator.of(context).push( + CharacterSelectionPage.route(), + ), + child: Text(l10n.retry), + ), + ], + ), + ), + ), + ); + } +} + +class _LeaderboardLoading extends StatelessWidget { + const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + } +} + +class _LeaderboardError extends StatelessWidget { + const _LeaderboardError({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20), + child: Text( + 'There was en error loading data!', + style: + Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red), + ), + ); + } +} + +class _LeaderboardRanking extends StatelessWidget { + const _LeaderboardRanking({ + Key? key, + required this.ranking, + required this.theme, + }) : super(key: key); + + final List ranking; + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _LeaderboardHeaders(theme: theme), + _LeaderboardList( + ranking: ranking, + theme: theme, + ), + ], + ), + ); + } +} + +class _LeaderboardHeaders extends StatelessWidget { + const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _LeaderboardHeaderItem(title: l10n.rank, theme: theme), + _LeaderboardHeaderItem(title: l10n.character, theme: theme), + _LeaderboardHeaderItem(title: l10n.username, theme: theme), + _LeaderboardHeaderItem(title: l10n.score, theme: theme), + ], + ); + } +} + +class _LeaderboardHeaderItem extends StatelessWidget { + const _LeaderboardHeaderItem({ + Key? key, + required this.title, + required this.theme, + }) : super(key: key); + + final CharacterTheme theme; + final String title; + + @override + Widget build(BuildContext context) { + return Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.ballColor, + ), + child: Text( + title, + style: Theme.of(context).textTheme.headline5, + ), + ), + ); + } +} + +class _LeaderboardList extends StatelessWidget { + const _LeaderboardList({ + Key? key, + required this.ranking, + required this.theme, + }) : super(key: key); + + final List ranking; + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + itemBuilder: (_, index) => _LeaderBoardCompetitor( + entry: ranking[index], + theme: theme, + ), + itemCount: ranking.length, + ); + } +} + +class _LeaderBoardCompetitor extends StatelessWidget { + const _LeaderBoardCompetitor({ + Key? key, + required this.entry, + required this.theme, + }) : super(key: key); + + final CharacterTheme theme; + + final LeaderboardEntry entry; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _LeaderboardCompetitorField( + text: entry.rank, + theme: theme, + ), + _LeaderboardCompetitorCharacter( + characterAsset: entry.character, + theme: theme, + ), + _LeaderboardCompetitorField( + text: entry.playerInitials, + theme: theme, + ), + _LeaderboardCompetitorField( + text: entry.score.toString(), + theme: theme, + ), + ], + ); + } +} + +class _LeaderboardCompetitorField extends StatelessWidget { + const _LeaderboardCompetitorField({ + Key? key, + required this.text, + required this.theme, + }) : super(key: key); + + final CharacterTheme theme; + final String text; + + @override + Widget build(BuildContext context) { + return Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: theme.ballColor, + width: 2, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(text), + ), + ), + ); + } +} + +class _LeaderboardCompetitorCharacter extends StatelessWidget { + const _LeaderboardCompetitorCharacter({ + Key? key, + required this.characterAsset, + required this.theme, + }) : super(key: key); + + final CharacterTheme theme; + final AssetGenImage characterAsset; + + @override + Widget build(BuildContext context) { + return Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: theme.ballColor, + width: 2, + ), + ), + child: SizedBox( + height: 30, + child: characterAsset.image(), + ), + ), + ); + } +} diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 5a6a249f..f8415a58 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -7,11 +7,10 @@ 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'; -class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} +import '../../helpers/mocks.dart'; void main() { group('App', () { diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 6419eef2..a872dc1f 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -4,6 +4,7 @@ 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_components/pinball_components.dart'; import '../../helpers/helpers.dart'; diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 5298d6ac..f16b8ef1 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -104,10 +104,7 @@ void main() { ); await tester.pump(); - expect( - find.text('Game Over'), - findsOneWidget, - ); + expect(find.byType(GameOverDialog), findsOneWidget); }, ); diff --git a/test/game/view/widgets/game_over_dialog_test.dart b/test/game/view/widgets/game_over_dialog_test.dart new file mode 100644 index 00000000..8150bcd5 --- /dev/null +++ b/test/game/view/widgets/game_over_dialog_test.dart @@ -0,0 +1,44 @@ +// 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/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('GameOverDialog', () { + testWidgets('renders correctly', (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + await tester.pumpApp( + const GameOverDialog( + theme: DashTheme(), + ), + ); + + expect(find.text(l10n.gameOver), findsOneWidget); + expect(find.text(l10n.leaderboard), findsOneWidget); + }); + + testWidgets('tapping on leaderboard button navigates to LeaderBoardPage', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + final navigator = MockNavigator(); + when(() => navigator.push(any())).thenAnswer((_) async {}); + + await tester.pumpApp( + const GameOverDialog( + theme: DashTheme(), + ), + navigator: navigator, + ); + + await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard)); + + verify(() => navigator.push(any())).called(1); + }); + }); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 88b9c04d..223ec627 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -8,4 +8,5 @@ export 'builders.dart'; export 'extensions.dart'; export 'key_testers.dart'; export 'mocks.dart'; +export 'navigator.dart'; export 'pump_app.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 8ddab690..c658c531 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,3 +1,4 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/theme/theme.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -37,6 +39,9 @@ class MockGameState extends Mock implements GameState {} class MockThemeCubit extends Mock implements ThemeCubit {} +class MockLeaderboardBloc extends MockBloc + implements LeaderboardBloc {} + class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { diff --git a/test/helpers/navigator.dart b/test/helpers/navigator.dart new file mode 100644 index 00000000..5a8ea52e --- /dev/null +++ b/test/helpers/navigator.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'helpers.dart'; + +Future expectNavigatesToRoute( + WidgetTester tester, + Route route, { + bool hasFlameGameInside = false, +}) async { + // ignore: avoid_dynamic_calls + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context).push(route); + }, + child: const Text('Tap me'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Tap me')); + if (hasFlameGameInside) { + // We can't use pumpAndSettle here because the page renders a Flame game + // which is an infinity animation, so it will timeout + await tester.pump(); // Runs the button action + await tester.pump(); // Runs the navigation + } else { + await tester.pumpAndSettle(); + } + + expect(find.byType(Type), findsOneWidget); +} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index e0b953d2..d5e819b4 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; @@ -22,26 +23,30 @@ extension PumpApp on WidgetTester { MockNavigator? navigator, GameBloc? gameBloc, ThemeCubit? themeCubit, + LeaderboardRepository? leaderboardRepository, }) { return pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), - ), - BlocProvider.value( - value: gameBloc ?? MockGameBloc(), - ), - ], - child: MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, + RepositoryProvider.value( + value: leaderboardRepository ?? MockLeaderboardRepository(), + child: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: themeCubit ?? MockThemeCubit(), + ), + BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + ), ], - supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + child: MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: navigator != null + ? MockNavigatorProvider(navigator: navigator, child: widget) + : widget, + ), ), ), ); diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart new file mode 100644 index 00000000..9460818d --- /dev/null +++ b/test/leaderboard/view/leaderboard_page_test.dart @@ -0,0 +1,150 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('LeaderboardPage', () { + testWidgets('renders LeaderboardView', (tester) async { + await tester.pumpApp( + LeaderboardPage( + theme: DashTheme(), + ), + ); + + expect(find.byType(LeaderboardView), findsOneWidget); + }); + + testWidgets('route returns a valid navigation route', (tester) async { + await expectNavigatesToRoute( + tester, + LeaderboardPage.route( + theme: DashTheme(), + ), + ); + }); + }); + + group('LeaderboardView', () { + late LeaderboardBloc leaderboardBloc; + + setUp(() { + leaderboardBloc = MockLeaderboardBloc(); + }); + + testWidgets('renders correctly', (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.text(l10n.leaderboard), findsOneWidget); + expect(find.text(l10n.retry), findsOneWidget); + }); + + testWidgets('renders loading view when bloc emits [loading]', + (tester) async { + when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.text('There was en error loading data!'), findsNothing); + expect(find.byType(ListView), findsNothing); + }); + + testWidgets('renders error view when bloc emits [error]', (tester) async { + when(() => leaderboardBloc.state).thenReturn( + LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('There was en error loading data!'), findsOneWidget); + expect(find.byType(ListView), findsNothing); + }); + + testWidgets('renders success view when bloc emits [success]', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + when(() => leaderboardBloc.state).thenReturn( + LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 0, outOf: 0), + leaderboard: [ + LeaderboardEntry( + rank: '1', + playerInitials: 'ABC', + score: 10000, + character: DashTheme().characterAsset, + ), + ], + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('There was en error loading data!'), findsNothing); + expect(find.text(l10n.rank), findsOneWidget); + expect(find.text(l10n.character), findsOneWidget); + expect(find.text(l10n.username), findsOneWidget); + expect(find.text(l10n.score), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + }); + + testWidgets('navigates to CharacterSelectionPage when retry is tapped', + (tester) async { + final navigator = MockNavigator(); + when(() => navigator.push(any())).thenAnswer((_) async {}); + + await tester.pumpApp( + LeaderboardPage( + theme: DashTheme(), + ), + navigator: navigator, + ); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + + verify(() => navigator.push(any())).called(1); + }); + }); +} From db29e5c7b1acc708201c6a40626c6e0d67a36e87 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:13:42 -0500 Subject: [PATCH 4/9] refactor: position and dimension google letters (#93) * refactor: relative positions * chore: remove unused code * test: fix failing tests * refactor: position logic * refactor: position logic * chore: clean up bonus word * refactor: use relative positions * refactor: await adding letters * refactor: remove local variable * chore: remove empty line Co-authored-by: Alejandro Santiago --- lib/game/components/bonus_word.dart | 40 +++++++++++++++-------- lib/game/pinball_game.dart | 4 +-- test/game/components/bonus_word_test.dart | 2 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart index 9dc9b0b0..e7f1626a 100644 --- a/lib/game/components/bonus_word.dart +++ b/lib/game/components/bonus_word.dart @@ -73,18 +73,32 @@ class BonusWord extends Component with BlocComponent { @override Future onLoad() async { await super.onLoad(); - final letters = GameBloc.bonusWord.split(''); - - for (var i = 0; i < letters.length; i++) { - unawaited( - add( - BonusLetter( - letter: letters[i], - index: i, - )..initialPosition = _position - Vector2(16 - (i * 6), -30), - ), + + final offsets = [ + Vector2(-12.92, -1.82), + Vector2(-8.33, 0.65), + Vector2(-2.88, 1.75), + ]; + offsets.addAll( + offsets.reversed + .map( + (offset) => Vector2(-offset.x, offset.y), + ) + .toList(), + ); + assert(offsets.length == GameBloc.bonusWord.length, 'Invalid positions'); + + final letters = []; + for (var i = 0; i < GameBloc.bonusWord.length; i++) { + letters.add( + BonusLetter( + letter: GameBloc.bonusWord[i], + index: i, + )..initialPosition = _position + offsets[i], ); } + + await addAll(letters); } } @@ -103,8 +117,8 @@ class BonusLetter extends BodyComponent paint = Paint()..color = _disableColor; } - /// The area size of this [BonusLetter]. - static final areaSize = Vector2.all(4); + /// The size of the [BonusLetter]. + static final size = Vector2.all(3.7); static const _activeColor = Colors.green; static const _disableColor = Colors.red; @@ -136,7 +150,7 @@ class BonusLetter extends BodyComponent @override Body createBody() { - final shape = CircleShape()..radius = areaSize.x / 2; + final shape = CircleShape()..radius = size.x / 2; final fixtureDef = FixtureDef(shape)..isSensor = true; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 681a6431..39cecb61 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -74,8 +74,8 @@ class PinballGame extends Forge2DGame await add( BonusWord( position: Vector2( - boardBounds.center.dx, - boardBounds.bottom + 10, + boardBounds.center.dx - 3.07, + boardBounds.center.dy - 2.4, ), ), ); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index afd69935..17b702dc 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -187,7 +187,7 @@ void main() { final fixture = bonusLetter.body.fixtures[0]; expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(2)); + expect(fixture.shape.radius, equals(1.85)); }, ); }); From e6dca1ed7fca5a9ef50d9eab6db26b7938fcded4 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Fri, 25 Mar 2022 08:00:35 -0500 Subject: [PATCH 5/9] refactor: dimension and position `Plunger` (#96) * refactor: dimension plunger * refactor: adjust launch ramp and ball size * refactor: use board center for positioning Co-authored-by: Erick --- lib/game/components/launcher_ramp.dart | 23 ++++++++----------- lib/game/components/plunger.dart | 18 ++++++++++----- lib/game/pinball_game.dart | 13 +++++------ .../lib/src/components/ball.dart | 2 +- .../test/src/components/ball_test.dart | 2 +- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index aae9265f..61b9e26f 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -27,33 +27,30 @@ class LauncherRamp extends Component with HasGameRef { RampOpeningBallContactCallback<_LauncherRampOpening>(), ); - final launcherRampRotation = - -math.atan(18.6 / PinballGame.boardBounds.height); - final straightPath = Pathway.straight( color: const Color.fromARGB(255, 34, 255, 0), - start: position + Vector2(-1.2, 10), - end: position + Vector2(-1.2, 117), - width: 5, - rotation: launcherRampRotation, + start: position + Vector2(-4.5, -10), + end: position + Vector2(-4.5, 117), + width: 4, + rotation: PinballGame.boardPerspectiveAngle, ) ..initialPosition = position ..layer = layer; final curvedPath = Pathway.arc( color: const Color.fromARGB(255, 251, 255, 0), - center: position + Vector2(-2.8, 87.2), + center: position + Vector2(-7, 87.2), radius: 16.3, angle: math.pi / 2, - width: 5, + width: 4, rotation: 3 * math.pi / 2, )..layer = layer; final leftOpening = _LauncherRampOpening(rotation: math.pi / 2) - ..initialPosition = position + Vector2(-11.8, 66.3) + ..initialPosition = position + Vector2(-13.8, 66.7) ..layer = Layer.opening; final rightOpening = _LauncherRampOpening(rotation: 0) - ..initialPosition = position + Vector2(-4.9, 59.4) + ..initialPosition = position + Vector2(-6.8, 59.4) ..layer = Layer.opening; await addAll([ @@ -81,9 +78,9 @@ class _LauncherRampOpening extends RampOpening { final double _rotation; - // TODO(ruialonso): Avoid magic number 3, should be propotional to + // TODO(ruialonso): Avoid magic number 2.5, should be propotional to // [JetpackRamp]. - static final Vector2 _size = Vector2(3, .1); + static final Vector2 _size = Vector2(2.5, .1); @override Shape get shape => PolygonShape() diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 9791ec66..d9137457 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -21,9 +21,15 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { @override Body createBody() { - final shape = PolygonShape()..setAsBoxXY(2, 0.75); + final shape = PolygonShape() + ..setAsBox( + 1.35, + 0.5, + Vector2.zero(), + PinballGame.boardPerspectiveAngle, + ); - final fixtureDef = FixtureDef(shape)..density = 5; + final fixtureDef = FixtureDef(shape)..density = 20; final bodyDef = BodyDef() ..position = initialPosition @@ -36,7 +42,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { /// Set a constant downward velocity on the [Plunger]. void _pull() { - body.linearVelocity = Vector2(0, -3); + body.linearVelocity = Vector2(0, -7); } /// Set an upward velocity on the [Plunger]. @@ -44,7 +50,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void _release() { - final velocity = (initialPosition.y - body.position.y) * 9; + final velocity = (initialPosition.y - body.position.y) * 4; body.linearVelocity = Vector2(0, velocity); } @@ -121,12 +127,12 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { plunger.body, anchor.body, anchor.body.position, - Vector2(0, -1), + Vector2(18.6, PinballGame.boardBounds.height), ); enableLimit = true; lowerTranslation = double.negativeInfinity; enableMotor = true; - motorSpeed = 50; + motorSpeed = 80; maxMotorForce = motorSpeed; collideConnected = true; } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 39cecb61..057809be 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,5 +1,7 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; +import 'dart:math' as math; + import 'package:flame/extensions.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; @@ -24,6 +26,8 @@ class PinballGame extends Forge2DGame width: boardSize.x, height: -boardSize.y, ); + static final boardPerspectiveAngle = + -math.atan(18.6 / PinballGame.boardBounds.height); @override void onAttach() { @@ -60,13 +64,8 @@ class PinballGame extends Forge2DGame } Future _addPlunger() async { - plunger = Plunger(compressionDistance: 2); - - plunger.initialPosition = boardBounds.bottomRight.toVector2() + - Vector2( - -5, - 10, - ); + plunger = Plunger(compressionDistance: 29) + ..initialPosition = boardBounds.center.toVector2() + Vector2(41.5, -49); await add(plunger); } diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index ec51cb47..674cdbf3 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -22,7 +22,7 @@ class Ball extends BodyComponent } /// The size of the [Ball] - final Vector2 size = Vector2.all(2); + final Vector2 size = Vector2.all(3); /// The base [Color] used to tint this [Ball] final Color baseColor; diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball_test.dart index 682f1f73..14a3de35 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball_test.dart @@ -86,7 +86,7 @@ void main() { final fixture = ball.body.fixtures[0]; expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(1)); + expect(fixture.shape.radius, equals(1.5)); }, ); From de963cbc862e4c5c9953203c59a8f9ec83e018fe Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 25 Mar 2022 10:19:35 -0300 Subject: [PATCH 6/9] feat: adds ball turbocharge effect (#66) * feat: adding ball boost effect * fix: lint --- .../lib/src/components/ball.dart | 26 ++++ .../lib/src/components/components.dart | 1 + .../lib/src/components/fire_effect.dart | 113 ++++++++++++++++++ .../sandbox/lib/common/common.dart | 13 +- .../sandbox/lib/common/games.dart | 74 ++++++++++++ .../sandbox/lib/common/methods.dart | 3 + .../pinball_components/sandbox/lib/main.dart | 2 + .../sandbox/lib/stories/ball/ball.dart | 28 +++-- .../lib/stories/ball/ball_booster.dart | 16 +++ .../sandbox/lib/stories/ball/basic.dart | 6 +- .../sandbox/lib/stories/effects/effects.dart | 13 ++ .../lib/stories/effects/fire_effect.dart | 46 +++++++ .../test/helpers/helpers.dart | 1 + .../test/helpers/mocks.dart | 5 + .../test/src/components/ball_test.dart | 24 ++++ .../test/src/components/fire_effect_test.dart | 55 +++++++++ 16 files changed, 403 insertions(+), 23 deletions(-) create mode 100644 packages/pinball_components/lib/src/components/fire_effect.dart create mode 100644 packages/pinball_components/sandbox/lib/common/games.dart create mode 100644 packages/pinball_components/sandbox/lib/common/methods.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/ball/ball_booster.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/effects/effects.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart create mode 100644 packages/pinball_components/test/helpers/mocks.dart create mode 100644 packages/pinball_components/test/src/components/fire_effect_test.dart diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 674cdbf3..2ceb56d7 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui'; import 'package:flame/components.dart'; @@ -27,6 +28,9 @@ class Ball extends BodyComponent /// The base [Color] used to tint this [Ball] final Color baseColor; + double _boostTimer = 0; + static const _boostDuration = 2.0; + @override Future onLoad() async { await super.onLoad(); @@ -69,4 +73,26 @@ class Ball extends BodyComponent void resume() { body.setType(BodyType.dynamic); } + + @override + void update(double dt) { + super.update(dt); + if (_boostTimer > 0) { + _boostTimer -= dt; + final direction = body.linearVelocity.normalized(); + final effect = FireEffect( + burstPower: _boostTimer, + direction: direction, + position: body.position, + ); + + unawaited(gameRef.add(effect)); + } + } + + /// Applies a boost on this [Ball] + void boost(Vector2 impulse) { + body.applyLinearImpulse(impulse); + _boostTimer = _boostDuration; + } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 677bbd0c..c1ef3e14 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,3 +1,4 @@ export 'ball.dart'; +export 'fire_effect.dart'; export 'initial_position.dart'; export 'layer.dart'; diff --git a/packages/pinball_components/lib/src/components/fire_effect.dart b/packages/pinball_components/lib/src/components/fire_effect.dart new file mode 100644 index 00000000..0a7cef2b --- /dev/null +++ b/packages/pinball_components/lib/src/components/fire_effect.dart @@ -0,0 +1,113 @@ +import 'dart:math' as math; + +import 'package:flame/extensions.dart'; +import 'package:flame/particles.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle; +import 'package:flutter/material.dart'; + +const _particleRadius = 0.25; + +// TODO(erickzanardo): This component could just be a ParticleComponet, +/// unfortunately there is a Particle Component is not a PositionComponent, +/// which makes it hard to be used since we have camera transformations and on +// top of that, PositionComponent has a bug inside forge 2d games +/// +/// https://github.com/flame-engine/flame/issues/1484 +/// https://github.com/flame-engine/flame/issues/1484 + +/// {@template fire_effect} +/// A [BodyComponent] which creates a fire trail effect using the given +/// parameters +/// {@endtemplate} +class FireEffect extends BodyComponent { + /// {@macro fire_effect} + FireEffect({ + required this.burstPower, + required this.position, + required this.direction, + }); + + /// A [double] value that will define how "strong" the burst of particles + /// will be + final double burstPower; + + /// The position of the burst + final Vector2 position; + + /// Which direction the burst will aim + final Vector2 direction; + late Particle _particle; + + @override + Body createBody() { + final bodyDef = BodyDef()..position = position; + + final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + @override + Future onLoad() async { + await super.onLoad(); + + final children = [ + ...List.generate(4, (index) { + return CircleParticle( + radius: _particleRadius, + paint: Paint()..color = Colors.yellow.darken((index + 1) / 4), + ); + }), + ...List.generate(4, (index) { + return CircleParticle( + radius: _particleRadius, + paint: Paint()..color = Colors.red.darken((index + 1) / 4), + ); + }), + ...List.generate(4, (index) { + return CircleParticle( + radius: _particleRadius, + paint: Paint()..color = Colors.orange.darken((index + 1) / 4), + ); + }), + ]; + final rng = math.Random(); + final spreadTween = Tween(begin: -0.2, end: 0.2); + + _particle = Particle.generate( + count: (rng.nextDouble() * (burstPower * 10)).toInt(), + generator: (_) { + final spread = Vector2( + spreadTween.transform(rng.nextDouble()), + spreadTween.transform(rng.nextDouble()), + ); + final finalDirection = Vector2(direction.x, -direction.y) + spread; + final speed = finalDirection * (burstPower * 20); + + return AcceleratedParticle( + lifespan: 5 / burstPower, + position: Vector2.zero(), + speed: speed, + child: children[rng.nextInt(children.length)], + ); + }, + ); + } + + @override + void update(double dt) { + super.update(dt); + _particle.update(dt); + + if (_particle.shouldRemove) { + removeFromParent(); + } + } + + @override + void render(Canvas canvas) { + super.render(canvas); + + _particle.render(canvas); + } +} diff --git a/packages/pinball_components/sandbox/lib/common/common.dart b/packages/pinball_components/sandbox/lib/common/common.dart index b7ee5a4a..578c9b38 100644 --- a/packages/pinball_components/sandbox/lib/common/common.dart +++ b/packages/pinball_components/sandbox/lib/common/common.dart @@ -1,11 +1,2 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; - -String buildSourceLink(String path) { - return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path'; -} - -class BasicGame extends Forge2DGame { - BasicGame() { - images.prefix = ''; - } -} +export 'games.dart'; +export 'methods.dart'; diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart new file mode 100644 index 00000000..bce1ff90 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; + +class BasicGame extends Forge2DGame { + BasicGame() { + images.prefix = ''; + } +} + +abstract class LineGame extends BasicGame with PanDetector { + Vector2? _lineEnd; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + unawaited(add(_PreviewLine())); + } + + @override + void onPanStart(DragStartInfo info) { + _lineEnd = info.eventPosition.game; + } + + @override + void onPanUpdate(DragUpdateInfo info) { + _lineEnd = info.eventPosition.game; + } + + @override + void onPanEnd(DragEndInfo info) { + if (_lineEnd != null) { + final line = _lineEnd! - Vector2.zero(); + onLine(line); + _lineEnd = null; + } + } + + void onLine(Vector2 line); +} + +class _PreviewLine extends PositionComponent with HasGameRef { + static final _previewLinePaint = Paint() + ..color = Colors.pink + ..strokeWidth = 0.2 + ..style = PaintingStyle.stroke; + + Vector2? lineEnd; + + @override + void update(double dt) { + super.update(dt); + + lineEnd = gameRef._lineEnd?.clone()?..multiply(Vector2(1, -1)); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + + if (lineEnd != null) { + canvas.drawLine( + Vector2.zero().toOffset(), + lineEnd!.toOffset(), + _previewLinePaint, + ); + } + } +} diff --git a/packages/pinball_components/sandbox/lib/common/methods.dart b/packages/pinball_components/sandbox/lib/common/methods.dart new file mode 100644 index 00000000..35198922 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/common/methods.dart @@ -0,0 +1,3 @@ +String buildSourceLink(String path) { + return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path'; +} diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 0cfd6f7f..dd6aeafe 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,11 +6,13 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; +import 'package:sandbox/stories/effects/effects.dart'; import 'package:sandbox/stories/stories.dart'; void main() { final dashbook = Dashbook(theme: ThemeData.dark()); addBallStories(dashbook); + addEffectsStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/ball.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball.dart index f8e49a57..35b29499 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/ball.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball.dart @@ -2,17 +2,27 @@ import 'package:dashbook/dashbook.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/ball_booster.dart'; import 'package:sandbox/stories/ball/basic.dart'; void addBallStories(Dashbook dashbook) { - dashbook.storiesOf('Ball').add( - 'Basic', - (context) => GameWidget( - game: BasicBallGame( - color: context.colorProperty('color', Colors.blue), - ), + dashbook.storiesOf('Ball') + ..add( + 'Basic', + (context) => GameWidget( + game: BasicBallGame( + color: context.colorProperty('color', Colors.blue), ), - codeLink: buildSourceLink('ball/basic.dart'), - info: BasicBallGame.info, - ); + ), + codeLink: buildSourceLink('ball/basic.dart'), + info: BasicBallGame.info, + ) + ..add( + 'Booster', + (context) => GameWidget( + game: BallBoosterExample(), + ), + codeLink: buildSourceLink('ball/ball_booster.dart'), + info: BallBoosterExample.info, + ); } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster.dart new file mode 100644 index 00000000..9f78953a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster.dart @@ -0,0 +1,16 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BallBoosterExample extends LineGame { + static const info = ''; + + @override + void onLine(Vector2 line) { + final ball = Ball(baseColor: Colors.transparent); + add(ball); + + ball.mounted.then((value) => ball.boost(line * -1 * 20)); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic.dart index 78948666..f133ee3f 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic.dart @@ -4,7 +4,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; class BasicBallGame extends BasicGame with TapDetector { - BasicBallGame({ required this.color }); + BasicBallGame({required this.color}); static const info = ''' Basic example of how a Ball works, tap anywhere on the @@ -15,8 +15,8 @@ class BasicBallGame extends BasicGame with TapDetector { @override void onTapUp(TapUpInfo info) { - add(Ball(baseColor: color) - ..initialPosition = info.eventPosition.game, + add( + Ball(baseColor: color)..initialPosition = info.eventPosition.game, ); } } diff --git a/packages/pinball_components/sandbox/lib/stories/effects/effects.dart b/packages/pinball_components/sandbox/lib/stories/effects/effects.dart new file mode 100644 index 00000000..3a89c73b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/effects/effects.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/effects/fire_effect.dart'; + +void addEffectsStories(Dashbook dashbook) { + dashbook.storiesOf('Effects').add( + 'Fire Effect', + (context) => GameWidget(game: FireEffectExample()), + codeLink: buildSourceLink('effects/fire_effect.dart'), + info: FireEffectExample.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart new file mode 100644 index 00000000..9f066952 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class FireEffectExample extends LineGame { + static const info = 'Demonstrate the fire trail effect ' + 'drag a line to define the trail direction'; + + @override + void onLine(Vector2 line) { + add(_EffectEmitter(line)); + } +} + +class _EffectEmitter extends Component { + _EffectEmitter(this.line) { + _direction = line.normalized(); + _force = line.length; + } + + static const _timerLimit = 2.0; + var _timer = _timerLimit; + + final Vector2 line; + + late Vector2 _direction; + late double _force; + + @override + void update(double dt) { + super.update(dt); + + if (_timer > 0) { + add( + FireEffect( + burstPower: (_timer / _timerLimit) * _force, + position: Vector2.zero(), + direction: _direction, + ), + ); + _timer -= dt; + } else { + removeFromParent(); + } + } +} diff --git a/packages/pinball_components/test/helpers/helpers.dart b/packages/pinball_components/test/helpers/helpers.dart index a8b9f7ff..312f42ec 100644 --- a/packages/pinball_components/test/helpers/helpers.dart +++ b/packages/pinball_components/test/helpers/helpers.dart @@ -1 +1,2 @@ +export 'mocks.dart'; export 'test_game.dart'; diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart new file mode 100644 index 00000000..67df9918 --- /dev/null +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -0,0 +1,5 @@ +import 'dart:ui'; + +import 'package:mocktail/mocktail.dart'; + +class MockCanvas extends Mock implements Canvas {} diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball_test.dart index 14a3de35..a9eb05ad 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball_test.dart @@ -158,5 +158,29 @@ void main() { ); }); }); + + group('boost', () { + flameTester.test('applies an impulse to the ball', (game) async { + final ball = Ball(baseColor: Colors.blue); + await game.ensureAdd(ball); + + expect(ball.body.linearVelocity, equals(Vector2.zero())); + + ball.boost(Vector2.all(10)); + expect(ball.body.linearVelocity.x, greaterThan(0)); + expect(ball.body.linearVelocity.y, greaterThan(0)); + }); + + flameTester.test('adds fire effect components to the game', (game) async { + final ball = Ball(baseColor: Colors.blue); + await game.ensureAdd(ball); + + ball.boost(Vector2.all(10)); + game.update(0); + await game.ready(); + + expect(game.children.whereType().length, greaterThan(0)); + }); + }); }); } diff --git a/packages/pinball_components/test/src/components/fire_effect_test.dart b/packages/pinball_components/test/src/components/fire_effect_test.dart new file mode 100644 index 00000000..bc6baa4b --- /dev/null +++ b/packages/pinball_components/test/src/components/fire_effect_test.dart @@ -0,0 +1,55 @@ +// ignore_for_file: cascade_invocations + +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + setUpAll(() { + registerFallbackValue(Offset.zero); + registerFallbackValue(Paint()); + }); + + group('FireEffect', () { + flameTester.test('is removed once its particles are done', (game) async { + await game.ensureAdd( + FireEffect( + burstPower: 1, + position: Vector2.zero(), + direction: Vector2.all(2), + ), + ); + await game.ready(); + expect(game.children.whereType().length, equals(1)); + game.update(5); + + await game.ready(); + expect(game.children.whereType().length, equals(0)); + }); + + flameTester.test('render circles on the canvas', (game) async { + final effect = FireEffect( + burstPower: 1, + position: Vector2.zero(), + direction: Vector2.all(2), + ); + await game.ensureAdd(effect); + await game.ready(); + + final canvas = MockCanvas(); + effect.render(canvas); + + verify(() => canvas.drawCircle(any(), any(), any())) + .called(greaterThan(0)); + }); + }); +} From e6eacfa9e76ec7929e46279d14a3ad7d14dc1168 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Fri, 25 Mar 2022 08:42:35 -0500 Subject: [PATCH 7/9] fix: sort scores in descending order (#100) --- .../lib/src/leaderboard_repository.dart | 4 ++-- .../test/src/leaderboard_repository_test.dart | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index d75a88b3..30f6810f 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -91,7 +91,7 @@ class LeaderboardRepository { try { final querySnapshot = await _firebaseFirestore .collection('leaderboard') - .orderBy('score') + .orderBy('score', descending: true) .limit(10) .get(); documents = querySnapshot.docs; @@ -130,7 +130,7 @@ class LeaderboardRepository { try { final querySnapshot = await _firebaseFirestore .collection('leaderboard') - .orderBy('score') + .orderBy('score', descending: true) .get(); // TODO(allisonryan0002): see if we can find a more performant solution. diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index 592425ec..1341d3f4 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -82,7 +82,7 @@ void main() { when(() => firestore.collection('leaderboard')) .thenAnswer((_) => collectionReference); - when(() => collectionReference.orderBy('score')) + when(() => collectionReference.orderBy('score', descending: true)) .thenAnswer((_) => query); when(() => query.limit(10)).thenAnswer((_) => query); when(query.get).thenAnswer((_) async => querySnapshot); @@ -173,7 +173,7 @@ void main() { .thenAnswer((_) => collectionReference); when(() => collectionReference.add(any())) .thenAnswer((_) async => documentReference); - when(() => collectionReference.orderBy('score')) + when(() => collectionReference.orderBy('score', descending: true)) .thenAnswer((_) => query); when(query.get).thenAnswer((_) async => querySnapshot); when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); @@ -203,7 +203,8 @@ void main() { test( 'throws FetchPlayerRankingException when Exception occurs ' 'when trying to retrieve information from firestore', () async { - when(() => collectionReference.orderBy('score')).thenThrow(Exception()); + when(() => collectionReference.orderBy('score', descending: true)) + .thenThrow(Exception()); expect( () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), From 9f1f9f44667dfdf658870cc3098c32489152c05c Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Fri, 25 Mar 2022 14:59:15 +0100 Subject: [PATCH 8/9] feat (sandbox): added layers story at sandbox to check collision with nested components (#97) * feat: added layers story to check collision with nested components * feat: added more components to layer story * chore: unused import * chore: renamed component --- .../pinball_components/sandbox/lib/main.dart | 1 + .../sandbox/lib/stories/layer/basic.dart | 98 +++++++++++++++++++ .../sandbox/lib/stories/layer/layer.dart | 18 ++++ .../sandbox/lib/stories/stories.dart | 1 + .../pinball_components/sandbox/pubspec.lock | 7 ++ 5 files changed, 125 insertions(+) create mode 100644 packages/pinball_components/sandbox/lib/stories/layer/basic.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/layer/layer.dart diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index dd6aeafe..44b594d7 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -13,6 +13,7 @@ void main() { final dashbook = Dashbook(theme: ThemeData.dark()); addBallStories(dashbook); + addLayerStories(dashbook); addEffectsStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/layer/basic.dart b/packages/pinball_components/sandbox/lib/stories/layer/basic.dart new file mode 100644 index 00000000..89ef337f --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/layer/basic.dart @@ -0,0 +1,98 @@ +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BasicLayerGame extends BasicGame with TapDetector { + BasicLayerGame({required this.color}); + + static const info = ''' + Basic example of how layers work with a Ball hitting other components, + tap anywhere on the screen to spawn a ball into the game. +'''; + + final Color color; + + @override + Future onLoad() async { + await add(BigSquare()..initialPosition = Vector2(30, -40)); + await add(SmallSquare()..initialPosition = Vector2(50, -40)); + await add(UnlayeredSquare()..initialPosition = Vector2(60, -40)); + } + + @override + void onTapUp(TapUpInfo info) { + add( + Ball(baseColor: color)..initialPosition = info.eventPosition.game, + ); + } +} + +class BigSquare extends BodyComponent with InitialPosition, Layered { + BigSquare() { + paint = Paint() + ..color = const Color.fromARGB(255, 8, 218, 241) + ..style = PaintingStyle.stroke; + layer = Layer.jetpack; + } + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(16, 16); + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef()..position = initialPosition; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + @override + Future onLoad() async { + await super.onLoad(); + + await addAll( + [ + UnlayeredSquare()..initialPosition = Vector2.all(4), + SmallSquare()..initialPosition = Vector2.all(-4), + ], + ); + } +} + +class SmallSquare extends BodyComponent with InitialPosition, Layered { + SmallSquare() { + paint = Paint() + ..color = const Color.fromARGB(255, 27, 241, 8) + ..style = PaintingStyle.stroke; + layer = Layer.board; + } + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(2, 2); + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef()..position = initialPosition; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class UnlayeredSquare extends BodyComponent with InitialPosition { + UnlayeredSquare() { + paint = Paint() + ..color = const Color.fromARGB(255, 241, 8, 8) + ..style = PaintingStyle.stroke; + } + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(3, 3); + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef()..position = initialPosition; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/layer/layer.dart b/packages/pinball_components/sandbox/lib/stories/layer/layer.dart new file mode 100644 index 00000000..6d3538dd --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/layer/layer.dart @@ -0,0 +1,18 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/layer/basic.dart'; + +void addLayerStories(Dashbook dashbook) { + dashbook.storiesOf('Layer').add( + 'Layer', + (context) => GameWidget( + game: BasicLayerGame( + color: context.colorProperty('color', Colors.blue), + ), + ), + codeLink: buildSourceLink('layer/basic.dart'), + info: BasicLayerGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 6070319c..1135fbaf 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1 +1,2 @@ export 'ball/ball.dart'; +export 'layer/layer.dart'; diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index 7452ccf4..bb132da7 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -142,6 +142,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + geometry: + dependency: transitive + description: + path: "../../geometry" + relative: true + source: path + version: "1.0.0+1" js: dependency: transitive description: From 2d647f96302e9e09748d39abfca908f36fcbbfb8 Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 25 Mar 2022 13:33:46 -0300 Subject: [PATCH 9/9] fix: sandbox github workflow (#99) * fix: sandbox github workflow * fix: forcing error to test * fix: lint * fix: lint --- .../sandbox/.github/PULL_REQUEST_TEMPLATE.md | 23 ------------------- .../sandbox/.github/workflows/main.yaml | 10 -------- 2 files changed, 33 deletions(-) delete mode 100644 packages/pinball_components/sandbox/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 packages/pinball_components/sandbox/.github/workflows/main.yaml diff --git a/packages/pinball_components/sandbox/.github/PULL_REQUEST_TEMPLATE.md b/packages/pinball_components/sandbox/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 6b9372ef..00000000 --- a/packages/pinball_components/sandbox/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,23 +0,0 @@ - - -## Description - - - -## Type of Change - - - -- [ ] โœจ New feature (non-breaking change which adds functionality) -- [ ] ๐Ÿ› ๏ธ Bug fix (non-breaking change which fixes an issue) -- [ ] โŒ Breaking change (fix or feature that would cause existing functionality to change) -- [ ] ๐Ÿงน Code refactor -- [ ] โœ… Build configuration change -- [ ] ๐Ÿ“ Documentation -- [ ] ๐Ÿ—‘๏ธ Chore diff --git a/packages/pinball_components/sandbox/.github/workflows/main.yaml b/packages/pinball_components/sandbox/.github/workflows/main.yaml deleted file mode 100644 index 553a0091..00000000 --- a/packages/pinball_components/sandbox/.github/workflows/main.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: sandbox - -on: [pull_request, push] - -jobs: - build: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 - with: - flutter_channel: stable - flutter_version: 2.10.0