feat: add plunger to board

pull/25/head
Allison Ryan 4 years ago
parent 360b5876cf
commit bf48a81ef9

@ -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<LogicalKeyboardKey> 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;
}
}

@ -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<Wall> 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),

@ -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<void> 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<void> 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<void> _addGameBoundaries() async {
await add(BottomWall(this));
createBoundaries(this).forEach(add);
}
Future<void> _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<void> _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,
),
);
}
}

@ -49,7 +49,7 @@ void main() {
);
});
group('first fixture', () {
group('fixture', () {
flameTester.test(
'exists',
(game) async {

@ -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', () {

@ -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);
},
);
});
});
}

@ -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));
},
);

@ -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,
);
});
},
);
},
);
});
});
}

Loading…
Cancel
Save