diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 364fc35e..ea5742da 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,23 +1,29 @@ +import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart' show Anchor; /// {@template plunger} /// [Plunger] serves as a spring, that shoots the ball on the right side of the /// playfield. /// -/// [Plunger] ignores gravity so the player controls its downward [pull]. +/// [Plunger] ignores gravity so the player controls its downward [_pull]. /// {@endtemplate} -class Plunger extends BodyComponent { +class Plunger extends BodyComponent with KeyboardHandler { /// {@macro plunger} Plunger({required Vector2 position}) : _position = position; + /// The initial position of the [Plunger] body. final Vector2 _position; + /// Distance the plunger can lower. + static const compressionDistance = 120.0; + @override Body createBody() { - final shape = PolygonShape()..setAsBoxXY(2.5, 1.5); + final shape = PolygonShape()..setAsBoxXY(2, 0.75); - final fixtureDef = FixtureDef(shape); + final fixtureDef = FixtureDef(shape)..density = 5; final bodyDef = BodyDef() ..userData = this @@ -29,18 +35,57 @@ class Plunger extends BodyComponent { } /// Set a constant downward velocity on the [Plunger]. - void pull() { - body.linearVelocity = Vector2(0, -7); + void _pull() { + body.linearVelocity = Vector2(0, -3); } /// Set an upward velocity on the [Plunger]. /// /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [_position]. - void release() { + void _release() { final velocity = (_position.y - body.position.y) * 9; body.linearVelocity = Vector2(0, velocity); } + + @override + bool onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + final keys = [ + LogicalKeyboardKey.space, + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.keyS, + ]; + // TODO(alestiago): Check why false cancels the event for other components. + // Investigate why return is of type [bool] expected instead of a type + // [KeyEventResult]. + if (!keys.contains(event.logicalKey)) return true; + + if (event is RawKeyDownEvent) { + _pull(); + } else if (event is RawKeyUpEvent) { + _release(); + } + + return true; + } +} + +/// {@template plunger_anchor} +/// [Anchor] positioned below a [Plunger]. +/// {@endtemplate} +class PlungerAnchor extends Anchor { + /// {@macro plunger_anchor} + PlungerAnchor({ + required Plunger plunger, + }) : super( + position: Vector2( + plunger.body.position.x, + plunger.body.position.y - Plunger.compressionDistance, + ), + ); } /// {@template plunger_anchor_prismatic_joint_def} @@ -67,6 +112,9 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { ); enableLimit = true; lowerTranslation = double.negativeInfinity; + enableMotor = true; + motorSpeed = 50; + maxMotorForce = motorSpeed; collideConnected = true; } } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index b784b8cb..a05b50b8 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -4,9 +4,10 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; /// {@template wall} -/// A continuos generic and [BodyType.static] barrier that divides a game area. +/// A continuous generic and [BodyType.static] barrier that divides a game area. /// {@endtemplate} class Wall extends BodyComponent { + /// {@macro wall} Wall({ required this.start, required this.end, @@ -20,7 +21,7 @@ class Wall extends BodyComponent { final shape = EdgeShape()..set(start, end); final fixtureDef = FixtureDef(shape) - ..restitution = 0.0 + ..restitution = 0.1 ..friction = 0.3; final bodyDef = BodyDef() @@ -32,6 +33,19 @@ class Wall extends BodyComponent { } } +List createBoundaries(Forge2DGame game) { + final topLeft = Vector2.zero(); + final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); + final topRight = Vector2(bottomRight.x, topLeft.y); + final bottomLeft = Vector2(topLeft.x, bottomRight.y); + + return [ + Wall(start: topLeft, end: topRight), + Wall(start: topRight, end: bottomRight), + Wall(start: bottomLeft, end: topLeft), + ]; +} + /// {@template bottom_wall} /// [Wall] located at the bottom of the board. /// @@ -39,6 +53,7 @@ class Wall extends BodyComponent { /// [BottomWallBallContactCallback]. /// {@endtemplate} class BottomWall extends Wall { + /// {@macro bottom_wall} BottomWall(Forge2DGame game) : super( start: game.screenToWorld(game.camera.viewport.effectiveSize), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 308d8faf..ec7c0e2f 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,17 +7,7 @@ import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents { - // TODO(erickzanardo): Change to the plumber position - late final ballStartingPosition = screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2, - camera.viewport.effectiveSize.y - 20, - ), - ) - - Vector2(0, -20); - - // TODO(alestiago): Change to the design position. - late final flippersPosition = ballStartingPosition - Vector2(0, 5); + late Plunger plunger; @override void onAttach() { @@ -25,21 +15,63 @@ class PinballGame extends Forge2DGame spawnBall(); } - void spawnBall() { - add(Ball(position: ballStartingPosition)); - } - @override Future onLoad() async { - addContactCallback(BallScorePointsCallback()); + _addContactCallbacks(); - await add(BottomWall(this)); + await _addGameBoundaries(); + unawaited(_addFlippers()); + await _addPlunger(); + + // Corner wall above plunger so the ball deflects into the rest of the + // board. + // TODO(allisonryan0002): remove once we have the launch track for the ball. + await add( + Wall( + start: screenToWorld( + Vector2( + camera.viewport.effectiveSize.x, + 100, + ), + ), + end: screenToWorld( + Vector2( + camera.viewport.effectiveSize.x - 100, + 0, + ), + ), + ), + ); + } + + Future spawnBall() async { + await add( + Ball( + position: Vector2( + plunger.body.position.x, + plunger.body.position.y + Ball.ballSize.y, + ), + ), + ); + } + + void _addContactCallbacks() { + addContactCallback(BallScorePointsCallback()); addContactCallback(BottomWallBallContactCallback()); + } - unawaited(_addFlippers()); + Future _addGameBoundaries() async { + await add(BottomWall(this)); + createBoundaries(this).forEach(add); } Future _addFlippers() async { + final flippersPosition = screenToWorld( + Vector2( + camera.viewport.effectiveSize.x / 2, + camera.viewport.effectiveSize.y - 120, + ), + ); const spaceBetweenFlippers = 2; final leftFlipper = Flipper.left( position: Vector2( @@ -98,4 +130,27 @@ class PinballGame extends Forge2DGame ), ); } + + Future _addPlunger() async { + late Anchor plungerAnchor; + + await add( + plunger = Plunger( + position: screenToWorld( + Vector2( + camera.viewport.effectiveSize.x - 30, + camera.viewport.effectiveSize.y - Plunger.compressionDistance, + ), + ), + ), + ); + await add(plungerAnchor = PlungerAnchor(plunger: plunger)); + + world.createJoint( + PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: plungerAnchor, + ), + ); + } } diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index bd2cbcfc..d7def330 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -49,7 +49,7 @@ void main() { ); }); - group('first fixture', () { + group('fixture', () { flameTester.test( 'exists', (game) async { diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index b9894d9a..52a2faeb 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -255,36 +255,33 @@ void main() { }, ); - group( - 'FlipperAnchor', - () { - flameTester.test( - 'position is at the left of the left Flipper', - (game) async { - final flipper = Flipper.left(position: Vector2.zero()); - await game.ensureAdd(flipper); + group('FlipperAnchor', () { + flameTester.test( + 'position is at the left of the left Flipper', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); - expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2)); - }, - ); + expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2)); + }, + ); - flameTester.test( - 'position is at the right of the right Flipper', - (game) async { - final flipper = Flipper.right(position: Vector2.zero()); - await game.ensureAdd(flipper); + flameTester.test( + 'position is at the right of the right Flipper', + (game) async { + final flipper = Flipper.right(position: Vector2.zero()); + await game.ensureAdd(flipper); - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); - expect(flipperAnchor.body.position.x, equals(Flipper.width / 2)); - }, - ); - }, - ); + expect(flipperAnchor.body.position.x, equals(Flipper.width / 2)); + }, + ); + }); group('FlipperAnchorRevoluteJointDef', () { group('initializes with', () { diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 67e215fd..ace0c9f5 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -1,8 +1,11 @@ // ignore_for_file: cascade_invocations +import 'dart:collection'; + import 'package:bloc_test/bloc_test.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; @@ -57,7 +60,7 @@ void main() { ); }); - group('first fixture', () { + group('fixture', () { flameTester.test( 'exists', (game) async { @@ -78,51 +81,101 @@ void main() { expect(fixture.shape.shapeType, equals(ShapeType.polygon)); }, ); - }); - - flameTester.test( - 'pull sets a negative linear velocity', - (game) async { - final plunger = Plunger(position: Vector2.zero()); - await game.ensureAdd(plunger); - - plunger.pull(); - expect(plunger.body.linearVelocity.y, isNegative); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - - group('release', () { flameTester.test( - 'does not set a linear velocity ' - 'when plunger is in starting position', + 'has density', (game) async { final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); - plunger.release(); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); + final fixture = plunger.body.fixtures[0]; + expect(fixture.density, greaterThan(0)); }, ); + }); - flameTester.test( - 'sets a positive linear velocity ' - 'when plunger is below starting position', - (game) async { - final plunger = Plunger(position: Vector2.zero()); - await game.ensureAdd(plunger); + group('onKeyEvent', () { + final keys = UnmodifiableListView([ + LogicalKeyboardKey.space, + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.keyS, + ]); + + late Plunger plunger; + + setUp(() { + plunger = Plunger(position: Vector2.zero()); + }); + + testRawKeyUpEvents(keys, (event) { + final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) + ? event.logicalKey.keyLabel + : 'Space'; + flameTester.test( + 'moves upwards when $keyLabel is released ' + 'and plunger is below its starting position', + (game) async { + await game.ensureAdd(plunger); + plunger.body.setTransform(Vector2(0, -1), 0); + plunger.onKeyEvent(event, {}); + + expect(plunger.body.linearVelocity.y, isPositive); + expect(plunger.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(keys, (event) { + final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) + ? event.logicalKey.keyLabel + : 'Space'; + flameTester.test( + 'does not move when $keyLabel is released ' + 'and plunger is in its starting position', + (game) async { + await game.ensureAdd(plunger); + plunger.onKeyEvent(event, {}); + + expect(plunger.body.linearVelocity.y, isZero); + expect(plunger.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyDownEvents(keys, (event) { + final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) + ? event.logicalKey.keyLabel + : 'Space'; + flameTester.test( + 'moves downwards when $keyLabel is pressed', + (game) async { + await game.ensureAdd(plunger); + plunger.onKeyEvent(event, {}); + + expect(plunger.body.linearVelocity.y, isNegative); + expect(plunger.body.linearVelocity.x, isZero); + }, + ); + }); + }); + }); - plunger.body.setTransform(Vector2(0, -1), 0); - plunger.release(); + group('PlungerAnchor', () { + flameTester.test( + 'position is a compression distance below the Plunger', + (game) async { + final plunger = Plunger(position: Vector2.zero()); + await game.ensureAdd(plunger); - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); + final plungerAnchor = PlungerAnchor(plunger: plunger); + await game.ensureAdd(plungerAnchor); + + expect( + plungerAnchor.body.position.y, + equals(plunger.body.position.y - Plunger.compressionDistance), + ); + }, + ); }); group('PlungerAnchorPrismaticJointDef', () { @@ -257,46 +310,47 @@ void main() { ); }); - flameTester.widgetTest( - 'plunger cannot go below anchor', - (game, tester) async { - await game.ensureAddAll([plunger, anchor]); + testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { + flameTester.widgetTest( + 'plunger cannot go below anchor', + (game, tester) async { + await game.ensureAddAll([plunger, anchor]); - // Giving anchor a shape for the plunger to collide with. - anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); + // Giving anchor a shape for the plunger to collide with. + anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(jointDef); + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); - plunger.pull(); - await tester.pump(const Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); - expect(plunger.body.position.y > anchor.body.position.y, isTrue); - }, - ); + expect(plunger.body.position.y > anchor.body.position.y, isTrue); + }, + ); + }); - flameTester.widgetTest( - 'plunger cannot excessively exceed starting position', - (game, tester) async { - await game.ensureAddAll([plunger, anchor]); + testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { + flameTester.widgetTest( + 'plunger cannot excessively exceed starting position', + (game, tester) async { + await game.ensureAddAll([plunger, anchor]); - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(jointDef); + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); - plunger.pull(); - await tester.pump(const Duration(seconds: 1)); + plunger.body.setTransform(Vector2(0, -1), 0); - plunger.release(); - await tester.pump(const Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); - expect(plunger.body.position.y < 1, isTrue); - }, - ); + expect(plunger.body.position.y < 1, isTrue); + }, + ); + }); }); } diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 8151055e..54263a85 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -76,7 +76,7 @@ void main() { ); }); - group('first fixture', () { + group('fixture', () { flameTester.test( 'exists', (game) async { @@ -91,7 +91,7 @@ void main() { ); flameTester.test( - 'has restitution equals 0', + 'has restitution', (game) async { final wall = Wall( start: Vector2.zero(), @@ -100,7 +100,7 @@ void main() { await game.ensureAdd(wall); final fixture = wall.body.fixtures[0]; - expect(fixture.restitution, equals(0)); + expect(fixture.restitution, greaterThan(0)); }, ); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 4dc93b7f..f06639f7 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -13,42 +13,84 @@ void main() { // TODO(alestiago): test if [PinballGame] registers // [BallScorePointsCallback] once the following issue is resolved: // https://github.com/flame-engine/flame/issues/1416 - group( - 'components', - () { - group('Flippers', () { - bool Function(Component) flipperSelector(BoardSide side) => - (component) => component is Flipper && component.side == side; - - flameTester.test( - 'has only one left Flipper', - (game) async { - await game.ready(); - - expect( - () => game.children.singleWhere( - flipperSelector(BoardSide.left), - ), - returnsNormally, - ); - }, - ); + group('components', () { + group('Walls', () { + flameTester.test( + 'has three Walls', + (game) async { + await game.ready(); + final walls = game.children + .where( + (component) => component is Wall && component is! BottomWall, + ) + .toList(); + // TODO(allisonryan0002): expect 3 when launch track is added and + // temporary wall is removed. + expect(walls.length, 4); + }, + ); + + flameTester.test( + 'has only one BottomWall', + (game) async { + await game.ready(); + + expect( + () => game.children.singleWhere( + (component) => component is BottomWall, + ), + returnsNormally, + ); + }, + ); + }); + + group('Flippers', () { + bool Function(Component) flipperSelector(BoardSide side) => + (component) => component is Flipper && component.side == side; + + flameTester.test( + 'has only one left Flipper', + (game) async { + await game.ready(); + + expect( + () => game.children.singleWhere( + flipperSelector(BoardSide.left), + ), + returnsNormally, + ); + }, + ); + + flameTester.test( + 'has only one right Flipper', + (game) async { + await game.ready(); + + expect( + () => game.children.singleWhere( + flipperSelector(BoardSide.right), + ), + returnsNormally, + ); + }, + ); + }); + + flameTester.test( + 'Plunger has only one Plunger', + (game) async { + await game.ready(); - flameTester.test( - 'has only one right Flipper', - (game) async { - await game.ready(); - - expect( - () => game.children.singleWhere( - flipperSelector(BoardSide.right), - ), - returnsNormally, - ); - }, + expect( + () => game.children.singleWhere( + (component) => component is Plunger, + ), + returnsNormally, ); - }); - }, - ); + }, + ); + }); }); }