mirror of https://github.com/flutter/pinball.git
feat: implemented `Flapper` (#312)
* feat: add flapper * chore: add assets to pinball game test * fix: add mocks in file * test: check animation onComplete * fix: image cache in test * Update packages/pinball_components/lib/src/components/flapper/flapper.dart * style: commas and userData removal * refactor: make launcher tests more robust * refactor: removed children parameter Co-authored-by: alestiago <dev@alestiago.com>pull/328/head
parent
82588602eb
commit
182e8f56cb
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1 @@
|
|||||||
|
export 'flapper_spinning_behavior.dart';
|
@ -0,0 +1,15 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
class FlapperSpinningBehavior extends ContactBehavior<FlapperEntrance> {
|
||||||
|
@override
|
||||||
|
void beginContact(Object other, Contact contact) {
|
||||||
|
super.beginContact(other, contact);
|
||||||
|
if (other is! Ball) return;
|
||||||
|
parent.parent?.firstChild<SpriteAnimationComponent>()?.playing = true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,215 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template flapper}
|
||||||
|
/// Flap to let a [Ball] out of the [LaunchRamp] and to prevent [Ball]s from
|
||||||
|
/// going back in.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Flapper extends Component {
|
||||||
|
/// {@macro flapper}
|
||||||
|
Flapper()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
FlapperEntrance(
|
||||||
|
children: [
|
||||||
|
FlapperSpinningBehavior(),
|
||||||
|
],
|
||||||
|
)..initialPosition = Vector2(4, -69.3),
|
||||||
|
_FlapperStructure(),
|
||||||
|
_FlapperExit()..initialPosition = Vector2(-0.6, -33.8),
|
||||||
|
_BackSupportSpriteComponent(),
|
||||||
|
_FrontSupportSpriteComponent(),
|
||||||
|
FlapSpriteAnimationComponent(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Creates a [Flapper] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [Flapper]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
Flapper.test();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template flapper_entrance}
|
||||||
|
/// Sensor used in [FlapperSpinningBehavior] to animate
|
||||||
|
/// [FlapSpriteAnimationComponent].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FlapperEntrance extends BodyComponent with InitialPosition, Layered {
|
||||||
|
/// {@macro flapper_entrance}
|
||||||
|
FlapperEntrance({
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : super(
|
||||||
|
children: children,
|
||||||
|
renderBody: false,
|
||||||
|
) {
|
||||||
|
layer = Layer.launcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EdgeShape()
|
||||||
|
..set(
|
||||||
|
Vector2.zero(),
|
||||||
|
Vector2(0, 3.2),
|
||||||
|
);
|
||||||
|
final fixtureDef = FixtureDef(
|
||||||
|
shape,
|
||||||
|
isSensor: true,
|
||||||
|
);
|
||||||
|
final bodyDef = BodyDef(position: initialPosition);
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlapperStructure extends BodyComponent with Layered {
|
||||||
|
_FlapperStructure() : super(renderBody: false) {
|
||||||
|
layer = Layer.board;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FixtureDef> _createFixtureDefs() {
|
||||||
|
final leftEdgeShape = EdgeShape()
|
||||||
|
..set(
|
||||||
|
Vector2(1.9, -69.3),
|
||||||
|
Vector2(1.9, -66),
|
||||||
|
);
|
||||||
|
|
||||||
|
final bottomEdgeShape = EdgeShape()
|
||||||
|
..set(
|
||||||
|
leftEdgeShape.vertex2,
|
||||||
|
Vector2(3.9, -66),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
FixtureDef(leftEdgeShape),
|
||||||
|
FixtureDef(bottomEdgeShape),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final body = world.createBody(BodyDef());
|
||||||
|
_createFixtureDefs().forEach(body.createFixture);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlapperExit extends LayerSensor {
|
||||||
|
_FlapperExit()
|
||||||
|
: super(
|
||||||
|
insideLayer: Layer.launcher,
|
||||||
|
outsideLayer: Layer.board,
|
||||||
|
orientation: LayerEntranceOrientation.down,
|
||||||
|
insideZIndex: ZIndexes.ballOnLaunchRamp,
|
||||||
|
outsideZIndex: ZIndexes.ballOnBoard,
|
||||||
|
) {
|
||||||
|
layer = Layer.launcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Shape get shape => PolygonShape()
|
||||||
|
..setAsBox(
|
||||||
|
1.7,
|
||||||
|
0.1,
|
||||||
|
initialPosition,
|
||||||
|
1.5708,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template flap_sprite_animation_component}
|
||||||
|
/// Flap suspended between supports that animates to let the [Ball] exit the
|
||||||
|
/// [LaunchRamp].
|
||||||
|
/// {@endtemplate}
|
||||||
|
@visibleForTesting
|
||||||
|
class FlapSpriteAnimationComponent extends SpriteAnimationComponent
|
||||||
|
with HasGameRef, ZIndex {
|
||||||
|
/// {@macro flap_sprite_animation_component}
|
||||||
|
FlapSpriteAnimationComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(2.8, -70.7),
|
||||||
|
playing: false,
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.flapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final spriteSheet = gameRef.images.fromCache(
|
||||||
|
Assets.images.flapper.flap.keyName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const amountPerRow = 14;
|
||||||
|
const amountPerColumn = 1;
|
||||||
|
final textureSize = Vector2(
|
||||||
|
spriteSheet.width / amountPerRow,
|
||||||
|
spriteSheet.height / amountPerColumn,
|
||||||
|
);
|
||||||
|
size = textureSize / 10;
|
||||||
|
|
||||||
|
animation = SpriteAnimation.fromFrameData(
|
||||||
|
spriteSheet,
|
||||||
|
SpriteAnimationData.sequenced(
|
||||||
|
amount: amountPerRow * amountPerColumn,
|
||||||
|
amountPerRow: amountPerRow,
|
||||||
|
stepTime: 1 / 24,
|
||||||
|
textureSize: textureSize,
|
||||||
|
loop: false,
|
||||||
|
),
|
||||||
|
)..onComplete = () {
|
||||||
|
animation?.reset();
|
||||||
|
playing = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackSupportSpriteComponent extends SpriteComponent
|
||||||
|
with HasGameRef, ZIndex {
|
||||||
|
_BackSupportSpriteComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(2.95, -70.6),
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.flapperBack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final sprite = Sprite(
|
||||||
|
gameRef.images.fromCache(
|
||||||
|
Assets.images.flapper.backSupport.keyName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.sprite = sprite;
|
||||||
|
size = sprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FrontSupportSpriteComponent extends SpriteComponent
|
||||||
|
with HasGameRef, ZIndex {
|
||||||
|
_FrontSupportSpriteComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(2.9, -67.6),
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.flapperFront;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final sprite = Sprite(
|
||||||
|
gameRef.images.fromCache(
|
||||||
|
Assets.images.flapper.frontSupport.keyName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.sprite = sprite;
|
||||||
|
size = sprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart';
|
||||||
|
|
||||||
|
import '../../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
class _MockBall extends Mock implements Ball {}
|
||||||
|
|
||||||
|
class _MockContact extends Mock implements Contact {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.flapper.flap.keyName,
|
||||||
|
Assets.images.flapper.backSupport.keyName,
|
||||||
|
Assets.images.flapper.frontSupport.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(() => TestGame(assets));
|
||||||
|
|
||||||
|
group(
|
||||||
|
'FlapperSpinningBehavior',
|
||||||
|
() {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
FlapperSpinningBehavior(),
|
||||||
|
isA<FlapperSpinningBehavior>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'beginContact plays the flapper animation',
|
||||||
|
(game) async {
|
||||||
|
final behavior = FlapperSpinningBehavior();
|
||||||
|
final entrance = FlapperEntrance();
|
||||||
|
final flap = FlapSpriteAnimationComponent();
|
||||||
|
final flapper = Flapper.test();
|
||||||
|
await flapper.addAll([entrance, flap]);
|
||||||
|
await entrance.add(behavior);
|
||||||
|
await game.ensureAdd(flapper);
|
||||||
|
|
||||||
|
behavior.beginContact(_MockBall(), _MockContact());
|
||||||
|
|
||||||
|
expect(flap.playing, isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
import '../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Flapper', () {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.flapper.flap.keyName,
|
||||||
|
Assets.images.flapper.backSupport.keyName,
|
||||||
|
Assets.images.flapper.frontSupport.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(() => TestGame(assets));
|
||||||
|
|
||||||
|
flameTester.test('loads correctly', (game) async {
|
||||||
|
final component = Flapper();
|
||||||
|
await game.ensureAdd(component);
|
||||||
|
expect(game.contains(component), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'renders correctly',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
final canvas = ZCanvasComponent(children: [Flapper()]);
|
||||||
|
await game.ensureAdd(canvas);
|
||||||
|
game.camera
|
||||||
|
..followVector2(Vector2(3, -70))
|
||||||
|
..zoom = 25;
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
const goldenFilePath = '../golden/flapper/';
|
||||||
|
final flapSpriteAnimationComponent = game
|
||||||
|
.descendants()
|
||||||
|
.whereType<FlapSpriteAnimationComponent>()
|
||||||
|
.first
|
||||||
|
..playing = true;
|
||||||
|
final animationDuration =
|
||||||
|
flapSpriteAnimationComponent.animation!.totalDuration();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('${goldenFilePath}start.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.update(animationDuration * 0.25);
|
||||||
|
await tester.pump();
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('${goldenFilePath}middle.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.update(animationDuration * 0.75);
|
||||||
|
await tester.pump();
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('${goldenFilePath}end.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test('adds a FlapperSpiningBehavior to FlapperEntrance',
|
||||||
|
(game) async {
|
||||||
|
final flapper = Flapper();
|
||||||
|
await game.ensureAdd(flapper);
|
||||||
|
|
||||||
|
final flapperEntrance = flapper.firstChild<FlapperEntrance>()!;
|
||||||
|
expect(
|
||||||
|
flapperEntrance.firstChild<FlapperSpinningBehavior>(),
|
||||||
|
isNotNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'flap stops animating after animation completes',
|
||||||
|
(game) async {
|
||||||
|
final flapper = Flapper();
|
||||||
|
await game.ensureAdd(flapper);
|
||||||
|
|
||||||
|
final flapSpriteAnimationComponent =
|
||||||
|
flapper.firstChild<FlapSpriteAnimationComponent>()!;
|
||||||
|
|
||||||
|
flapSpriteAnimationComponent.playing = true;
|
||||||
|
game.update(
|
||||||
|
flapSpriteAnimationComponent.animation!.totalDuration() + 0.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(flapSpriteAnimationComponent.playing, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,85 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.launchRamp.ramp.keyName,
|
||||||
|
Assets.images.launchRamp.backgroundRailing.keyName,
|
||||||
|
Assets.images.launchRamp.foregroundRailing.keyName,
|
||||||
|
Assets.images.flapper.backSupport.keyName,
|
||||||
|
Assets.images.flapper.frontSupport.keyName,
|
||||||
|
Assets.images.flapper.flap.keyName,
|
||||||
|
Assets.images.plunger.plunger.keyName,
|
||||||
|
Assets.images.plunger.rocket.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(
|
||||||
|
() => EmptyPinballTestGame(assets: assets),
|
||||||
|
);
|
||||||
|
|
||||||
|
group('Launcher', () {
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
final launcher = Launcher();
|
||||||
|
await game.ensureAdd(launcher);
|
||||||
|
|
||||||
|
expect(game.contains(launcher), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('loads', () {
|
||||||
|
flameTester.test(
|
||||||
|
'a LaunchRamp',
|
||||||
|
(game) async {
|
||||||
|
final launcher = Launcher();
|
||||||
|
await game.ensureAdd(launcher);
|
||||||
|
|
||||||
|
final descendantsQuery =
|
||||||
|
launcher.descendants().whereType<LaunchRamp>();
|
||||||
|
expect(descendantsQuery.length, equals(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'a Flapper',
|
||||||
|
(game) async {
|
||||||
|
final launcher = Launcher();
|
||||||
|
await game.ensureAdd(launcher);
|
||||||
|
|
||||||
|
final descendantsQuery = launcher.descendants().whereType<Flapper>();
|
||||||
|
expect(descendantsQuery.length, equals(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'a Plunger',
|
||||||
|
(game) async {
|
||||||
|
final launcher = Launcher();
|
||||||
|
await game.ensureAdd(launcher);
|
||||||
|
|
||||||
|
final descendantsQuery = launcher.descendants().whereType<Plunger>();
|
||||||
|
expect(descendantsQuery.length, equals(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'a RocketSpriteComponent',
|
||||||
|
(game) async {
|
||||||
|
final launcher = Launcher();
|
||||||
|
await game.ensureAdd(launcher);
|
||||||
|
|
||||||
|
final descendantsQuery =
|
||||||
|
launcher.descendants().whereType<RocketSpriteComponent>();
|
||||||
|
expect(descendantsQuery.length, equals(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue