From 4c0774b7923955bd912f2b4eb2b2e96106ab57ce Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 18 Mar 2022 15:05:50 -0300 Subject: [PATCH] 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 * feat: pr suggestion * fix: removing duplicated class * feat: pr suggestions Co-authored-by: Alejandro Santiago --- .gitignore | 4 +- assets/images/components/sauce.png | Bin 0 -> 7862 bytes .../components/spaceship/android-bottom.png | Bin 0 -> 9692 bytes .../components/spaceship/android-top.png | Bin 0 -> 3442 bytes assets/images/components/spaceship/lower.png | Bin 0 -> 10001 bytes assets/images/components/spaceship/saucer.png | Bin 0 -> 6451 bytes assets/images/components/spaceship/upper.png | Bin 0 -> 7875 bytes lib/game/components/components.dart | 1 + lib/game/components/spaceship.dart | 331 ++++++++++++++++++ lib/game/game_assets.dart | 4 + lib/game/pinball_game.dart | 19 + pubspec.yaml | 1 + test/game/components/spaceship_test.dart | 103 ++++++ test/helpers/mocks.dart | 8 + 14 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 assets/images/components/sauce.png create mode 100644 assets/images/components/spaceship/android-bottom.png create mode 100644 assets/images/components/spaceship/android-top.png create mode 100644 assets/images/components/spaceship/lower.png create mode 100644 assets/images/components/spaceship/saucer.png create mode 100644 assets/images/components/spaceship/upper.png create mode 100644 lib/game/components/spaceship.dart create mode 100644 test/game/components/spaceship_test.dart diff --git a/.gitignore b/.gitignore index 9bf37325..e47b373d 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,6 @@ app.*.map.json # Firebase related .firebase -web/__/firebase/init.js + +test/.test_runner.dart +web/__/firebase/init.js \ No newline at end of file diff --git a/assets/images/components/sauce.png b/assets/images/components/sauce.png new file mode 100644 index 0000000000000000000000000000000000000000..743a920a29d76879989b94e50cabf907247a9435 GIT binary patch literal 7862 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 S;db6{6%39U>*XD_yZL|a0|#jU literal 0 HcmV?d00001 diff --git a/assets/images/components/spaceship/android-top.png b/assets/images/components/spaceship/android-top.png new file mode 100644 index 0000000000000000000000000000000000000000..92c99db77286eba49b4dd5316fd0779757ac0037 GIT binary patch literal 3442 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 UyEW@elK=n!07*qoM6N<$f*dqx-2eap literal 0 HcmV?d00001 diff --git a/assets/images/components/spaceship/lower.png b/assets/images/components/spaceship/lower.png new file mode 100644 index 0000000000000000000000000000000000000000..1f0d9b10ed056b05d9e6100e7a12f52ab2245726 GIT binary patch literal 10001 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 N$jd4tN~KLh{|9n-*a-jt literal 0 HcmV?d00001 diff --git a/assets/images/components/spaceship/saucer.png b/assets/images/components/spaceship/saucer.png new file mode 100644 index 0000000000000000000000000000000000000000..93af98b5144abb1eb99e3e785230dbb348159d59 GIT binary patch literal 6451 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 N002ovPDHLkV1ijm7FyAb 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 g 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 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 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 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 { + @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 { + @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; + } + } +} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 778e2bc2..00e9d09c 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -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), ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 18835c98..c02b74cd 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -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 _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 _addGameBoundaries() async { diff --git a/pubspec.yaml b/pubspec.yaml index 81b056b3..35d8190f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,3 +41,4 @@ flutter: assets: - assets/images/components/ + - assets/images/components/spaceship/ diff --git a/test/game/components/spaceship_test.dart b/test/game/components/spaceship_test.dart new file mode 100644 index 00000000..7e16edd8 --- /dev/null +++ b/test/game/components/spaceship_test.dart @@ -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); + }); + }); + }); +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index cace878a..4ce05663 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -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 {}