From 7338b95ef55a05a031a3e81dee470dd353a7ec87 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Wed, 2 Mar 2022 08:42:11 -0600 Subject: [PATCH 01/53] feat: plunger --- lib/game/components/boundaries.dart | 41 ++++++++++++++++++++++ lib/game/components/components.dart | 2 ++ lib/game/components/plunger.dart | 31 +++++++++++++++++ lib/game/game.dart | 1 + lib/game/pinball_game.dart | 54 ++++++++++++++++++++++++++++- 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 lib/game/components/boundaries.dart create mode 100644 lib/game/components/components.dart create mode 100644 lib/game/components/plunger.dart diff --git a/lib/game/components/boundaries.dart b/lib/game/components/boundaries.dart new file mode 100644 index 00000000..08e9a2c3 --- /dev/null +++ b/lib/game/components/boundaries.dart @@ -0,0 +1,41 @@ +import 'package:flame_forge2d/body_component.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:forge2d/forge2d.dart'; + +List createBoundaries(Forge2DGame game) { + final topLeft = Vector2.zero(); + final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); + final topRight = Vector2(bottomRight.x, topLeft.y); + final bottomLeft = Vector2(topLeft.x, bottomRight.y); + + return [ + Wall(topLeft, topRight), + Wall(topRight, bottomRight), + Wall(bottomRight, bottomLeft), + Wall(bottomLeft, topLeft), + ]; +} + +class Wall extends BodyComponent { + Wall(this.start, this.end); + + final Vector2 start; + final Vector2 end; + + @override + Body createBody() { + final shape = EdgeShape()..set(start, end); + + final fixtureDef = FixtureDef(shape) + ..restitution = 0.0 + ..friction = 0.3; + + final bodyDef = BodyDef() + ..userData = this + ..position = Vector2.zero() + ..type = BodyType.static; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart new file mode 100644 index 00000000..85ec5ae3 --- /dev/null +++ b/lib/game/components/components.dart @@ -0,0 +1,2 @@ +export 'boundaries.dart'; +export 'plunger.dart'; diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart new file mode 100644 index 00000000..7b168629 --- /dev/null +++ b/lib/game/components/plunger.dart @@ -0,0 +1,31 @@ +import 'package:flame_forge2d/body_component.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class Plunger extends BodyComponent { + Plunger(this._position); + + final Vector2 _position; + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(2.5, 1.5); + + final fixtureDef = FixtureDef(shape)..friction = 0.1; + + final bodyDef = BodyDef() + ..userData = this + ..position = _position + ..type = BodyType.dynamic; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + void pull() { + body.linearVelocity = Vector2(0, -5); + } + + void release() { + final velocity = (_position.y - body.position.y) * 9; + body.linearVelocity = Vector2(0, velocity); + } +} diff --git a/lib/game/game.dart b/lib/game/game.dart index ec8e0824..0a8dac1b 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,2 +1,3 @@ +export 'components/components.dart'; export 'pinball_game.dart'; export 'view/pinball_game_page.dart'; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 306d03f0..031e51d7 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,3 +1,55 @@ +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball/game/game.dart'; -class PinballGame extends Forge2DGame {} +class PinballGame extends Forge2DGame with KeyboardEvents { + late Plunger plunger; + + @override + Future onLoad() async { + await super.onLoad(); + + final boundaries = createBoundaries(this)..forEach(add); + final bottomWall = boundaries[2]; + + final center = screenToWorld(camera.viewport.effectiveSize / 2); + + await add(plunger = Plunger(Vector2(center.x, center.y - 50))); + + final prismaticJointDef = PrismaticJointDef() + ..initialize( + plunger.body, + bottomWall.body, + bottomWall.body.position, + Vector2(0, 0), + ) + ..localAnchorA.setFrom(Vector2(0, 0)) + ..enableLimit = true + ..upperTranslation = 0 + ..lowerTranslation = -5 + ..collideConnected = true; + + world.createJoint(prismaticJointDef); + print(prismaticJointDef.localAnchorA); + print(prismaticJointDef.localAnchorB); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyUpEvent && + event.data.logicalKey == LogicalKeyboardKey.space) { + plunger.release(); + } + if (event is RawKeyDownEvent && + event.data.logicalKey == LogicalKeyboardKey.space) { + plunger.pull(); + } + return KeyEventResult.handled; + } +} From 518e183ad89920a55bd47802381ba22b7b2704bb Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 2 Mar 2022 18:18:25 -0300 Subject: [PATCH 02/53] feat: game over mechanics --- lib/game/components/ball.dart | 16 ++++ lib/game/components/components.dart | 1 + lib/game/components/wall.dart | 24 +++++ lib/game/game.dart | 2 +- lib/game/pinball_game.dart | 39 ++++++++ lib/game/view/pinball_game_page.dart | 13 ++- lib/game/view/pinball_game_view.dart | 33 +++++++ lib/game/view/view.dart | 2 + test/game/components/wall_test.dart | 81 ++++++++++++++++ test/game/pinball_game_test.dart | 106 ++++++++++++++++++++- test/game/view/pinball_game_page_test.dart | 42 +++++++- test/game/view/pinball_game_view_test.dart | 46 +++++++++ test/helpers/pump_app.dart | 14 ++- 13 files changed, 405 insertions(+), 14 deletions(-) create mode 100644 lib/game/components/wall.dart create mode 100644 lib/game/view/pinball_game_view.dart create mode 100644 lib/game/view/view.dart create mode 100644 test/game/components/wall_test.dart create mode 100644 test/game/view/pinball_game_view_test.dart diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 8ff24094..3aa978d3 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -6,6 +6,7 @@ import 'package:pinball/game/game.dart'; class Ball extends BodyComponent with BlocComponent { + Ball({ required Vector2 position, }) : _position = position { @@ -28,4 +29,19 @@ class Ball extends BodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } + + @override + void onRemove() { + final bloc = gameRef.read(); + + final shouldBallrespwan = bloc.state.balls > 1; + + bloc.add(const BallLost()); + + if (shouldBallrespwan) { + gameRef.resetBall(); + } + + super.onRemove(); + } } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index e3d2c6ce..52e460a5 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,2 +1,3 @@ export 'ball.dart'; export 'score_points.dart'; +export 'wall.dart'; diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart new file mode 100644 index 00000000..de93e1ce --- /dev/null +++ b/lib/game/components/wall.dart @@ -0,0 +1,24 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +class Wall extends BodyComponent { + Wall(this.start, this.end); + + final Vector2 start; + final Vector2 end; + + @override + Body createBody() { + final shape = EdgeShape()..set(start, end); + + final fixtureDef = FixtureDef(shape) + ..restitution = 0.0 + ..friction = 0.3; + + final bodyDef = BodyDef() + ..userData = this + ..position = Vector2.zero() + ..type = BodyType.static; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/lib/game/game.dart b/lib/game/game.dart index e2e5361f..253dcc9f 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,4 +1,4 @@ export 'bloc/game_bloc.dart'; export 'components/components.dart'; export 'pinball_game.dart'; -export 'view/pinball_game_page.dart'; +export 'view/view.dart'; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index c4977435..0c160316 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,10 +1,49 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:pinball/game/game.dart'; +class BallWallContactCallback extends ContactCallback { + @override + void begin(Ball ball, Wall wall, Contact contact) { + ball.shouldRemove = true; + } + + @override + void end(_, __, ___) {} +} + class PinballGame extends Forge2DGame with FlameBloc { + void resetBall() { + add(Ball(position: ballStartingPosition)); + } + + late final ballStartingPosition = screenToWorld( + Vector2( + camera.viewport.effectiveSize.x / 2, + camera.viewport.effectiveSize.y - 20, + ), + ) - + Vector2( + 0, + -20, + ); + @override Future onLoad() async { + await super.onLoad(); addContactCallback(BallScorePointsCallback()); + + final topLeft = Vector2.zero(); + final bottomRight = screenToWorld(camera.viewport.effectiveSize); + final bottomLeft = Vector2(topLeft.x, bottomRight.y); + + await add( + Wall(bottomRight, bottomLeft), + ); + addContactCallback(BallWallContactCallback()); + + resetBall(); } } diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index cfe8d5bc..c4387f34 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -1,16 +1,23 @@ -import 'package:flame/game.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; class PinballGamePage extends StatelessWidget { const PinballGamePage({Key? key}) : super(key: key); static Route route() { - return MaterialPageRoute(builder: (_) => const PinballGamePage()); + return MaterialPageRoute( + builder: (_) { + return BlocProvider( + create: (_) => GameBloc(), + child: const PinballGamePage(), + ); + }, + ); } @override Widget build(BuildContext context) { - return GameWidget(game: PinballGame()); + return const PinballGameView(); } } diff --git a/lib/game/view/pinball_game_view.dart b/lib/game/view/pinball_game_view.dart new file mode 100644 index 00000000..5fbc8ae8 --- /dev/null +++ b/lib/game/view/pinball_game_view.dart @@ -0,0 +1,33 @@ +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; + +class PinballGameView extends StatelessWidget { + const PinballGameView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.isGameOver) { + showDialog( + context: context, + builder: (_) { + return const Dialog( + child: SizedBox( + width: 200, + height: 200, + child: Center( + child: Text('Game Over'), + ), + ), + ); + }, + ); + } + }, + child: GameWidget(game: PinballGame()), + ); + } +} diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart new file mode 100644 index 00000000..66134941 --- /dev/null +++ b/lib/game/view/view.dart @@ -0,0 +1,2 @@ +export 'pinball_game_page.dart'; +export 'pinball_game_view.dart'; diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart new file mode 100644 index 00000000..e17537c0 --- /dev/null +++ b/test/game/components/wall_test.dart @@ -0,0 +1,81 @@ +// ignore_for_file: cascade_invocations + +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Wall', () { + final flameTester = FlameTester(PinballGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + expect(game.contains(wall), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + game.contains(wall); + + expect(wall.body.position, Vector2.zero()); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + expect(wall.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + expect(wall.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'has restitution equals 0', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + final fixture = wall.body.fixtures[0]; + expect(fixture.restitution, equals(0)); + }, + ); + + flameTester.test( + 'has friction', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + final fixture = wall.body.fixtures[0]; + expect(fixture.friction, greaterThan(0)); + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 75a77aa9..a576252b 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,9 +1,109 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/game.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/game.dart'; + +class MockPinballGame extends Mock implements PinballGame {} + +class MockWall extends Mock implements Wall {} + +class MockBall extends Mock implements Ball {} + +class MockContact extends Mock implements Contact {} + +class MockGameBloc extends Mock implements GameBloc {} void main() { + // TODO(alestiago): test if [PinballGame] registers + // [BallScorePointsCallback] once the following issue is resolved: + // https://github.com/flame-engine/flame/issues/1416 group('PinballGame', () { - // TODO(alestiago): test if [PinballGame] registers - // [BallScorePointsCallback] once the following issue is resolved: - // https://github.com/flame-engine/flame/issues/1416 + group('BallWallContactCallback', () { + test('removes the ball on begin contact', () { + final game = MockPinballGame(); + final wall = MockWall(); + final ball = MockBall(); + + when(() => ball.gameRef).thenReturn(game); + + BallWallContactCallback() + // Remove once https://github.com/flame-engine/flame/pull/1415 + // is merged + ..end(MockBall(), MockWall(), MockContact()) + ..begin(ball, wall, MockContact()); + + verify(() => ball.shouldRemove = true).called(1); + }); + }); + + group('resetting a ball', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + FlameTester( + PinballGame.new, + pumpWidget: (gameWidget, tester) async { + await tester.pumpWidget( + BlocProvider.value( + value: gameBloc, + child: gameWidget, + ), + ); + }, + ) + ..widgetTest('adds BallLost to GameBloc', (game, tester) async { + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await tester.pump(); + + verify(() => gameBloc.add(const BallLost())).called(1); + }) + ..widgetTest( + 'resets the ball if the game is not over', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await game.ready(); // Making sure that all additions are done + + expect( + game.children.whereType().length, + equals(1), + ); + }, + ) + ..widgetTest( + 'no ball is added on game over', + (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState(score: 10, balls: 1), + ); + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await tester.pump(); + + expect( + game.children.whereType().length, + equals(0), + ); + }, + ); + }); }); } diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 955ce763..2e005773 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -1,4 +1,5 @@ -import 'package:flame/game.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; @@ -6,9 +7,42 @@ import '../../helpers/helpers.dart'; void main() { group('PinballGamePage', () { - testWidgets('renders single GameWidget with PinballGame', (tester) async { - await tester.pumpApp(const PinballGamePage()); - expect(find.byType(GameWidget), findsOneWidget); + testWidgets('renders PinballGameView', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp(const PinballGamePage(), gameBloc: gameBloc); + expect(find.byType(PinballGameView), 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(PinballGamePage.route()); + }, + child: const Text('Tap me'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Tap me')); + + // 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 + + expect(find.byType(PinballGamePage), findsOneWidget); }); }); } diff --git a/test/game/view/pinball_game_view_test.dart b/test/game/view/pinball_game_view_test.dart new file mode 100644 index 00000000..c00db60f --- /dev/null +++ b/test/game/view/pinball_game_view_test.dart @@ -0,0 +1,46 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/game.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('PinballGameView', () { + testWidgets('renders', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + expect( + find.byWidgetPredicate((w) => w is GameWidget), + findsOneWidget, + ); + }); + + testWidgets( + 'renders a game over dialog when the user has lost', + (tester) async { + final gameBloc = MockGameBloc(); + const state = GameState(score: 0, balls: 0); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + await tester.pump(); + + expect( + find.text('Game Over'), + findsOneWidget, + ); + }, + ); + }); +} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 97ca4590..ed5d4b1c 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -6,15 +6,20 @@ // 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:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +class MockGameBloc extends Mock implements GameBloc {} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { MockNavigator? navigator, + GameBloc? gameBloc, }) { return pumpWidget( MaterialApp( @@ -23,9 +28,12 @@ extension PumpApp on WidgetTester { GlobalMaterialLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + home: BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + child: navigator != null + ? MockNavigatorProvider(navigator: navigator, child: widget) + : widget, + ), ), ); } From 92590b9c827521613e20d029ddb550dabc7b1002 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Wed, 2 Mar 2022 15:23:07 -0600 Subject: [PATCH 03/53] docs: plunger issue comments --- lib/game/components/plunger.dart | 3 ++- lib/game/pinball_game.dart | 37 +++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 7b168629..d52c7c31 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,4 +1,3 @@ -import 'package:flame_forge2d/body_component.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class Plunger extends BodyComponent { @@ -20,10 +19,12 @@ class Plunger extends BodyComponent { return world.createBody(bodyDef)..createFixture(fixtureDef); } + // Unused for now - from the previous kinematic plunger implementation. void pull() { body.linearVelocity = Vector2(0, -5); } + // Unused for now - from the previous kinematic plunger implementation. void release() { final velocity = (_position.y - body.position.y) * 9; body.linearVelocity = Vector2(0, velocity); diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 597c53ca..ff112a43 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,6 +7,7 @@ import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { late Plunger plunger; + late PrismaticJointDef prismaticJointDef; @override Future onLoad() async { @@ -20,22 +21,30 @@ class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { await add(plunger = Plunger(Vector2(center.x, center.y - 50))); - final prismaticJointDef = PrismaticJointDef() + prismaticJointDef = PrismaticJointDef() ..initialize( plunger.body, bottomWall.body, - bottomWall.body.position, - Vector2(0, 0), + plunger.body.position, + // Logically, I feel like this should be (0, 1), but it has to be + // negative for lowerTranslation limit to work as expected. + Vector2(0, -1), ) - ..localAnchorA.setFrom(Vector2(0, 0)) ..enableLimit = true - ..upperTranslation = 0 - ..lowerTranslation = -5 + // Given the above inverted vertical axis, the lowerTranslation works as + // expected and this lets the plunger fall down 10 units before being + // stopped. + // + // Ideally, we shouldn't need to set any limits here - this is just for + // demo purposes to see how the limits work. We should be leaving this at + // 0 and altering it as the user holds the space bar. The longer they hold + // it, the lower the lowerTranslation becomes - allowing the plunger to + // slowly fall down (see key event handlers below). + ..lowerTranslation = -10 + // This prevents the plunger from falling through the bottom wall. ..collideConnected = true; world.createJoint(prismaticJointDef); - print(prismaticJointDef.localAnchorA); - print(prismaticJointDef.localAnchorB); } @override @@ -45,11 +54,19 @@ class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { ) { if (event is RawKeyUpEvent && event.data.logicalKey == LogicalKeyboardKey.space) { - plunger.release(); + // I haven't been able to successfully pull down the plunger, so this is + // completely untested. I imagine we could calculate the distance between + // the prismaticJoinDef.upperTranslation (plunger starting position) and + // the ground, then use that value as a multiplier on the speed so the + // ball moves faster when you pull the plunger farther down. + prismaticJointDef.motorSpeed = 5; } if (event is RawKeyDownEvent && event.data.logicalKey == LogicalKeyboardKey.space) { - plunger.pull(); + // This was my attempt to decrement the lower limit but it doesn't seem to + // render. If you debug, you can see that this value is being lowered, + // but the game isn't reflecting these value changes. + prismaticJointDef.lowerTranslation--; } return KeyEventResult.handled; } From ad6afddf155125682c826c0fabe8108eda373b55 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 2 Mar 2022 18:24:45 -0300 Subject: [PATCH 04/53] fix: rebasing issues --- lib/game/components/ball.dart | 1 - lib/game/pinball_game.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 3aa978d3..39257bb6 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -6,7 +6,6 @@ import 'package:pinball/game/game.dart'; class Ball extends BodyComponent with BlocComponent { - Ball({ required Vector2 position, }) : _position = position { diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 0c160316..2ee242da 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; class BallWallContactCallback extends ContactCallback { From 8865c2f32e8ade17ca12eb8ce2b2c3c56f6f7c7f Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 2 Mar 2022 18:35:42 -0300 Subject: [PATCH 05/53] fix: lint --- test/game/pinball_game_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index a576252b..582fc11b 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/game.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; From fbbd04d91b482552c1c9017ecfb0171579674ba2 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 3 Mar 2022 09:16:45 -0300 Subject: [PATCH 06/53] Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- test/game/view/pinball_game_view_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/game/view/pinball_game_view_test.dart b/test/game/view/pinball_game_view_test.dart index c00db60f..4f4fe246 100644 --- a/test/game/view/pinball_game_view_test.dart +++ b/test/game/view/pinball_game_view_test.dart @@ -7,7 +7,7 @@ import '../../helpers/helpers.dart'; void main() { group('PinballGameView', () { - testWidgets('renders', (tester) async { + testWidgets('renders game', (tester) async { final gameBloc = MockGameBloc(); whenListen( gameBloc, From 0eac420e83f8c427b5eb4cb0b7eeec2a6270bd53 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 08:11:14 -0600 Subject: [PATCH 07/53] feat: add themes --- lib/character_themes/android_theme.dart | 9 +++++++++ lib/character_themes/character_theme.dart | 15 +++++++++++++++ lib/character_themes/character_themes.dart | 6 ++++++ lib/character_themes/dash_theme.dart | 9 +++++++++ lib/character_themes/dino_theme.dart | 9 +++++++++ lib/character_themes/sparky_theme.dart | 9 +++++++++ 6 files changed, 57 insertions(+) create mode 100644 lib/character_themes/android_theme.dart create mode 100644 lib/character_themes/character_theme.dart create mode 100644 lib/character_themes/character_themes.dart create mode 100644 lib/character_themes/dash_theme.dart create mode 100644 lib/character_themes/dino_theme.dart create mode 100644 lib/character_themes/sparky_theme.dart diff --git a/lib/character_themes/android_theme.dart b/lib/character_themes/android_theme.dart new file mode 100644 index 00000000..8c91d9e1 --- /dev/null +++ b/lib/character_themes/android_theme.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +class AndroidTheme extends CharacterTheme { + const AndroidTheme(); + + @override + Color get ballColor => Colors.green; +} diff --git a/lib/character_themes/character_theme.dart b/lib/character_themes/character_theme.dart new file mode 100644 index 00000000..d755a2c7 --- /dev/null +++ b/lib/character_themes/character_theme.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; +import 'package:flame/palette.dart'; + +/// {@template character_theme} +/// Template for creating character themes. +/// {@endtemplate} +abstract class CharacterTheme extends Equatable { + /// {@macro character_theme} + const CharacterTheme(); + + Color get ballColor; + + @override + List get props => []; +} diff --git a/lib/character_themes/character_themes.dart b/lib/character_themes/character_themes.dart new file mode 100644 index 00000000..21f1efd5 --- /dev/null +++ b/lib/character_themes/character_themes.dart @@ -0,0 +1,6 @@ +export 'android_theme.dart'; +export 'character_theme.dart'; +export 'cubit/theme_cubit.dart'; +export 'dash_theme.dart'; +export 'dino_theme.dart'; +export 'sparky_theme.dart'; diff --git a/lib/character_themes/dash_theme.dart b/lib/character_themes/dash_theme.dart new file mode 100644 index 00000000..caaf2bdf --- /dev/null +++ b/lib/character_themes/dash_theme.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +class DashTheme extends CharacterTheme { + const DashTheme(); + + @override + Color get ballColor => Colors.blue; +} diff --git a/lib/character_themes/dino_theme.dart b/lib/character_themes/dino_theme.dart new file mode 100644 index 00000000..27f76199 --- /dev/null +++ b/lib/character_themes/dino_theme.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +class DinoTheme extends CharacterTheme { + const DinoTheme(); + + @override + Color get ballColor => Colors.grey; +} diff --git a/lib/character_themes/sparky_theme.dart b/lib/character_themes/sparky_theme.dart new file mode 100644 index 00000000..63d8d241 --- /dev/null +++ b/lib/character_themes/sparky_theme.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +class SparkyTheme extends CharacterTheme { + const SparkyTheme(); + + @override + Color get ballColor => Colors.orange; +} From dba7761e01bf9dbcd881244a7ca14ccd12340007 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 14:49:48 -0300 Subject: [PATCH 08/53] feat: addressing PR comments --- lib/game/bloc/game_state.dart | 3 + lib/game/components/ball.dart | 11 +- lib/game/components/wall.dart | 42 +++++++- lib/game/pinball_game.dart | 31 +----- lib/game/view/pinball_game_view.dart | 10 +- lib/game/view/view.dart | 1 + lib/game/view/widgets/game_over_dialog.dart | 18 ++++ lib/game/view/widgets/widgets.dart | 1 + test/game/bloc/game_state_test.dart | 26 +++++ test/game/components/ball_test.dart | 70 +++++++++++++ test/game/components/wall_test.dart | 60 +++++++++-- test/game/pinball_game_test.dart | 105 +------------------- test/helpers/builders.dart | 17 ++++ test/helpers/helpers.dart | 2 + test/helpers/mocks.dart | 13 +++ test/helpers/pump_app.dart | 2 +- 16 files changed, 259 insertions(+), 153 deletions(-) create mode 100644 lib/game/view/widgets/game_over_dialog.dart create mode 100644 lib/game/view/widgets/widgets.dart create mode 100644 test/helpers/builders.dart create mode 100644 test/helpers/mocks.dart diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 235e264d..862edf96 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -26,6 +26,9 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; + /// Determines when player has only one chance left. + bool get isLastBall => balls == 1; + GameState copyWith({ int? score, int? balls, diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 39257bb6..fd5f5605 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -29,18 +29,15 @@ class Ball extends BodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } - @override - void onRemove() { + void ballLost() { final bloc = gameRef.read(); - final shouldBallrespwan = bloc.state.balls > 1; + final shouldBallRespwan = bloc.state.balls > 1; bloc.add(const BallLost()); - if (shouldBallrespwan) { - gameRef.resetBall(); + if (shouldBallRespwan) { + gameRef.spawnBall(); } - - super.onRemove(); } } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index de93e1ce..c80e5bb3 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -1,10 +1,50 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/components/components.dart'; + +class BallWallContactCallback extends ContactCallback { + @override + void begin(Ball ball, Wall wall, Contact contact) { + if (wall.type == WallType.bottom) { + ball + ..ballLost() + ..shouldRemove = true; + } + } + + @override + void end(_, __, ___) {} +} + +enum WallType { + top, + bottom, + left, + right, +} class Wall extends BodyComponent { - Wall(this.start, this.end); + Wall({ + required this.type, + required this.start, + required this.end, + }); + + factory Wall.bottom(Forge2DGame game) { + final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); + final bottomLeft = Vector2(0, bottomRight.y); + + return Wall( + type: WallType.bottom, + start: bottomRight, + end: bottomLeft, + ); + } final Vector2 start; final Vector2 end; + final WallType type; @override Body createBody() { diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2ee242da..3d586f12 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,21 +1,9 @@ -// ignore_for_file: avoid_renaming_method_parameters - import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; -class BallWallContactCallback extends ContactCallback { - @override - void begin(Ball ball, Wall wall, Contact contact) { - ball.shouldRemove = true; - } - - @override - void end(_, __, ___) {} -} - class PinballGame extends Forge2DGame with FlameBloc { - void resetBall() { + void spawnBall() { add(Ball(position: ballStartingPosition)); } @@ -25,25 +13,14 @@ class PinballGame extends Forge2DGame with FlameBloc { camera.viewport.effectiveSize.y - 20, ), ) - - Vector2( - 0, - -20, - ); + Vector2(0, -20); @override Future onLoad() async { - await super.onLoad(); + spawnBall(); addContactCallback(BallScorePointsCallback()); - final topLeft = Vector2.zero(); - final bottomRight = screenToWorld(camera.viewport.effectiveSize); - final bottomLeft = Vector2(topLeft.x, bottomRight.y); - - await add( - Wall(bottomRight, bottomLeft), - ); + await add(Wall.bottom(this)); addContactCallback(BallWallContactCallback()); - - resetBall(); } } diff --git a/lib/game/view/pinball_game_view.dart b/lib/game/view/pinball_game_view.dart index 5fbc8ae8..44715c25 100644 --- a/lib/game/view/pinball_game_view.dart +++ b/lib/game/view/pinball_game_view.dart @@ -14,15 +14,7 @@ class PinballGameView extends StatelessWidget { showDialog( context: context, builder: (_) { - return const Dialog( - child: SizedBox( - width: 200, - height: 200, - child: Center( - child: Text('Game Over'), - ), - ), - ); + return const GameOverDialog(); }, ); } diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart index 66134941..39cdb071 100644 --- a/lib/game/view/view.dart +++ b/lib/game/view/view.dart @@ -1,2 +1,3 @@ export 'pinball_game_page.dart'; export 'pinball_game_view.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart new file mode 100644 index 00000000..586d6c56 --- /dev/null +++ b/lib/game/view/widgets/game_over_dialog.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class GameOverDialog extends StatelessWidget { + const GameOverDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Dialog( + child: SizedBox( + width: 200, + height: 200, + child: Center( + child: Text('Game Over'), + ), + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart new file mode 100644 index 00000000..9c457b1c --- /dev/null +++ b/lib/game/view/widgets/widgets.dart @@ -0,0 +1 @@ +export 'game_over_dialog.dart'; diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index f62bae67..c5131492 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -62,6 +62,32 @@ void main() { }); }); + group('isGameOver', () { + test( + 'is true ' + 'when there is only on ball left', + () { + const gameState = GameState( + balls: 1, + score: 0, + ); + expect(gameState.isLastBall, isTrue); + }, + ); + + test( + 'is false ' + 'when there are more balls left', + () { + const gameState = GameState( + balls: 2, + score: 0, + ); + expect(gameState.isLastBall, isFalse); + }, + ); + }); + group('copyWith', () { test( 'throws AssertionError ' diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index c4576c68..8521a80f 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -1,10 +1,14 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; 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 '../../helpers/helpers.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -79,5 +83,71 @@ void main() { }, ); }); + + group('resetting a ball', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final tester = flameBlocTester( + gameBlocBuilder: () { + return gameBloc; + }, + ); + + tester.widgetTest( + 'adds BallLost to GameBloc', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.ballLost(); + await tester.pump(); + + verify(() => gameBloc.add(const BallLost())).called(1); + }, + ); + + tester.widgetTest( + 'resets the ball if the game is not over', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await game.ready(); // Making sure that all additions are done + + expect( + game.children.whereType().length, + equals(1), + ); + }, + ); + + tester.widgetTest( + 'no ball is added on game over', + (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState(score: 10, balls: 1), + ); + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await tester.pump(); + + expect( + game.children.whereType().length, + equals(0), + ); + }, + ); + }); }); } diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index e17537c0..2c4b241a 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -3,18 +3,46 @@ import 'package:flame_forge2d/flame_forge2d.dart'; 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 '../../helpers/helpers.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Wall', () { + group('BallWallContactCallback', () { + test( + 'removes the ball on begin contact when the wall is a bottom one', + () { + final game = MockPinballGame(); + final wall = MockWall(); + final ball = MockBall(); + + when(() => wall.type).thenReturn(WallType.bottom); + when(() => ball.gameRef).thenReturn(game); + + BallWallContactCallback() + // Remove once https://github.com/flame-engine/flame/pull/1415 + // is merged + ..end(MockBall(), MockWall(), MockContact()) + ..begin(ball, wall, MockContact()); + + verify(() => ball.shouldRemove = true).called(1); + }, + ); + }); final flameTester = FlameTester(PinballGame.new); flameTester.test( 'loads correctly', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.bottom, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); expect(game.contains(wall), isTrue); @@ -25,7 +53,11 @@ void main() { flameTester.test( 'positions correctly', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); game.contains(wall); @@ -36,7 +68,11 @@ void main() { flameTester.test( 'is static', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); expect(wall.body.bodyType, equals(BodyType.static)); @@ -48,7 +84,11 @@ void main() { flameTester.test( 'exists', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); expect(wall.body.fixtures[0], isA()); @@ -58,7 +98,11 @@ void main() { flameTester.test( 'has restitution equals 0', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); final fixture = wall.body.fixtures[0]; @@ -69,7 +113,11 @@ void main() { flameTester.test( 'has friction', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); final fixture = wall.body.fixtures[0]; diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 582fc11b..75a77aa9 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,108 +1,9 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/game/game.dart'; - -class MockPinballGame extends Mock implements PinballGame {} - -class MockWall extends Mock implements Wall {} - -class MockBall extends Mock implements Ball {} - -class MockContact extends Mock implements Contact {} - -class MockGameBloc extends Mock implements GameBloc {} void main() { - // TODO(alestiago): test if [PinballGame] registers - // [BallScorePointsCallback] once the following issue is resolved: - // https://github.com/flame-engine/flame/issues/1416 group('PinballGame', () { - group('BallWallContactCallback', () { - test('removes the ball on begin contact', () { - final game = MockPinballGame(); - final wall = MockWall(); - final ball = MockBall(); - - when(() => ball.gameRef).thenReturn(game); - - BallWallContactCallback() - // Remove once https://github.com/flame-engine/flame/pull/1415 - // is merged - ..end(MockBall(), MockWall(), MockContact()) - ..begin(ball, wall, MockContact()); - - verify(() => ball.shouldRemove = true).called(1); - }); - }); - - group('resetting a ball', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - FlameTester( - PinballGame.new, - pumpWidget: (gameWidget, tester) async { - await tester.pumpWidget( - BlocProvider.value( - value: gameBloc, - child: gameWidget, - ), - ); - }, - ) - ..widgetTest('adds BallLost to GameBloc', (game, tester) async { - await game.ready(); - - game.children.whereType().first.removeFromParent(); - await tester.pump(); - - verify(() => gameBloc.add(const BallLost())).called(1); - }) - ..widgetTest( - 'resets the ball if the game is not over', - (game, tester) async { - await game.ready(); - - game.children.whereType().first.removeFromParent(); - await game.ready(); // Making sure that all additions are done - - expect( - game.children.whereType().length, - equals(1), - ); - }, - ) - ..widgetTest( - 'no ball is added on game over', - (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState(score: 10, balls: 1), - ); - await game.ready(); - - game.children.whereType().first.removeFromParent(); - await tester.pump(); - - expect( - game.children.whereType().length, - equals(0), - ); - }, - ); - }); + // TODO(alestiago): test if [PinballGame] registers + // [BallScorePointsCallback] once the following issue is resolved: + // https://github.com/flame-engine/flame/issues/1416 }); } diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart new file mode 100644 index 00000000..5ef98226 --- /dev/null +++ b/test/helpers/builders.dart @@ -0,0 +1,17 @@ +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; + +FlameTester flameBlocTester({required GameBloc Function() gameBlocBuilder}) { + return FlameTester( + PinballGame.new, + pumpWidget: (gameWidget, tester) async { + await tester.pumpWidget( + BlocProvider.value( + value: gameBlocBuilder(), + child: gameWidget, + ), + ); + }, + ); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 695f8309..97bc22be 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -5,4 +5,6 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +export 'builders.dart'; +export 'mocks.dart'; export 'pump_app.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart new file mode 100644 index 00000000..559ea87a --- /dev/null +++ b/test/helpers/mocks.dart @@ -0,0 +1,13 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +class MockPinballGame extends Mock implements PinballGame {} + +class MockWall extends Mock implements Wall {} + +class MockBall extends Mock implements Ball {} + +class MockContact extends Mock implements Contact {} + +class MockGameBloc extends Mock implements GameBloc {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index ed5d4b1c..2c1efd9f 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -13,7 +13,7 @@ import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -class MockGameBloc extends Mock implements GameBloc {} +import 'helpers.dart'; extension PumpApp on WidgetTester { Future pumpApp( From ca989eac8d875443096f26148ae457ef92bf1641 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 15:05:11 -0300 Subject: [PATCH 09/53] feat: addressing PR comments --- lib/game/view/pinball_game_page.dart | 22 +++++++++++ lib/game/view/pinball_game_view.dart | 25 ------------ lib/game/view/view.dart | 1 - test/game/view/pinball_game_page_test.dart | 39 ++++++++++++++++++ test/game/view/pinball_game_view_test.dart | 46 ---------------------- 5 files changed, 61 insertions(+), 72 deletions(-) delete mode 100644 lib/game/view/pinball_game_view.dart delete mode 100644 test/game/view/pinball_game_view_test.dart diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index c4387f34..28834907 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -1,3 +1,4 @@ +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; @@ -21,3 +22,24 @@ class PinballGamePage extends StatelessWidget { return const PinballGameView(); } } + +class PinballGameView extends StatelessWidget { + const PinballGameView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.isGameOver) { + showDialog( + context: context, + builder: (_) { + return const GameOverDialog(); + }, + ); + } + }, + child: GameWidget(game: PinballGame()), + ); + } +} diff --git a/lib/game/view/pinball_game_view.dart b/lib/game/view/pinball_game_view.dart deleted file mode 100644 index 44715c25..00000000 --- a/lib/game/view/pinball_game_view.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flame/game.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/game/game.dart'; - -class PinballGameView extends StatelessWidget { - const PinballGameView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state.isGameOver) { - showDialog( - context: context, - builder: (_) { - return const GameOverDialog(); - }, - ); - } - }, - child: GameWidget(game: PinballGame()), - ); - } -} diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart index 39cdb071..53d3813a 100644 --- a/lib/game/view/view.dart +++ b/lib/game/view/view.dart @@ -1,3 +1,2 @@ export 'pinball_game_page.dart'; -export 'pinball_game_view.dart'; export 'widgets/widgets.dart'; diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 2e005773..be418c1d 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; @@ -45,4 +46,42 @@ void main() { expect(find.byType(PinballGamePage), findsOneWidget); }); }); + + group('PinballGameView', () { + testWidgets('renders game', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + expect( + find.byWidgetPredicate((w) => w is GameWidget), + findsOneWidget, + ); + }); + + testWidgets( + 'renders a game over dialog when the user has lost', + (tester) async { + final gameBloc = MockGameBloc(); + const state = GameState(score: 0, balls: 0); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + await tester.pump(); + + expect( + find.text('Game Over'), + findsOneWidget, + ); + }, + ); + }); } diff --git a/test/game/view/pinball_game_view_test.dart b/test/game/view/pinball_game_view_test.dart deleted file mode 100644 index 4f4fe246..00000000 --- a/test/game/view/pinball_game_view_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/game.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('PinballGameView', () { - testWidgets('renders game', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - - await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); - expect( - find.byWidgetPredicate((w) => w is GameWidget), - findsOneWidget, - ); - }); - - testWidgets( - 'renders a game over dialog when the user has lost', - (tester) async { - final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0); - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); - - await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); - await tester.pump(); - - expect( - find.text('Game Over'), - findsOneWidget, - ); - }, - ); - }); -} From 174cbae1bf93eff63ddb32a43f568822b6cac515 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 3 Mar 2022 15:11:40 -0300 Subject: [PATCH 10/53] Update lib/game/pinball_game.dart --- lib/game/pinball_game.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 3d586f12..934d958e 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,6 +7,7 @@ class PinballGame extends Forge2DGame with FlameBloc { add(Ball(position: ballStartingPosition)); } + // TODO(erickzanardo): Change to the plumber position late final ballStartingPosition = screenToWorld( Vector2( camera.viewport.effectiveSize.x / 2, From d5c84acd8eb5b4d299a58f902091120f9d7fed32 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 15:27:43 -0300 Subject: [PATCH 11/53] feat: making use of isLastBall on ball component --- lib/game/components/ball.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index fd5f5605..04d3cf6f 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -32,7 +32,7 @@ class Ball extends BodyComponent void ballLost() { final bloc = gameRef.read(); - final shouldBallRespwan = bloc.state.balls > 1; + final shouldBallRespwan = !bloc.state.isLastBall; bloc.add(const BallLost()); From 7048618ce5e68cdd2da8584df7a205d373eb1c57 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 13:16:52 -0600 Subject: [PATCH 12/53] test: plunger and joint --- lib/game/components/plunger.dart | 46 +++++- lib/game/pinball_game.dart | 42 +----- test/game/components/plunger_test.dart | 192 +++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 41 deletions(-) create mode 100644 test/game/components/plunger_test.dart diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index d52c7c31..2b55cdc7 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,6 +1,14 @@ import 'package:flame_forge2d/flame_forge2d.dart'; +/// {@template plunger} +/// Plunger body component to be pulled and released by the player to launch +/// the pinball. +/// +/// The plunger body ignores gravity so the player can control its downward +/// pull. +/// {@endtemplate} class Plunger extends BodyComponent { + /// {@macro plunger} Plunger(this._position); final Vector2 _position; @@ -9,24 +17,52 @@ class Plunger extends BodyComponent { Body createBody() { final shape = PolygonShape()..setAsBoxXY(2.5, 1.5); - final fixtureDef = FixtureDef(shape)..friction = 0.1; + final fixtureDef = FixtureDef(shape); final bodyDef = BodyDef() ..userData = this ..position = _position - ..type = BodyType.dynamic; + ..type = BodyType.dynamic + ..gravityScale = 0; return world.createBody(bodyDef)..createFixture(fixtureDef); } - // Unused for now - from the previous kinematic plunger implementation. + /// Set a contstant downward velocity on the plunger body. void pull() { - body.linearVelocity = Vector2(0, -5); + body.linearVelocity = Vector2(0, -7); } - // Unused for now - from the previous kinematic plunger implementation. + /// Set an upward velocity on the plunger body. The velocity's magnitude + /// depends on how far the plunger has been pulled from its original position. void release() { final velocity = (_position.y - body.position.y) * 9; body.linearVelocity = Vector2(0, velocity); } } + +/// {@template plunger_anchor_prismatic_joint_def} +/// Prismatic joint def between a [Plunger] and an anchor body given motion on +/// the vertical axis. +/// +/// The [Plunger] is constrained to vertical motion between its starting +/// position and the anchor body. The anchor needs to be below the plunger for +/// this joint to function properly. +/// {@endtemplate} +class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { + /// {@macro plunger_anchor_prismatic_joint_def} + PlungerAnchorPrismaticJointDef({ + required Plunger plunger, + required BodyComponent anchor, + }) { + initialize( + plunger.body, + anchor.body, + anchor.body.position, + Vector2(0, -1), + ); + enableLimit = true; + lowerTranslation = double.negativeInfinity; + collideConnected = true; + } +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index ff112a43..fbcea02f 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,7 +7,6 @@ import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { late Plunger plunger; - late PrismaticJointDef prismaticJointDef; @override Future onLoad() async { @@ -19,32 +18,11 @@ class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { final center = screenToWorld(camera.viewport.effectiveSize / 2); - await add(plunger = Plunger(Vector2(center.x, center.y - 50))); + await add(plunger = Plunger(Vector2(center.x, center.y))); - prismaticJointDef = PrismaticJointDef() - ..initialize( - plunger.body, - bottomWall.body, - plunger.body.position, - // Logically, I feel like this should be (0, 1), but it has to be - // negative for lowerTranslation limit to work as expected. - Vector2(0, -1), - ) - ..enableLimit = true - // Given the above inverted vertical axis, the lowerTranslation works as - // expected and this lets the plunger fall down 10 units before being - // stopped. - // - // Ideally, we shouldn't need to set any limits here - this is just for - // demo purposes to see how the limits work. We should be leaving this at - // 0 and altering it as the user holds the space bar. The longer they hold - // it, the lower the lowerTranslation becomes - allowing the plunger to - // slowly fall down (see key event handlers below). - ..lowerTranslation = -10 - // This prevents the plunger from falling through the bottom wall. - ..collideConnected = true; - - world.createJoint(prismaticJointDef); + world.createJoint( + PlungerAnchorPrismaticJointDef(plunger: plunger, anchor: bottomWall), + ); } @override @@ -54,19 +32,11 @@ class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { ) { if (event is RawKeyUpEvent && event.data.logicalKey == LogicalKeyboardKey.space) { - // I haven't been able to successfully pull down the plunger, so this is - // completely untested. I imagine we could calculate the distance between - // the prismaticJoinDef.upperTranslation (plunger starting position) and - // the ground, then use that value as a multiplier on the speed so the - // ball moves faster when you pull the plunger farther down. - prismaticJointDef.motorSpeed = 5; + plunger.release(); } if (event is RawKeyDownEvent && event.data.logicalKey == LogicalKeyboardKey.space) { - // This was my attempt to decrement the lower limit but it doesn't seem to - // render. If you debug, you can see that this value is being lowered, - // but the game isn't reflecting these value changes. - prismaticJointDef.lowerTranslation--; + plunger.pull(); } return KeyEventResult.handled; } diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart new file mode 100644 index 00000000..6156c5ff --- /dev/null +++ b/test/game/components/plunger_test.dart @@ -0,0 +1,192 @@ +// ignore_for_file: cascade_invocations + +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGame.new); + + group('Plunger', () { + flameTester.test( + 'loads correctly', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(game.contains(plunger), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final plunger = Plunger(position); + await game.ensureAdd(plunger); + game.contains(plunger); + + expect(plunger.body.position, position); + }, + ); + + flameTester.test( + 'is dynamic', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(plunger.body.bodyType, equals(BodyType.dynamic)); + }, + ); + + flameTester.test( + 'ignores gravity', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(plunger.body.gravityScale, isZero); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(plunger.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'shape is a polygon', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + final fixture = plunger.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.polygon)); + }, + ); + }); + + flameTester.test( + 'pull sets a negative linear velocity', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + plunger.pull(); + + expect(plunger.body.linearVelocity.y, isNegative); + }, + ); + + group('release', () { + flameTester.test( + 'does not set a linear velocity ' + 'when plunger is in starting position', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + plunger.release(); + + expect(plunger.body.linearVelocity.y, isZero); + }, + ); + + flameTester.test( + 'sets a positive linear velocity ' + 'when plunger is below starting position', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + plunger.body.setTransform(Vector2(0, -1), 0); + plunger.release(); + + expect(plunger.body.linearVelocity.y, isPositive); + }, + ); + }); + }); + + group('PlungerAnchorPrismaticJointDef', () { + final plunger = Plunger(Vector2.zero())..createBody(); + final anchor = Plunger(Vector2(0, -5))..createBody(); + + group('initializes with', () { + flameTester.test( + 'plunger as bodyA', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + + expect(jointDef.bodyA, equals(plunger)); + }, + ); + + flameTester.test( + 'anchor as bodyB', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.bodyB, equals(anchor)); + }, + ); + + flameTester.test( + 'limits enabled', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.enableLimit, isTrue); + }, + ); + + flameTester.test( + 'lower translation limit as negative infinity', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); + }, + ); + + flameTester.test( + 'connected body collison enabled', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.collideConnected, isTrue); + }, + ); + }); + }); +} From 902d3092ee3aeffa4ff072051f14671a36bf8373 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 16:40:27 -0300 Subject: [PATCH 13/53] fix: plunger tests --- test/game/components/plunger_test.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 6156c5ff..f310874d 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -120,13 +120,20 @@ void main() { }); group('PlungerAnchorPrismaticJointDef', () { - final plunger = Plunger(Vector2.zero())..createBody(); - final anchor = Plunger(Vector2(0, -5))..createBody(); + late Plunger plunger; + late Plunger anchor; + + setUp(() { + plunger = Plunger(Vector2.zero()); + anchor = Plunger(Vector2(0, -5)); + }); group('initializes with', () { flameTester.test( 'plunger as bodyA', (game) async { + await game.ensureAddAll([plunger, anchor]); + final jointDef = PlungerAnchorPrismaticJointDef( plunger: plunger, anchor: anchor, @@ -139,6 +146,8 @@ void main() { flameTester.test( 'anchor as bodyB', (game) async { + await game.ensureAddAll([plunger, anchor]); + final jointDef = PlungerAnchorPrismaticJointDef( plunger: plunger, anchor: anchor, @@ -152,6 +161,8 @@ void main() { flameTester.test( 'limits enabled', (game) async { + await game.ensureAddAll([plunger, anchor]); + final jointDef = PlungerAnchorPrismaticJointDef( plunger: plunger, anchor: anchor, @@ -165,6 +176,8 @@ void main() { flameTester.test( 'lower translation limit as negative infinity', (game) async { + await game.ensureAddAll([plunger, anchor]); + final jointDef = PlungerAnchorPrismaticJointDef( plunger: plunger, anchor: anchor, @@ -178,6 +191,8 @@ void main() { flameTester.test( 'connected body collison enabled', (game) async { + await game.ensureAddAll([plunger, anchor]); + final jointDef = PlungerAnchorPrismaticJointDef( plunger: plunger, anchor: anchor, From adcc43d04a4ea31853daf1af733b2954d9d4f621 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 3 Mar 2022 17:00:08 -0300 Subject: [PATCH 14/53] Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- test/game/bloc/game_state_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index c5131492..59cc0d1d 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -62,10 +62,10 @@ void main() { }); }); - group('isGameOver', () { + group('isLastBall', () { test( 'is true ' - 'when there is only on ball left', + 'when there is only one ball left', () { const gameState = GameState( balls: 1, From 4ccd172b4fdb01772a735b3b7279ce6861ebaf11 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 17:05:09 -0300 Subject: [PATCH 15/53] feat: pr suggestions --- lib/game/components/wall.dart | 38 ++++++++++++++--------------- test/game/components/wall_test.dart | 10 ++------ 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index c80e5bb3..bb527c3b 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -3,32 +3,16 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; -class BallWallContactCallback extends ContactCallback { - @override - void begin(Ball ball, Wall wall, Contact contact) { - if (wall.type == WallType.bottom) { - ball - ..ballLost() - ..shouldRemove = true; - } - } - - @override - void end(_, __, ___) {} -} - enum WallType { - top, - bottom, - left, - right, + fatal, + passive, } class Wall extends BodyComponent { Wall({ - required this.type, required this.start, required this.end, + this.type = WallType.passive, }); factory Wall.bottom(Forge2DGame game) { @@ -36,7 +20,7 @@ class Wall extends BodyComponent { final bottomLeft = Vector2(0, bottomRight.y); return Wall( - type: WallType.bottom, + type: WallType.fatal, start: bottomRight, end: bottomLeft, ); @@ -62,3 +46,17 @@ class Wall extends BodyComponent { return world.createBody(bodyDef)..createFixture(fixtureDef); } } + +class BallWallContactCallback extends ContactCallback { + @override + void begin(Ball ball, Wall wall, Contact contact) { + if (wall.type == WallType.fatal) { + ball + ..ballLost() + ..shouldRemove = true; + } + } + + @override + void end(_, __, ___) {} +} diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 2c4b241a..1ff5df95 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -14,13 +14,13 @@ void main() { group('Wall', () { group('BallWallContactCallback', () { test( - 'removes the ball on begin contact when the wall is a bottom one', + 'removes the ball on begin contact when the wall is a fatal one', () { final game = MockPinballGame(); final wall = MockWall(); final ball = MockBall(); - when(() => wall.type).thenReturn(WallType.bottom); + when(() => wall.type).thenReturn(WallType.fatal); when(() => ball.gameRef).thenReturn(game); BallWallContactCallback() @@ -39,7 +39,6 @@ void main() { 'loads correctly', (game) async { final wall = Wall( - type: WallType.bottom, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -54,7 +53,6 @@ void main() { 'positions correctly', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -69,7 +67,6 @@ void main() { 'is static', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -85,7 +82,6 @@ void main() { 'exists', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -99,7 +95,6 @@ void main() { 'has restitution equals 0', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -114,7 +109,6 @@ void main() { 'has friction', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); From 85936df74e8c0de7905ccbaec233b1a5de6ab76d Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 14:18:29 -0600 Subject: [PATCH 16/53] test: joint tests --- lib/game/components/boundaries.dart | 41 --------------------- lib/game/components/components.dart | 1 - lib/game/pinball_game.dart | 35 ++---------------- test/game/components/plunger_test.dart | 49 +++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 80 deletions(-) delete mode 100644 lib/game/components/boundaries.dart diff --git a/lib/game/components/boundaries.dart b/lib/game/components/boundaries.dart deleted file mode 100644 index 08e9a2c3..00000000 --- a/lib/game/components/boundaries.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flame_forge2d/body_component.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; -import 'package:forge2d/forge2d.dart'; - -List createBoundaries(Forge2DGame game) { - final topLeft = Vector2.zero(); - final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); - final topRight = Vector2(bottomRight.x, topLeft.y); - final bottomLeft = Vector2(topLeft.x, bottomRight.y); - - return [ - Wall(topLeft, topRight), - Wall(topRight, bottomRight), - Wall(bottomRight, bottomLeft), - Wall(bottomLeft, topLeft), - ]; -} - -class Wall extends BodyComponent { - Wall(this.start, this.end); - - final Vector2 start; - final Vector2 end; - - @override - Body createBody() { - final shape = EdgeShape()..set(start, end); - - final fixtureDef = FixtureDef(shape) - ..restitution = 0.0 - ..friction = 0.3; - - final bodyDef = BodyDef() - ..userData = this - ..position = Vector2.zero() - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 97f7eb69..28541aed 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,3 @@ export 'ball.dart'; -export 'boundaries.dart'; export 'plunger.dart'; export 'score_points.dart'; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index fbcea02f..80b4ffea 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,43 +1,12 @@ -import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:pinball/game/game.dart'; -class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { - late Plunger plunger; +import 'package:pinball/game/game.dart'; +class PinballGame extends Forge2DGame with FlameBloc { @override Future onLoad() async { await super.onLoad(); addContactCallback(BallScorePointsCallback()); - - final boundaries = createBoundaries(this)..forEach(add); - final bottomWall = boundaries[2]; - - final center = screenToWorld(camera.viewport.effectiveSize / 2); - - await add(plunger = Plunger(Vector2(center.x, center.y))); - - world.createJoint( - PlungerAnchorPrismaticJointDef(plunger: plunger, anchor: bottomWall), - ); - } - - @override - KeyEventResult onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - if (event is RawKeyUpEvent && - event.data.logicalKey == LogicalKeyboardKey.space) { - plunger.release(); - } - if (event is RawKeyDownEvent && - event.data.logicalKey == LogicalKeyboardKey.space) { - plunger.pull(); - } - return KeyEventResult.handled; } } diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index f310874d..54a477e3 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -125,12 +125,12 @@ void main() { setUp(() { plunger = Plunger(Vector2.zero()); - anchor = Plunger(Vector2(0, -5)); + anchor = Plunger(Vector2(0, -1)); }); group('initializes with', () { flameTester.test( - 'plunger as bodyA', + 'plunger body as bodyA', (game) async { await game.ensureAddAll([plunger, anchor]); @@ -139,12 +139,12 @@ void main() { anchor: anchor, ); - expect(jointDef.bodyA, equals(plunger)); + expect(jointDef.bodyA, equals(plunger.body)); }, ); flameTester.test( - 'anchor as bodyB', + 'anchor body as bodyB', (game) async { await game.ensureAddAll([plunger, anchor]); @@ -154,7 +154,7 @@ void main() { ); game.world.createJoint(jointDef); - expect(jointDef.bodyB, equals(anchor)); + expect(jointDef.bodyB, equals(anchor.body)); }, ); @@ -203,5 +203,44 @@ void main() { }, ); }); + + flameTester.widgetTest( + 'plunger cannot go below anchor', + (game, tester) async { + await game.ensureAddAll([plunger, anchor]); + + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + plunger.pull(); + await tester.pump(const Duration(seconds: 1)); + + expect(plunger.body.position.y > anchor.body.position.y, isTrue); + }, + ); + + flameTester.widgetTest( + 'plunger cannot excessively exceed starting position', + (game, tester) async { + await game.ensureAddAll([plunger, anchor]); + + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + plunger.pull(); + await tester.pump(const Duration(seconds: 1)); + + plunger.release(); + await tester.pump(const Duration(seconds: 1)); + + expect(plunger.body.position.y < 1, isTrue); + }, + ); }); } From 64d4677b3f5077f76e12cd1da23b56e0cbc8975d Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 14:20:03 -0600 Subject: [PATCH 17/53] chore: remove modifications to game --- lib/game/pinball_game.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 80b4ffea..f7b2777f 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,12 +1,10 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; - import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc { @override Future onLoad() async { - await super.onLoad(); addContactCallback(BallScorePointsCallback()); } } From 2e28cd1a1d9ec52576b6b289578486316da0a4b6 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 14:30:01 -0600 Subject: [PATCH 18/53] docs: fix typo --- lib/game/components/plunger.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 2b55cdc7..8af6b167 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -28,7 +28,7 @@ class Plunger extends BodyComponent { return world.createBody(bodyDef)..createFixture(fixtureDef); } - /// Set a contstant downward velocity on the plunger body. + /// Set a constant downward velocity on the plunger body. void pull() { body.linearVelocity = Vector2(0, -7); } From 5c095e8f9380a1c0111c9f741db1ece86e195033 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 15:01:44 -0600 Subject: [PATCH 19/53] feat: add theme cubit --- lib/character_themes/character_theme.dart | 3 +++ lib/character_themes/cubit/theme_cubit.dart | 13 +++++++++++ lib/character_themes/cubit/theme_state.dart | 10 ++++++++ .../cubit/theme_cubit_test.dart | 23 +++++++++++++++++++ .../cubit/theme_state_test.dart | 19 +++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 lib/character_themes/cubit/theme_cubit.dart create mode 100644 lib/character_themes/cubit/theme_state.dart create mode 100644 test/character_themes/cubit/theme_cubit_test.dart create mode 100644 test/character_themes/cubit/theme_state_test.dart diff --git a/lib/character_themes/character_theme.dart b/lib/character_themes/character_theme.dart index d755a2c7..459ab69a 100644 --- a/lib/character_themes/character_theme.dart +++ b/lib/character_themes/character_theme.dart @@ -3,6 +3,9 @@ import 'package:flame/palette.dart'; /// {@template character_theme} /// Template for creating character themes. +/// +/// Any character specific game pieces should have a getter specified here so +/// their corresponding assets can be retrieved for the game. /// {@endtemplate} abstract class CharacterTheme extends Equatable { /// {@macro character_theme} diff --git a/lib/character_themes/cubit/theme_cubit.dart b/lib/character_themes/cubit/theme_cubit.dart new file mode 100644 index 00000000..ae60877e --- /dev/null +++ b/lib/character_themes/cubit/theme_cubit.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +part 'theme_state.dart'; + +class ThemeCubit extends Cubit { + ThemeCubit() : super(const ThemeState(DashTheme())); + + void themeSelected(CharacterTheme theme) { + emit(ThemeState(theme)); + } +} diff --git a/lib/character_themes/cubit/theme_state.dart b/lib/character_themes/cubit/theme_state.dart new file mode 100644 index 00000000..de15b437 --- /dev/null +++ b/lib/character_themes/cubit/theme_state.dart @@ -0,0 +1,10 @@ +part of 'theme_cubit.dart'; + +class ThemeState extends Equatable { + const ThemeState(this.theme); + + final CharacterTheme theme; + + @override + List get props => [theme]; +} diff --git a/test/character_themes/cubit/theme_cubit_test.dart b/test/character_themes/cubit/theme_cubit_test.dart new file mode 100644 index 00000000..fda2b7cd --- /dev/null +++ b/test/character_themes/cubit/theme_cubit_test.dart @@ -0,0 +1,23 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +void main() { + group('ThemeCubit', () { + test('initial state has Dash theme', () { + final themeCubit = ThemeCubit(); + expect(themeCubit.state.theme, equals(const DashTheme())); + }); + + group('themeSelected', () { + blocTest( + 'emits selected theme', + build: ThemeCubit.new, + act: (bloc) => bloc.themeSelected(const SparkyTheme()), + expect: () => [ + const ThemeState(SparkyTheme()), + ], + ); + }); + }); +} diff --git a/test/character_themes/cubit/theme_state_test.dart b/test/character_themes/cubit/theme_state_test.dart new file mode 100644 index 00000000..0161dc10 --- /dev/null +++ b/test/character_themes/cubit/theme_state_test.dart @@ -0,0 +1,19 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +void main() { + group('ThemeState', () { + test('can be instantiated', () { + expect(const ThemeState(DashTheme()), isNotNull); + }); + + test('supports value equality', () { + expect( + ThemeState(DashTheme()), + equals(const ThemeState(DashTheme())), + ); + }); + }); +} From 5a44f0abf328aced22e66fbcfaf97e22e8e31507 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 15:07:29 -0600 Subject: [PATCH 20/53] test: character themes --- test/character_themes/android_theme_test.dart | 21 +++++++++++++++++++ test/character_themes/dash_theme_test.dart | 21 +++++++++++++++++++ test/character_themes/dino_theme_test.dart | 21 +++++++++++++++++++ test/character_themes/sparky_theme_test.dart | 21 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 test/character_themes/android_theme_test.dart create mode 100644 test/character_themes/dash_theme_test.dart create mode 100644 test/character_themes/dino_theme_test.dart create mode 100644 test/character_themes/sparky_theme_test.dart diff --git a/test/character_themes/android_theme_test.dart b/test/character_themes/android_theme_test.dart new file mode 100644 index 00000000..7a5bf565 --- /dev/null +++ b/test/character_themes/android_theme_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +void main() { + group('AndroidTheme', () { + test('can be instantiated', () { + expect(AndroidTheme(), isNotNull); + }); + + test('supports value equality', () { + expect(AndroidTheme(), equals(AndroidTheme())); + }); + + test('ballColor is correct', () { + expect(AndroidTheme().ballColor, equals(Colors.green)); + }); + }); +} diff --git a/test/character_themes/dash_theme_test.dart b/test/character_themes/dash_theme_test.dart new file mode 100644 index 00000000..6d06a70a --- /dev/null +++ b/test/character_themes/dash_theme_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +void main() { + group('DashTheme', () { + test('can be instantiated', () { + expect(DashTheme(), isNotNull); + }); + + test('supports value equality', () { + expect(DashTheme(), equals(DashTheme())); + }); + + test('ballColor is correct', () { + expect(DashTheme().ballColor, equals(Colors.blue)); + }); + }); +} diff --git a/test/character_themes/dino_theme_test.dart b/test/character_themes/dino_theme_test.dart new file mode 100644 index 00000000..0c0f83e8 --- /dev/null +++ b/test/character_themes/dino_theme_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +void main() { + group('DinoTheme', () { + test('can be instantiated', () { + expect(DinoTheme(), isNotNull); + }); + + test('supports value equality', () { + expect(DinoTheme(), equals(DinoTheme())); + }); + + test('ballColor is correct', () { + expect(DinoTheme().ballColor, equals(Colors.grey)); + }); + }); +} diff --git a/test/character_themes/sparky_theme_test.dart b/test/character_themes/sparky_theme_test.dart new file mode 100644 index 00000000..a57c8969 --- /dev/null +++ b/test/character_themes/sparky_theme_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/character_themes/character_themes.dart'; + +void main() { + group('SparkyTheme', () { + test('can be instantiated', () { + expect(SparkyTheme(), isNotNull); + }); + + test('supports value equality', () { + expect(SparkyTheme(), equals(SparkyTheme())); + }); + + test('ballColor is correct', () { + expect(SparkyTheme().ballColor, equals(Colors.orange)); + }); + }); +} From 8f3fc4b248ed3a98c55b9005acd8442873d0ebfb Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Thu, 3 Mar 2022 15:31:24 -0600 Subject: [PATCH 21/53] test: remove grouping --- .../cubit/theme_cubit_test.dart | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/character_themes/cubit/theme_cubit_test.dart b/test/character_themes/cubit/theme_cubit_test.dart index fda2b7cd..5dacba87 100644 --- a/test/character_themes/cubit/theme_cubit_test.dart +++ b/test/character_themes/cubit/theme_cubit_test.dart @@ -9,15 +9,13 @@ void main() { expect(themeCubit.state.theme, equals(const DashTheme())); }); - group('themeSelected', () { - blocTest( - 'emits selected theme', - build: ThemeCubit.new, - act: (bloc) => bloc.themeSelected(const SparkyTheme()), - expect: () => [ - const ThemeState(SparkyTheme()), - ], - ); - }); + blocTest( + 'themeSelected emits selected theme', + build: ThemeCubit.new, + act: (bloc) => bloc.themeSelected(const SparkyTheme()), + expect: () => [ + const ThemeState(SparkyTheme()), + ], + ); }); } From 9fc554927d67c2cd871ea917d0f44a6327c91837 Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 4 Mar 2022 09:03:41 -0300 Subject: [PATCH 22/53] Apply suggestions from code review Co-authored-by: Alejandro Santiago --- lib/game/bloc/game_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 862edf96..1a0568f7 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -26,7 +26,7 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; - /// Determines when player has only one chance left. + /// Determines when the player has only one ball left. bool get isLastBall => balls == 1; GameState copyWith({ From 5a2ee2c2ac3c5847e4383ecc18b6c70ef3f9773c Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Fri, 4 Mar 2022 09:25:09 -0300 Subject: [PATCH 23/53] feat: applying suggestions --- lib/game/components/ball.dart | 9 +++---- lib/game/components/wall.dart | 39 +++++++++++------------------ lib/game/pinball_game.dart | 4 +-- test/game/components/ball_test.dart | 2 +- test/game/components/wall_test.dart | 13 +++++----- test/helpers/mocks.dart | 2 ++ 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 04d3cf6f..e285b14b 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -29,13 +29,12 @@ class Ball extends BodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } - void ballLost() { - final bloc = gameRef.read(); + void lost() { + shouldRemove = true; - final shouldBallRespwan = !bloc.state.isLastBall; - - bloc.add(const BallLost()); + final bloc = gameRef.read()..add(const BallLost()); + final shouldBallRespwan = !bloc.state.isLastBall; if (shouldBallRespwan) { gameRef.spawnBall(); } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index bb527c3b..7bf58273 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -3,32 +3,14 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; -enum WallType { - fatal, - passive, -} - class Wall extends BodyComponent { Wall({ required this.start, required this.end, - this.type = WallType.passive, }); - factory Wall.bottom(Forge2DGame game) { - final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); - final bottomLeft = Vector2(0, bottomRight.y); - - return Wall( - type: WallType.fatal, - start: bottomRight, - end: bottomLeft, - ); - } - final Vector2 start; final Vector2 end; - final WallType type; @override Body createBody() { @@ -47,14 +29,21 @@ class Wall extends BodyComponent { } } -class BallWallContactCallback extends ContactCallback { +class BottomWall extends Wall { + BottomWall(Forge2DGame game) + : super( + start: game.screenToWorld(game.camera.viewport.effectiveSize), + end: Vector2( + 0, + game.screenToWorld(game.camera.viewport.effectiveSize).y, + ), + ); +} + +class BottomWallBallContactCallback extends ContactCallback { @override - void begin(Ball ball, Wall wall, Contact contact) { - if (wall.type == WallType.fatal) { - ball - ..ballLost() - ..shouldRemove = true; - } + void begin(Ball ball, BottomWall wall, Contact contact) { + ball.lost(); } @override diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 934d958e..91e12854 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -21,7 +21,7 @@ class PinballGame extends Forge2DGame with FlameBloc { spawnBall(); addContactCallback(BallScorePointsCallback()); - await add(Wall.bottom(this)); - addContactCallback(BallWallContactCallback()); + await add(BottomWall(this)); + addContactCallback(BottomWallBallContactCallback()); } } diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 8521a80f..b32d16d5 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -107,7 +107,7 @@ void main() { (game, tester) async { await game.ready(); - game.children.whereType().first.ballLost(); + game.children.whereType().first.lost(); await tester.pump(); verify(() => gameBloc.add(const BallLost())).called(1); diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 1ff5df95..8151055e 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -12,24 +12,23 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Wall', () { - group('BallWallContactCallback', () { + group('BottomWallBallContactCallback', () { test( - 'removes the ball on begin contact when the wall is a fatal one', + 'removes the ball on begin contact when the wall is a bottom one', () { final game = MockPinballGame(); - final wall = MockWall(); + final wall = MockBottomWall(); final ball = MockBall(); - when(() => wall.type).thenReturn(WallType.fatal); when(() => ball.gameRef).thenReturn(game); - BallWallContactCallback() + BottomWallBallContactCallback() // Remove once https://github.com/flame-engine/flame/pull/1415 // is merged - ..end(MockBall(), MockWall(), MockContact()) + ..end(MockBall(), MockBottomWall(), MockContact()) ..begin(ball, wall, MockContact()); - verify(() => ball.shouldRemove = true).called(1); + verify(ball.lost).called(1); }, ); }); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 559ea87a..b46e2c5c 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -6,6 +6,8 @@ class MockPinballGame extends Mock implements PinballGame {} class MockWall extends Mock implements Wall {} +class MockBottomWall extends Mock implements BottomWall {} + class MockBall extends Mock implements Ball {} class MockContact extends Mock implements Contact {} From 9891931ffe9b9c007bc02cf2c1699b732b236d63 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Fri, 4 Mar 2022 13:48:48 +0000 Subject: [PATCH 24/53] feat: anchor component (#12) * feat: implemented Anchor component * docs: included initialize example * docs: improved Anchor doc comment * chore: fixed white space difference --- lib/game/components/anchor.dart | 32 ++++++++++++++ lib/game/components/components.dart | 1 + test/game/components/anchor_test.dart | 60 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 lib/game/components/anchor.dart create mode 100644 test/game/components/anchor_test.dart diff --git a/lib/game/components/anchor.dart b/lib/game/components/anchor.dart new file mode 100644 index 00000000..0e78aa1c --- /dev/null +++ b/lib/game/components/anchor.dart @@ -0,0 +1,32 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +/// {@template anchor} +/// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s +/// with this [BodyType.static]. +/// +/// It is recommended to [_position] the anchor first and then use the body +/// position as the anchor point when initializing a [JointDef]. +/// +/// ```dart +/// initialize( +/// dynamicBody.body, +/// anchor.body, +/// anchor.body.position, +/// ); +/// ``` +/// {@endtemplate} +class Anchor extends BodyComponent { + /// {@macro anchor} + Anchor({ + required Vector2 position, + }) : _position = position; + + final Vector2 _position; + + @override + Body createBody() { + final bodyDef = BodyDef()..position = _position; + + return world.createBody(bodyDef); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 52e460a5..42c79ae6 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,3 +1,4 @@ +export 'anchor.dart'; export 'ball.dart'; export 'score_points.dart'; export 'wall.dart'; diff --git a/test/game/components/anchor_test.dart b/test/game/components/anchor_test.dart new file mode 100644 index 00000000..5cc37eca --- /dev/null +++ b/test/game/components/anchor_test.dart @@ -0,0 +1,60 @@ +// ignore_for_file: cascade_invocations + +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Anchor', () { + final flameTester = FlameTester(PinballGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAdd(anchor); + + expect(game.contains(anchor), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final anchor = Anchor(position: position); + await game.ensureAdd(anchor); + game.contains(anchor); + + expect(anchor.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAdd(anchor); + + expect(anchor.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixtures', () { + flameTester.test( + 'has none', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAdd(anchor); + + expect(anchor.body.fixtures, isEmpty); + }, + ); + }); + }); +} From bb1663111f858609a7fab866b9bdfa59ee016053 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 08:39:29 -0600 Subject: [PATCH 25/53] refactor: suggestions --- lib/game/components/plunger.dart | 32 ++++++++++-------- test/game/components/plunger_test.dart | 45 +++++++++++++++++++------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 8af6b167..7e6dbd89 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,15 +1,15 @@ import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; /// {@template plunger} -/// Plunger body component to be pulled and released by the player to launch -/// the pinball. +/// [Plunger] serves as a spring, that shoots the ball on the right side of the +/// playfield. /// -/// The plunger body ignores gravity so the player can control its downward -/// pull. +/// [Plunger] ignores gravity so the player controls its downward [pull]. /// {@endtemplate} class Plunger extends BodyComponent { /// {@macro plunger} - Plunger(this._position); + Plunger({required Vector2 position}) : _position = position; final Vector2 _position; @@ -28,13 +28,15 @@ class Plunger extends BodyComponent { return world.createBody(bodyDef)..createFixture(fixtureDef); } - /// Set a constant downward velocity on the plunger body. + /// Set a constant downward velocity on the [Plunger]. void pull() { body.linearVelocity = Vector2(0, -7); } - /// Set an upward velocity on the plunger body. The velocity's magnitude - /// depends on how far the plunger has been pulled from its original position. + /// Set an upward velocity on the [Plunger]. + /// + /// The velocity's magnitude depends on how far the [Plunger] has been pulled + /// from its original [_position]. void release() { final velocity = (_position.y - body.position.y) * 9; body.linearVelocity = Vector2(0, velocity); @@ -42,19 +44,21 @@ class Plunger extends BodyComponent { } /// {@template plunger_anchor_prismatic_joint_def} -/// Prismatic joint def between a [Plunger] and an anchor body given motion on +/// [PrismaticJointDef] between a [Plunger] and an [Anchor] with motion on /// the vertical axis. /// -/// The [Plunger] is constrained to vertical motion between its starting -/// position and the anchor body. The anchor needs to be below the plunger for -/// this joint to function properly. +/// The [Plunger] is constrained to vertically between its starting position and +/// the [Anchor]. The [Anchor] must be below the [Plunger]. /// {@endtemplate} class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { /// {@macro plunger_anchor_prismatic_joint_def} PlungerAnchorPrismaticJointDef({ required Plunger plunger, - required BodyComponent anchor, - }) { + required Anchor anchor, + }) : assert( + anchor.body.position.y < plunger.body.position.y, + "Anchor can't be positioned above the Plunger", + ) { initialize( plunger.body, anchor.body, diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 54a477e3..913cfa82 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,7 +14,7 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); expect(game.contains(plunger), isTrue); @@ -25,7 +26,7 @@ void main() { 'positions correctly', (game) async { final position = Vector2.all(10); - final plunger = Plunger(position); + final plunger = Plunger(position: position); await game.ensureAdd(plunger); game.contains(plunger); @@ -36,7 +37,7 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); expect(plunger.body.bodyType, equals(BodyType.dynamic)); @@ -46,7 +47,7 @@ void main() { flameTester.test( 'ignores gravity', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); expect(plunger.body.gravityScale, isZero); @@ -58,7 +59,7 @@ void main() { flameTester.test( 'exists', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); expect(plunger.body.fixtures[0], isA()); @@ -68,7 +69,7 @@ void main() { flameTester.test( 'shape is a polygon', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); final fixture = plunger.body.fixtures[0]; @@ -80,12 +81,13 @@ void main() { flameTester.test( 'pull sets a negative linear velocity', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); plunger.pull(); expect(plunger.body.linearVelocity.y, isNegative); + expect(plunger.body.linearVelocity.x, isZero); }, ); @@ -94,12 +96,13 @@ void main() { 'does not set a linear velocity ' 'when plunger is in starting position', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); plunger.release(); expect(plunger.body.linearVelocity.y, isZero); + expect(plunger.body.linearVelocity.x, isZero); }, ); @@ -107,13 +110,14 @@ void main() { 'sets a positive linear velocity ' 'when plunger is below starting position', (game) async { - final plunger = Plunger(Vector2.zero()); + final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); plunger.body.setTransform(Vector2(0, -1), 0); plunger.release(); expect(plunger.body.linearVelocity.y, isPositive); + expect(plunger.body.linearVelocity.x, isZero); }, ); }); @@ -121,13 +125,30 @@ void main() { group('PlungerAnchorPrismaticJointDef', () { late Plunger plunger; - late Plunger anchor; + late Anchor anchor; setUp(() { - plunger = Plunger(Vector2.zero()); - anchor = Plunger(Vector2(0, -1)); + plunger = Plunger(position: Vector2.zero()); + anchor = Anchor(position: Vector2(0, -1)); }); + flameTester.test( + 'throws AssertionError ' + 'when anchor is above plunger', + (game) async { + final anchor = Anchor(position: Vector2(0, 1)); + await game.ensureAddAll([plunger, anchor]); + + expect( + () => PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ), + throwsAssertionError, + ); + }, + ); + group('initializes with', () { flameTester.test( 'plunger body as bodyA', From 0e98a74b176472c29e1bafc3cf86503e912028a2 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 10:17:12 -0600 Subject: [PATCH 26/53] refactor: move themes to package --- lib/character_themes/android_theme.dart | 9 ----- lib/character_themes/character_theme.dart | 18 --------- lib/character_themes/dash_theme.dart | 9 ----- lib/character_themes/dino_theme.dart | 9 ----- lib/character_themes/sparky_theme.dart | 9 ----- .../cubit/theme_cubit.dart | 4 +- .../cubit/theme_state.dart | 2 +- lib/theme/theme.dart | 1 + packages/pinball_theme/.gitignore | 39 +++++++++++++++++++ packages/pinball_theme/README.md | 11 ++++++ packages/pinball_theme/analysis_options.yaml | 1 + packages/pinball_theme/lib/pinball_theme.dart | 4 ++ .../pinball_theme/lib/src/pinball_theme.dart | 19 +++++++++ .../lib/src/themes/android_theme.dart | 13 +++++++ .../lib/src/themes/dash_theme.dart | 13 +++++++ .../lib/src/themes/dino_theme.dart | 13 +++++++ .../lib/src/themes/sparky_theme.dart | 13 +++++++ .../pinball_theme/lib/src/themes/themes.dart | 2 - packages/pinball_theme/pubspec.yaml | 17 ++++++++ .../test/src/themes}/android_theme_test.dart | 2 +- .../test/src/themes}/dash_theme_test.dart | 2 +- .../test/src/themes}/dino_theme_test.dart | 2 +- .../test/src/themes}/sparky_theme_test.dart | 2 +- pubspec.lock | 7 ++++ pubspec.yaml | 2 + .../cubit/theme_cubit_test.dart | 3 +- .../cubit/theme_state_test.dart | 3 +- 27 files changed, 164 insertions(+), 65 deletions(-) delete mode 100644 lib/character_themes/android_theme.dart delete mode 100644 lib/character_themes/character_theme.dart delete mode 100644 lib/character_themes/dash_theme.dart delete mode 100644 lib/character_themes/dino_theme.dart delete mode 100644 lib/character_themes/sparky_theme.dart rename lib/{character_themes => theme}/cubit/theme_cubit.dart (68%) rename lib/{character_themes => theme}/cubit/theme_state.dart (83%) create mode 100644 lib/theme/theme.dart create mode 100644 packages/pinball_theme/.gitignore create mode 100644 packages/pinball_theme/README.md create mode 100644 packages/pinball_theme/analysis_options.yaml create mode 100644 packages/pinball_theme/lib/pinball_theme.dart create mode 100644 packages/pinball_theme/lib/src/pinball_theme.dart create mode 100644 packages/pinball_theme/lib/src/themes/android_theme.dart create mode 100644 packages/pinball_theme/lib/src/themes/dash_theme.dart create mode 100644 packages/pinball_theme/lib/src/themes/dino_theme.dart create mode 100644 packages/pinball_theme/lib/src/themes/sparky_theme.dart rename lib/character_themes/character_themes.dart => packages/pinball_theme/lib/src/themes/themes.dart (63%) create mode 100644 packages/pinball_theme/pubspec.yaml rename {test/character_themes => packages/pinball_theme/test/src/themes}/android_theme_test.dart (88%) rename {test/character_themes => packages/pinball_theme/test/src/themes}/dash_theme_test.dart (87%) rename {test/character_themes => packages/pinball_theme/test/src/themes}/dino_theme_test.dart (87%) rename {test/character_themes => packages/pinball_theme/test/src/themes}/sparky_theme_test.dart (88%) rename test/{character_themes => theme}/cubit/theme_cubit_test.dart (85%) rename test/{character_themes => theme}/cubit/theme_state_test.dart (80%) diff --git a/lib/character_themes/android_theme.dart b/lib/character_themes/android_theme.dart deleted file mode 100644 index 8c91d9e1..00000000 --- a/lib/character_themes/android_theme.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pinball/character_themes/character_themes.dart'; - -class AndroidTheme extends CharacterTheme { - const AndroidTheme(); - - @override - Color get ballColor => Colors.green; -} diff --git a/lib/character_themes/character_theme.dart b/lib/character_themes/character_theme.dart deleted file mode 100644 index 459ab69a..00000000 --- a/lib/character_themes/character_theme.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flame/palette.dart'; - -/// {@template character_theme} -/// Template for creating character themes. -/// -/// Any character specific game pieces should have a getter specified here so -/// their corresponding assets can be retrieved for the game. -/// {@endtemplate} -abstract class CharacterTheme extends Equatable { - /// {@macro character_theme} - const CharacterTheme(); - - Color get ballColor; - - @override - List get props => []; -} diff --git a/lib/character_themes/dash_theme.dart b/lib/character_themes/dash_theme.dart deleted file mode 100644 index caaf2bdf..00000000 --- a/lib/character_themes/dash_theme.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pinball/character_themes/character_themes.dart'; - -class DashTheme extends CharacterTheme { - const DashTheme(); - - @override - Color get ballColor => Colors.blue; -} diff --git a/lib/character_themes/dino_theme.dart b/lib/character_themes/dino_theme.dart deleted file mode 100644 index 27f76199..00000000 --- a/lib/character_themes/dino_theme.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pinball/character_themes/character_themes.dart'; - -class DinoTheme extends CharacterTheme { - const DinoTheme(); - - @override - Color get ballColor => Colors.grey; -} diff --git a/lib/character_themes/sparky_theme.dart b/lib/character_themes/sparky_theme.dart deleted file mode 100644 index 63d8d241..00000000 --- a/lib/character_themes/sparky_theme.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pinball/character_themes/character_themes.dart'; - -class SparkyTheme extends CharacterTheme { - const SparkyTheme(); - - @override - Color get ballColor => Colors.orange; -} diff --git a/lib/character_themes/cubit/theme_cubit.dart b/lib/theme/cubit/theme_cubit.dart similarity index 68% rename from lib/character_themes/cubit/theme_cubit.dart rename to lib/theme/cubit/theme_cubit.dart index ae60877e..080d5f5e 100644 --- a/lib/character_themes/cubit/theme_cubit.dart +++ b/lib/theme/cubit/theme_cubit.dart @@ -1,13 +1,13 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball_theme/pinball_theme.dart'; part 'theme_state.dart'; class ThemeCubit extends Cubit { ThemeCubit() : super(const ThemeState(DashTheme())); - void themeSelected(CharacterTheme theme) { + void themeSelected(PinballTheme theme) { emit(ThemeState(theme)); } } diff --git a/lib/character_themes/cubit/theme_state.dart b/lib/theme/cubit/theme_state.dart similarity index 83% rename from lib/character_themes/cubit/theme_state.dart rename to lib/theme/cubit/theme_state.dart index de15b437..71491960 100644 --- a/lib/character_themes/cubit/theme_state.dart +++ b/lib/theme/cubit/theme_state.dart @@ -3,7 +3,7 @@ part of 'theme_cubit.dart'; class ThemeState extends Equatable { const ThemeState(this.theme); - final CharacterTheme theme; + final PinballTheme theme; @override List get props => [theme]; diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart new file mode 100644 index 00000000..fcf5d9ee --- /dev/null +++ b/lib/theme/theme.dart @@ -0,0 +1 @@ +export 'cubit/theme_cubit.dart'; diff --git a/packages/pinball_theme/.gitignore b/packages/pinball_theme/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_theme/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/pinball_theme/README.md b/packages/pinball_theme/README.md new file mode 100644 index 00000000..e9730e1b --- /dev/null +++ b/packages/pinball_theme/README.md @@ -0,0 +1,11 @@ +# pinball_theme + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Package containing themes for pinball game. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/pinball_theme/analysis_options.yaml b/packages/pinball_theme/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/pinball_theme/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/pinball_theme/lib/pinball_theme.dart b/packages/pinball_theme/lib/pinball_theme.dart new file mode 100644 index 00000000..0206fa7b --- /dev/null +++ b/packages/pinball_theme/lib/pinball_theme.dart @@ -0,0 +1,4 @@ +library pinball_theme; + +export 'src/pinball_theme.dart'; +export 'src/themes/themes.dart'; diff --git a/packages/pinball_theme/lib/src/pinball_theme.dart b/packages/pinball_theme/lib/src/pinball_theme.dart new file mode 100644 index 00000000..f8605708 --- /dev/null +++ b/packages/pinball_theme/lib/src/pinball_theme.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +/// {@template pinball_theme} +/// Base class for creating pinball themes. +/// +/// Character specific game components should have a getter specified here to +/// load their corresponding assets for the game. +/// {@endtemplate} +abstract class PinballTheme extends Equatable { + /// {@macro pinball_theme} + const PinballTheme(); + + /// Ball color for this theme. + Color get ballColor; + + @override + List get props => []; +} diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart new file mode 100644 index 00000000..bf29da27 --- /dev/null +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template android_theme} +/// Class defining Android theme assets and attributes. +/// {@endtemplate} +class AndroidTheme extends PinballTheme { + /// {@macro android_theme} + const AndroidTheme(); + + @override + Color get ballColor => Colors.green; +} diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart new file mode 100644 index 00000000..34faac9c --- /dev/null +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template dash_theme} +/// Class defining Dash theme assets and attributes. +/// {@endtemplate} +class DashTheme extends PinballTheme { + /// {@macro dash_theme} + const DashTheme(); + + @override + Color get ballColor => Colors.blue; +} diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart new file mode 100644 index 00000000..1af5b5c8 --- /dev/null +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template dino_theme} +/// Class defining Dino theme assets and attributes. +/// {@endtemplate} +class DinoTheme extends PinballTheme { + /// {@macro dino_theme} + const DinoTheme(); + + @override + Color get ballColor => Colors.grey; +} diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart new file mode 100644 index 00000000..75ddd3b7 --- /dev/null +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template sparky_theme} +/// Class defining Sparky theme assets and attributes. +/// {@endtemplate} +class SparkyTheme extends PinballTheme { + /// {@macro sparky_theme} + const SparkyTheme(); + + @override + Color get ballColor => Colors.orange; +} diff --git a/lib/character_themes/character_themes.dart b/packages/pinball_theme/lib/src/themes/themes.dart similarity index 63% rename from lib/character_themes/character_themes.dart rename to packages/pinball_theme/lib/src/themes/themes.dart index 21f1efd5..895e7c8d 100644 --- a/lib/character_themes/character_themes.dart +++ b/packages/pinball_theme/lib/src/themes/themes.dart @@ -1,6 +1,4 @@ export 'android_theme.dart'; -export 'character_theme.dart'; -export 'cubit/theme_cubit.dart'; export 'dash_theme.dart'; export 'dino_theme.dart'; export 'sparky_theme.dart'; diff --git a/packages/pinball_theme/pubspec.yaml b/packages/pinball_theme/pubspec.yaml new file mode 100644 index 00000000..e9b3f215 --- /dev/null +++ b/packages/pinball_theme/pubspec.yaml @@ -0,0 +1,17 @@ +name: pinball_theme +description: Package containing themes for pinball game. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + equatable: ^2.0.3 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/test/character_themes/android_theme_test.dart b/packages/pinball_theme/test/src/themes/android_theme_test.dart similarity index 88% rename from test/character_themes/android_theme_test.dart rename to packages/pinball_theme/test/src/themes/android_theme_test.dart index 7a5bf565..a6148042 100644 --- a/test/character_themes/android_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/android_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('AndroidTheme', () { diff --git a/test/character_themes/dash_theme_test.dart b/packages/pinball_theme/test/src/themes/dash_theme_test.dart similarity index 87% rename from test/character_themes/dash_theme_test.dart rename to packages/pinball_theme/test/src/themes/dash_theme_test.dart index 6d06a70a..0d5c8293 100644 --- a/test/character_themes/dash_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/dash_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('DashTheme', () { diff --git a/test/character_themes/dino_theme_test.dart b/packages/pinball_theme/test/src/themes/dino_theme_test.dart similarity index 87% rename from test/character_themes/dino_theme_test.dart rename to packages/pinball_theme/test/src/themes/dino_theme_test.dart index 0c0f83e8..6efd8cbd 100644 --- a/test/character_themes/dino_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/dino_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('DinoTheme', () { diff --git a/test/character_themes/sparky_theme_test.dart b/packages/pinball_theme/test/src/themes/sparky_theme_test.dart similarity index 88% rename from test/character_themes/sparky_theme_test.dart rename to packages/pinball_theme/test/src/themes/sparky_theme_test.dart index a57c8969..513ca219 100644 --- a/test/character_themes/sparky_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/sparky_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('SparkyTheme', () { diff --git a/pubspec.lock b/pubspec.lock index e218776d..7bf08da4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 5d708073..6c3bd98e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,8 @@ dependencies: flutter_localizations: sdk: flutter intl: ^0.17.0 + pinball_theme: + path: packages/pinball_theme dev_dependencies: bloc_test: ^9.0.2 diff --git a/test/character_themes/cubit/theme_cubit_test.dart b/test/theme/cubit/theme_cubit_test.dart similarity index 85% rename from test/character_themes/cubit/theme_cubit_test.dart rename to test/theme/cubit/theme_cubit_test.dart index 5dacba87..59aecd37 100644 --- a/test/character_themes/cubit/theme_cubit_test.dart +++ b/test/theme/cubit/theme_cubit_test.dart @@ -1,6 +1,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('ThemeCubit', () { diff --git a/test/character_themes/cubit/theme_state_test.dart b/test/theme/cubit/theme_state_test.dart similarity index 80% rename from test/character_themes/cubit/theme_state_test.dart rename to test/theme/cubit/theme_state_test.dart index 0161dc10..e89abe01 100644 --- a/test/character_themes/cubit/theme_state_test.dart +++ b/test/theme/cubit/theme_state_test.dart @@ -1,7 +1,8 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/character_themes/character_themes.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('ThemeState', () { From 5d583fb07eb2c25bd09c834eead72c1188405b1d Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 10:21:55 -0600 Subject: [PATCH 27/53] chore: add workflow --- .github/workflows/pinball_theme.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/pinball_theme.yaml diff --git a/.github/workflows/pinball_theme.yaml b/.github/workflows/pinball_theme.yaml new file mode 100644 index 00000000..268fe535 --- /dev/null +++ b/.github/workflows/pinball_theme.yaml @@ -0,0 +1,18 @@ +name: pinball_theme + +on: + push: + paths: + - "packages/pinball_theme/**" + - ".github/workflows/pinball_theme.yaml" + + pull_request: + paths: + - "packages/pinball_theme/**" + - ".github/workflows/pinball_theme.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + with: + working_directory: packages/pinball_theme \ No newline at end of file From 7421acf4a027248907d364cb0d44bb6174c86be2 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 10:27:05 -0600 Subject: [PATCH 28/53] fix: workflow --- .github/workflows/pinball_theme.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pinball_theme.yaml b/.github/workflows/pinball_theme.yaml index 268fe535..f6fa14aa 100644 --- a/.github/workflows/pinball_theme.yaml +++ b/.github/workflows/pinball_theme.yaml @@ -13,6 +13,6 @@ on: jobs: build: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: packages/pinball_theme \ No newline at end of file From 3ef9cb878b2966f2ad6c8546bdc6e01d8eb9ac18 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 10:52:23 -0600 Subject: [PATCH 29/53] test: provide mock game bloc --- test/game/components/plunger_test.dart | 20 +++++++++++++++++++- test/helpers/builders.dart | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 913cfa82..5d3c3a55 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -1,11 +1,13 @@ // ignore_for_file: cascade_invocations -import 'package:flame/extensions.dart'; +import 'package:bloc_test/bloc_test.dart'; 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 '../../helpers/helpers.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGame.new); @@ -124,14 +126,27 @@ void main() { }); group('PlungerAnchorPrismaticJointDef', () { + late GameBloc gameBloc; late Plunger plunger; late Anchor anchor; setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); plunger = Plunger(position: Vector2.zero()); anchor = Anchor(position: Vector2(0, -1)); }); + final flameTester = flameBlocTester( + gameBlocBuilder: () { + return gameBloc; + }, + ); + flameTester.test( 'throws AssertionError ' 'when anchor is above plunger', @@ -230,6 +245,9 @@ void main() { (game, tester) async { await game.ensureAddAll([plunger, anchor]); + // Giving anchor a shape for the plunger to collide with. + anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); + final jointDef = PlungerAnchorPrismaticJointDef( plunger: plunger, anchor: anchor, diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index 5ef98226..e124052e 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -2,8 +2,10 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; -FlameTester flameBlocTester({required GameBloc Function() gameBlocBuilder}) { - return FlameTester( +FlameTester flameBlocTester({ + required GameBloc Function() gameBlocBuilder, +}) { + return FlameTester( PinballGame.new, pumpWidget: (gameWidget, tester) async { await tester.pumpWidget( From ea3b126b6255b6ea61e7d33267faf45ca6a4fae7 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Fri, 4 Mar 2022 11:06:09 -0600 Subject: [PATCH 30/53] Update lib/game/components/plunger.dart Co-authored-by: Alejandro Santiago --- lib/game/components/plunger.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 7e6dbd89..caef1670 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -47,7 +47,7 @@ class Plunger extends BodyComponent { /// [PrismaticJointDef] between a [Plunger] and an [Anchor] with motion on /// the vertical axis. /// -/// The [Plunger] is constrained to vertically between its starting position and +/// The [Plunger] is constrained vertically between its starting position and /// the [Anchor]. The [Anchor] must be below the [Plunger]. /// {@endtemplate} class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { From 16516764628864da5c5aef85355e0476c773f235 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 11:12:13 -0600 Subject: [PATCH 31/53] test: same position --- lib/game/components/plunger.dart | 2 +- test/game/components/plunger_test.dart | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index caef1670..ed1ef36f 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -57,7 +57,7 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { required Anchor anchor, }) : assert( anchor.body.position.y < plunger.body.position.y, - "Anchor can't be positioned above the Plunger", + 'Anchor must be below the Plunger', ) { initialize( plunger.body, diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 5d3c3a55..67e215fd 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -164,6 +164,23 @@ void main() { }, ); + flameTester.test( + 'throws AssertionError ' + 'when anchor is in same position as plunger', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAddAll([plunger, anchor]); + + expect( + () => PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ), + throwsAssertionError, + ); + }, + ); + group('initializes with', () { flameTester.test( 'plunger body as bodyA', From f0db7c720d8c1e848b8ef7a4182b0876d7a10928 Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 11:16:13 -0600 Subject: [PATCH 32/53] docs: character themes update --- packages/pinball_theme/lib/src/themes/android_theme.dart | 2 +- packages/pinball_theme/lib/src/themes/dash_theme.dart | 2 +- packages/pinball_theme/lib/src/themes/dino_theme.dart | 2 +- packages/pinball_theme/lib/src/themes/sparky_theme.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index bf29da27..3b605411 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template android_theme} -/// Class defining Android theme assets and attributes. +/// Defines Android theme assets and attributes. /// {@endtemplate} class AndroidTheme extends PinballTheme { /// {@macro android_theme} diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index 34faac9c..fd0cdfa1 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dash_theme} -/// Class defining Dash theme assets and attributes. +/// Defines Dash theme assets and attributes. /// {@endtemplate} class DashTheme extends PinballTheme { /// {@macro dash_theme} diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 1af5b5c8..9337ddde 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dino_theme} -/// Class defining Dino theme assets and attributes. +/// Defines Dino theme assets and attributes. /// {@endtemplate} class DinoTheme extends PinballTheme { /// {@macro dino_theme} diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index 75ddd3b7..d2cecf47 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template sparky_theme} -/// Class defining Sparky theme assets and attributes. +/// Defines Sparky theme assets and attributes. /// {@endtemplate} class SparkyTheme extends PinballTheme { /// {@macro sparky_theme} From ca18de6172c94bc49c242f58f78f8174faacc74a Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Fri, 4 Mar 2022 12:15:32 -0600 Subject: [PATCH 33/53] refactor: one theme for entire game --- lib/theme/cubit/theme_cubit.dart | 6 ++-- lib/theme/cubit/theme_state.dart | 3 ++ .../pinball_theme/lib/src/pinball_theme.dart | 20 +++++++------ .../lib/src/themes/android_theme.dart | 4 +-- .../lib/src/themes/character_theme.dart | 19 +++++++++++++ .../lib/src/themes/dash_theme.dart | 4 +-- .../lib/src/themes/dino_theme.dart | 4 +-- .../lib/src/themes/sparky_theme.dart | 4 +-- .../pinball_theme/lib/src/themes/themes.dart | 1 + .../test/src/pinball_theme_test.dart | 28 +++++++++++++++++++ test/theme/cubit/theme_cubit_test.dart | 10 +++---- test/theme/cubit/theme_state_test.dart | 7 ++--- 12 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 packages/pinball_theme/lib/src/themes/character_theme.dart create mode 100644 packages/pinball_theme/test/src/pinball_theme_test.dart diff --git a/lib/theme/cubit/theme_cubit.dart b/lib/theme/cubit/theme_cubit.dart index 080d5f5e..7ba79e59 100644 --- a/lib/theme/cubit/theme_cubit.dart +++ b/lib/theme/cubit/theme_cubit.dart @@ -5,9 +5,9 @@ import 'package:pinball_theme/pinball_theme.dart'; part 'theme_state.dart'; class ThemeCubit extends Cubit { - ThemeCubit() : super(const ThemeState(DashTheme())); + ThemeCubit() : super(const ThemeState.initial()); - void themeSelected(PinballTheme theme) { - emit(ThemeState(theme)); + void characterSelected(CharacterTheme characterTheme) { + emit(ThemeState(PinballTheme(characterTheme: characterTheme))); } } diff --git a/lib/theme/cubit/theme_state.dart b/lib/theme/cubit/theme_state.dart index 71491960..13b3ea5f 100644 --- a/lib/theme/cubit/theme_state.dart +++ b/lib/theme/cubit/theme_state.dart @@ -3,6 +3,9 @@ part of 'theme_cubit.dart'; class ThemeState extends Equatable { const ThemeState(this.theme); + const ThemeState.initial() + : theme = const PinballTheme(characterTheme: DashTheme()); + final PinballTheme theme; @override diff --git a/packages/pinball_theme/lib/src/pinball_theme.dart b/packages/pinball_theme/lib/src/pinball_theme.dart index f8605708..b4c404e5 100644 --- a/packages/pinball_theme/lib/src/pinball_theme.dart +++ b/packages/pinball_theme/lib/src/pinball_theme.dart @@ -1,18 +1,22 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:pinball_theme/pinball_theme.dart'; /// {@template pinball_theme} -/// Base class for creating pinball themes. +/// Defines all theme assets and attributes. /// -/// Character specific game components should have a getter specified here to -/// load their corresponding assets for the game. +/// Game components should have a getter specified here to load their +/// corresponding assets for the game. /// {@endtemplate} -abstract class PinballTheme extends Equatable { +class PinballTheme extends Equatable { /// {@macro pinball_theme} - const PinballTheme(); + const PinballTheme({ + required CharacterTheme characterTheme, + }) : _characterTheme = characterTheme; - /// Ball color for this theme. - Color get ballColor; + final CharacterTheme _characterTheme; + + /// [CharacterTheme] for the chosen character. + CharacterTheme get characterTheme => _characterTheme; @override List get props => []; diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index 3b605411..59c16bd9 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template android_theme} -/// Defines Android theme assets and attributes. +/// Defines Android character theme assets and attributes. /// {@endtemplate} -class AndroidTheme extends PinballTheme { +class AndroidTheme extends CharacterTheme { /// {@macro android_theme} const AndroidTheme(); diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart new file mode 100644 index 00000000..50b6b557 --- /dev/null +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +/// {@template character_theme} +/// Base class for creating character themes. +/// +/// Character specific game components should have a getter specified here to +/// load their corresponding assets for the game. +/// {@endtemplate} +abstract class CharacterTheme extends Equatable { + /// {@macro character_theme} + const CharacterTheme(); + + /// Ball color for this theme. + Color get ballColor; + + @override + List get props => []; +} diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index fd0cdfa1..e4875a11 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dash_theme} -/// Defines Dash theme assets and attributes. +/// Defines Dash character theme assets and attributes. /// {@endtemplate} -class DashTheme extends PinballTheme { +class DashTheme extends CharacterTheme { /// {@macro dash_theme} const DashTheme(); diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 9337ddde..07776771 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dino_theme} -/// Defines Dino theme assets and attributes. +/// Defines Dino character theme assets and attributes. /// {@endtemplate} -class DinoTheme extends PinballTheme { +class DinoTheme extends CharacterTheme { /// {@macro dino_theme} const DinoTheme(); diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index d2cecf47..5264bad6 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template sparky_theme} -/// Defines Sparky theme assets and attributes. +/// Defines Sparky character theme assets and attributes. /// {@endtemplate} -class SparkyTheme extends PinballTheme { +class SparkyTheme extends CharacterTheme { /// {@macro sparky_theme} const SparkyTheme(); diff --git a/packages/pinball_theme/lib/src/themes/themes.dart b/packages/pinball_theme/lib/src/themes/themes.dart index 895e7c8d..d4062a4f 100644 --- a/packages/pinball_theme/lib/src/themes/themes.dart +++ b/packages/pinball_theme/lib/src/themes/themes.dart @@ -1,4 +1,5 @@ export 'android_theme.dart'; +export 'character_theme.dart'; export 'dash_theme.dart'; export 'dino_theme.dart'; export 'sparky_theme.dart'; diff --git a/packages/pinball_theme/test/src/pinball_theme_test.dart b/packages/pinball_theme/test/src/pinball_theme_test.dart new file mode 100644 index 00000000..899eec64 --- /dev/null +++ b/packages/pinball_theme/test/src/pinball_theme_test.dart @@ -0,0 +1,28 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('PinballTheme', () { + const characterTheme = SparkyTheme(); + + test('can be instantiated', () { + expect(PinballTheme(characterTheme: characterTheme), isNotNull); + }); + + test('supports value equality', () { + expect( + PinballTheme(characterTheme: characterTheme), + equals(PinballTheme(characterTheme: characterTheme)), + ); + }); + + test('characterTheme is correct', () { + expect( + PinballTheme(characterTheme: characterTheme).characterTheme, + equals(characterTheme), + ); + }); + }); +} diff --git a/test/theme/cubit/theme_cubit_test.dart b/test/theme/cubit/theme_cubit_test.dart index 59aecd37..1f2d24e0 100644 --- a/test/theme/cubit/theme_cubit_test.dart +++ b/test/theme/cubit/theme_cubit_test.dart @@ -5,17 +5,17 @@ import 'package:pinball_theme/pinball_theme.dart'; void main() { group('ThemeCubit', () { - test('initial state has Dash theme', () { + test('initial state has Dash character theme', () { final themeCubit = ThemeCubit(); - expect(themeCubit.state.theme, equals(const DashTheme())); + expect(themeCubit.state.theme.characterTheme, equals(const DashTheme())); }); blocTest( - 'themeSelected emits selected theme', + 'charcterSelected emits selected character theme', build: ThemeCubit.new, - act: (bloc) => bloc.themeSelected(const SparkyTheme()), + act: (bloc) => bloc.characterSelected(const SparkyTheme()), expect: () => [ - const ThemeState(SparkyTheme()), + const ThemeState(PinballTheme(characterTheme: SparkyTheme())), ], ); }); diff --git a/test/theme/cubit/theme_state_test.dart b/test/theme/cubit/theme_state_test.dart index e89abe01..49a2a387 100644 --- a/test/theme/cubit/theme_state_test.dart +++ b/test/theme/cubit/theme_state_test.dart @@ -2,18 +2,17 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; void main() { group('ThemeState', () { test('can be instantiated', () { - expect(const ThemeState(DashTheme()), isNotNull); + expect(const ThemeState.initial(), isNotNull); }); test('supports value equality', () { expect( - ThemeState(DashTheme()), - equals(const ThemeState(DashTheme())), + ThemeState.initial(), + equals(const ThemeState.initial()), ); }); }); From 0827924955c6c1db048e3d494f2426b6cdceb22f Mon Sep 17 00:00:00 2001 From: Allison Ryan Date: Mon, 7 Mar 2022 08:10:59 -0600 Subject: [PATCH 34/53] chore: add to props --- packages/pinball_theme/lib/src/pinball_theme.dart | 2 +- packages/pinball_theme/lib/src/themes/character_theme.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pinball_theme/lib/src/pinball_theme.dart b/packages/pinball_theme/lib/src/pinball_theme.dart index b4c404e5..a766a129 100644 --- a/packages/pinball_theme/lib/src/pinball_theme.dart +++ b/packages/pinball_theme/lib/src/pinball_theme.dart @@ -19,5 +19,5 @@ class PinballTheme extends Equatable { CharacterTheme get characterTheme => _characterTheme; @override - List get props => []; + List get props => [_characterTheme]; } diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 50b6b557..8f81486a 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -15,5 +15,5 @@ abstract class CharacterTheme extends Equatable { Color get ballColor; @override - List get props => []; + List get props => [ballColor]; } From 9587a4fe7792577fae6823c8c56ee2e62ccc0869 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Mon, 7 Mar 2022 14:50:01 -0300 Subject: [PATCH 35/53] docs: adding dart docs to the wall component --- lib/game/components/wall.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 7bf58273..8e359f20 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -3,6 +3,12 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; +/// {@template wall} +/// +/// A generic wall component, a static component that can +/// be used to create more complex structures. +/// +/// {@endtemplate} class Wall extends BodyComponent { Wall({ required this.start, @@ -29,6 +35,12 @@ class Wall extends BodyComponent { } } +/// {@template bottom_wall} +/// +/// An specifc [Wall] used to create bottom boundary of the +/// game board +/// +/// {@endtemplate} class BottomWall extends Wall { BottomWall(Forge2DGame game) : super( @@ -40,6 +52,12 @@ class BottomWall extends Wall { ); } +/// {@template bottom_wall_ball_contact_callback} +/// +/// The [ContactCallback] responsible for indentifying when a [Ball] +/// has fall into the bottom of the board +/// +/// {@endtemplate} class BottomWallBallContactCallback extends ContactCallback { @override void begin(Ball ball, BottomWall wall, Contact contact) { From a8c2257ce39f8a414785ffe287e834c1a7516894 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Mon, 7 Mar 2022 15:54:40 -0300 Subject: [PATCH 36/53] fix: changing ball spawning point to avoid context errors --- lib/game/pinball_game.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 91e12854..5b5d7885 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -18,10 +18,15 @@ class PinballGame extends Forge2DGame with FlameBloc { @override Future onLoad() async { - spawnBall(); addContactCallback(BallScorePointsCallback()); await add(BottomWall(this)); addContactCallback(BottomWallBallContactCallback()); } + + @override + void onAttach() { + super.onAttach(); + spawnBall(); + } } From 618ce97adc1020eaeb1ec938c8f34ed4749151bb Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 7 Mar 2022 16:23:01 -0300 Subject: [PATCH 37/53] Apply suggestions from code review Co-authored-by: Alejandro Santiago --- lib/game/components/wall.dart | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 8e359f20..910d5b62 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -4,10 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; /// {@template wall} -/// -/// A generic wall component, a static component that can -/// be used to create more complex structures. -/// +/// A continuos generic and [BodyType.static] barrier that divides a game area. /// {@endtemplate} class Wall extends BodyComponent { Wall({ @@ -36,10 +33,10 @@ class Wall extends BodyComponent { } /// {@template bottom_wall} +/// [Wall] located at the bottom of the [board]. /// -/// An specifc [Wall] used to create bottom boundary of the -/// game board -/// +/// Collisions with [BottomWall] are listened by +/// [BottomWallBallContactCallback]. /// {@endtemplate} class BottomWall extends Wall { BottomWall(Forge2DGame game) @@ -53,10 +50,7 @@ class BottomWall extends Wall { } /// {@template bottom_wall_ball_contact_callback} -/// -/// The [ContactCallback] responsible for indentifying when a [Ball] -/// has fall into the bottom of the board -/// +/// Listens when a [Ball] falls into a [BottomWall]. /// {@endtemplate} class BottomWallBallContactCallback extends ContactCallback { @override From cde0dfaa250de037ba209c3ef634ce82a27a5f6f Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Mon, 7 Mar 2022 17:50:07 -0300 Subject: [PATCH 38/53] feat: adding configs for firebase hosting --- .gitignore | 2 ++ README.md | 27 +++++++++++++++++++++++++++ firebase.json | 10 ++++++++++ 3 files changed, 39 insertions(+) create mode 100644 firebase.json diff --git a/.gitignore b/.gitignore index bd315f72..eeb2b0f6 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,5 @@ app.*.map.json !.idea/codeStyles/ !.idea/dictionaries/ !.idea/runConfigurations/ + +.firebase diff --git a/README.md b/README.md index b51926b5..80ef7b6a 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,33 @@ Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info } ``` +### Deploy application to Firebase hosting + +Follow the following steps to deploy the application. + +## Firebase CLI + +Install and authenticate with [Firebase CLI tools](https://firebase.google.com/docs/cli) + +## Build the project using the desired enviroment + +```bash +# Development +$ flutter build web --release --target lib/main_development.dart + +# Staging +$ flutter build web --release --target lib/main_staging.dart + +# Production +$ flutter build web --release --target lib/main_production.dart +``` + +## Deploy + +```bash +$ firebase deploy +``` + [coverage_badge]: coverage_badge.svg [flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html [internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..0d25a779 --- /dev/null +++ b/firebase.json @@ -0,0 +1,10 @@ +{ + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } +} From d70122aee53c0183f8ed8297f5a092370afd9e25 Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 7 Mar 2022 17:51:53 -0300 Subject: [PATCH 39/53] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80ef7b6a..b91a1c98 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Follow the following steps to deploy the application. Install and authenticate with [Firebase CLI tools](https://firebase.google.com/docs/cli) -## Build the project using the desired enviroment +## Build the project using the desired environment ```bash # Development From ac0aca5f8f6e157932dbf106b6303880a4401336 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Mon, 7 Mar 2022 19:13:36 -0300 Subject: [PATCH 40/53] feat: adding placeholder sprite for the ball component --- assets/images/components/ball.png | Bin 0 -> 27915 bytes lib/game/components/ball.dart | 27 ++++++++++++++++++--------- lib/game/game.dart | 1 + lib/game/game_assets.dart | 12 ++++++++++++ lib/game/pinball_game.dart | 9 ++++++++- pubspec.lock | 8 +++++--- pubspec.yaml | 9 ++++++++- 7 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 assets/images/components/ball.png create mode 100644 lib/game/game_assets.dart diff --git a/assets/images/components/ball.png b/assets/images/components/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..af80811b138b9834be6812cbc44e277639d0be51 GIT binary patch literal 27915 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 literal 0 HcmV?d00001 diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index e285b14b..65a0bffc 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -1,23 +1,32 @@ +import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/body_component.dart'; -import 'package:flutter/material.dart'; -import 'package:forge2d/forge2d.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; -class Ball extends BodyComponent +class Ball extends PositionBodyComponent with BlocComponent { Ball({ required Vector2 position, - }) : _position = position { - // TODO(alestiago): Use asset instead of color when provided. - paint = Paint()..color = const Color(0xFFFFFFFF); - } + required Vector2 size, + }) : _position = position, + _size = size, + super(size: size); final Vector2 _position; + final Vector2 _size; + + static const spritePath = 'components/ball.png'; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite(spritePath); + positionComponent = SpriteComponent(sprite: sprite, size: _size); + } @override Body createBody() { - final shape = CircleShape()..radius = 2; + final shape = CircleShape()..radius = _size.x / 2; final fixtureDef = FixtureDef(shape)..density = 1; diff --git a/lib/game/game.dart b/lib/game/game.dart index 253dcc9f..ad02533d 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,4 +1,5 @@ export 'bloc/game_bloc.dart'; export 'components/components.dart'; +export 'game_assets.dart'; export 'pinball_game.dart'; export 'view/view.dart'; diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart new file mode 100644 index 00000000..8a889ab7 --- /dev/null +++ b/lib/game/game_assets.dart @@ -0,0 +1,12 @@ +import 'package:pinball/game/game.dart'; + +/// Add methods to help loading and caching game assets +extension PinballGameAssetsX on PinballGame { + + /// Pre load the initial assets of the game + Future preLoadAssets() async { + await Future.wait([ + images.load(Ball.spritePath), + ]); + } +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 5b5d7885..72c8f66a 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -4,7 +4,12 @@ import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc { void spawnBall() { - add(Ball(position: ballStartingPosition)); + add( + Ball( + position: ballStartingPosition, + size: Vector2.all(4), + ), + ); } // TODO(erickzanardo): Change to the plumber position @@ -18,6 +23,8 @@ class PinballGame extends Forge2DGame with FlameBloc { @override Future onLoad() async { + await preLoadAssets(); + addContactCallback(BallScorePointsCallback()); await add(BottomWall(this)); diff --git a/pubspec.lock b/pubspec.lock index 7bf08da4..d4678cdc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -151,9 +151,11 @@ packages: flame_forge2d: dependency: "direct main" description: - name: flame_forge2d - url: "https://pub.dartlang.org" - source: hosted + path: "packages/flame_forge2d" + ref: "erick.fix_position_body_component_on_mount" + resolved-ref: cccdfe0054743400ad4262aecac54f89e43be944 + url: "git@github.com:flame-engine/flame.git" + source: git version: "0.9.0-releasecandidate.1" flame_test: dependency: "direct dev" diff --git a/pubspec.yaml b/pubspec.yaml index 6c3bd98e..683eee81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,11 @@ dependencies: equatable: ^2.0.3 flame: ^1.1.0-releasecandidate.1 flame_bloc: ^1.2.0-releasecandidate.1 - flame_forge2d: ^0.9.0-releasecandidate.1 + flame_forge2d: + git: + url: git@github.com:flame-engine/flame.git + path: packages/flame_forge2d + ref: erick.fix_position_body_component_on_mount flutter: sdk: flutter flutter_bloc: ^8.0.1 @@ -33,3 +37,6 @@ dev_dependencies: flutter: uses-material-design: true generate: true + + assets: + - assets/images/components/ From bf4b262a9d6bdc08b5bad093ab0ba09df29515be Mon Sep 17 00:00:00 2001 From: alestiago Date: Tue, 8 Mar 2022 09:05:44 +0000 Subject: [PATCH 41/53] docs: removed erroneous reference --- lib/game/components/wall.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 910d5b62..b784b8cb 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -33,7 +33,7 @@ class Wall extends BodyComponent { } /// {@template bottom_wall} -/// [Wall] located at the bottom of the [board]. +/// [Wall] located at the bottom of the board. /// /// Collisions with [BottomWall] are listened by /// [BottomWallBallContactCallback]. From a2093811fa46132c66c357148088cca0b56b265a Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 11:20:29 -0300 Subject: [PATCH 42/53] feat: adding tests --- lib/game/components/ball.dart | 11 +++++------ lib/game/game_assets.dart | 1 - lib/game/pinball_game.dart | 9 +++------ lib/game/view/pinball_game_page.dart | 21 +++++++++++++++++++-- test/game/components/ball_test.dart | 2 +- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 65a0bffc..2d9dddf0 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -7,13 +7,12 @@ class Ball extends PositionBodyComponent with BlocComponent { Ball({ required Vector2 position, - required Vector2 size, }) : _position = position, - _size = size, - super(size: size); + super(size: ballSize); + + static final ballSize = Vector2.all(2); final Vector2 _position; - final Vector2 _size; static const spritePath = 'components/ball.png'; @@ -21,12 +20,12 @@ class Ball extends PositionBodyComponent Future onLoad() async { await super.onLoad(); final sprite = await gameRef.loadSprite(spritePath); - positionComponent = SpriteComponent(sprite: sprite, size: _size); + positionComponent = SpriteComponent(sprite: sprite, size: ballSize); } @override Body createBody() { - final shape = CircleShape()..radius = _size.x / 2; + final shape = CircleShape()..radius = ballSize.x / 2; final fixtureDef = FixtureDef(shape)..density = 1; diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 8a889ab7..62983e23 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -2,7 +2,6 @@ import 'package:pinball/game/game.dart'; /// Add methods to help loading and caching game assets extension PinballGameAssetsX on PinballGame { - /// Pre load the initial assets of the game Future preLoadAssets() async { await Future.wait([ diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 72c8f66a..7c701c09 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; @@ -5,10 +7,7 @@ import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc { void spawnBall() { add( - Ball( - position: ballStartingPosition, - size: Vector2.all(4), - ), + Ball(position: ballStartingPosition), ); } @@ -23,8 +22,6 @@ class PinballGame extends Forge2DGame with FlameBloc { @override Future onLoad() async { - await preLoadAssets(); - addContactCallback(BallScorePointsCallback()); await add(BottomWall(this)); diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 28834907..02f5b34c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -23,9 +23,26 @@ class PinballGamePage extends StatelessWidget { } } -class PinballGameView extends StatelessWidget { +class PinballGameView extends StatefulWidget { const PinballGameView({Key? key}) : super(key: key); + @override + State createState() => _PinballGameViewState(); +} + +class _PinballGameViewState extends State { + late PinballGame _game; + + @override + void initState() { + super.initState(); + + // TODO(erickzanardo): Revisit this when we start to have more assets + // this could expose a Stream (maybe even a cubit?) so we could show the + // the loading progress with some fancy widgets. + _game = PinballGame()..preLoadAssets(); + } + @override Widget build(BuildContext context) { return BlocListener( @@ -39,7 +56,7 @@ class PinballGameView extends StatelessWidget { ); } }, - child: GameWidget(game: PinballGame()), + child: GameWidget(game: _game), ); } } diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index b32d16d5..7ac3ceff 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -79,7 +79,7 @@ void main() { final fixture = ball.body.fixtures[0]; expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(2)); + expect(fixture.shape.radius, equals(1)); }, ); }); From 8a5fbda51f4319816b4664c905737925b34aa751 Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 8 Mar 2022 12:23:24 -0300 Subject: [PATCH 43/53] Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/game_assets.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 62983e23..964aeda1 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -1,8 +1,8 @@ import 'package:pinball/game/game.dart'; -/// Add methods to help loading and caching game assets +/// Add methods to help loading and caching game assets. extension PinballGameAssetsX on PinballGame { - /// Pre load the initial assets of the game + /// Pre load the initial assets of the game. Future preLoadAssets() async { await Future.wait([ images.load(Ball.spritePath), From 7a0852642b703a01345f0d0d554d16070b2e5d0d Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 17:31:06 -0300 Subject: [PATCH 44/53] feat: updating flame, flame bloc and flame forge2d versions --- pubspec.lock | 14 ++++++-------- pubspec.yaml | 10 +++------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index d4678cdc..861dae5b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -140,23 +140,21 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-releasecandidate.1" + version: "1.1.0-releasecandidate.2" flame_bloc: dependency: "direct main" description: name: flame_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-releasecandidate.1" + version: "1.2.0-releasecandidate.2" flame_forge2d: dependency: "direct main" description: - path: "packages/flame_forge2d" - ref: "erick.fix_position_body_component_on_mount" - resolved-ref: cccdfe0054743400ad4262aecac54f89e43be944 - url: "git@github.com:flame-engine/flame.git" - source: git - version: "0.9.0-releasecandidate.1" + name: flame_forge2d + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0-releasecandidate.2" flame_test: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 683eee81..8738f2bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,13 +9,9 @@ environment: dependencies: bloc: ^8.0.2 equatable: ^2.0.3 - flame: ^1.1.0-releasecandidate.1 - flame_bloc: ^1.2.0-releasecandidate.1 - flame_forge2d: - git: - url: git@github.com:flame-engine/flame.git - path: packages/flame_forge2d - ref: erick.fix_position_body_component_on_mount + flame: ^1.1.0-releasecandidate.2 + flame_bloc: ^1.2.0-releasecandidate.2 + flame_forge2d: ^0.9.0-releasecandidate.2 flutter: sdk: flutter flutter_bloc: ^8.0.1 From 0780266e2ac1d2605ad4e5086de3a543cbdce0b7 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 17:57:04 -0300 Subject: [PATCH 45/53] feat: adding deploy site --- firebase.json | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase.json b/firebase.json index 0d25a779..80e2ae69 100644 --- a/firebase.json +++ b/firebase.json @@ -1,6 +1,7 @@ { "hosting": { "public": "build/web", + "site": "ashehwkdkdjruejdnensjsjdne", "ignore": [ "firebase.json", "**/.*", From df2ce91b063daab7cad9ec75fd726626372de4ff Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 19:23:10 -0300 Subject: [PATCH 46/53] feat: adding bonus letter feature state management --- lib/game/bloc/game_bloc.dart | 12 +++++ lib/game/bloc/game_event.dart | 9 ++++ lib/game/bloc/game_state.dart | 10 +++- test/game/bloc/game_bloc_test.dart | 62 +++++++++++++++++++--- test/game/bloc/game_event_test.dart | 17 ++++++ test/game/bloc/game_state_test.dart | 29 ++++++++-- test/game/components/ball_test.dart | 2 +- test/game/view/pinball_game_page_test.dart | 2 +- 8 files changed, 127 insertions(+), 16 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 71c527a8..3cfc521f 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -9,6 +9,7 @@ class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { on(_onBallLost); on(_onScored); + on(_onBonusLetterActivated); } void _onBallLost(BallLost event, Emitter emit) { @@ -22,4 +23,15 @@ class GameBloc extends Bloc { emit(state.copyWith(score: state.score + event.points)); } } + + void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { + emit( + state.copyWith( + bonusLetter: [ + ...state.bonusLetter, + event.letter, + ], + ), + ); + } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index 88ef265b..417f6322 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -24,3 +24,12 @@ class Scored extends GameEvent { @override List get props => [points]; } + +class BonusLetterActivated extends GameEvent { + const BonusLetterActivated(this.letter); + + final String letter; + + @override + List get props => [letter]; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 1a0568f7..8454bab7 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -8,12 +8,14 @@ class GameState extends Equatable { const GameState({ required this.score, required this.balls, + required this.bonusLetter, }) : assert(score >= 0, "Score can't be negative"), assert(balls >= 0, "Number of balls can't be negative"); const GameState.initial() : score = 0, - balls = 3; + balls = 3, + bonusLetter = const []; /// The current score of the game. final int score; @@ -23,6 +25,9 @@ class GameState extends Equatable { /// When the number of balls is 0, the game is over. final int balls; + /// Active bonus letters + final List bonusLetter; + /// Determines when the game is over. bool get isGameOver => balls == 0; @@ -32,6 +37,7 @@ class GameState extends Equatable { GameState copyWith({ int? score, int? balls, + List? bonusLetter, }) { assert( score == null || score >= this.score, @@ -41,6 +47,7 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, balls: balls ?? this.balls, + bonusLetter: bonusLetter ?? this.bonusLetter, ); } @@ -48,5 +55,6 @@ class GameState extends Equatable { List get props => [ score, balls, + bonusLetter, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 2676a286..3dc5dda7 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,9 +21,9 @@ void main() { } }, expect: () => [ - const GameState(score: 0, balls: 2), - const GameState(score: 0, balls: 1), - const GameState(score: 0, balls: 0), + const GameState(score: 0, balls: 2, bonusLetter: []), + const GameState(score: 0, balls: 1, bonusLetter: []), + const GameState(score: 0, balls: 0, bonusLetter: []), ], ); }); @@ -37,8 +37,8 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState(score: 2, balls: 3), - const GameState(score: 5, balls: 3), + const GameState(score: 2, balls: 3, bonusLetter: []), + const GameState(score: 5, balls: 3, bonusLetter: []), ], ); @@ -53,9 +53,55 @@ void main() { bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState(score: 0, balls: 2), - const GameState(score: 0, balls: 1), - const GameState(score: 0, balls: 0), + const GameState(score: 0, balls: 2, bonusLetter: []), + const GameState(score: 0, balls: 1, bonusLetter: []), + const GameState(score: 0, balls: 0, bonusLetter: []), + ], + ); + }); + + group('BonusLetterActivated', () { + blocTest( + 'adds the letter to the state', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const BonusLetterActivated('G')) + ..add(const BonusLetterActivated('O')) + ..add(const BonusLetterActivated('O')) + ..add(const BonusLetterActivated('G')) + ..add(const BonusLetterActivated('L')) + ..add(const BonusLetterActivated('E')), + expect: () => [ + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O', 'G'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O', 'G', 'L'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O', 'G', 'L', 'E'], + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index e839ab56..0e7a0f71 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -40,5 +40,22 @@ void main() { expect(() => Scored(points: 0), throwsAssertionError); }); }); + + group('BonusLetterActivated', () { + test('can be instantiated', () { + expect(const BonusLetterActivated('A'), isNotNull); + }); + + test('supports value equality', () { + expect( + BonusLetterActivated('A'), + equals(BonusLetterActivated('A')), + ); + expect( + BonusLetterActivated('B'), + isNot(equals(BonusLetterActivated('A'))), + ); + }); + }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 59cc0d1d..e50acbcd 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -7,14 +7,24 @@ void main() { group('GameState', () { test('supports value equality', () { expect( - GameState(score: 0, balls: 0), - equals(const GameState(score: 0, balls: 0)), + GameState( + score: 0, + balls: 0, + bonusLetter: const [], + ), + equals( + const GameState( + score: 0, + balls: 0, + bonusLetter: [], + ), + ), ); }); group('constructor', () { test('can be instantiated', () { - expect(const GameState(score: 0, balls: 0), isNotNull); + expect(const GameState(score: 0, balls: 0, bonusLetter: []), isNotNull); }); }); @@ -23,7 +33,7 @@ void main() { 'when balls are negative', () { expect( - () => GameState(balls: -1, score: 0), + () => GameState(balls: -1, score: 0, bonusLetter: const []), throwsAssertionError, ); }, @@ -34,7 +44,7 @@ void main() { 'when score is negative', () { expect( - () => GameState(balls: 0, score: -1), + () => GameState(balls: 0, score: -1, bonusLetter: const []), throwsAssertionError, ); }, @@ -47,6 +57,7 @@ void main() { const gameState = GameState( balls: 0, score: 0, + bonusLetter: [], ); expect(gameState.isGameOver, isTrue); }); @@ -57,6 +68,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, + bonusLetter: [], ); expect(gameState.isGameOver, isFalse); }); @@ -70,6 +82,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, + bonusLetter: [], ); expect(gameState.isLastBall, isTrue); }, @@ -82,6 +95,7 @@ void main() { const gameState = GameState( balls: 2, score: 0, + bonusLetter: [], ); expect(gameState.isLastBall, isFalse); }, @@ -96,6 +110,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, + bonusLetter: [], ); expect( () => gameState.copyWith(score: gameState.score - 1), @@ -111,6 +126,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, + bonusLetter: [], ); expect( gameState.copyWith(), @@ -126,10 +142,12 @@ void main() { const gameState = GameState( score: 2, balls: 0, + bonusLetter: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, + bonusLetter: const ['A'], ); expect(gameState, isNot(equals(otherGameState))); @@ -137,6 +155,7 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, + bonusLetter: otherGameState.bonusLetter, ), equals(otherGameState), ); diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 7ac3ceff..9885b310 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -135,7 +135,7 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState(score: 10, balls: 1), + initialState: const GameState(score: 10, balls: 1, bonusLetter: []), ); await game.ready(); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index be418c1d..eacee734 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -67,7 +67,7 @@ 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 state = GameState(score: 0, balls: 0, bonusLetter: []); whenListen( gameBloc, Stream.value(state), From de25974f8c9b50bc31fd416f28a08bab0445a98a Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 9 Mar 2022 09:48:09 -0300 Subject: [PATCH 47/53] Update lib/game/bloc/game_state.dart Co-authored-by: Alejandro Santiago --- lib/game/bloc/game_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 8454bab7..09207b86 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -25,7 +25,7 @@ class GameState extends Equatable { /// When the number of balls is 0, the game is over. final int balls; - /// Active bonus letters + /// Active bonus letters. final List bonusLetter; /// Determines when the game is over. From e4cd4342c0c2bc40724cfed8deac93caf3abffa0 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 09:53:52 -0300 Subject: [PATCH 48/53] feat: pr suggestions --- lib/game/bloc/game_bloc.dart | 4 +-- lib/game/bloc/game_state.dart | 12 ++++----- test/game/bloc/game_bloc_test.dart | 28 +++++++++---------- test/game/bloc/game_state_test.dart | 31 ++++++++++++---------- test/game/components/ball_test.dart | 6 ++++- test/game/view/pinball_game_page_test.dart | 2 +- 6 files changed, 45 insertions(+), 38 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 3cfc521f..3b5c16b0 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -27,8 +27,8 @@ class GameBloc extends Bloc { void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { emit( state.copyWith( - bonusLetter: [ - ...state.bonusLetter, + bonusLetters: [ + ...state.bonusLetters, event.letter, ], ), diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 09207b86..8a5ab298 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -8,14 +8,14 @@ class GameState extends Equatable { const GameState({ required this.score, required this.balls, - required this.bonusLetter, + required this.bonusLetters, }) : assert(score >= 0, "Score can't be negative"), assert(balls >= 0, "Number of balls can't be negative"); const GameState.initial() : score = 0, balls = 3, - bonusLetter = const []; + bonusLetters = const []; /// The current score of the game. final int score; @@ -26,7 +26,7 @@ class GameState extends Equatable { final int balls; /// Active bonus letters. - final List bonusLetter; + final List bonusLetters; /// Determines when the game is over. bool get isGameOver => balls == 0; @@ -37,7 +37,7 @@ class GameState extends Equatable { GameState copyWith({ int? score, int? balls, - List? bonusLetter, + List? bonusLetters, }) { assert( score == null || score >= this.score, @@ -47,7 +47,7 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, balls: balls ?? this.balls, - bonusLetter: bonusLetter ?? this.bonusLetter, + bonusLetters: bonusLetters ?? this.bonusLetters, ); } @@ -55,6 +55,6 @@ class GameState extends Equatable { List get props => [ score, balls, - bonusLetter, + bonusLetters, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 3dc5dda7..bd669397 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,9 +21,9 @@ void main() { } }, expect: () => [ - const GameState(score: 0, balls: 2, bonusLetter: []), - const GameState(score: 0, balls: 1, bonusLetter: []), - const GameState(score: 0, balls: 0, bonusLetter: []), + const GameState(score: 0, balls: 2, bonusLetters: []), + const GameState(score: 0, balls: 1, bonusLetters: []), + const GameState(score: 0, balls: 0, bonusLetters: []), ], ); }); @@ -37,8 +37,8 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState(score: 2, balls: 3, bonusLetter: []), - const GameState(score: 5, balls: 3, bonusLetter: []), + const GameState(score: 2, balls: 3, bonusLetters: []), + const GameState(score: 5, balls: 3, bonusLetters: []), ], ); @@ -53,9 +53,9 @@ void main() { bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState(score: 0, balls: 2, bonusLetter: []), - const GameState(score: 0, balls: 1, bonusLetter: []), - const GameState(score: 0, balls: 0, bonusLetter: []), + const GameState(score: 0, balls: 2, bonusLetters: []), + const GameState(score: 0, balls: 1, bonusLetters: []), + const GameState(score: 0, balls: 0, bonusLetters: []), ], ); }); @@ -75,32 +75,32 @@ void main() { const GameState( score: 0, balls: 3, - bonusLetter: ['G'], + bonusLetters: ['G'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O'], + bonusLetters: ['G', 'O'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O'], + bonusLetters: ['G', 'O', 'O'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O', 'G'], + bonusLetters: ['G', 'O', 'O', 'G'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O', 'G', 'L'], + bonusLetters: ['G', 'O', 'O', 'G', 'L'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O', 'G', 'L', 'E'], + bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'], ), ], ); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index e50acbcd..7345d3bd 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -10,13 +10,13 @@ void main() { GameState( score: 0, balls: 0, - bonusLetter: const [], + bonusLetters: const [], ), equals( const GameState( score: 0, balls: 0, - bonusLetter: [], + bonusLetters: [], ), ), ); @@ -24,7 +24,10 @@ void main() { group('constructor', () { test('can be instantiated', () { - expect(const GameState(score: 0, balls: 0, bonusLetter: []), isNotNull); + expect( + const GameState(score: 0, balls: 0, bonusLetters: []), + isNotNull, + ); }); }); @@ -33,7 +36,7 @@ void main() { 'when balls are negative', () { expect( - () => GameState(balls: -1, score: 0, bonusLetter: const []), + () => GameState(balls: -1, score: 0, bonusLetters: const []), throwsAssertionError, ); }, @@ -44,7 +47,7 @@ void main() { 'when score is negative', () { expect( - () => GameState(balls: 0, score: -1, bonusLetter: const []), + () => GameState(balls: 0, score: -1, bonusLetters: const []), throwsAssertionError, ); }, @@ -57,7 +60,7 @@ void main() { const gameState = GameState( balls: 0, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isGameOver, isTrue); }); @@ -68,7 +71,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isGameOver, isFalse); }); @@ -82,7 +85,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isLastBall, isTrue); }, @@ -95,7 +98,7 @@ void main() { const gameState = GameState( balls: 2, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isLastBall, isFalse); }, @@ -110,7 +113,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, - bonusLetter: [], + bonusLetters: [], ); expect( () => gameState.copyWith(score: gameState.score - 1), @@ -126,7 +129,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, - bonusLetter: [], + bonusLetters: [], ); expect( gameState.copyWith(), @@ -142,12 +145,12 @@ void main() { const gameState = GameState( score: 2, balls: 0, - bonusLetter: [], + bonusLetters: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, - bonusLetter: const ['A'], + bonusLetters: const ['A'], ); expect(gameState, isNot(equals(otherGameState))); @@ -155,7 +158,7 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, - bonusLetter: otherGameState.bonusLetter, + bonusLetters: otherGameState.bonusLetters, ), equals(otherGameState), ); diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 9885b310..bd2cbcfc 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -135,7 +135,11 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState(score: 10, balls: 1, bonusLetter: []), + initialState: const GameState( + score: 10, + balls: 1, + bonusLetters: [], + ), ); await game.ready(); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index eacee734..d578a1db 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -67,7 +67,7 @@ void main() { 'renders a game over dialog when the user has lost', (tester) async { final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0, bonusLetter: []); + const state = GameState(score: 0, balls: 0, bonusLetters: []); whenListen( gameBloc, Stream.value(state), From a3dc9d09cff15dfb97409f099981eca355585c5d Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 18:45:51 -0300 Subject: [PATCH 49/53] feat: adding game hud --- lib/game/view/game_hud.dart | 40 ++++++++++++++++++++++ lib/game/view/pinball_game_page.dart | 13 ++++++- lib/game/view/view.dart | 1 + test/game/view/game_hud_test.dart | 32 +++++++++++++++++ test/game/view/pinball_game_page_test.dart | 6 +++- 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 lib/game/view/game_hud.dart create mode 100644 test/game/view/game_hud_test.dart diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart new file mode 100644 index 00000000..b694e812 --- /dev/null +++ b/lib/game/view/game_hud.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; + +class GameHud extends StatelessWidget { + const GameHud({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.watch().state; + + return Container( + color: Colors.redAccent, + width: 200, + height: 100, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${state.score}', + style: Theme.of(context).textTheme.headline3, + ), + Column( + children: [ + for (var i = 0; i < state.balls; i++) + const Padding( + padding: EdgeInsets.only(top: 6), + child: CircleAvatar( + radius: 8, + backgroundColor: Colors.black, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 02f5b34c..8a9a981c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -56,7 +56,18 @@ class _PinballGameViewState extends State { ); } }, - child: GameWidget(game: _game), + child: Stack( + children: [ + Positioned.fill( + child: GameWidget(game: _game), + ), + const Positioned( + top: 8, + left: 8, + child: GameHud(), + ), + ], + ), ); } } diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart index 53d3813a..26b700d3 100644 --- a/lib/game/view/view.dart +++ b/lib/game/view/view.dart @@ -1,2 +1,3 @@ +export 'game_hud.dart'; export 'pinball_game_page.dart'; export 'widgets/widgets.dart'; diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart new file mode 100644 index 00000000..40079a2f --- /dev/null +++ b/test/game/view/game_hud_test.dart @@ -0,0 +1,32 @@ +// 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:pinball/game/game.dart'; +import '../../helpers/helpers.dart'; + +void main() { + group('GameHud', () { + testWidgets( + 'renders the current score and balls', + (tester) async { + final state = GameState(score: 10, balls: 2); + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.text('10'), findsOneWidget); + expect(find.byType(CircleAvatar), findsNWidgets(2)); + }, + ); + }); +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index d578a1db..746dc2c7 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -48,7 +48,7 @@ void main() { }); group('PinballGameView', () { - testWidgets('renders game', (tester) async { + testWidgets('renders game and a hud', (tester) async { final gameBloc = MockGameBloc(); whenListen( gameBloc, @@ -61,6 +61,10 @@ void main() { find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); + expect( + find.byType(GameHud), + findsOneWidget, + ); }); testWidgets( From f9f109ba5aeadfd8618439187b8ea3bad8a88b5f Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 9 Mar 2022 10:02:50 -0300 Subject: [PATCH 50/53] Apply suggestions from code review Co-authored-by: Alejandro Santiago --- lib/game/view/game_hud.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart index b694e812..beff3391 100644 --- a/lib/game/view/game_hud.dart +++ b/lib/game/view/game_hud.dart @@ -2,7 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +/// {@template game_hud} +/// Overlay of a [PinballGame] that displays the current [GameState.score] and +/// [GameState.balls]. +/// {@endtemplate} class GameHud extends StatelessWidget { + /// {@macro game_hud} const GameHud({Key? key}) : super(key: key); @override From 38b8a28ffc661563374f37f64e6dc0012475304f Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 10:18:54 -0300 Subject: [PATCH 51/53] feat: pr suggestions --- lib/game/view/game_hud.dart | 5 ++- test/game/view/game_hud_test.dart | 75 +++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart index beff3391..7bf1e8c3 100644 --- a/lib/game/view/game_hud.dart +++ b/lib/game/view/game_hud.dart @@ -26,11 +26,12 @@ class GameHud extends StatelessWidget { '${state.score}', style: Theme.of(context).textTheme.headline3, ), - Column( + Wrap( + direction: Axis.vertical, children: [ for (var i = 0; i < state.balls; i++) const Padding( - padding: EdgeInsets.only(top: 6), + padding: EdgeInsets.only(top: 6, right: 6), child: CircleAvatar( radius: 8, backgroundColor: Colors.black, diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 40079a2f..27a423ed 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -8,25 +8,72 @@ import '../../helpers/helpers.dart'; void main() { group('GameHud', () { + late GameBloc gameBloc; + const initialState = GameState(score: 10, balls: 2); + + void _mockState(GameState state) { + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + } + + Future _pumpHud(WidgetTester tester) async { + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + } + + setUp(() { + gameBloc = MockGameBloc(); + _mockState(initialState); + }); + testWidgets( - 'renders the current score and balls', + 'renders the current score', (tester) async { - final state = GameState(score: 10, balls: 2); - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); + await _pumpHud(tester); + expect(find.text(initialState.score.toString()), findsOneWidget); + }, + ); - await tester.pumpApp( - GameHud(), - gameBloc: gameBloc, + testWidgets( + 'renders the current ball number', + (tester) async { + await _pumpHud(tester); + expect( + find.byType(CircleAvatar), + findsNWidgets(initialState.balls), ); - - expect(find.text('10'), findsOneWidget); - expect(find.byType(CircleAvatar), findsNWidgets(2)); }, ); + + testWidgets('updates the score', (tester) async { + await _pumpHud(tester); + expect(find.text(initialState.score.toString()), findsOneWidget); + + _mockState(initialState.copyWith(score: 20)); + + await tester.pump(); + expect(find.text('20'), findsOneWidget); + }); + + testWidgets('updates the ball number', (tester) async { + await _pumpHud(tester); + expect( + find.byType(CircleAvatar), + findsNWidgets(initialState.balls), + ); + + _mockState(initialState.copyWith(balls: 1)); + + await tester.pump(); + expect( + find.byType(CircleAvatar), + findsNWidgets(1), + ); + }); }); } From 884d8b36b07974dff833f37cc669721d58a26671 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 10:22:17 -0300 Subject: [PATCH 52/53] fix: lint --- lib/game/view/game_hud.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart index 7bf1e8c3..00eedd2b 100644 --- a/lib/game/view/game_hud.dart +++ b/lib/game/view/game_hud.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; /// {@template game_hud} -/// Overlay of a [PinballGame] that displays the current [GameState.score] and +/// Overlay of a [PinballGame] that displays the current [GameState.score] and /// [GameState.balls]. /// {@endtemplate} class GameHud extends StatelessWidget { From 84282fe83a67e95eaf5a3b3cb4a66d14192bc7c1 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 10:51:25 -0300 Subject: [PATCH 53/53] fix: test from rebase --- test/game/view/game_hud_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 27a423ed..e7334e41 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -9,7 +9,7 @@ import '../../helpers/helpers.dart'; void main() { group('GameHud', () { late GameBloc gameBloc; - const initialState = GameState(score: 10, balls: 2); + const initialState = GameState(score: 10, balls: 2, bonusLetters: []); void _mockState(GameState state) { whenListen(