feat: implemented Flipper grouping (#32)

* feat: started implementing FlipperGroup

* refactor: simplified Flipper logic

* refactor: used extension instead of condition

* docs: used "loads" over "adds"

* feat: used size rather than width and height

* refactor: removed unecessary mixin

* feat: reorder methods

* refactor: removed _joint over joint

* docs: fixed macro typo

* feat: unawait add operation

* feat: included tests

* refactor: remove Flutter dep from geometry (#27)

* fix: removed flutter dependency

* test: fixed tests for assertions

* test: check assertion with isA

* ci: added geometry workflow file

* refactor: changed flame dep to vector_math for vector2

* fix: changed import for vector to vector_math_64

* Update .github/workflows/geometry.yaml

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* chore: rename pinball game test extension (#33)

* chore: rename pinball game test extension

* refactor: initial to create

* docs: small change

* chore: removed unecessary end callback (#30)

* feat: adding ball spawning upon click on debug mode (#28)

* feat: adding ball spawming upon click on debug mode

* PR suggestions

* fix: coverage

* fix: rebase

* feat: rebase fixes

* feat: adding bonus logic to the game bloc (#24)

* feat: adding bonus logic to the game bloc

* feat: PR suggestions

* Apply suggestions from code review

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* feat: pr suggestions

* chore: main rebase

* feat: pr suggestions

* Apply suggestions from code review

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* feat: pr suggestion

* feat: pr suggestions

* feat: pr suggestions

* Apply suggestions from code review

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* feat: pr suggestions

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* feat: add plunger to board (#25)

* feat: add plunger to board

* refactor: leave spawn ball synchronous

* fix: ball test

* refactor: position ball internally

* fix: ball position test

* refactor: use joint specific anchor

* refactor: remove ballSize

* fix: plunger position

* refactor: use relative positioning

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* refactor: added missing white space

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: adding missing white space

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: renamed test

* refactor: used ! instead of ?

* chore: rebasing

* refactor: simplified Flipper logic

* refactor: used extension instead of condition

* docs: used "loads" over "adds"

* feat: used size rather than width and height

* refactor: removed unecessary mixin

* feat: reorder methods

* refactor: removed _joint over joint

* docs: fixed macro typo

* chore: rebasing

* refactor: added missing white space

* refactor: used ! instead of ?

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
Co-authored-by: Erick <erickzanardoo@gmail.com>
pull/41/head
Alejandro Santiago 4 years ago committed by GitHub
parent 19c0172cac
commit ddec3c0f44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +1,45 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart' show SpriteComponent;
import 'package:flame/components.dart' show PositionComponent, SpriteComponent;
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.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 PositionComponent {
/// {@macro flipper_group}
FlipperGroup({
required Vector2 position,
required this.spacing,
}) : super(position: position);
/// The amount of space between the [Flipper.right] and [Flipper.left].
final double spacing;
@override
Future<void> 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.
///
@ -76,20 +109,6 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
/// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(
sprite: sprite,
size: size,
);
if (side == BoardSide.right) {
positionComponent?.flipHorizontally();
}
}
/// Applies downward linear velocity to the [Flipper], moving it to its
/// resting position.
void _moveDown() {
@ -102,15 +121,50 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
body.linearVelocity = Vector2(0, _speed);
}
/// Loads the sprite that renders with the [Flipper].
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(
sprite: sprite,
size: size,
);
if (side.isRight) {
positionComponent!.flipHorizontally();
}
}
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async {
final anchor = FlipperAnchor(flipper: this);
await add(anchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: this,
anchor: anchor,
);
// TODO(alestiago): Remove casting once the following is closed:
// https://github.com/flame-engine/forge2d/issues/36
final joint = world.createJoint(jointDef) as RevoluteJoint;
// FIXME(erickzanardo): when mounted the initial position is not fully
// reached.
unawaited(
mounted.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock(joint, side),
),
);
}
List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[];
final isLeft = side.isLeft;
final bigCircleShape = CircleShape()..radius = height / 2;
final bigCircleShape = CircleShape()..radius = size.y / 2;
bigCircleShape.position.setValues(
isLeft
? -(width / 2) + bigCircleShape.radius
: (width / 2) - bigCircleShape.radius,
? -(size.x / 2) + bigCircleShape.radius
: (size.x / 2) - bigCircleShape.radius,
0,
);
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
@ -119,8 +173,8 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2;
smallCircleShape.position.setValues(
isLeft
? (width / 2) - smallCircleShape.radius
: -(width / 2) + smallCircleShape.radius,
? (size.x / 2) - smallCircleShape.radius
: -(size.x / 2) + smallCircleShape.radius,
0,
);
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
@ -148,6 +202,15 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
return fixtures;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await Future.wait([
_loadSprite(),
_anchorToJoint(),
]);
}
@override
Body createBody() {
final bodyDef = BodyDef()
@ -161,17 +224,6 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
return body;
}
// TODO(erickzanardo): Remove this once the issue is solved:
// https://github.com/flame-engine/flame/issues/1417
// ignore: public_member_api_docs
final Completer hasMounted = Completer<void>();
@override
void onMount() {
super.onMount();
hasMounted.complete();
}
@override
bool onKeyEvent(
RawKeyEvent event,
@ -204,8 +256,8 @@ class FlipperAnchor extends Anchor {
}) : super(
position: Vector2(
flipper.side.isLeft
? flipper.body.position.x - Flipper.width / 2
: flipper.body.position.x + Flipper.width / 2,
? flipper.body.position.x - flipper.size.x / 2
: flipper.body.position.x + flipper.size.x / 2,
flipper.body.position.y,
),
);

@ -26,7 +26,6 @@ class PinballGame extends Forge2DGame
_addContactCallbacks();
await _addGameBoundaries();
unawaited(_addFlippers());
unawaited(_addPlunger());
// Corner wall above plunger so the ball deflects into the rest of the
@ -48,6 +47,22 @@ class PinballGame extends Forge2DGame
),
),
);
final flippersPosition = screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y / 1.1,
),
);
unawaited(
add(
FlipperGroup(
position: flippersPosition,
spacing: 2,
),
),
);
}
void spawnBall() {
@ -64,72 +79,6 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add);
}
Future<void> _addFlippers() async {
final flippersPosition = screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y / 1.1,
),
);
const spaceBetweenFlippers = 2;
final leftFlipper = Flipper.left(
position: Vector2(
flippersPosition.x - (Flipper.width / 2) - (spaceBetweenFlippers / 2),
flippersPosition.y,
),
);
await add(leftFlipper);
final leftFlipperAnchor = FlipperAnchor(flipper: leftFlipper);
await add(leftFlipperAnchor);
final leftFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef(
flipper: leftFlipper,
anchor: leftFlipperAnchor,
);
// TODO(alestiago): Remove casting once the following is closed:
// https://github.com/flame-engine/forge2d/issues/36
final leftFlipperRevoluteJoint =
world.createJoint(leftFlipperRevoluteJointDef) as RevoluteJoint;
final rightFlipper = Flipper.right(
position: Vector2(
flippersPosition.x + (Flipper.width / 2) + (spaceBetweenFlippers / 2),
flippersPosition.y,
),
);
await add(rightFlipper);
final rightFlipperAnchor = FlipperAnchor(flipper: rightFlipper);
await add(rightFlipperAnchor);
final rightFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef(
flipper: rightFlipper,
anchor: rightFlipperAnchor,
);
// TODO(alestiago): Remove casting once the following is closed:
// https://github.com/flame-engine/forge2d/issues/36
final rightFlipperRevoluteJoint =
world.createJoint(rightFlipperRevoluteJointDef) as RevoluteJoint;
// TODO(erickzanardo): Clean this once the issue is solved:
// https://github.com/flame-engine/flame/issues/1417
// FIXME(erickzanardo): when mounted the initial position is not fully
// reached.
unawaited(
leftFlipper.hasMounted.future.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock(
leftFlipperRevoluteJoint,
leftFlipper.side,
),
),
);
unawaited(
rightFlipper.hasMounted.future.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock(
rightFlipperRevoluteJoint,
rightFlipper.side,
),
),
);
}
Future<void> _addPlunger() async {
late PlungerAnchor plungerAnchor;
final compressionDistance = camera.viewport.effectiveSize.y / 12;

@ -2,6 +2,7 @@
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';
@ -13,6 +14,105 @@ import '../../helpers/helpers.dart';
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 +
leftFlipper.size.x +
flipperGroup.spacing,
equals(rightFlipper.body.position.x),
);
},
);
});
});
group(
'Flipper',
() {
@ -21,9 +121,11 @@ void main() {
(game) async {
final leftFlipper = Flipper.left(position: Vector2.zero());
final rightFlipper = Flipper.right(position: Vector2.zero());
await game.ready();
await game.ensureAddAll([leftFlipper, rightFlipper]);
expect(game.contains(leftFlipper), isTrue);
expect(game.contains(rightFlipper), isTrue);
},
);

@ -18,7 +18,9 @@ void main() {
// [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416
group('components', () {
group('Walls', () {
bool Function(Component) componentSelector<T>() =>
(component) => component is T;
flameTester.test(
'has three Walls',
(game) async {
@ -41,60 +43,37 @@ void main() {
expect(
() => game.children.singleWhere(
(component) => component is BottomWall,
componentSelector<BottomWall>(),
),
returnsNormally,
);
},
);
});
group('Flippers', () {
bool Function(Component) flipperSelector(BoardSide side) =>
(component) => component is Flipper && component.side == side;
flameTester.test(
'has only one left Flipper',
'has only one Plunger',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
flipperSelector(BoardSide.left),
(component) => component is Plunger,
),
returnsNormally,
);
},
);
flameTester.test(
'has only one right Flipper',
(game) async {
flameTester.test('has only one FlipperGroup', (game) async {
await game.ready();
expect(
() => game.children.singleWhere(
flipperSelector(BoardSide.right),
(component) => component is FlipperGroup,
),
returnsNormally,
);
},
);
});
flameTester.test(
'Plunger has only one Plunger',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is Plunger,
),
returnsNormally,
);
},
);
});
debugModeFlameTester.test('adds a ball on tap up', (game) async {

Loading…
Cancel
Save