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..674c3e43 --- /dev/null +++ b/lib/game/components/board.dart @@ -0,0 +1,83 @@ +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): 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, + ), + ); + final slingShot = SlingShot( + side: _side, + position: _position + + Vector2( + (Flipper.width) * direction, + Flipper.height + SlingShot.size.y, + ), + ); + + await addAll([flipper, baseboard, slingShot]); + } +} diff --git a/lib/game/components/board_side.dart b/lib/game/components/board_side.dart index 611f70b8..f7587f47 100644 --- a/lib/game/components/board_side.dart +++ b/lib/game/components/board_side.dart @@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart'; /// Indicates a side of the board. /// /// Usually used to position or mirror elements of a [PinballGame]; such as a -/// [Flipper]. +/// [Flipper] or [SlingShot]. enum BoardSide { /// The left side of the board. left, @@ -19,4 +19,9 @@ extension BoardSideX on BoardSide { /// Whether this side is [BoardSide.right]. bool get isRight => this == BoardSide.right; + + /// Direction of the [BoardSide]. + /// + /// Represents the path which the [BoardSide] moves along. + int get direction => isLeft ? -1 : 1; } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 71108fcc..86fa3845 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,10 +1,14 @@ export 'ball.dart'; export 'baseboard.dart'; +export 'board.dart'; export 'board_side.dart'; export 'bonus_word.dart'; export 'flipper.dart'; +export 'initial_position.dart'; export 'joint_anchor.dart'; export 'pathway.dart'; export 'plunger.dart'; +export 'round_bumper.dart'; export 'score_points.dart'; +export 'sling_shot.dart'; export 'wall.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/components/initial_position.dart b/lib/game/components/initial_position.dart new file mode 100644 index 00000000..aa4acb46 --- /dev/null +++ b/lib/game/components/initial_position.dart @@ -0,0 +1,17 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +/// Forces a given [BodyComponent] to position their [body] to an +/// [initialPosition]. +mixin InitialPosition on BodyComponent { + /// The initial position of the [body]. + late final Vector2 initialPosition; + + @override + void onMount() { + super.onMount(); + assert( + body.position == initialPosition, + 'Body position is not equal to initial position.', + ); + } +} diff --git a/lib/game/components/round_bumper.dart b/lib/game/components/round_bumper.dart new file mode 100644 index 00000000..753ed15b --- /dev/null +++ b/lib/game/components/round_bumper.dart @@ -0,0 +1,41 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; + +/// {@template round_bumper} +/// Circular body that repels a [Ball] on contact, increasing the score. +/// {@endtemplate} +class RoundBumper extends BodyComponent with ScorePoints { + /// {@macro round_bumper} + RoundBumper({ + required Vector2 position, + required double radius, + required int points, + }) : _position = position, + _radius = radius, + _points = points; + + /// The position of the [RoundBumper] body. + final Vector2 _position; + + /// The radius of the [RoundBumper]. + final double _radius; + + /// Points awarded from hitting this [RoundBumper]. + final int _points; + + @override + int get points => _points; + + @override + Body createBody() { + final shape = CircleShape()..radius = _radius; + + final fixtureDef = FixtureDef(shape)..restitution = 1; + + final bodyDef = BodyDef() + ..position = _position + ..type = BodyType.static; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/lib/game/components/sling_shot.dart b/lib/game/components/sling_shot.dart new file mode 100644 index 00000000..0791cf4f --- /dev/null +++ b/lib/game/components/sling_shot.dart @@ -0,0 +1,97 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:geometry/geometry.dart' show centroid; +import 'package:pinball/game/game.dart'; + +/// {@template sling_shot} +/// Triangular [BodyType.static] body that propels the [Ball] towards the +/// opposite side. +/// +/// [SlingShot]s are usually positioned above each [Flipper]. +/// {@endtemplate sling_shot} +class SlingShot extends BodyComponent { + /// {@macro sling_shot} + SlingShot({ + required Vector2 position, + required BoardSide side, + }) : _position = position, + _side = side { + // TODO(alestiago): Use sprite instead of color when provided. + paint = Paint() + ..color = const Color(0xFF00FF00) + ..style = PaintingStyle.fill; + } + + /// The initial position of the [SlingShot] body. + final Vector2 _position; + + /// Whether the [SlingShot] is on the left or right side of the board. + /// + /// A [SlingShot] with [BoardSide.left] propels the [Ball] to the right, + /// whereas a [SlingShot] with [BoardSide.right] propels the [Ball] to the + /// left. + final BoardSide _side; + + /// The size of the [SlingShot] body. + // TODO(alestiago): Use size from PositionedBodyComponent instead, + // once a sprite is given. + static final Vector2 size = Vector2(6, 8); + + List _createFixtureDefs() { + final fixtures = []; + + // TODO(alestiago): This magic number can be deduced by specifying the + // angle and using polar coordinate system to place the bottom right + // vertex. + // Something as: y = -size.y * math.cos(angle) + const additionalIncrement = 3; + final triangleVertices = _side.isLeft + ? [ + Vector2(0, 0), + Vector2(0, -size.y), + Vector2( + size.x, + -size.y - additionalIncrement, + ), + ] + : [ + Vector2(size.x, 0), + Vector2(size.x, -size.y), + Vector2( + 0, + -size.y - additionalIncrement, + ), + ]; + final triangleCentroid = centroid(triangleVertices); + for (final vertex in triangleVertices) { + vertex.setFrom(vertex - triangleCentroid); + } + + final triangle = PolygonShape()..set(triangleVertices); + final triangleFixtureDef = FixtureDef(triangle)..friction = 0; + fixtures.add(triangleFixtureDef); + + final kicker = EdgeShape() + ..set( + triangleVertices.first, + triangleVertices.last, + ); + // TODO(alestiago): Play with restitution value once game is bundled. + final kickerFixtureDef = FixtureDef(kicker) + ..restitution = 10.0 + ..friction = 0; + fixtures.add(kickerFixtureDef); + + return fixtures; + } + + @override + Body createBody() { + final bodyDef = BodyDef()..position = _position; + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index c433365c..017f8c4d 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -26,7 +26,7 @@ class Wall extends BodyComponent { final fixtureDef = FixtureDef(shape) ..restitution = 0.1 - ..friction = 0.3; + ..friction = 0; final bodyDef = BodyDef() ..userData = this 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/ball_test.dart b/test/game/components/ball_test.dart index e83dd619..e6172d6d 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -18,6 +18,7 @@ void main() { 'loads correctly', (game) async { final ball = Ball(position: Vector2.zero()); + await game.ready(); await game.ensureAdd(ball); expect(game.contains(ball), isTrue); 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_side_test.dart b/test/game/components/board_side_test.dart index 3d6d3fa1..ba201065 100644 --- a/test/game/components/board_side_test.dart +++ b/test/game/components/board_side_test.dart @@ -23,5 +23,12 @@ void main() { expect(side.isLeft, isFalse); expect(side.isRight, isTrue); }); + + test('direction is correct', () { + const side = BoardSide.left; + expect(side.direction, equals(-1)); + const side2 = BoardSide.right; + expect(side2.direction, equals(1)); + }); }); } diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart new file mode 100644 index 00000000..34a628a8 --- /dev/null +++ b/test/game/components/board_test.dart @@ -0,0 +1,80 @@ +// 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 rightFlippers = bottomGroup.findNestedChildren( + condition: (flipper) => flipper.side.isRight, + ); + expect(rightFlippers.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 baseboards = bottomGroup.findNestedChildren(); + expect(baseboards.length, equals(2)); + }, + ); + + flameTester.test( + 'has two SlingShots', + (game) async { + final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0); + await game.ready(); + await game.ensureAdd(bottomGroup); + + final slingShots = bottomGroup.findNestedChildren(); + expect(slingShots.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/components/initial_position_test.dart b/test/game/components/initial_position_test.dart new file mode 100644 index 00000000..393d780a --- /dev/null +++ b/test/game/components/initial_position_test.dart @@ -0,0 +1,66 @@ +// 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'; + +class TestBodyComponent extends BodyComponent with InitialPosition { + @override + Body createBody() { + return world.createBody(BodyDef()); + } +} + +class TestPositionedBodyComponent extends BodyComponent with InitialPosition { + @override + Body createBody() { + return world.createBody(BodyDef()..position = initialPosition); + } +} + +void main() { + final flameTester = FlameTester(Forge2DGame.new); + group('InitialPosition', () { + test('correctly sets and gets', () { + final component = TestBodyComponent()..initialPosition = Vector2(1, 2); + expect(component.initialPosition, Vector2(1, 2)); + }); + + test('can only be set once', () { + final component = TestBodyComponent()..initialPosition = Vector2(1, 2); + expect( + () => component.initialPosition = Vector2(3, 4), + throwsA(isA()), + ); + }); + + flameTester.test( + 'returns normally ' + 'when the body sets the position to initial position', + (game) async { + final component = TestPositionedBodyComponent() + ..initialPosition = Vector2.zero(); + + await expectLater( + () async => game.ensureAdd(component), + returnsNormally, + ); + }, + ); + + flameTester.test( + 'throws AssertionError ' + 'when not setting initialPosition to body', + (game) async { + final component = TestBodyComponent()..initialPosition = Vector2.zero(); + await game.ensureAdd(component); + + await expectLater( + () async => game.ensureAdd(component), + throwsAssertionError, + ); + }, + ); + }); +} diff --git a/test/game/components/round_bumper_test.dart b/test/game/components/round_bumper_test.dart new file mode 100644 index 00000000..c780dd0b --- /dev/null +++ b/test/game/components/round_bumper_test.dart @@ -0,0 +1,124 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RoundBumper', () { + final flameTester = FlameTester(Forge2DGame.new); + const radius = 1.0; + const points = 1; + + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(game.contains(roundBumper), isTrue); + }, + ); + + flameTester.test( + 'has points', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(roundBumper.points, equals(points)); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final roundBumper = RoundBumper( + position: position, + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + game.contains(roundBumper); + + expect(roundBumper.body.position, equals(position)); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(roundBumper.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixture', () { + flameTester.test( + 'exists', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(roundBumper.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'has restitution', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + final fixture = roundBumper.body.fixtures[0]; + expect(fixture.restitution, greaterThan(0)); + }, + ); + + flameTester.test( + 'shape is circular', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + final fixture = roundBumper.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.circle)); + expect(fixture.shape.radius, equals(1)); + }, + ); + }); + }); +} diff --git a/test/game/components/sling_shot_test.dart b/test/game/components/sling_shot_test.dart new file mode 100644 index 00000000..e7e89ead --- /dev/null +++ b/test/game/components/sling_shot_test.dart @@ -0,0 +1,180 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + group('SlingShot', () { + final flameTester = FlameTester(Forge2DGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(game.contains(slingShot), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final slingShot = SlingShot( + position: position, + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.position, equals(position)); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'shape is triangular', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.polygon)); + expect((fixture.shape as PolygonShape).vertices.length, equals(3)); + }, + ); + + flameTester.test( + 'triangular shapes are different ' + 'when side is left or right', + (game) async { + final leftSlingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + final rightSlingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.right, + ); + + await game.ensureAdd(leftSlingShot); + await game.ensureAdd(rightSlingShot); + + final rightShape = + rightSlingShot.body.fixtures[0].shape as PolygonShape; + final leftShape = + leftSlingShot.body.fixtures[0].shape as PolygonShape; + + expect(rightShape.vertices, isNot(equals(leftShape.vertices))); + }, + ); + + flameTester.test( + 'has no friction', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[0]; + expect(fixture.friction, equals(0)); + }, + ); + }); + + group('second fixture', () { + flameTester.test( + 'exists', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.fixtures[1], isA()); + }, + ); + + flameTester.test( + 'shape is edge', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[1]; + expect(fixture.shape.shapeType, equals(ShapeType.edge)); + }, + ); + + flameTester.test( + 'has restitution', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[1]; + expect(fixture.restitution, greaterThan(0)); + }, + ); + + flameTester.test( + 'has no friction', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[1]; + expect(fixture.friction, equals(0)); + }, + ); + }); + }); +} diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 774cd675..53d387fa 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -106,7 +106,7 @@ void main() { ); flameTester.test( - 'has friction', + 'has no friction', (game) async { final wall = Wall( start: Vector2.zero(), @@ -115,7 +115,7 @@ void main() { await game.ensureAdd(wall); final fixture = wall.body.fixtures[0]; - expect(fixture.friction, greaterThan(0)); + expect(fixture.friction, equals(0)); }, ); }); 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; + } +}