feat: adds the spaceship component (#60)

* feat: implementing spaceship

* feat: spaceship working

* feat: adding dartdoc to spaceship

* feat: more tests and improving sizes

* fix: lint

* fix: lint

* feat: pre fetching spaceship assets

* fix: typo

* Apply suggestions from code review

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

* feat: pr suggestion

* fix: removing duplicated class

* feat: pr suggestions

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
pull/64/head
Erick 4 years ago committed by GitHub
parent 6867187659
commit 4c0774b792
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

2
.gitignore vendored

@ -128,4 +128,6 @@ app.*.map.json
# Firebase related
.firebase
test/.test_runner.dart
web/__/firebase/init.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -15,4 +15,5 @@ export 'ramp_opening.dart';
export 'round_bumper.dart';
export 'score_points.dart';
export 'sling_shot.dart';
export 'spaceship.dart';
export 'wall.dart';

@ -0,0 +1,331 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
// TODO(erickzanardo): change this to use the layer class
// that will be introduced on the path PR
const _spaceShipBits = 0x0002;
const _spaceShipSize = 20.0;
/// {@template spaceship_saucer}
/// A [BodyComponent] for the base, or the saucer of the spaceship
/// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition {
/// {@macro spaceship_saucer}
SpaceshipSaucer() : super(priority: 2);
/// Path for the base sprite
static const saucerSpritePath = 'components/spaceship/saucer.png';
/// Path for the upper wall sprite
static const upperWallPath = 'components/spaceship/upper.png';
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = await Future.wait([
gameRef.loadSprite(saucerSpritePath),
gameRef.loadSprite(upperWallPath),
]);
await add(
SpriteComponent(
sprite: sprites.first,
size: Vector2.all(_spaceShipSize),
anchor: Anchor.center,
),
);
await add(
SpriteComponent(
sprite: sprites.last,
size: Vector2(_spaceShipSize + 0.5, _spaceShipSize / 2),
anchor: Anchor.center,
position: Vector2(0, -(_spaceShipSize / 3.5)),
),
);
renderBody = false;
}
@override
Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 2;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef)
..createFixture(
FixtureDef(circleShape)
..isSensor = true
..filter.maskBits = _spaceShipBits
..filter.categoryBits = _spaceShipBits,
);
}
}
/// {@spaceship_bridge_top}
/// The bridge of the spaceship (the android head) is divided in two
// [BodyComponent]s, this is the top part of it which contains a single sprite
/// {@endtemplate}
class SpaceshipBridgeTop extends BodyComponent with InitialPosition {
/// {@macro spaceship_bridge_top}
SpaceshipBridgeTop() : super(priority: 6);
/// Path to the top of this sprite
static const spritePath = 'components/spaceship/android-top.png';
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath);
await add(
SpriteComponent(
sprite: sprite,
anchor: Anchor.center,
size: Vector2(_spaceShipSize / 2.5 - 1, _spaceShipSize / 5),
),
);
}
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef);
}
}
/// {@template spaceship_bridge}
/// The main part of the [SpaceshipBridge], this [BodyComponent]
/// provides both the collision and the rotation animation for the bridge.
/// {@endtemplate}
class SpaceshipBridge extends BodyComponent with InitialPosition {
/// {@macro spaceship_bridge}
SpaceshipBridge() : super(priority: 3);
/// Path to the spaceship bridge
static const spritePath = 'components/spaceship/android-bottom.png';
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
final sprite = await gameRef.images.load(spritePath);
await add(
SpriteAnimationComponent.fromFrameData(
sprite,
SpriteAnimationData.sequenced(
amount: 14,
stepTime: 0.2,
textureSize: Vector2(160, 114),
),
size: Vector2.all(_spaceShipSize / 2.5),
anchor: Anchor.center,
),
);
}
@override
Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 5;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef)
..createFixture(
FixtureDef(circleShape)
..restitution = 0.4
..filter.maskBits = _spaceShipBits
..filter.categoryBits = _spaceShipBits,
);
}
}
/// {@template spaceship_entrance}
/// A sensor [BodyComponent] used to detect when the ball enters the
/// the spaceship area in order to modify its filter data so the ball
/// can correctly collide only with the Spaceship
/// {@endtemplate}
// TODO(erickzanardo): Use RampOpening once provided.
class SpaceshipEntrance extends BodyComponent with InitialPosition {
/// {@macro spaceship_entrance}
SpaceshipEntrance();
@override
Body createBody() {
const radius = _spaceShipSize / 2;
final entranceShape = PolygonShape()
..setAsEdge(
Vector2(
radius * cos(20 * pi / 180),
radius * sin(20 * pi / 180),
),
Vector2(
radius * cos(340 * pi / 180),
radius * sin(340 * pi / 180),
),
);
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..angle = 90 * pi / 180
..type = BodyType.static;
return world.createBody(bodyDef)
..createFixture(
FixtureDef(entranceShape)..isSensor = true,
);
}
}
/// {@template spaceship_hole}
/// A sensor [BodyComponent] responsible for sending the [Ball]
/// back to the board.
/// {@endtemplate}
class SpaceshipHole extends BodyComponent with InitialPosition {
/// {@macro spaceship_hole}
SpaceshipHole();
@override
Body createBody() {
renderBody = false;
final circleShape = CircleShape()..radius = _spaceShipSize / 80;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef)
..createFixture(
FixtureDef(circleShape)
..isSensor = true
..filter.maskBits = _spaceShipBits
..filter.categoryBits = _spaceShipBits,
);
}
}
/// {@template spaceship_wall}
/// A [BodyComponent] that provides the collision for the wall
/// surrounding the spaceship, with a small opening to allow the
/// [Ball] to get inside the spaceship saucer.
/// It also contains the [SpriteComponent] for the lower wall
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition {
/// {@macro spaceship_wall}
SpaceshipWall() : super(priority: 4);
/// Sprite path for the lower wall
static const lowerWallPath = 'components/spaceship/lower.png';
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(lowerWallPath);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(_spaceShipSize, (_spaceShipSize / 2) + 1),
anchor: Anchor.center,
position: Vector2(-_spaceShipSize / 4, 0),
angle: 90 * pi / 180,
),
);
}
@override
Body createBody() {
renderBody = false;
const radius = _spaceShipSize / 2;
final wallShape = ChainShape()
..createChain(
[
for (var angle = 20; angle <= 340; angle++)
Vector2(
radius * cos(angle * pi / 180),
radius * sin(angle * pi / 180),
),
],
);
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..angle = 90 * pi / 180
..type = BodyType.static;
return world.createBody(bodyDef)
..createFixture(
FixtureDef(wallShape)
..restitution = 1
..filter.maskBits = _spaceShipBits
..filter.categoryBits = _spaceShipBits,
);
}
}
/// [ContactCallback] that handles the contact between the [Ball]
/// and the [SpaceshipEntrance].
///
/// It modifies the [Ball] priority and filter data so it can appear on top of
/// the spaceship and also only collide with the spaceship.
// TODO(alestiago): modify once Layer is implemented in Spaceship.
class SpaceshipEntranceBallContactCallback
extends ContactCallback<SpaceshipEntrance, Ball> {
@override
void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball
..priority = 3
..gameRef.reorderChildren();
for (final fixture in ball.body.fixtures) {
fixture.filterData.categoryBits = _spaceShipBits;
fixture.filterData.maskBits = _spaceShipBits;
}
}
}
/// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipHole].
///
/// It resets the [Ball] priority and filter data so it will "be back" on the
/// board.
// TODO(alestiago): modify once Layer is implemented in Spaceship.
class SpaceshipHoleBallContactCallback
extends ContactCallback<SpaceshipHole, Ball> {
@override
void begin(SpaceshipHole hole, Ball ball, _) {
ball
..priority = 1
..gameRef.reorderChildren();
for (final fixture in ball.body.fixtures) {
fixture.filterData.categoryBits = 0xFFFF;
fixture.filterData.maskBits = 0x0001;
}
}
}

@ -7,6 +7,10 @@ extension PinballGameAssetsX on PinballGame {
await Future.wait([
images.load(Ball.spritePath),
images.load(Flipper.spritePath),
images.load(SpaceshipBridge.spritePath),
images.load(SpaceshipBridgeTop.spritePath),
images.load(SpaceshipWall.lowerWallPath),
images.load(SpaceshipSaucer.upperWallPath),
]);
}
}

@ -29,6 +29,8 @@ class PinballGame extends Forge2DGame
unawaited(_addPlunger());
unawaited(_addPaths());
unawaited(_addSpaceship());
// 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.
@ -78,6 +80,21 @@ class PinballGame extends Forge2DGame
);
}
Future<void> _addSpaceship() async {
final position = Vector2(20, -24);
await addAll(
[
SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position,
SpaceshipBridge()..initialPosition = position,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5),
SpaceshipHole()..initialPosition = position - Vector2(5, 4),
SpaceshipHole()..initialPosition = position - Vector2(-5, 4),
SpaceshipWall()..initialPosition = position,
],
);
}
void spawnBall() {
final ball = Ball();
add(
@ -90,6 +107,8 @@ class PinballGame extends Forge2DGame
addContactCallback(BallScorePointsCallback());
addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback());
addContactCallback(SpaceshipHoleBallContactCallback());
addContactCallback(SpaceshipEntranceBallContactCallback());
}
Future<void> _addGameBoundaries() async {

@ -41,3 +41,4 @@ flutter:
assets:
- assets/images/components/
- assets/images/components/spaceship/

@ -0,0 +1,103 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('Spaceship', () {
late Filter filterData;
late Fixture fixture;
late Body body;
late PinballGame game;
late Ball ball;
late SpaceshipEntrance entrance;
late SpaceshipHole hole;
setUp(() {
filterData = MockFilter();
fixture = MockFixture();
when(() => fixture.filterData).thenReturn(filterData);
body = MockBody();
when(() => body.fixtures).thenReturn([fixture]);
game = MockPinballGame();
ball = MockBall();
when(() => ball.gameRef).thenReturn(game);
when(() => ball.body).thenReturn(body);
entrance = MockSpaceshipEntrance();
hole = MockSpaceshipHole();
});
group('SpaceshipEntranceBallContactCallback', () {
test('changes the ball priority on contact', () {
SpaceshipEntranceBallContactCallback().begin(
entrance,
ball,
MockContact(),
);
verify(() => ball.priority = 3).called(1);
});
test('re order the game children', () {
SpaceshipEntranceBallContactCallback().begin(
entrance,
ball,
MockContact(),
);
verify(game.reorderChildren).called(1);
});
test('changes the filter data from the ball fixtures', () {
SpaceshipEntranceBallContactCallback().begin(
entrance,
ball,
MockContact(),
);
verify(() => filterData.maskBits = 0x0002).called(1);
verify(() => filterData.categoryBits = 0x0002).called(1);
});
});
group('SpaceshipHoleBallContactCallback', () {
test('changes the ball priority on contact', () {
SpaceshipHoleBallContactCallback().begin(
hole,
ball,
MockContact(),
);
verify(() => ball.priority = 1).called(1);
});
test('re order the game children', () {
SpaceshipHoleBallContactCallback().begin(
hole,
ball,
MockContact(),
);
verify(game.reorderChildren).called(1);
});
test('changes the filter data from the ball fixtures', () {
SpaceshipHoleBallContactCallback().begin(
hole,
ball,
MockContact(),
);
verify(() => filterData.categoryBits = 0xFFFF).called(1);
verify(() => filterData.maskBits = 0x0001).called(1);
});
});
});
}

@ -48,3 +48,11 @@ class MockTapUpInfo extends Mock implements TapUpInfo {}
class MockEventPosition extends Mock implements EventPosition {}
class MockBonusLetter extends Mock implements BonusLetter {}
class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {}
class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {}

Loading…
Cancel
Save