From 6395835f20acb2ae22ef853178c918891688376a Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Thu, 28 Apr 2022 22:18:37 +0200 Subject: [PATCH 01/16] feat: add mobile touch controls (#232) * feat: add mobile touch controls * feat: add mobile touch controls * Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * feat: add mobile touch controls * feat: add mobile touch controls * feat: add mobile touch controls * feat: add mobile touch controls Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/pinball_game.dart | 69 +++++++++++- test/game/pinball_game_test.dart | 180 +++++++++++++++++++++++++++++++ test/helpers/mocks.dart | 8 +- 3 files changed, 251 insertions(+), 6 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 4b57f1dd..2736a07a 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -6,6 +6,7 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; @@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents, - Controls<_GameBallsController> { + Controls<_GameBallsController>, + TapDetector { PinballGame({ required this.characterTheme, required this.audio, @@ -70,6 +72,61 @@ class PinballGame extends Forge2DGame controller.attachTo(launcher.components.whereType().first); await super.onLoad(); } + + BoardSide? focusedBoardSide; + + @override + void onTapDown(TapDownInfo info) { + if (info.raw.kind == PointerDeviceKind.touch) { + final rocket = children.whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.pull(); + } else { + final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; + focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveUp(); + } + } + + super.onTapDown(info); + } + + @override + void onTapUp(TapUpInfo info) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.release(); + } else { + _moveFlippersDown(); + } + super.onTapUp(info); + } + + @override + void onTapCancel() { + children.whereType().first.release(); + + _moveFlippersDown(); + super.onTapCancel(); + } + + void _moveFlippersDown() { + if (focusedBoardSide != null) { + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveDown(); + focusedBoardSide = null; + } + } } class _GameBallsController extends ComponentController @@ -116,7 +173,7 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter { DebugPinballGame({ required CharacterTheme characterTheme, required PinballAudio audio, @@ -153,9 +210,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { @override void onTapUp(TapUpInfo info) { - add( - ControlledBall.debug()..initialPosition = info.eventPosition.game, - ); + super.onTapUp(info); + + if (info.raw.kind == PointerDeviceKind.mouse) { + add(ControlledBall.debug()..initialPosition = info.eventPosition.game); + } } } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2fdbe6c4..2e585d66 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -3,6 +3,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -229,6 +230,181 @@ void main() { ); }); }); + + group('flipper control', () { + flameTester.test('tap down moves left flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap down moves right flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(game.canvasSize); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.right, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap up moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + + flameTester.test('tap cancel moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + game.onTapCancel(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + }); + + group('plunger control', () { + flameTester.test('tap down moves plunger down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + }); + + flameTester.test('tap up releases plunger', (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + + flameTester.test('tap cancel releases plunger', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + game.onTapCancel(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + }); }); group('DebugPinballGame', () { @@ -238,8 +414,12 @@ void main() { final eventPosition = MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final raw = MockTapUpDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); + final tapUpEvent = MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + when(() => tapUpEvent.raw).thenReturn(raw); final previousBalls = game.descendants().whereType().toList(); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index b58dc619..ad999aff 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -55,8 +55,14 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class MockTapDownInfo extends Mock implements TapDownInfo {} + +class MockTapDownDetails extends Mock implements TapDownDetails {} + class MockTapUpInfo extends Mock implements TapUpInfo {} +class MockTapUpDetails extends Mock implements TapUpDetails {} + class MockEventPosition extends Mock implements EventPosition {} class MockFilter extends Mock implements Filter {} From cf688f0f26d65ad70839aa5f78bc6c7d0bd17828 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Thu, 28 Apr 2022 15:40:09 -0500 Subject: [PATCH 02/16] chore: removed `Wall` and `Board` (#224) * chore: remove wall and board * fix: lint * refactor: bottom group test * chore: remove start and end variables * refactor: group stories * refactor: bottom group blueprint * style: remove whitespace and add comma * refactor: add drain behavior * refactor: bottom group to component * fix: render issue --- lib/game/components/board.dart | 86 --------- lib/game/components/bottom_group.dart | 58 ++++++ lib/game/components/components.dart | 5 +- lib/game/components/controlled_ball.dart | 2 + lib/game/components/dino_desert.dart | 23 +++ lib/game/components/drain.dart | 34 ++++ .../flutter_forest/flutter_forest.dart | 2 +- lib/game/components/sparky_fire_zone.dart | 2 +- lib/game/components/wall.dart | 60 ------- lib/game/pinball_game.dart | 10 +- .../lib/src/components/render_priority.dart | 4 +- .../pinball_components/sandbox/lib/main.dart | 7 +- .../lib/stories/baseboard/stories.dart | 11 -- .../baseboard_game.dart | 0 .../stories/bottom_group/bottom_group.dart | 1 + .../flipper_game.dart | 0 .../{kicker => bottom_group}/kicker_game.dart | 0 .../lib/stories/bottom_group/stories.dart | 24 +++ .../sandbox/lib/stories/flipper/stories.dart | 11 -- .../lib/stories/flutter_forest/stories.dart | 2 +- .../sandbox/lib/stories/kicker/stories.dart | 11 -- .../sandbox/lib/stories/stories.dart | 3 +- test/game/components/board_test.dart | 110 ------------ test/game/components/bottom_group_test.dart | 86 +++++++++ test/game/components/drain_test.dart | 60 +++++++ test/game/components/wall_test.dart | 165 ------------------ test/game/pinball_game_test.dart | 21 ++- test/helpers/mocks.dart | 4 +- 28 files changed, 319 insertions(+), 483 deletions(-) delete mode 100644 lib/game/components/board.dart create mode 100644 lib/game/components/bottom_group.dart create mode 100644 lib/game/components/dino_desert.dart create mode 100644 lib/game/components/drain.dart delete mode 100644 lib/game/components/wall.dart delete mode 100644 packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart rename packages/pinball_components/sandbox/lib/stories/{baseboard => bottom_group}/baseboard_game.dart (100%) create mode 100644 packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart rename packages/pinball_components/sandbox/lib/stories/{flipper => bottom_group}/flipper_game.dart (100%) rename packages/pinball_components/sandbox/lib/stories/{kicker => bottom_group}/kicker_game.dart (100%) create mode 100644 packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart delete mode 100644 packages/pinball_components/sandbox/lib/stories/flipper/stories.dart delete mode 100644 packages/pinball_components/sandbox/lib/stories/kicker/stories.dart delete mode 100644 test/game/components/board_test.dart create mode 100644 test/game/components/bottom_group_test.dart create mode 100644 test/game/components/drain_test.dart delete mode 100644 test/game/components/wall_test.dart diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart deleted file mode 100644 index 666cec5b..00000000 --- a/lib/game/components/board.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template board} -/// The main flat surface of the [PinballGame]. -/// {endtemplate} -class Board extends Component { - /// {@macro board} - // TODO(alestiago): Make Board a Blueprint and sort out priorities. - Board() : super(priority: 1); - - @override - Future onLoad() async { - // TODO(allisonryan0002): add bottom group and flutter forest to pinball - // game directly. Then remove board. - final bottomGroup = _BottomGroup(); - - final flutterForest = FlutterForest(); - - await addAll([ - bottomGroup, - flutterForest, - ]); - } -} - -/// {@template bottom_group} -/// Grouping of the board's bottom [Component]s. -/// -/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s. -/// {@endtemplate} -// TODO(alestiago): Consider renaming once entire Board is defined. -class _BottomGroup extends Component { - /// {@macro bottom_group} - _BottomGroup() : super(priority: RenderPriority.bottomGroup); - - @override - Future onLoad() async { - final rightSide = _BottomGroupSide( - side: BoardSide.right, - ); - final leftSide = _BottomGroupSide( - side: BoardSide.left, - ); - - await addAll([rightSide, leftSide]); - } -} - -/// {@template bottom_group_side} -/// Group with one side of [_BottomGroup]'s symmetric [Component]s. -/// -/// For example, [Flipper]s are symmetric components. -/// {@endtemplate} -class _BottomGroupSide extends Component { - /// {@macro bottom_group_side} - _BottomGroupSide({ - required BoardSide side, - }) : _side = side; - - final BoardSide _side; - - @override - Future onLoad() async { - final direction = _side.direction; - final centerXAdjustment = _side.isLeft ? 0 : -6.5; - - final flipper = ControlledFlipper( - side: _side, - )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); - final baseboard = Baseboard(side: _side) - ..initialPosition = Vector2( - (25.58 * direction) + centerXAdjustment, - 28.69, - ); - final kicker = Kicker( - side: _side, - )..initialPosition = Vector2( - (22.4 * direction) + centerXAdjustment, - 25, - ); - - await addAll([flipper, baseboard, kicker]); - } -} diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart new file mode 100644 index 00000000..d0184d33 --- /dev/null +++ b/lib/game/components/bottom_group.dart @@ -0,0 +1,58 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template bottom_group} +/// Grouping of the board's symmetrical bottom [Component]s. +/// +/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. +/// {@endtemplate} +// TODO(allisonryan0002): Consider renaming. +class BottomGroup extends Component { + /// {@macro bottom_group} + BottomGroup() + : super( + children: [ + _BottomGroupSide(side: BoardSide.right), + _BottomGroupSide(side: BoardSide.left), + ], + ); +} + +/// {@template bottom_group_side} +/// Group with one side of [BottomGroup]'s symmetric [Component]s. +/// +/// For example, [Flipper]s are symmetric components. +/// {@endtemplate} +class _BottomGroupSide extends Component { + /// {@macro bottom_group_side} + _BottomGroupSide({ + required BoardSide side, + }) : _side = side, + super(priority: RenderPriority.bottomGroup); + + final BoardSide _side; + + @override + Future onLoad() async { + final direction = _side.direction; + final centerXAdjustment = _side.isLeft ? 0 : -6.5; + + final flipper = ControlledFlipper( + side: _side, + )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); + final baseboard = Baseboard(side: _side) + ..initialPosition = Vector2( + (25.58 * direction) + centerXAdjustment, + 28.69, + ); + final kicker = Kicker( + side: _side, + )..initialPosition = Vector2( + (22.4 * direction) + centerXAdjustment, + 25, + ); + + await addAll([flipper, baseboard, kicker]); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 37de1948..3fae6abd 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,13 +1,14 @@ export 'android_acres.dart'; -export 'board.dart'; +export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; +export 'dino_desert.dart'; +export 'drain.dart'; export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; export 'scoring_behavior.dart'; export 'sparky_fire_zone.dart'; -export 'wall.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index e76aabe1..ff05ad62 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart new file mode 100644 index 00000000..9e912575 --- /dev/null +++ b/lib/game/components/dino_desert.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template dino_desert} +/// Area located next to the [Launcher] containing the [ChromeDino] and +/// [DinoWalls]. +/// {@endtemplate} +// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is +// fully implemented. +class DinoDesert extends Blueprint { + /// {@macro dino_desert} + DinoDesert() + : super( + components: [ + ChromeDino()..initialPosition = Vector2(12.3, -6.9), + ], + blueprints: [ + DinoWalls(), + ], + ); +} diff --git a/lib/game/components/drain.dart b/lib/game/components/drain.dart new file mode 100644 index 00000000..1dc3e211 --- /dev/null +++ b/lib/game/components/drain.dart @@ -0,0 +1,34 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template drain} +/// Area located at the bottom of the board to detect when a [Ball] is lost. +/// {@endtemplate} +// TODO(allisonryan0002): move to components package when possible. +class Drain extends BodyComponent with ContactCallbacks { + /// {@macro drain} + Drain() : super(renderBody: false); + + @override + Body createBody() { + final shape = EdgeShape() + ..set( + BoardDimensions.bounds.bottomLeft.toVector2(), + BoardDimensions.bounds.bottomRight.toVector2(), + ); + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef(userData: this); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + +// TODO(allisonryan0002): move this to ball.dart when BallLost is removed. + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + other.controller.lost(); + } +} diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 02483159..d7447543 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -7,7 +7,7 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template flutter_forest} -/// Area positioned at the top right of the [Board] where the [Ball] can bounce +/// Area positioned at the top right of the board where the [Ball] can bounce /// off [DashNestBumper]s. /// {@endtemplate} class FlutterForest extends Component { diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index a23a4fbc..5c00e5c9 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_fire_zone} -/// Area positioned at the top left of the [Board] where the [Ball] +/// Area positioned at the top left of the board where the [Ball] /// can bounce off [SparkyBumper]s. /// /// When a [Ball] hits [SparkyBumper]s, the bumper animates. diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart deleted file mode 100644 index 2f180d61..00000000 --- a/lib/game/components/wall.dart +++ /dev/null @@ -1,60 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/extensions.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -/// {@template wall} -/// A continuous generic and [BodyType.static] barrier that divides a game area. -/// {@endtemplate} -// TODO(alestiago): Remove [Wall] for [Pathway.straight]. -class Wall extends BodyComponent { - /// {@macro wall} - Wall({ - required this.start, - required this.end, - }); - - /// The [start] of the [Wall]. - final Vector2 start; - - /// The [end] of the [Wall]. - final Vector2 end; - - @override - Body createBody() { - final shape = EdgeShape()..set(start, end); - - final fixtureDef = FixtureDef(shape) - ..restitution = 0.1 - ..friction = 0; - - final bodyDef = BodyDef() - ..userData = this - ..position = Vector2.zero() - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template bottom_wall} -/// [Wall] located at the bottom of the board. -/// -/// {@endtemplate} -class BottomWall extends Wall with ContactCallbacks { - /// {@macro bottom_wall} - BottomWall() - : super( - start: BoardDimensions.bounds.bottomLeft.toVector2(), - end: BoardDimensions.bounds.bottomRight.toVector2(), - ); - - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! ControlledBall) return; - other.controller.lost(); - } -} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2736a07a..a4b740fb 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -46,20 +46,18 @@ class PinballGame extends Forge2DGame unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); - - // TODO(allisonryan0002): banish Wall and Board classes in later PR. - await add(BottomWall()); + await add(Drain()); + await add(BottomGroup()); unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(LaunchRamp())); final launcher = Launcher(); unawaited(addFromBlueprint(launcher)); - unawaited(add(Board())); + await add(FlutterForest()); await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(AndroidAcres()); + await addFromBlueprint(DinoDesert()); unawaited(addFromBlueprint(Slingshots())); - unawaited(addFromBlueprint(DinoWalls())); - await add(ChromeDino()..initialPosition = Vector2(12.3, -6.9)); await add( GoogleWord( position: Vector2( diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart index 395ca49c..0f530b64 100644 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -27,7 +27,7 @@ abstract class RenderPriority { static const int ballOnSpaceshipRail = _above + spaceshipRail; /// Render priority for the [Ball] while it's on the [LaunchRamp]. - static const int ballOnLaunchRamp = _above + launchRamp; + static const int ballOnLaunchRamp = launchRamp; // Background @@ -51,7 +51,7 @@ abstract class RenderPriority { static const int launchRamp = _above + outerBoundary; - static const int launchRampForegroundRailing = _below + ballOnBoard; + static const int launchRampForegroundRailing = ballOnBoard; static const int plunger = _above + launchRamp; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 40396b88..96083717 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,7 +6,6 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; -import 'package:sandbox/stories/kicker/stories.dart'; import 'package:sandbox/stories/stories.dart'; void main() { @@ -15,11 +14,9 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); - addFlipperStories(dashbook); - addBaseboardStories(dashbook); addChromeDinoStories(dashbook); - addDashNestBumperStories(dashbook); - addKickerStories(dashbook); + addFlutterForestStories(dashbook); + addBottomGroupStories(dashbook); addPlungerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart deleted file mode 100644 index b07e3a73..00000000 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/baseboard/baseboard_game.dart'; - -void addBaseboardStories(Dashbook dashbook) { - dashbook.storiesOf('Baseboard').addGame( - title: 'Traced', - description: BaseboardGame.description, - gameBuilder: (_) => BaseboardGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart new file mode 100644 index 00000000..d0cc7322 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart @@ -0,0 +1 @@ +export 'stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart new file mode 100644 index 00000000..7712ca79 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart @@ -0,0 +1,24 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/bottom_group/baseboard_game.dart'; +import 'package:sandbox/stories/bottom_group/flipper_game.dart'; +import 'package:sandbox/stories/bottom_group/kicker_game.dart'; + +void addBottomGroupStories(Dashbook dashbook) { + dashbook.storiesOf('Bottom Group') + ..addGame( + title: 'Flipper', + description: FlipperGame.description, + gameBuilder: (_) => FlipperGame(), + ) + ..addGame( + title: 'Kicker', + description: KickerGame.description, + gameBuilder: (_) => KickerGame(), + ) + ..addGame( + title: 'Baseboard', + description: BaseboardGame.description, + gameBuilder: (_) => BaseboardGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart deleted file mode 100644 index 2ef2a4b6..00000000 --- a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/flipper/flipper_game.dart'; - -void addFlipperStories(Dashbook dashbook) { - dashbook.storiesOf('Flipper').addGame( - title: 'Traced', - description: FlipperGame.description, - gameBuilder: (_) => FlipperGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart index ef9c1ffb..dd557a27 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -5,7 +5,7 @@ import 'package:sandbox/stories/flutter_forest/signpost_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; -void addDashNestBumperStories(Dashbook dashbook) { +void addFlutterForestStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') ..addGame( title: 'Signpost', diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart deleted file mode 100644 index cfebb7e4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/kicker/kicker_game.dart'; - -void addKickerStories(Dashbook dashbook) { - dashbook.storiesOf('Kickers').addGame( - title: 'Traced', - description: KickerGame.description, - gameBuilder: (_) => KickerGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index df51fc0f..9e1d44d8 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,12 +1,11 @@ export 'android_acres/stories.dart'; export 'backboard/stories.dart'; export 'ball/stories.dart'; -export 'baseboard/stories.dart'; +export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; export 'dino_wall/stories.dart'; export 'effects/stories.dart'; -export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart deleted file mode 100644 index 63b7251b..00000000 --- a/test/game/components/board_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); - - group('Board', () { - flameTester.test( - 'loads correctly', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - expect(game.contains(board), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'one left flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final leftFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isLeft, - ); - expect(leftFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'one right flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - final rightFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isRight, - ); - expect(rightFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'two Baseboards', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final baseboards = board.descendants().whereType(); - expect(baseboards.length, equals(2)); - }, - ); - - flameTester.test( - 'two Kickers', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final kickers = board.descendants().whereType(); - expect(kickers.length, equals(2)); - }, - ); - - flameTester.test( - 'one FlutterForest', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final flutterForest = board.descendants().whereType(); - expect(flutterForest.length, equals(1)); - }, - ); - }); - }); -} diff --git a/test/game/components/bottom_group_test.dart b/test/game/components/bottom_group_test.dart new file mode 100644 index 00000000..3254f155 --- /dev/null +++ b/test/game/components/bottom_group_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('BottomGroup', () { + flameTester.test( + 'loads correctly', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + expect(game.contains(bottomGroup), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'one left flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isLeft, + ); + expect(leftFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'one right flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final rightFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isRight, + ); + expect(rightFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'two Baseboards', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final basebottomGroups = + bottomGroup.descendants().whereType(); + expect(basebottomGroups.length, equals(2)); + }, + ); + + flameTester.test( + 'two Kickers', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final kickers = bottomGroup.descendants().whereType(); + expect(kickers.length, equals(2)); + }, + ); + }); + }); +} diff --git a/test/game/components/drain_test.dart b/test/game/components/drain_test.dart new file mode 100644 index 00000000..f1875a56 --- /dev/null +++ b/test/game/components/drain_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:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Drain', () { + flameTester.test( + 'loads correctly', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(game.contains(drain), isTrue); + }, + ); + + flameTester.test( + 'body is static', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.bodyType, equals(BodyType.static)); + }, + ); + + flameTester.test( + 'is sensor', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.fixtures.first.isSensor, isTrue); + }, + ); + + test( + 'calls lost on contact with ball', + () async { + final drain = Drain(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + + drain.beginContact(ball, MockContact()); + + verify(controller.lost).called(1); + }, + ); + }); +} diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart deleted file mode 100644 index 16f7ce34..00000000 --- a/test/game/components/wall_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -// 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'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); - - group('Wall', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(game.contains(wall), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: 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( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.restitution, greaterThan(0)); - }, - ); - - flameTester.test( - 'has no friction', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.friction, equals(0)); - }, - ); - }); - }); - - group( - 'BottomWall', - () { - group('removes ball on contact', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); - - flameBlocTester.testGameWidget( - 'when ball is launch', - setUp: (game, tester) async { - final ball = ControlledBall.launch( - characterTheme: game.characterTheme, - ); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'when ball is bonus', - setUp: (game, tester) async { - final ball = ControlledBall.bonus( - characterTheme: game.characterTheme, - ); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'when ball is debug', - setUp: (game, tester) async { - final ball = ControlledBall.debug(); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - }); - }, - ); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2e585d66..ef831b5c 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -104,38 +104,47 @@ void main() { // TODO(alestiago): tests that Blueprints get added once the Blueprint // class is removed. flameTester.test( - 'has only one BottomWall', + 'has only one Drain', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, ); flameTester.test( - 'has only one Plunger', + 'has only one BottomGroup', (game) async { await game.ready(); + expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, ); flameTester.test( - 'has one Board', + 'has only one Plunger', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, ); + flameTester.test('has one FlutterForest', (game) async { + await game.ready(); + expect( + game.children.whereType().length, + equals(1), + ); + }); + flameTester.test( 'one GoogleWord', (game) async { diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index ad999aff..6aab19a2 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -15,9 +15,7 @@ import 'package:pinball_components/pinball_components.dart'; class MockPinballGame extends Mock implements PinballGame {} -class MockWall extends Mock implements Wall {} - -class MockBottomWall extends Mock implements BottomWall {} +class MockDrain extends Mock implements Drain {} class MockBody extends Mock implements Body {} From 61ed719bc540aadd2bb13289f50edde21d4531c1 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 28 Apr 2022 21:50:57 +0100 Subject: [PATCH 03/16] fix: flipper not fully locking (#175) * fix: waited for game to unlock * fix: flipper lock Co-authored-by: Allison Ryan Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- .../lib/src/components/flipper.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index dccd7ce7..bb982e96 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -55,7 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { ); final joint = _FlipperJoint(jointDef); world.createJoint(joint); - unawaited(mounted.whenComplete(joint.unlock)); } List _createFixtureDefs() { @@ -132,6 +131,15 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { return body; } + + @override + void onMount() { + super.onMount(); + + gameRef.ready().whenComplete( + () => body.joints.whereType<_FlipperJoint>().first.unlock(), + ); + } } class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { @@ -215,11 +223,8 @@ class _FlipperJoint extends RevoluteJoint { /// The joint is locked when initialized in order to force the [Flipper] /// at its resting position. void lock() { - const angle = _halfSweepingAngle; - setLimits( - angle * side.direction, - angle * side.direction, - ); + final angle = _halfSweepingAngle * side.direction; + setLimits(angle, angle); } /// Unlocks the [Flipper] from its resting position. From 45cd89face7bb59cbd69faca9cf3f13a55f2d2bd Mon Sep 17 00:00:00 2001 From: arturplaczek <33895544+arturplaczek@users.noreply.github.com> Date: Fri, 29 Apr 2022 10:30:52 +0200 Subject: [PATCH 04/16] chore: dialog background for select character & how to play (#236) --- .github/workflows/pinball_ui.yaml | 23 ++++++ lib/game/view/widgets/game_hud.dart | 6 +- .../view/widgets/play_button_overlay.dart | 1 + .../view/character_selection_page.dart | 24 ++++-- .../widgets/how_to_play_dialog.dart | 31 +++---- packages/pinball_ui/.gitignore | 39 +++++++++ packages/pinball_ui/README.md | 11 +++ packages/pinball_ui/analysis_options.yaml | 4 + .../assets/images/dialog/background.png | Bin 0 -> 20606 bytes packages/pinball_ui/lib/gen/assets.gen.dart | 78 ++++++++++++++++++ packages/pinball_ui/lib/gen/gen.dart | 1 + packages/pinball_ui/lib/pinball_ui.dart | 3 + .../pinball_ui/lib/src/dialog/dialog.dart | 1 + .../lib/src/dialog/pixelated_decoration.dart | 56 +++++++++++++ packages/pinball_ui/pubspec.yaml | 29 +++++++ .../src/dialog/pixelated_decoration_test.dart | 26 ++++++ pubspec.lock | 7 ++ pubspec.yaml | 2 + .../widgets/how_to_play_dialog_test.dart | 7 +- 19 files changed, 317 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/pinball_ui.yaml create mode 100644 packages/pinball_ui/.gitignore create mode 100644 packages/pinball_ui/README.md create mode 100644 packages/pinball_ui/analysis_options.yaml create mode 100644 packages/pinball_ui/assets/images/dialog/background.png create mode 100644 packages/pinball_ui/lib/gen/assets.gen.dart create mode 100644 packages/pinball_ui/lib/gen/gen.dart create mode 100644 packages/pinball_ui/lib/pinball_ui.dart create mode 100644 packages/pinball_ui/lib/src/dialog/dialog.dart create mode 100644 packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart create mode 100644 packages/pinball_ui/pubspec.yaml create mode 100644 packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart diff --git a/.github/workflows/pinball_ui.yaml b/.github/workflows/pinball_ui.yaml new file mode 100644 index 00000000..98643ffa --- /dev/null +++ b/.github/workflows/pinball_ui.yaml @@ -0,0 +1,23 @@ +name: pinball_ui + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/pinball_ui/**" + - ".github/workflows/pinball_ui.yaml" + + pull_request: + paths: + - "packages/pinball_ui/**" + - ".github/workflows/pinball_ui.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_ui + coverage_excludes: "lib/gen/*.dart" diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 9cfb2d67..e3c44877 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -66,14 +66,14 @@ class _ScoreViewDecoration extends StatelessWidget { @override Widget build(BuildContext context) { const radius = BorderRadius.all(Radius.circular(12)); - const boardWidth = 5.0; + const borderWidth = 5.0; return DecoratedBox( decoration: BoxDecoration( borderRadius: radius, border: Border.all( color: AppColors.white, - width: boardWidth, + width: borderWidth, ), image: DecorationImage( fit: BoxFit.cover, @@ -83,7 +83,7 @@ class _ScoreViewDecoration extends StatelessWidget { ), ), child: Padding( - padding: const EdgeInsets.all(boardWidth - 1), + padding: const EdgeInsets.all(borderWidth - 1), child: ClipRRect( borderRadius: radius, child: child, diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index f90ebb98..3db62a50 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -28,6 +28,7 @@ class PlayButtonOverlay extends StatelessWidget { context: context, barrierDismissible: false, builder: (_) { + // TODO(arturplaczek): remove after merge StarBlocListener final height = MediaQuery.of(context).size.height * 0.5; return Center( diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 0e83db8d..83dc6ee6 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -6,6 +6,7 @@ import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; class CharacterSelectionDialog extends StatelessWidget { const CharacterSelectionDialog({Key? key}) : super(key: key); @@ -32,25 +33,32 @@ class CharacterSelectionView extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return PixelatedDecoration( + header: Text( + l10n.characterSelectionTitle, + style: Theme.of(context).textTheme.headline3, + ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 80), - Text( - l10n.characterSelectionTitle, - style: Theme.of(context).textTheme.headline3, - ), - const SizedBox(height: 80), const _CharacterSelectionGridView(), const SizedBox(height: 20), TextButton( onPressed: () { Navigator.of(context).pop(); + // TODO(arturplaczek): remove after merge StarBlocListener + final height = MediaQuery.of(context).size.height * 0.5; + showDialog( context: context, - builder: (_) => const HowToPlayDialog(), + builder: (_) => Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: const HowToPlayDialog(), + ), + ), ); }, child: Text(l10n.start), diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index aed7a3e3..bc5166e4 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_ui/pinball_ui.dart'; class HowToPlayDialog extends StatelessWidget { const HowToPlayDialog({Key? key}) : super(key: key); @@ -11,19 +12,15 @@ class HowToPlayDialog extends StatelessWidget { final l10n = context.l10n; const spacing = SizedBox(height: 16); - return Dialog( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.howToPlay), - spacing, - const _LaunchControls(), - spacing, - const _FlipperControls(), - ], - ), + return PixelatedDecoration( + header: Text(l10n.howToPlay), + body: ListView( + children: const [ + spacing, + _LaunchControls(), + spacing, + _FlipperControls(), + ], ), ); } @@ -41,9 +38,7 @@ class _LaunchControls extends StatelessWidget { children: [ Text(l10n.launchControls), const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + Wrap( children: const [ KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down), spacing, @@ -81,9 +76,7 @@ class _FlipperControls extends StatelessWidget { ], ), const SizedBox(height: 8), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + Wrap( children: const [ KeyIndicator.fromKeyName(keyName: 'A'), rowSpacing, diff --git a/packages/pinball_ui/.gitignore b/packages/pinball_ui/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_ui/.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_ui/README.md b/packages/pinball_ui/README.md new file mode 100644 index 00000000..cabc194a --- /dev/null +++ b/packages/pinball_ui/README.md @@ -0,0 +1,11 @@ +# pinball_ui + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +UI Toolkit for the Pinball Flutter Application + +[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 diff --git a/packages/pinball_ui/analysis_options.yaml b/packages/pinball_ui/analysis_options.yaml new file mode 100644 index 00000000..f8155aa6 --- /dev/null +++ b/packages/pinball_ui/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart diff --git a/packages/pinball_ui/assets/images/dialog/background.png b/packages/pinball_ui/assets/images/dialog/background.png new file mode 100644 index 0000000000000000000000000000000000000000..0aad300f947035b97e2d7a17e40411e1a3437a63 GIT binary patch literal 20606 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_ui/lib/gen/assets.gen.dart b/packages/pinball_ui/lib/gen/assets.gen.dart new file mode 100644 index 00000000..41c45ece --- /dev/null +++ b/packages/pinball_ui/lib/gen/assets.gen.dart @@ -0,0 +1,78 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// ignore_for_file: directives_ordering,unnecessary_import + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + $AssetsImagesDialogGen get dialog => const $AssetsImagesDialogGen(); +} + +class $AssetsImagesDialogGen { + const $AssetsImagesDialogGen(); + + /// File path: assets/images/dialog/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/dialog/background.png'); +} + +class Assets { + Assets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) + : super(assetName, package: 'pinball_ui'); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/packages/pinball_ui/lib/gen/gen.dart b/packages/pinball_ui/lib/gen/gen.dart new file mode 100644 index 00000000..e7ad4c54 --- /dev/null +++ b/packages/pinball_ui/lib/gen/gen.dart @@ -0,0 +1 @@ +export 'assets.gen.dart'; diff --git a/packages/pinball_ui/lib/pinball_ui.dart b/packages/pinball_ui/lib/pinball_ui.dart new file mode 100644 index 00000000..b46adf95 --- /dev/null +++ b/packages/pinball_ui/lib/pinball_ui.dart @@ -0,0 +1,3 @@ +library pinball_ui; + +export 'src/dialog/dialog.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/dialog.dart b/packages/pinball_ui/lib/src/dialog/dialog.dart new file mode 100644 index 00000000..7a224272 --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/dialog.dart @@ -0,0 +1 @@ +export 'pixelated_decoration.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart b/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart new file mode 100644 index 00000000..542a00db --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; + +/// {@template pixelated_decoration} +/// Widget with pixelated background and layout defined for dialog displays. +/// {@endtemplate} +class PixelatedDecoration extends StatelessWidget { + /// {@macro pixelated_decoration} + const PixelatedDecoration({ + Key? key, + required Widget header, + required Widget body, + }) : _header = header, + _body = body, + super(key: key); + + final Widget _header; + final Widget _body; + + @override + Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(12)); + + return Material( + borderRadius: radius, + child: Padding( + padding: const EdgeInsets.all(5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage(Assets.images.dialog.background.keyName), + ), + ), + child: ClipRRect( + borderRadius: radius, + child: Column( + children: [ + Expanded( + child: Center( + child: _header, + ), + ), + Expanded( + flex: 4, + child: _body, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/pinball_ui/pubspec.yaml b/packages/pinball_ui/pubspec.yaml new file mode 100644 index 00000000..79c65338 --- /dev/null +++ b/packages/pinball_ui/pubspec.yaml @@ -0,0 +1,29 @@ +name: pinball_ui +description: UI Toolkit for the Pinball Flutter Application +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + test: ^1.19.2 + very_good_analysis: ^2.4.0 + +flutter: + uses-material-design: true + generate: true + + assets: + - assets/images/dialog/ + +flutter_gen: + line_length: 80 + assets: + package_parameter_enabled: true diff --git a/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart b/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart new file mode 100644 index 00000000..772f2570 --- /dev/null +++ b/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart @@ -0,0 +1,26 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PixelatedDecoration', () { + testWidgets('renders header and body', (tester) async { + const headerText = 'header'; + const bodyText = 'body'; + + await tester.pumpWidget( + MaterialApp( + home: PixelatedDecoration( + header: Text(headerText), + body: Text(bodyText), + ), + ), + ); + + expect(find.text(headerText), findsOneWidget); + expect(find.text(bodyText), findsOneWidget); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 9ee8ae6c..4b71c77b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -499,6 +499,13 @@ packages: relative: true source: path version: "1.0.0+1" + pinball_ui: + dependency: "direct main" + description: + path: "packages/pinball_ui" + relative: true + source: path + version: "1.0.0+1" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 48c570c3..f129ea19 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,8 @@ dependencies: path: packages/pinball_flame pinball_theme: path: packages/pinball_theme + pinball_ui: + path: packages/pinball_ui dev_dependencies: bloc_test: ^9.0.2 diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart index 082f102e..c31ac1a3 100644 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -2,16 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/start_game/start_game.dart'; import '../../helpers/helpers.dart'; void main() { group('HowToPlayDialog', () { - testWidgets('displays dialog', (tester) async { + testWidgets('displays content', (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + await tester.pumpApp(HowToPlayDialog()); - expect(find.byType(Dialog), findsOneWidget); + expect(find.text(l10n.launchControls), findsOneWidget); }); }); From 849129c16a88f8632047bcca158c7bd5a64b3e89 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:19:38 +0200 Subject: [PATCH 05/16] chore: move StartGameBloc over the app --- lib/app/view/app.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 97cfec9b..8165b2f4 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -14,6 +14,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; class App extends StatelessWidget { @@ -35,8 +36,11 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], - child: BlocProvider( - create: (context) => CharacterThemeCubit(), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => CharacterThemeCubit()), + BlocProvider(create: (context) => StartGameBloc()), + ], child: const MaterialApp( title: 'I/O Pinball', localizationsDelegates: [ From 7266e47fd71a0c70705c9e64a132cddd3f854870 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:20:52 +0200 Subject: [PATCH 06/16] chore: remove pinballGame from StartGameBloc --- lib/start_game/bloc/start_game_bloc.dart | 10 +--------- test/start_game/bloc/start_game_bloc_test.dart | 12 +++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/start_game/bloc/start_game_bloc.dart b/lib/start_game/bloc/start_game_bloc.dart index ba44d88c..3a96b57b 100644 --- a/lib/start_game/bloc/start_game_bloc.dart +++ b/lib/start_game/bloc/start_game_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:pinball/game/game.dart'; part 'start_game_event.dart'; part 'start_game_state.dart'; @@ -10,23 +9,16 @@ part 'start_game_state.dart'; /// {@endtemplate} class StartGameBloc extends Bloc { /// {@macro start_game_bloc} - StartGameBloc({ - required PinballGame game, - }) : _game = game, - super(const StartGameState.initial()) { + StartGameBloc() : super(const StartGameState.initial()) { on(_onPlayTapped); on(_onCharacterSelected); on(_onHowToPlayFinished); } - final PinballGame _game; - void _onPlayTapped( PlayTapped event, Emitter emit, ) { - _game.gameFlowController.start(); - emit( state.copyWith( status: StartGameStatus.selectCharacter, diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index ec1b3ced..0300d1f0 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -22,9 +22,7 @@ void main() { group('StartGameBloc', () { blocTest( 'on PlayTapped changes status to selectCharacter', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const PlayTapped()), expect: () => [ const StartGameState( @@ -35,9 +33,7 @@ void main() { blocTest( 'on CharacterSelected changes status to howToPlay', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const CharacterSelected()), expect: () => [ const StartGameState( @@ -48,9 +44,7 @@ void main() { blocTest( 'on HowToPlayFinished changes status to play', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const HowToPlayFinished()), expect: () => [ const StartGameState( From 11c62fdfde79ed142d47b081438f9614e0fa21bc Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:22:06 +0200 Subject: [PATCH 07/16] chore: remove pinballGame from PlayButtonOverlay --- .../view/widgets/play_button_overlay.dart | 30 +++------------- .../widgets/play_button_overlay_test.dart | 36 +++++++++---------- 2 files changed, 20 insertions(+), 46 deletions(-) diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 3db62a50..21493ca2 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,20 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:pinball/game/pinball_game.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; /// {@template play_button_overlay} /// [Widget] that renders the button responsible to starting the game /// {@endtemplate} class PlayButtonOverlay extends StatelessWidget { /// {@macro play_button_overlay} - const PlayButtonOverlay({ - Key? key, - required PinballGame game, - }) : _game = game, - super(key: key); - - final PinballGame _game; + const PlayButtonOverlay({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -23,23 +17,7 @@ class PlayButtonOverlay extends StatelessWidget { return Center( child: ElevatedButton( onPressed: () { - _game.gameFlowController.start(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - // TODO(arturplaczek): remove after merge StarBlocListener - final height = MediaQuery.of(context).size.height * 0.5; - - return Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: const CharacterSelectionDialog(), - ), - ); - }, - ); + context.read().add(const PlayTapped()); }, child: Text(l10n.play), ), diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 0345978d..10277dee 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,46 +1,42 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/bloc/start_game_bloc.dart'; import '../../../helpers/helpers.dart'; void main() { group('PlayButtonOverlay', () { - late PinballGame game; - late GameFlowController gameFlowController; + late StartGameBloc startGameBloc; setUp(() { - game = MockPinballGame(); - gameFlowController = MockGameFlowController(); + startGameBloc = MockStartGameBloc(); - when(() => game.gameFlowController).thenReturn(gameFlowController); - when(gameFlowController.start).thenAnswer((_) {}); + whenListen( + startGameBloc, + Stream.value(const StartGameState.initial()), + initialState: const StartGameState.initial(), + ); }); testWidgets('renders correctly', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); + await tester.pumpApp(const PlayButtonOverlay()); expect(find.text('Play'), findsOneWidget); }); - testWidgets('calls gameFlowController.start when taped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - - await tester.tap(find.text('Play')); - await tester.pump(); - - verify(gameFlowController.start).called(1); - }); - - testWidgets('displays CharacterSelectionDialog when tapped', + testWidgets('calls PlayTapped event to StartGameBloc when taped on play', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); + await tester.pumpApp( + const PlayButtonOverlay(), + startGameBloc: startGameBloc, + ); await tester.tap(find.text('Play')); await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); + verify(() => startGameBloc.add(const PlayTapped())).called(1); }); }); } From f60d29b7d15c06803cc98c53177be441bc2741cb Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:24:25 +0200 Subject: [PATCH 08/16] chore: create StartGameListener --- .../widgets/start_game_listener.dart | 85 +++++++++ .../widgets/start_game_listener_test.dart | 178 ++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 lib/start_game/widgets/start_game_listener.dart create mode 100644 test/start_game/widgets/start_game_listener_test.dart diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart new file mode 100644 index 00000000..466c3c39 --- /dev/null +++ b/lib/start_game/widgets/start_game_listener.dart @@ -0,0 +1,85 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball/theme/theme.dart'; + +class StartGameListener extends StatelessWidget { + const StartGameListener({ + Key? key, + required Widget child, + required PinballGame game, + }) : _child = child, + _game = game, + super(key: key); + + final Widget _child; + final PinballGame _game; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + switch (state.status) { + case StartGameStatus.selectCharacter: + _onSelectCharacter(context); + break; + case StartGameStatus.howToPlay: + _handleHowToPlay(context); + break; + case StartGameStatus.play: + _game.gameFlowController.start(); + break; + case StartGameStatus.initial: + break; + } + }, + child: _child, + ); + } + + void _onSelectCharacter( + BuildContext context, + ) { + showDialog( + context: context, + builder: (_) { + // TODO(arturplaczek): remove that when PR with PinballLayout will be + // merged + final height = MediaQuery.of(context).size.height * 0.5; + + return Center( + child: SizedBox( + height: height, + width: height * 1.2, + child: const CharacterSelectionDialog(), + ), + ); + }, + barrierDismissible: false, + ); + } +} + +Future _handleHowToPlay( + BuildContext context, +) async { + final startGameBloc = context.read(); + + await showDialog( + context: context, + barrierColor: AppColors.transparent, + builder: (_) { + return Center( + child: HowToPlayDialog( + onDismissCallback: () { + startGameBloc.add(const HowToPlayFinished()); + }, + ), + ); + }, + ); +} diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart new file mode 100644 index 00000000..e35cef78 --- /dev/null +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -0,0 +1,178 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + late StartGameBloc startGameBloc; + late PinballGame pinballGame; + + group('StartGameListener', () { + setUp(() { + startGameBloc = MockStartGameBloc(); + pinballGame = MockPinballGame(); + }); + + testWidgets( + 'on selectCharacter status shows SelectCharacter dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(CharacterSelectionDialog), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'on howToPlay status shows HowToPlay dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'on play status call start on game controller', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.play), + ), + initialState: const StartGameState.initial(), + ); + + final gameController = MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((invocation) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(kThemeAnimationDuration); + await tester.pumpAndSettle(kThemeAnimationDuration); + + verify(gameController.start).called(1); + }, + ); + + testWidgets( + 'do nothing on initial status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.initial), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + testWidgets( + 'calls HowToPlayFinished event after HowToPlayDialog is closed', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify( + () => startGameBloc.add(const HowToPlayFinished()), + ).called(1); + }, + ); + }); +} From 12ce2083b34ce0b049d38d1ab08ce65985841965 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:25:02 +0200 Subject: [PATCH 09/16] chore: add on dismiss callback to HowToPlayDialog --- .../widgets/how_to_play_dialog.dart | 31 +++++++++++++------ .../widgets/how_to_play_dialog_test.dart | 8 +++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index bc5166e4..c9703834 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -5,22 +5,33 @@ import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_ui/pinball_ui.dart'; class HowToPlayDialog extends StatelessWidget { - const HowToPlayDialog({Key? key}) : super(key: key); + const HowToPlayDialog({ + Key? key, + required this.onDismissCallback, + }) : super(key: key); + + final VoidCallback onDismissCallback; @override Widget build(BuildContext context) { final l10n = context.l10n; const spacing = SizedBox(height: 16); - return PixelatedDecoration( - header: Text(l10n.howToPlay), - body: ListView( - children: const [ - spacing, - _LaunchControls(), - spacing, - _FlipperControls(), - ], + return WillPopScope( + onWillPop: () { + onDismissCallback.call(); + return Future.value(true); + }, + child: PixelatedDecoration( + header: Text(l10n.howToPlay), + body: ListView( + children: const [ + spacing, + _LaunchControls(), + spacing, + _FlipperControls(), + ], + ), ), ); } diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart index c31ac1a3..6b240c48 100644 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -9,10 +9,14 @@ import '../../helpers/helpers.dart'; void main() { group('HowToPlayDialog', () { - testWidgets('displays content', (tester) async { + testWidgets('displays dialog', (tester) async { final l10n = await AppLocalizations.delegate.load(Locale('en')); - await tester.pumpApp(HowToPlayDialog()); + await tester.pumpApp( + HowToPlayDialog( + onDismissCallback: () {}, + ), + ); expect(find.text(l10n.launchControls), findsOneWidget); }); From 52bb7ea1198146e5dbd6a8228f521725f242033f Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:25:27 +0200 Subject: [PATCH 10/16] chore: update CharacterSelectionDialog --- .../view/character_selection_page.dart | 14 +------------- .../view/character_selection_page_test.dart | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 83dc6ee6..80a184ab 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -47,19 +47,7 @@ class CharacterSelectionView extends StatelessWidget { TextButton( onPressed: () { Navigator.of(context).pop(); - // TODO(arturplaczek): remove after merge StarBlocListener - final height = MediaQuery.of(context).size.height * 0.5; - - showDialog( - context: context, - builder: (_) => Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: const HowToPlayDialog(), - ), - ), - ); + context.read().add(const CharacterSelected()); }, child: Text(l10n.start), ), diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 0dda92d7..2debdb3f 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -12,9 +12,12 @@ import '../../helpers/helpers.dart'; void main() { late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; setUp(() { characterThemeCubit = MockCharacterThemeCubit(); + startGameBloc = MockStartGameBloc(); + whenListen( characterThemeCubit, const Stream.empty(), @@ -84,17 +87,24 @@ void main() { .called(1); }); - testWidgets('displays how to play dialog when start is tapped', + testWidgets('calls CharacterSelected event when start is tapped', (tester) async { + whenListen( + startGameBloc, + Stream.value(const StartGameState.initial()), + initialState: const StartGameState.initial(), + ); + await tester.pumpApp( CharacterSelectionView(), characterThemeCubit: characterThemeCubit, + startGameBloc: startGameBloc, ); await tester.ensureVisible(find.byType(TextButton)); await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); + verify(() => startGameBloc.add(CharacterSelected())).called(1); }); }); From 82122cf54ba6dc4ff953f99e25a0495b9cc7a5f3 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 12:25:41 +0200 Subject: [PATCH 11/16] chore: update PinballGamePage --- lib/game/view/pinball_game_page.dart | 56 +++++++++++++++------------- lib/start_game/widgets/widgets.dart | 1 + 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index be11a15c..2fd10424 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -48,7 +48,6 @@ class PinballGamePage extends StatelessWidget { return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => GameBloc()), BlocProvider( create: (_) => AssetsManagerCubit(loadables)..load(), @@ -114,36 +113,43 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { + final isPlaying = context.select( + (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, + ); final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final screenWidth = MediaQuery.of(context).size.width; final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); - return Stack( - children: [ - Positioned.fill( - child: GameWidget( - game: game, - initialActiveOverlays: const [PinballGame.playButtonOverlay], - overlayBuilderMap: { - PinballGame.playButtonOverlay: (context, game) { - return Positioned( - bottom: 20, - right: 0, - left: 0, - child: PlayButtonOverlay(game: game), - ); + return StartGameListener( + game: game, + child: Stack( + children: [ + Positioned.fill( + child: GameWidget( + game: game, + initialActiveOverlays: const [PinballGame.playButtonOverlay], + overlayBuilderMap: { + PinballGame.playButtonOverlay: (context, game) { + return const Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(), + ); + }, }, - }, + ), ), - ), - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - Positioned( - top: 16, - left: leftMargin, - child: const GameHud(), - ), - ], + Positioned( + top: 16, + left: leftMargin, + child: Visibility( + visible: isPlaying, + child: const GameHud(), + ), + ), + ], + ), ); } } diff --git a/lib/start_game/widgets/widgets.dart b/lib/start_game/widgets/widgets.dart index bad2c6b5..fa7c7253 100644 --- a/lib/start_game/widgets/widgets.dart +++ b/lib/start_game/widgets/widgets.dart @@ -1 +1,2 @@ export 'how_to_play_dialog.dart'; +export 'start_game_listener.dart'; From 2ed170e8416affbc8ca2815d8bc18f2c38494a74 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 15:39:42 +0200 Subject: [PATCH 12/16] fix: apply self review --- .../widgets/how_to_play_dialog.dart | 4 +- .../widgets/start_game_listener.dart | 54 +++++++++---------- test/game/view/pinball_game_page_test.dart | 11 ++-- .../widgets/start_game_listener_test.dart | 2 +- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index c9703834..79959669 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -77,9 +77,7 @@ class _FlipperControls extends StatelessWidget { const SizedBox(height: 10), Column( children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + Wrap( children: const [ KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), rowSpacing, diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index 466c3c39..d0616b27 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -28,7 +28,7 @@ class StartGameListener extends StatelessWidget { _onSelectCharacter(context); break; case StartGameStatus.howToPlay: - _handleHowToPlay(context); + _onHowToPlay(context); break; case StartGameStatus.play: _game.gameFlowController.start(); @@ -41,43 +41,43 @@ class StartGameListener extends StatelessWidget { ); } - void _onSelectCharacter( - BuildContext context, - ) { - showDialog( + void _onSelectCharacter(BuildContext context) { + _showPinballDialog( context: context, - builder: (_) { - // TODO(arturplaczek): remove that when PR with PinballLayout will be - // merged - final height = MediaQuery.of(context).size.height * 0.5; - - return Center( - child: SizedBox( - height: height, - width: height * 1.2, - child: const CharacterSelectionDialog(), - ), - ); - }, + child: const CharacterSelectionDialog(), barrierDismissible: false, ); } } -Future _handleHowToPlay( - BuildContext context, -) async { - final startGameBloc = context.read(); +Future _onHowToPlay(BuildContext context) async { + _showPinballDialog( + context: context, + child: HowToPlayDialog( + onDismissCallback: () { + context.read().add(const HowToPlayFinished()); + }, + ), + ); +} + +void _showPinballDialog({ + required BuildContext context, + required Widget child, + bool barrierDismissible = true, +}) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; - await showDialog( + showDialog( context: context, barrierColor: AppColors.transparent, + barrierDismissible: barrierDismissible, builder: (_) { return Center( - child: HowToPlayDialog( - onDismissCallback: () { - startGameBloc.add(const HowToPlayFinished()); - }, + child: SizedBox( + height: gameWidgetWidth, + width: gameWidgetWidth, + child: child, ), ); }, diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index f8b62d05..1795b88e 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -198,12 +198,11 @@ void main() { find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - // expect( - // find.byType(GameHud), - // findsNothing, - // ); + + expect( + find.byType(GameHud), + findsNothing, + ); }); testWidgets('renders a hud on play state', (tester) async { diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart index e35cef78..5ce4ca94 100644 --- a/test/start_game/widgets/start_game_listener_test.dart +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -137,7 +137,7 @@ void main() { ); testWidgets( - 'calls HowToPlayFinished event after HowToPlayDialog is closed', + 'adds HowToPlayFinished event after closing HowToPlayDialog', (tester) async { whenListen( startGameBloc, From a5107c0f1cbdce159e16a0751b9ccd81d5b657a6 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 18:05:36 +0200 Subject: [PATCH 13/16] fix: apply code review --- lib/start_game/widgets/start_game_listener.dart | 4 ++-- test/game/view/widgets/play_button_overlay_test.dart | 2 +- test/select_character/view/character_selection_page_test.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index d0616b27..84ae80d8 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -24,6 +24,8 @@ class StartGameListener extends StatelessWidget { return BlocListener( listener: (context, state) { switch (state.status) { + case StartGameStatus.initial: + break; case StartGameStatus.selectCharacter: _onSelectCharacter(context); break; @@ -33,8 +35,6 @@ class StartGameListener extends StatelessWidget { case StartGameStatus.play: _game.gameFlowController.start(); break; - case StartGameStatus.initial: - break; } }, child: _child, diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 10277dee..a4d53617 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -26,7 +26,7 @@ void main() { expect(find.text('Play'), findsOneWidget); }); - testWidgets('calls PlayTapped event to StartGameBloc when taped on play', + testWidgets('adds PlayTapped event to StartGameBloc when taped', (tester) async { await tester.pumpApp( const PlayButtonOverlay(), diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 2debdb3f..5cd22f54 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -87,7 +87,7 @@ void main() { .called(1); }); - testWidgets('calls CharacterSelected event when start is tapped', + testWidgets('adds CharacterSelected event when start is tapped', (tester) async { whenListen( startGameBloc, From 01a44c3a51600bc0b80848c6d45620960a8dc184 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Thu, 28 Apr 2022 22:17:33 +0200 Subject: [PATCH 14/16] fix: update dialog size --- lib/start_game/widgets/start_game_listener.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index 84ae80d8..a7fed6aa 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -75,7 +75,7 @@ void _showPinballDialog({ builder: (_) { return Center( child: SizedBox( - height: gameWidgetWidth, + height: gameWidgetWidth * 0.87, width: gameWidgetWidth, child: child, ), From 2c5ac4224f5f2796e97ce845f4be558a7cbd29b1 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Fri, 29 Apr 2022 00:22:15 +0200 Subject: [PATCH 15/16] fix: update widgets --- .../widgets/start_game_listener.dart | 14 +++++++-- .../widgets/start_game_listener_test.dart | 31 ++++++++++++------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index a7fed6aa..a29b1f12 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -12,12 +12,15 @@ class StartGameListener extends StatelessWidget { Key? key, required Widget child, required PinballGame game, + int selectCharacterDelay = 1300, }) : _child = child, _game = game, + _selectCharacterDelay = selectCharacterDelay, super(key: key); final Widget _child; final PinballGame _game; + final int _selectCharacterDelay; @override Widget build(BuildContext context) { @@ -27,13 +30,13 @@ class StartGameListener extends StatelessWidget { case StartGameStatus.initial: break; case StartGameStatus.selectCharacter: + _game.gameFlowController.start(); _onSelectCharacter(context); break; case StartGameStatus.howToPlay: _onHowToPlay(context); break; case StartGameStatus.play: - _game.gameFlowController.start(); break; } }, @@ -41,7 +44,12 @@ class StartGameListener extends StatelessWidget { ); } - void _onSelectCharacter(BuildContext context) { + Future _onSelectCharacter(BuildContext context) async { + // We need to add a delay between starting the game and showing + // the dialog. + await Future.delayed( + Duration(milliseconds: _selectCharacterDelay), + ); _showPinballDialog( context: context, child: const CharacterSelectionDialog(), @@ -50,7 +58,7 @@ class StartGameListener extends StatelessWidget { } } -Future _onHowToPlay(BuildContext context) async { +void _onHowToPlay(BuildContext context) { _showPinballDialog( context: context, child: HowToPlayDialog( diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart index 5ce4ca94..39cc348b 100644 --- a/test/start_game/widgets/start_game_listener_test.dart +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -18,8 +18,10 @@ void main() { pinballGame = MockPinballGame(); }); + // TODO(arturplaczek): need to fix that test testWidgets( - 'on selectCharacter status shows SelectCharacter dialog', + 'on selectCharacter status calls start on the game controller and shows ' + 'SelectCharacter dialog', (tester) async { whenListen( startGameBloc, @@ -28,18 +30,23 @@ void main() { ), initialState: const StartGameState.initial(), ); + final gameController = MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); await tester.pumpApp( StartGameListener( game: pinballGame, + selectCharacterDelay: 0, child: const SizedBox.shrink(), ), startGameBloc: startGameBloc, ); + verify(gameController.start).called(1); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(kThemeAnimationDuration); - expect( + await expectLater( find.byType(CharacterSelectionDialog), findsOneWidget, ); @@ -75,7 +82,7 @@ void main() { ); testWidgets( - 'on play status call start on game controller', + 'do nothing on play status', (tester) async { whenListen( startGameBloc, @@ -85,10 +92,6 @@ void main() { initialState: const StartGameState.initial(), ); - final gameController = MockGameFlowController(); - when(() => pinballGame.gameFlowController) - .thenAnswer((invocation) => gameController); - await tester.pumpApp( StartGameListener( game: pinballGame, @@ -97,10 +100,16 @@ void main() { startGameBloc: startGameBloc, ); - await tester.pumpAndSettle(kThemeAnimationDuration); - await tester.pumpAndSettle(kThemeAnimationDuration); + await tester.pumpAndSettle(); - verify(gameController.start).called(1); + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); }, ); From f5056ca2186ec2f157360cb577a79ae78c610a85 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Fri, 29 Apr 2022 10:55:18 +0200 Subject: [PATCH 16/16] fix: update StartGameListener --- .../widgets/start_game_listener.dart | 10 +-- .../widgets/start_game_listener_test.dart | 90 ++++++++++++------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index a29b1f12..19e956f3 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -12,15 +12,12 @@ class StartGameListener extends StatelessWidget { Key? key, required Widget child, required PinballGame game, - int selectCharacterDelay = 1300, }) : _child = child, _game = game, - _selectCharacterDelay = selectCharacterDelay, super(key: key); final Widget _child; final PinballGame _game; - final int _selectCharacterDelay; @override Widget build(BuildContext context) { @@ -30,8 +27,8 @@ class StartGameListener extends StatelessWidget { case StartGameStatus.initial: break; case StartGameStatus.selectCharacter: - _game.gameFlowController.start(); _onSelectCharacter(context); + _game.gameFlowController.start(); break; case StartGameStatus.howToPlay: _onHowToPlay(context); @@ -45,11 +42,6 @@ class StartGameListener extends StatelessWidget { } Future _onSelectCharacter(BuildContext context) async { - // We need to add a delay between starting the game and showing - // the dialog. - await Future.delayed( - Duration(milliseconds: _selectCharacterDelay), - ); _showPinballDialog( context: context, child: const CharacterSelectionDialog(), diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart index 39cc348b..c9221cc3 100644 --- a/test/start_game/widgets/start_game_listener_test.dart +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -18,40 +18,62 @@ void main() { pinballGame = MockPinballGame(); }); - // TODO(arturplaczek): need to fix that test - testWidgets( - 'on selectCharacter status calls start on the game controller and shows ' - 'SelectCharacter dialog', - (tester) async { - whenListen( - startGameBloc, - Stream.value( - const StartGameState(status: StartGameStatus.selectCharacter), - ), - initialState: const StartGameState.initial(), - ); - final gameController = MockGameFlowController(); - when(() => pinballGame.gameFlowController) - .thenAnswer((_) => gameController); - - await tester.pumpApp( - StartGameListener( - game: pinballGame, - selectCharacterDelay: 0, - child: const SizedBox.shrink(), - ), - startGameBloc: startGameBloc, - ); - verify(gameController.start).called(1); - - await tester.pumpAndSettle(kThemeAnimationDuration); - - await expectLater( - find.byType(CharacterSelectionDialog), - findsOneWidget, - ); - }, - ); + group('on selectCharacter status', () { + testWidgets( + 'calls start on the game controller', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + final gameController = MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + }, + ); + + testWidgets( + 'shows SelectCharacter dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + final gameController = MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(kThemeAnimationDuration); + + expect( + find.byType(CharacterSelectionDialog), + findsOneWidget, + ); + }, + ); + }); testWidgets( 'on howToPlay status shows HowToPlay dialog',