refactor: removed `ControlledFlipper` (#343)

* feat: implemented Flippers behaviors

* test: tested Flipper behaviors

* refactor: fixed typo

* refactor: removed unecessary import

* feat: removed and added behavior depending on game status

* test: renamed test

* test: changed golden path

* refactor: removed TODOs
pull/388/head
Alejandro Santiago 2 years ago committed by GitHub
parent eb8cc4bb6f
commit 0b22b712b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,5 @@
import 'package:flame/components.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -40,7 +39,7 @@ class _BottomGroupSide extends Component {
final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper(
final flipper = Flipper(
side: _side,
)..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side)

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
export 'drain/drain.dart';

@ -3,6 +3,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Listens to the [GameBloc] and updates the game accordingly.
@ -20,6 +21,11 @@ class GameBlocStatusListener extends Component
break;
case GameStatus.playing:
readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_addFlipperKeyControls);
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
@ -30,7 +36,20 @@ class GameBlocStatusListener extends Component
.state
.characterTheme,
);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_removeFlipperKeyControls);
break;
}
}
void _addFlipperKeyControls(Flipper flipper) =>
flipper.add(FlipperKeyControllingBehavior());
void _removeFlipperKeyControls(Flipper flipper) => flipper
.descendants()
.whereType<FlipperKeyControllingBehavior>()
.forEach(flipper.remove);
}

@ -15,7 +15,7 @@ export 'dino_walls.dart';
export 'error_component.dart';
export 'fire_effect.dart';
export 'flapper/flapper.dart';
export 'flipper.dart';
export 'flipper/flipper.dart';
export 'google_letter/google_letter.dart';
export 'initial_position.dart';
export 'joint_anchor.dart';

@ -0,0 +1,2 @@
export 'flipper_jointing_behavior.dart';
export 'flipper_key_controlling_behavior.dart';

@ -0,0 +1,103 @@
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';
/// Joints the [Flipper] to allow pivoting around one end.
class FlipperJointingBehavior extends Component
with ParentIsA<Flipper>, HasGameRef {
late final RevoluteJoint _joint;
@override
Future<void> onLoad() async {
await super.onLoad();
final anchor = _FlipperAnchor(flipper: parent);
await add(anchor);
final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: parent,
anchor: anchor,
);
_joint = _FlipperJoint(jointDef);
parent.world.createJoint(_joint);
}
@override
void onMount() {
gameRef.ready().whenComplete(
() => parent.body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_FlipperAnchor({
required Flipper flipper,
}) {
initialPosition = Vector2(
(Flipper.size.x * flipper.side.direction) / 2 -
(1.65 * flipper.side.direction),
-0.15,
);
}
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a potivoting
/// motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) : side = flipper.side {
enableLimit = true;
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
}
final BoardSide side;
}
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the pivoting motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def) {
lock();
}
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.611;
final BoardSide side;
/// Locks the [Flipper] to its resting position.
///
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.
void unlock() {
const angle = _halfSweepingAngle;
setLimits(-angle, angle);
}
}

@ -1,49 +1,33 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_flipper}
/// A [Flipper] with a [FlipperController] attached.
/// {@endtemplate}
class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// {@macro controlled_flipper}
ControlledFlipper({
required BoardSide side,
}) : super(side: side) {
controller = FlipperController(this);
}
}
/// {@template flipper_controller}
/// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate}
class FlipperController extends ComponentController<Flipper>
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro flipper_controller}
FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys,
super(flipper);
/// Allows controlling the [Flipper]'s movement with keyboard input.
class FlipperKeyControllingBehavior extends Component
with KeyboardHandler, ParentIsA<Flipper> {
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys;
late final List<LogicalKeyboardKey> _keys;
@override
Future<void> onLoad() async {
await super.onLoad();
_keys = parent.side.flipperKeys;
}
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!bloc.state.status.isPlaying) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.moveUp();
parent.moveUp();
} else if (event is RawKeyUpEvent) {
component.moveDown();
parent.moveDown();
}
return false;

@ -2,8 +2,11 @@ import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart';
/// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board.
///
@ -15,9 +18,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
required this.side,
}) : super(
renderBody: false,
children: [_FlipperSpriteComponent(side: side)],
children: [
_FlipperSpriteComponent(side: side),
FlipperJointingBehavior(),
],
);
/// Creates a [Flipper] without any children.
///
/// This can be used for testing [Flipper]'s behaviors in isolation.
@visibleForTesting
Flipper.test({required this.side});
/// The size of the [Flipper].
static final size = Vector2(13.5, 4.3);
@ -44,19 +56,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
body.linearVelocity = Vector2(0, -_speed);
}
/// 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,
);
final joint = _FlipperJoint(jointDef);
world.createJoint(joint);
}
List<FixtureDef> _createFixtureDefs() {
final direction = side.direction;
@ -73,7 +72,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow,
0,
);
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
final smallCircleShape = CircleShape()..radius = size.y * 0.23;
smallCircleShape.position.setValues(
@ -82,7 +80,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow,
0,
);
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
final trapeziumVertices = side.isLeft
? [
@ -98,26 +95,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
];
final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(
trapezium,
density: 50,
friction: .1,
);
return [
bigCircleFixtureDef,
smallCircleFixtureDef,
trapeziumFixtureDef,
FixtureDef(bigCircleShape),
FixtureDef(smallCircleShape),
FixtureDef(
trapezium,
density: 50,
friction: .1,
),
];
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
@override
Body createBody() {
final bodyDef = BodyDef(
@ -131,15 +120,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body;
}
@override
void onMount() {
super.onMount();
gameRef.ready().whenComplete(
() => body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
}
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
@ -163,73 +143,3 @@ class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
size = sprite.originalSize / 10;
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_FlipperAnchor({
required Flipper flipper,
}) {
initialPosition = Vector2(
(Flipper.size.x * flipper.side.direction) / 2 -
(1.65 * flipper.side.direction),
-0.15,
);
}
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) : side = flipper.side {
enableLimit = true;
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
}
final BoardSide side;
}
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the arc motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def) {
lock();
}
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.611;
final BoardSide side;
/// Locks the [Flipper] to its resting position.
///
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.
void unlock() {
const angle = _halfSweepingAngle;
setLimits(-angle, angle);
}
}

@ -1,6 +1,4 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
@ -23,16 +21,6 @@ class FlipperGame extends BallGame with KeyboardEvents {
- Press right arrow key or "D" to move the right flipper.
''';
static const _leftFlipperKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
static const _rightFlipperKeys = [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
late Flipper leftFlipper;
late Flipper rightFlipper;
@ -50,32 +38,4 @@ class FlipperGame extends BallGame with KeyboardEvents {
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final movedLeftFlipper = _leftFlipperKeys.contains(event.logicalKey);
if (movedLeftFlipper) {
if (event is RawKeyDownEvent) {
leftFlipper.moveUp();
} else if (event is RawKeyUpEvent) {
leftFlipper.moveDown();
}
}
final movedRightFlipper = _rightFlipperKeys.contains(event.logicalKey);
if (movedRightFlipper) {
if (event is RawKeyDownEvent) {
rightFlipper.moveUp();
} else if (event is RawKeyUpEvent) {
rightFlipper.moveDown();
}
}
return movedLeftFlipper || movedRightFlipper
? KeyEventResult.handled
: KeyEventResult.ignored;
}
}

@ -0,0 +1,38 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('FlipperJointingBehavior', () {
final flameTester = FlameTester(TestGame.new);
test('can be instantiated', () {
expect(
FlipperJointingBehavior(),
isA<FlipperJointingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final behavior = FlipperJointingBehavior();
final parent = Flipper.test(side: BoardSide.left);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.contains(behavior), isTrue);
});
flameTester.test('creates a joint', (game) async {
final behavior = FlipperJointingBehavior();
final parent = Flipper.test(side: BoardSide.left);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.body.joints, isNotEmpty);
});
});
}

@ -0,0 +1,357 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('FlipperKeyControllingBehavior', () {
final flameTester = FlameTester(TestGame.new);
group(
'onKeyEvent',
() {
late Flipper rightFlipper;
late Flipper leftFlipper;
setUp(() {
rightFlipper = Flipper.test(side: BoardSide.right);
leftFlipper = Flipper.test(side: BoardSide.left);
});
group('on right Flipper', () {
flameTester.test(
'moves upwards when right arrow is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isNegative);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when right arrow is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isPositive);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves upwards when D is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isNegative);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when D is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isPositive);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
group("doesn't move when", () {
flameTester.test(
'left arrow is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'left arrow is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'A is pressed',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'A is released',
(game) async {
await game.ensureAdd(rightFlipper);
final behavior = FlipperKeyControllingBehavior();
await rightFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(rightFlipper.body.linearVelocity.y, isZero);
expect(rightFlipper.body.linearVelocity.x, isZero);
},
);
});
});
group('on left Flipper', () {
flameTester.test(
'moves upwards when left arrow is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isNegative);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when left arrow is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowLeft,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isPositive);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves upwards when A is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isNegative);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'moves downwards when A is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyA,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isPositive);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
group("doesn't move when", () {
flameTester.test(
'right arrow is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'right arrow is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowRight,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'D is pressed',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'D is released',
(game) async {
await game.ensureAdd(leftFlipper);
final behavior = FlipperKeyControllingBehavior();
await leftFlipper.ensureAdd(behavior);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.keyD,
);
behavior.onKeyEvent(event, {});
expect(leftFlipper.body.linearVelocity.y, isZero);
expect(leftFlipper.body.linearVelocity.x, isZero);
},
);
});
});
},
);
});
}

@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -36,7 +36,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/flipper.png'),
matchesGoldenFile('../golden/flipper.png'),
);
},
);

@ -1,260 +0,0 @@
import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
]);
}
Future<void> pump(Flipper flipper, {required GameBloc gameBloc}) {
return ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [flipper],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('FlipperController', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
group('onKeyEvent', () {
final leftKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
]);
final rightKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
]);
group('and Flipper is left', () {
late Flipper flipper;
late FlipperController controller;
setUp(() {
flipper = Flipper(side: BoardSide.left);
controller = FlipperController(flipper);
flipper.add(controller);
});
testRawKeyDownEvents(leftKeys, (event) {
flameTester.test(
'moves upwards '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.playing,
),
);
await game.ready();
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isNegative);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyDownEvents(leftKeys, (event) {
flameTester.test(
'does nothing when is game over',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.gameOver,
),
);
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(leftKeys, (event) {
flameTester.test(
'moves downwards '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.playing,
),
);
await game.ready();
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isPositive);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(rightKeys, (event) {
flameTester.test(
'does nothing '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ready();
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
});
group('and Flipper is right', () {
late Flipper flipper;
late FlipperController controller;
setUp(() {
flipper = Flipper(side: BoardSide.right);
controller = FlipperController(flipper);
flipper.add(controller);
});
testRawKeyDownEvents(rightKeys, (event) {
flameTester.test(
'moves upwards '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.playing,
),
);
await game.ready();
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isNegative);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(rightKeys, (event) {
flameTester.test(
'moves downwards '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.playing,
),
);
await game.ready();
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isPositive);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyDownEvents(rightKeys, (event) {
flameTester.test(
'does nothing when is game over',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.gameOver,
),
);
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(leftKeys, (event) {
flameTester.test(
'does nothing '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.playing,
),
);
await game.ready();
await game.pump(flipper, gameBloc: gameBloc);
controller.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
});
});
});
}

@ -8,16 +8,24 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.load(Assets.images.backbox.marquee.keyName);
await images.loadAll(
[
const theme.DashTheme().leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
],
);
}
Future<void> pump(
@ -35,8 +43,15 @@ class _TestGame extends Forge2DGame {
),
],
children: [
FlameProvider<PinballPlayer>.value(
pinballPlayer ?? _MockPinballPlayer(),
MultiFlameProvider(
providers: [
FlameProvider<PinballPlayer>.value(
pinballPlayer ?? _MockPinballPlayer(),
),
FlameProvider<AppLocalizations>.value(
_MockAppLocalizations(),
),
],
children: children,
),
],
@ -50,6 +65,35 @@ class _MockPinballPlayer extends Mock implements PinballPlayer {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get rank => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
@override
String get loading => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -92,54 +136,113 @@ void main() {
});
group('onNewState', () {
flameTester.test(
'changes the backbox display when the game is over',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final backbox = Backbox(leaderboardRepository: repository);
final state = const GameState.initial()
..copyWith(
status: GameStatus.gameOver,
group('on game over', () {
late GameState state;
setUp(() {
state = const GameState.initial().copyWith(
status: GameStatus.gameOver,
);
});
flameTester.test(
'changes the backbox display',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final backbox = Backbox(leaderboardRepository: repository);
await game.pump([component, backbox]);
expect(() => component.onNewState(state), returnsNormally);
},
);
flameTester.test(
'removes FlipperKeyControllingBehavior from Fipper',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final backbox = Backbox(leaderboardRepository: repository);
final flipper = Flipper.test(side: BoardSide.left);
final behavior = FlipperKeyControllingBehavior();
await game.pump([component, backbox, flipper]);
await flipper.ensureAdd(behavior);
expect(state.status, GameStatus.gameOver);
component.onNewState(state);
await game.ready();
expect(
flipper.children.whereType<FlipperKeyControllingBehavior>(),
isEmpty,
);
},
);
await game.pump([component, backbox]);
flameTester.test(
'plays the game over voice over',
(game) async {
final player = _MockPinballPlayer();
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final backbox = Backbox(leaderboardRepository: repository);
await game.pump([component, backbox], pinballPlayer: player);
expect(() => component.onNewState(state), returnsNormally);
},
);
component.onNewState(state);
verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1);
},
);
});
flameTester.test(
'plays the background music on start',
(game) async {
final player = _MockPinballPlayer();
final component = GameBlocStatusListener();
await game.pump([component], pinballPlayer: player);
group('on playing', () {
late GameState state;
component.onNewState(
const GameState.initial().copyWith(status: GameStatus.playing),
setUp(() {
state = const GameState.initial().copyWith(
status: GameStatus.playing,
);
});
verify(() => player.play(PinballAudio.backgroundMusic)).called(1);
},
);
flameTester.test(
'plays the background music on start',
(game) async {
final player = _MockPinballPlayer();
final component = GameBlocStatusListener();
await game.pump([component], pinballPlayer: player);
flameTester.test(
'plays the game over voice over when it is game over',
(game) async {
final player = _MockPinballPlayer();
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final backbox = Backbox(leaderboardRepository: repository);
await game.pump([component, backbox], pinballPlayer: player);
component.onNewState(
const GameState.initial().copyWith(status: GameStatus.gameOver),
);
expect(state.status, equals(GameStatus.playing));
component.onNewState(state);
verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1);
},
);
verify(() => player.play(PinballAudio.backgroundMusic)).called(1);
},
);
flameTester.test(
'adds key controlling behavior to Fippers when the game is started',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final backbox = Backbox(leaderboardRepository: repository);
final flipper = Flipper.test(side: BoardSide.left);
await game.pump([component, backbox, flipper]);
component.onNewState(state);
await game.ready();
expect(
flipper.children
.whereType<FlipperKeyControllingBehavior>()
.length,
equals(1),
);
},
);
});
});
});
}

Loading…
Cancel
Save