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 1/3] 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 2/3] 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 3/3] 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); }); });