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