diff --git a/lib/game/components/baseboard.dart b/lib/game/components/baseboard.dart index 9153d4f3..60e51593 100644 --- a/lib/game/components/baseboard.dart +++ b/lib/game/components/baseboard.dart @@ -8,27 +8,11 @@ import 'package:pinball/game/game.dart'; /// {@endtemplate} class Baseboard extends BodyComponent { /// {@macro baseboard} - Baseboard._({ - required Vector2 position, + Baseboard({ required BoardSide side, - }) : _position = position, - _side = side; - - /// A left positioned [Baseboard]. - Baseboard.left({ - required Vector2 position, - }) : this._( - position: position, - side: BoardSide.left, - ); - - /// A right positioned [Baseboard]. - Baseboard.right({ required Vector2 position, - }) : this._( - position: position, - side: BoardSide.right, - ); + }) : _side = side, + _position = position; /// The width of the [Baseboard]. static const width = 10.0; diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart new file mode 100644 index 00000000..80db1711 --- /dev/null +++ b/lib/game/components/board.dart @@ -0,0 +1,76 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; + +/// {@template bottom_group} +/// Grouping of the board's bottom [Component]s. +/// +/// The bottom [Component]s are the [Flipper]s and the [Baseboard]s. +/// {@endtemplate} +// TODO(alestiago): Add [SlingShot] once provided. +// TODO(alestiago): Consider renaming once entire Board is defined. +class BottomGroup extends Component { + /// {@macro bottom_group} + BottomGroup({ + required this.position, + required this.spacing, + }); + + /// The amount of space between the line of symmetry. + final double spacing; + + /// The position of this [BottomGroup]. + final Vector2 position; + + @override + Future onLoad() async { + final spacing = this.spacing + Flipper.width / 2; + final rightSide = _BottomGroupSide( + side: BoardSide.right, + position: position + Vector2(spacing, 0), + ); + final leftSide = _BottomGroupSide( + side: BoardSide.left, + position: position + Vector2(-spacing, 0), + ); + + 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, + required Vector2 position, + }) : _side = side, + _position = position; + + final BoardSide _side; + + final Vector2 _position; + + @override + Future onLoad() async { + final direction = _side.direction; + + final flipper = Flipper.fromSide( + side: _side, + position: _position, + ); + final baseboard = Baseboard( + side: _side, + position: _position + + Vector2( + (Flipper.width * direction) - direction, + Flipper.height, + ), + ); + + await addAll([flipper, baseboard]); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index d5c479c7..f733cfcd 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,5 +1,6 @@ export 'ball.dart'; export 'baseboard.dart'; +export 'board.dart'; export 'board_side.dart'; export 'bonus_word.dart'; export 'flipper.dart'; diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 9035daaf..ff4754ea 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -8,42 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; -/// {@template flipper_group} -/// Loads a [Flipper.right] and a [Flipper.left]. -/// {@endtemplate} -class FlipperGroup extends Component { - /// {@macro flipper_group} - FlipperGroup({ - required this.position, - required this.spacing, - }); - - /// The amount of space between the [Flipper.right] and [Flipper.left]. - final double spacing; - - /// The position of this [FlipperGroup] - final Vector2 position; - - @override - Future onLoad() async { - final leftFlipper = Flipper.left( - position: Vector2( - position.x - (Flipper.width / 2) - (spacing / 2), - position.y, - ), - ); - await add(leftFlipper); - - final rightFlipper = Flipper.right( - position: Vector2( - position.x + (Flipper.width / 2) + (spacing / 2), - position.y, - ), - ); - await add(rightFlipper); - } -} - /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. /// @@ -58,8 +22,7 @@ class Flipper extends BodyComponent with KeyboardHandler { }) : _position = position, _keys = keys; - /// A left positioned [Flipper]. - Flipper.left({ + Flipper._left({ required Vector2 position, }) : this._( position: position, @@ -70,8 +33,7 @@ class Flipper extends BodyComponent with KeyboardHandler { ], ); - /// A right positioned [Flipper]. - Flipper.right({ + Flipper._right({ required Vector2 position, }) : this._( position: position, @@ -82,6 +44,22 @@ class Flipper extends BodyComponent with KeyboardHandler { ], ); + /// Constructs a [Flipper] from a [BoardSide]. + /// + /// A [Flipper._right] and [Flipper._left] besides being mirrored + /// horizontally, also have different [LogicalKeyboardKey]s that control them. + factory Flipper.fromSide({ + required BoardSide side, + required Vector2 position, + }) { + switch (side) { + case BoardSide.left: + return Flipper._left(position: position); + case BoardSide.right: + return Flipper._right(position: position); + } + } + /// Asset location of the sprite that renders with the [Flipper]. /// /// Sprite is preloaded by [PinballGameAssetsX]. diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index b67a8dad..112f0522 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -48,9 +48,20 @@ class PinballGame extends Forge2DGame ), ); - unawaited(_addFlippers()); - unawaited(_addBonusWord()); + unawaited( + add( + BottomGroup( + position: screenToWorld( + Vector2( + camera.viewport.effectiveSize.x / 2, + camera.viewport.effectiveSize.y / 1.25, + ), + ), + spacing: 2, + ), + ), + ); } Future _addBonusWord() async { @@ -66,25 +77,6 @@ class PinballGame extends Forge2DGame ); } - Future _addFlippers() async { - final flippersPosition = screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2, - camera.viewport.effectiveSize.y / 1.1, - ), - ); - - unawaited( - add( - FlipperGroup( - position: flippersPosition, - spacing: 2, - ), - ), - ); - unawaited(_addBaseboards()); - } - void spawnBall() { add(Ball(position: plunger.body.position)); } @@ -115,31 +107,6 @@ class PinballGame extends Forge2DGame ), ); } - - Future _addBaseboards() async { - final spaceBetweenBaseboards = camera.viewport.effectiveSize.x / 2; - final baseboardY = camera.viewport.effectiveSize.y / 1.12; - - final leftBaseboard = Baseboard.left( - position: screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2 - (spaceBetweenBaseboards / 2), - baseboardY, - ), - ), - ); - await add(leftBaseboard); - - final rightBaseboard = Baseboard.right( - position: screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2 + (spaceBetweenBaseboards / 2), - baseboardY, - ), - ), - ); - await add(rightBaseboard); - } } class DebugPinballGame extends PinballGame with TapDetector { diff --git a/test/game/components/baseboard_test.dart b/test/game/components/baseboard_test.dart index bc9f68af..8f1874b1 100644 --- a/test/game/components/baseboard_test.dart +++ b/test/game/components/baseboard_test.dart @@ -14,8 +14,14 @@ void main() { 'loads correctly', (game) async { await game.ready(); - final leftBaseboard = Baseboard.left(position: Vector2.zero()); - final rightBaseboard = Baseboard.right(position: Vector2.zero()); + final leftBaseboard = Baseboard( + position: Vector2.zero(), + side: BoardSide.left, + ); + final rightBaseboard = Baseboard( + position: Vector2.zero(), + side: BoardSide.right, + ); await game.ensureAddAll([leftBaseboard, rightBaseboard]); expect(game.contains(leftBaseboard), isTrue); @@ -28,7 +34,10 @@ void main() { 'positions correctly', (game) async { final position = Vector2.all(10); - final baseboard = Baseboard.left(position: position); + final baseboard = Baseboard( + position: position, + side: BoardSide.left, + ); await game.ensureAdd(baseboard); game.contains(baseboard); @@ -39,7 +48,10 @@ void main() { flameTester.test( 'is static', (game) async { - final baseboard = Baseboard.left(position: Vector2.zero()); + final baseboard = Baseboard( + position: Vector2.zero(), + side: BoardSide.left, + ); await game.ensureAdd(baseboard); expect(baseboard.body.bodyType, equals(BodyType.static)); @@ -49,8 +61,14 @@ void main() { flameTester.test( 'is at an angle', (game) async { - final leftBaseboard = Baseboard.left(position: Vector2.zero()); - final rightBaseboard = Baseboard.right(position: Vector2.zero()); + final leftBaseboard = Baseboard( + position: Vector2.zero(), + side: BoardSide.left, + ); + final rightBaseboard = Baseboard( + position: Vector2.zero(), + side: BoardSide.right, + ); await game.ensureAddAll([leftBaseboard, rightBaseboard]); expect(leftBaseboard.body.angle, isNegative); @@ -63,7 +81,10 @@ void main() { flameTester.test( 'has three', (game) async { - final baseboard = Baseboard.left(position: Vector2.zero()); + final baseboard = Baseboard( + position: Vector2.zero(), + side: BoardSide.left, + ); await game.ensureAdd(baseboard); expect(baseboard.body.fixtures.length, equals(3)); diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart new file mode 100644 index 00000000..eaa916c3 --- /dev/null +++ b/test/game/components/board_test.dart @@ -0,0 +1,68 @@ +// 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(Forge2DGame.new); + + group('BottomGroup', () { + flameTester.test( + 'loads correctly', + (game) async { + final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0); + await game.ready(); + await game.ensureAdd(bottomGroup); + + expect(game.contains(bottomGroup), isTrue); + }, + ); + + group('children', () { + flameTester.test( + 'has one left flipper', + (game) async { + final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0); + await game.ready(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = bottomGroup.findNestedChildren( + condition: (flipper) => flipper.side.isLeft, + ); + expect(leftFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'has one right flipper', + (game) async { + final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0); + await game.ready(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = bottomGroup.findNestedChildren( + condition: (flipper) => flipper.side.isRight, + ); + expect(leftFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'has two Baseboards', + (game) async { + final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0); + await game.ready(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = bottomGroup.findNestedChildren(); + expect(leftFlippers.length, equals(2)); + }, + ); + }); + }); +} diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index 0e281d07..eae238c9 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -2,7 +2,6 @@ import 'dart:collection'; -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; @@ -15,110 +14,20 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); - group('FlipperGroup', () { - flameTester.test( - 'loads correctly', - (game) async { - final flipperGroup = FlipperGroup( - position: Vector2.zero(), - spacing: 0, - ); - await game.ensureAdd(flipperGroup); - - expect(game.contains(flipperGroup), isTrue); - }, - ); - - group('constructor', () { - flameTester.test( - 'positions correctly', - (game) async { - final position = Vector2.all(10); - final flipperGroup = FlipperGroup( - position: position, - spacing: 0, - ); - await game.ensureAdd(flipperGroup); - - expect(flipperGroup.position, equals(position)); - }, - ); - }); - - group('children', () { - bool Function(Component) flipperSelector(BoardSide side) => - (component) => component is Flipper && component.side == side; - - flameTester.test( - 'has only one left Flipper', - (game) async { - final flipperGroup = FlipperGroup( - position: Vector2.zero(), - spacing: 0, - ); - await game.ensureAdd(flipperGroup); - - expect( - () => flipperGroup.children.singleWhere( - flipperSelector(BoardSide.left), - ), - returnsNormally, - ); - }, - ); - - flameTester.test( - 'has only one right Flipper', - (game) async { - final flipperGroup = FlipperGroup( - position: Vector2.zero(), - spacing: 0, - ); - await game.ensureAdd(flipperGroup); - - expect( - () => flipperGroup.children.singleWhere( - flipperSelector(BoardSide.right), - ), - returnsNormally, - ); - }, - ); - - flameTester.test( - 'spaced correctly', - (game) async { - final flipperGroup = FlipperGroup( - position: Vector2.zero(), - spacing: 2, - ); - await game.ready(); - await game.ensureAdd(flipperGroup); - - final leftFlipper = flipperGroup.children.singleWhere( - flipperSelector(BoardSide.left), - ) as Flipper; - final rightFlipper = flipperGroup.children.singleWhere( - flipperSelector(BoardSide.right), - ) as Flipper; - - expect( - leftFlipper.body.position.x + Flipper.width + flipperGroup.spacing, - equals(rightFlipper.body.position.x), - ); - }, - ); - }); - }); - group( 'Flipper', () { flameTester.test( 'loads correctly', (game) async { - final leftFlipper = Flipper.left(position: Vector2.zero()); - final rightFlipper = Flipper.right(position: Vector2.zero()); + final leftFlipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); + final rightFlipper = Flipper.fromSide( + side: BoardSide.right, + position: Vector2.zero(), + ); await game.ready(); await game.ensureAddAll([leftFlipper, rightFlipper]); @@ -129,10 +38,17 @@ void main() { group('constructor', () { test('sets BoardSide', () { - final leftFlipper = Flipper.left(position: Vector2.zero()); + final leftFlipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); + expect(leftFlipper.side, equals(leftFlipper.side)); - final rightFlipper = Flipper.right(position: Vector2.zero()); + final rightFlipper = Flipper.fromSide( + side: BoardSide.right, + position: Vector2.zero(), + ); expect(rightFlipper.side, equals(rightFlipper.side)); }); }); @@ -142,7 +58,10 @@ void main() { 'positions correctly', (game) async { final position = Vector2.all(10); - final flipper = Flipper.left(position: position); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: position, + ); await game.ensureAdd(flipper); game.contains(flipper); @@ -153,7 +72,10 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); expect(flipper.body.bodyType, equals(BodyType.dynamic)); @@ -163,7 +85,10 @@ void main() { flameTester.test( 'ignores gravity', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); expect(flipper.body.gravityScale, isZero); @@ -173,7 +98,10 @@ void main() { flameTester.test( 'has greater mass than Ball', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); final ball = Ball(position: Vector2.zero()); await game.ready(); @@ -191,7 +119,10 @@ void main() { flameTester.test( 'has three', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); expect(flipper.body.fixtures.length, equals(3)); @@ -201,7 +132,10 @@ void main() { flameTester.test( 'has density', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final fixtures = flipper.body.fixtures; @@ -229,7 +163,10 @@ void main() { late Flipper flipper; setUp(() { - flipper = Flipper.left(position: Vector2.zero()); + flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); }); testRawKeyDownEvents(leftKeys, (event) { @@ -293,7 +230,10 @@ void main() { late Flipper flipper; setUp(() { - flipper = Flipper.right(position: Vector2.zero()); + flipper = Flipper.fromSide( + side: BoardSide.right, + position: Vector2.zero(), + ); }); testRawKeyDownEvents(rightKeys, (event) { @@ -360,7 +300,10 @@ void main() { flameTester.test( 'position is at the left of the left Flipper', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); @@ -373,7 +316,10 @@ void main() { flameTester.test( 'position is at the right of the right Flipper', (game) async { - final flipper = Flipper.right(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.right, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); @@ -389,7 +335,10 @@ void main() { flameTester.test( 'limits enabled', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); @@ -408,7 +357,10 @@ void main() { flameTester.test( 'when Flipper is left', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); @@ -426,7 +378,10 @@ void main() { flameTester.test( 'when Flipper is right', (game) async { - final flipper = Flipper.right(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.right, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); @@ -449,7 +404,10 @@ void main() { flameTester.test( 'when Flipper is left', (game) async { - final flipper = Flipper.left(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.left, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); @@ -473,7 +431,10 @@ void main() { flameTester.test( 'when Flipper is right', (game) async { - final flipper = Flipper.right(position: Vector2.zero()); + final flipper = Flipper.fromSide( + side: BoardSide.right, + position: Vector2.zero(), + ); await game.ensureAdd(flipper); final flipperAnchor = FlipperAnchor(flipper: flipper); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f79d19d5..f7d0f7db 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -54,22 +54,13 @@ void main() { }, ); - flameTester.test('has only one FlipperGroup', (game) async { + flameTester.test('has only one BottomGroup', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }); - - flameTester.test( - 'has two Baseboards', - (game) async { - await game.ready(); - final baseboards = game.children.whereType(); - expect(baseboards.length, 2); - }, - ); }); debugModeFlameTester.test('adds a ball on tap up', (game) async { diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index 2a0a7e59..a5c56a86 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,3 +1,4 @@ +import 'package:flame/components.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -20,3 +21,41 @@ extension DebugPinballGameTest on DebugPinballGame { ), ); } + +extension ComponentX on Component { + T findNestedChild({ + bool Function(T)? condition, + }) { + T? nestedChild; + propagateToChildren((child) { + final foundChild = (condition ?? (_) => true)(child); + if (foundChild) { + nestedChild = child; + } + + return !foundChild; + }); + + if (nestedChild == null) { + throw Exception('No child of type $T found.'); + } else { + return nestedChild!; + } + } + + List findNestedChildren({ + bool Function(T)? condition, + }) { + final nestedChildren = []; + propagateToChildren((child) { + final foundChild = (condition ?? (_) => true)(child); + if (foundChild) { + nestedChildren.add(child); + } + + return true; + }); + + return nestedChildren; + } +}