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/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 313ee6cd..afef04f0 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,9 +1,11 @@ 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'; @@ -11,4 +13,3 @@ export 'launcher.dart'; export 'multipliers/multipliers.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 43a02bcc..262a3e29 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, @@ -44,22 +46,19 @@ 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())); - unawaited(add(Multipliers())); - + await add(Multipliers()); + 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( @@ -72,6 +71,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 @@ -118,7 +172,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, @@ -155,9 +209,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/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_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. 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 6b84b895..1f0077fb 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 39f23080..d5e410b4 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/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 00000000..0aad300f Binary files /dev/null and b/packages/pinball_ui/assets/images/dialog/background.png differ 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/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 b1128b75..90901915 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'; @@ -113,38 +114,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 { @@ -239,6 +249,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', () { @@ -248,8 +433,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 0ca8ddd0..62f97236 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'; @@ -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 {} @@ -55,8 +53,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 {} 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); }); });