diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 4ba63092..49f40d1f 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -1,5 +1,5 @@ // ignore_for_file: public_member_api_docs - +import 'dart:math' as math; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; @@ -9,19 +9,41 @@ part 'game_state.dart'; class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { - on(_onBallLost); + on(_onRoundLost); on(_onScored); + on(_onIncreasedMultiplier); on(_onBonusActivated); on(_onSparkyTurboChargeActivated); } - void _onBallLost(BallLost event, Emitter emit) { - emit(state.copyWith(balls: state.balls - 1)); + void _onRoundLost(RoundLost event, Emitter emit) { + final score = state.score * state.multiplier; + final roundsLeft = math.max(state.rounds - 1, 0); + + emit( + state.copyWith( + score: score, + multiplier: 1, + rounds: roundsLeft, + ), + ); } void _onScored(Scored event, Emitter emit) { if (!state.isGameOver) { - emit(state.copyWith(score: state.score + event.points)); + emit( + state.copyWith(score: state.score + event.points), + ); + } + } + + void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) { + if (!state.isGameOver) { + emit( + state.copyWith( + multiplier: math.min(state.multiplier + 1, 6), + ), + ); } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index bbb89028..c81ce526 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -7,12 +7,12 @@ abstract class GameEvent extends Equatable { const GameEvent(); } -/// {@template ball_lost_game_event} -/// Event added when a user drops a ball off the screen. +/// {@template round_lost_game_event} +/// Event added when a user drops all balls off the screen and loses a round. /// {@endtemplate} -class BallLost extends GameEvent { - /// {@macro ball_lost_game_event} - const BallLost(); +class RoundLost extends GameEvent { + /// {@macro round_lost_game_event} + const RoundLost(); @override List get props => []; @@ -48,3 +48,14 @@ class SparkyTurboChargeActivated extends GameEvent { @override List get props => []; } + +/// {@template multiplier_increased_game_event} +/// Added when a multiplier is gained. +/// {@endtemplate} +class MultiplierIncreased extends GameEvent { + /// {@macro multiplier_increased_game_event} + const MultiplierIncreased(); + + @override + List get props => []; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 772bfea3..4ce9042d 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -27,34 +27,42 @@ class GameState extends Equatable { /// {@macro game_state} const GameState({ required this.score, - required this.balls, + required this.multiplier, + required this.rounds, required this.bonusHistory, }) : assert(score >= 0, "Score can't be negative"), - assert(balls >= 0, "Number of balls can't be negative"); + assert(multiplier > 0, 'Multiplier must be greater than zero'), + assert(rounds >= 0, "Number of rounds can't be negative"); const GameState.initial() : score = 0, - balls = 3, + multiplier = 1, + rounds = 3, bonusHistory = const []; /// The current score of the game. final int score; - /// The number of balls left in the game. + /// The current multiplier for the score. + final int multiplier; + + /// The number of rounds left in the game. /// - /// When the number of balls is 0, the game is over. - final int balls; + /// When the number of rounds is 0, the game is over. + final int rounds; /// Holds the history of all the [GameBonus]es earned by the player during a /// PinballGame. final List bonusHistory; /// Determines when the game is over. - bool get isGameOver => balls == 0; + bool get isGameOver => rounds == 0; GameState copyWith({ int? score, + int? multiplier, int? balls, + int? rounds, List? bonusHistory, }) { assert( @@ -64,7 +72,8 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, - balls: balls ?? this.balls, + multiplier: multiplier ?? this.multiplier, + rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -72,7 +81,8 @@ class GameState extends Equatable { @override List get props => [ score, - balls, + multiplier, + rounds, bonusHistory, ]; } diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart deleted file mode 100644 index 8ee4128f..00000000 --- a/lib/game/components/board.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template board} -/// The main flat surface of the [PinballGame]. -/// {endtemplate} -class Board extends Component { - /// {@macro board} - // TODO(alestiago): Make Board a Blueprint and sort out priorities. - Board() : super(priority: 1); - - @override - Future onLoad() async { - // TODO(allisonryan0002): add bottom group and flutter forest to pinball - // game directly. Then remove board. - final bottomGroup = _BottomGroup(); - - final flutterForest = FlutterForest(); - - // TODO(alestiago): adjust positioning to real design. - // TODO(alestiago): add dino in pinball game. - final dino = ChromeDino() - ..initialPosition = Vector2( - BoardDimensions.bounds.center.dx + 25, - BoardDimensions.bounds.center.dy - 10, - ); - - await addAll([ - bottomGroup, - dino, - flutterForest, - ]); - } -} - -/// {@template bottom_group} -/// Grouping of the board's bottom [Component]s. -/// -/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s. -/// {@endtemplate} -// TODO(alestiago): Consider renaming once entire Board is defined. -class _BottomGroup extends Component { - /// {@macro bottom_group} - _BottomGroup() : super(priority: RenderPriority.bottomGroup); - - @override - Future onLoad() async { - final rightSide = _BottomGroupSide( - side: BoardSide.right, - ); - final leftSide = _BottomGroupSide( - side: BoardSide.left, - ); - - await addAll([rightSide, leftSide]); - } -} - -/// {@template bottom_group_side} -/// Group with one side of [_BottomGroup]'s symmetric [Component]s. -/// -/// For example, [Flipper]s are symmetric components. -/// {@endtemplate} -class _BottomGroupSide extends Component { - /// {@macro bottom_group_side} - _BottomGroupSide({ - required BoardSide side, - }) : _side = side; - - final BoardSide _side; - - @override - Future onLoad() async { - final direction = _side.direction; - final centerXAdjustment = _side.isLeft ? 0 : -6.5; - - final flipper = ControlledFlipper( - side: _side, - )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); - final baseboard = Baseboard(side: _side) - ..initialPosition = Vector2( - (25.58 * direction) + centerXAdjustment, - 28.69, - ); - final kicker = Kicker( - side: _side, - )..initialPosition = Vector2( - (22.4 * direction) + centerXAdjustment, - 25, - ); - - await addAll([flipper, baseboard, kicker]); - } -} diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart new file mode 100644 index 00000000..d0184d33 --- /dev/null +++ b/lib/game/components/bottom_group.dart @@ -0,0 +1,58 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template bottom_group} +/// Grouping of the board's symmetrical bottom [Component]s. +/// +/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. +/// {@endtemplate} +// TODO(allisonryan0002): Consider renaming. +class BottomGroup extends Component { + /// {@macro bottom_group} + BottomGroup() + : super( + children: [ + _BottomGroupSide(side: BoardSide.right), + _BottomGroupSide(side: BoardSide.left), + ], + ); +} + +/// {@template bottom_group_side} +/// Group with one side of [BottomGroup]'s symmetric [Component]s. +/// +/// For example, [Flipper]s are symmetric components. +/// {@endtemplate} +class _BottomGroupSide extends Component { + /// {@macro bottom_group_side} + _BottomGroupSide({ + required BoardSide side, + }) : _side = side, + super(priority: RenderPriority.bottomGroup); + + final BoardSide _side; + + @override + Future onLoad() async { + final direction = _side.direction; + final centerXAdjustment = _side.isLeft ? 0 : -6.5; + + final flipper = ControlledFlipper( + side: _side, + )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); + final baseboard = Baseboard(side: _side) + ..initialPosition = Vector2( + (25.58 * direction) + centerXAdjustment, + 28.69, + ); + final kicker = Kicker( + side: _side, + )..initialPosition = Vector2( + (22.4 * direction) + centerXAdjustment, + 25, + ); + + await addAll([flipper, baseboard, kicker]); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 99dba9fb..48bb9631 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,9 +1,11 @@ export 'android_acres.dart'; -export 'board.dart'; +export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; +export 'dino_desert.dart'; +export 'drain.dart'; export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; export 'google_word/google_word.dart'; @@ -11,4 +13,3 @@ export 'launcher.dart'; export 'multiballs/multiballs.dart'; export 'scoring_behavior.dart'; export 'sparky_fire_zone.dart'; -export 'wall.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index f36cfef2..ff05ad62 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,5 +1,6 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame/components.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -8,12 +9,12 @@ import 'package:pinball_theme/pinball_theme.dart'; /// {@template controlled_ball} /// A [Ball] with a [BallController] attached. +/// +/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is +/// not over, a new [Ball] will be spawned. /// {@endtemplate} class ControlledBall extends Ball with Controls { /// A [Ball] that launches from the [Plunger]. - /// - /// When a launched [Ball] is lost, it will decrease the [GameState.balls] - /// count, and a new [Ball] is spawned. ControlledBall.launch({ required CharacterTheme characterTheme, }) : super(baseColor: characterTheme.ballColor) { @@ -24,8 +25,6 @@ class ControlledBall extends Ball with Controls { /// {@template bonus_ball} /// {@macro controlled_ball} - /// - /// When a bonus [Ball] is lost, the [GameState.balls] doesn't change. /// {@endtemplate} ControlledBall.bonus({ required CharacterTheme characterTheme, @@ -36,7 +35,7 @@ class ControlledBall extends Ball with Controls { /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = DebugBallController(this); + controller = BallController(this); priority = RenderPriority.ballOnBoard; } } @@ -74,15 +73,9 @@ class BallController extends ComponentController @override void onRemove() { super.onRemove(); - gameRef.read().add(const BallLost()); + final noBallsLeft = gameRef.descendants().whereType().isEmpty; + if (noBallsLeft) { + gameRef.read().add(const RoundLost()); + } } } - -/// {@macro ball_controller} -class DebugBallController extends BallController { - /// {@macro ball_controller} - DebugBallController(Ball component) : super(component); - - @override - void onRemove() {} -} diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart new file mode 100644 index 00000000..9e912575 --- /dev/null +++ b/lib/game/components/dino_desert.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template dino_desert} +/// Area located next to the [Launcher] containing the [ChromeDino] and +/// [DinoWalls]. +/// {@endtemplate} +// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is +// fully implemented. +class DinoDesert extends Blueprint { + /// {@macro dino_desert} + DinoDesert() + : super( + components: [ + ChromeDino()..initialPosition = Vector2(12.3, -6.9), + ], + blueprints: [ + DinoWalls(), + ], + ); +} diff --git a/lib/game/components/drain.dart b/lib/game/components/drain.dart new file mode 100644 index 00000000..1dc3e211 --- /dev/null +++ b/lib/game/components/drain.dart @@ -0,0 +1,34 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template drain} +/// Area located at the bottom of the board to detect when a [Ball] is lost. +/// {@endtemplate} +// TODO(allisonryan0002): move to components package when possible. +class Drain extends BodyComponent with ContactCallbacks { + /// {@macro drain} + Drain() : super(renderBody: false); + + @override + Body createBody() { + final shape = EdgeShape() + ..set( + BoardDimensions.bounds.bottomLeft.toVector2(), + BoardDimensions.bounds.bottomRight.toVector2(), + ); + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef(userData: this); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + +// TODO(allisonryan0002): move this to ball.dart when BallLost is removed. + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + other.controller.lost(); + } +} diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 02483159..d7447543 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -7,7 +7,7 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template flutter_forest} -/// Area positioned at the top right of the [Board] where the [Ball] can bounce +/// Area positioned at the top right of the board where the [Ball] can bounce /// off [DashNestBumper]s. /// {@endtemplate} class FlutterForest extends Component { diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index a23a4fbc..5c00e5c9 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_fire_zone} -/// Area positioned at the top left of the [Board] where the [Ball] +/// Area positioned at the top left of the board where the [Ball] /// can bounce off [SparkyBumper]s. /// /// When a [Ball] hits [SparkyBumper]s, the bumper animates. diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart deleted file mode 100644 index 2f180d61..00000000 --- a/lib/game/components/wall.dart +++ /dev/null @@ -1,60 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/extensions.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -/// {@template wall} -/// A continuous generic and [BodyType.static] barrier that divides a game area. -/// {@endtemplate} -// TODO(alestiago): Remove [Wall] for [Pathway.straight]. -class Wall extends BodyComponent { - /// {@macro wall} - Wall({ - required this.start, - required this.end, - }); - - /// The [start] of the [Wall]. - final Vector2 start; - - /// The [end] of the [Wall]. - final Vector2 end; - - @override - Body createBody() { - final shape = EdgeShape()..set(start, end); - - final fixtureDef = FixtureDef(shape) - ..restitution = 0.1 - ..friction = 0; - - final bodyDef = BodyDef() - ..userData = this - ..position = Vector2.zero() - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template bottom_wall} -/// [Wall] located at the bottom of the board. -/// -/// {@endtemplate} -class BottomWall extends Wall with ContactCallbacks { - /// {@macro bottom_wall} - BottomWall() - : super( - start: BoardDimensions.bounds.bottomLeft.toVector2(), - end: BoardDimensions.bounds.bottomRight.toVector2(), - ); - - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! ControlledBall) return; - other.controller.lost(); - } -} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 421caf8f..5ab130d4 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -34,8 +34,10 @@ extension PinballGameAssetsX on PinballGame { images.load( components.Assets.images.launchRamp.backgroundRailing.keyName, ), - images.load(components.Assets.images.dino.dinoLandTop.keyName), - images.load(components.Assets.images.dino.dinoLandBottom.keyName), + images.load(components.Assets.images.dino.bottomWall.keyName), + images.load(components.Assets.images.dino.topWall.keyName), + images.load(components.Assets.images.dino.animatronic.head.keyName), + images.load(components.Assets.images.dino.animatronic.mouth.keyName), images.load(components.Assets.images.dash.animatronic.keyName), images.load(components.Assets.images.dash.bumper.a.active.keyName), images.load(components.Assets.images.dash.bumper.a.inactive.keyName), @@ -76,13 +78,11 @@ extension PinballGameAssetsX on PinballGame { components.Assets.images.spaceship.ramp.arrow.active5.keyName, ), images.load(components.Assets.images.spaceship.rail.main.keyName), - images.load(components.Assets.images.spaceship.rail.foreground.keyName), + images.load(components.Assets.images.spaceship.rail.exit.keyName), images.load(components.Assets.images.androidBumper.a.lit.keyName), images.load(components.Assets.images.androidBumper.a.dimmed.keyName), images.load(components.Assets.images.androidBumper.b.lit.keyName), images.load(components.Assets.images.androidBumper.b.dimmed.keyName), - images.load(components.Assets.images.chromeDino.mouth.keyName), - images.load(components.Assets.images.chromeDino.head.keyName), images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.animatronic.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 930b7bbd..0dd35afc 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -6,6 +6,7 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; @@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents, - Controls<_GameBallsController> { + Controls<_GameBallsController>, + TapDetector { PinballGame({ required this.characterTheme, required this.audio, @@ -44,21 +46,20 @@ class PinballGame extends Forge2DGame unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); - - // TODO(allisonryan0002): banish Wall and Board classes in later PR. - await add(BottomWall()); + await add(Drain()); + await add(BottomGroup()); unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(LaunchRamp())); final launcher = Launcher(); unawaited(addFromBlueprint(launcher)); - unawaited(add(Board())); await add(Multiballs()); + await add(FlutterForest()); await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(AndroidAcres()); + await addFromBlueprint(DinoDesert()); unawaited(addFromBlueprint(Slingshots())); - unawaited(addFromBlueprint(DinoWalls())); await add( GoogleWord( position: Vector2( @@ -71,10 +72,65 @@ class PinballGame extends Forge2DGame controller.attachTo(launcher.components.whereType().first); await super.onLoad(); } + + BoardSide? focusedBoardSide; + + @override + void onTapDown(TapDownInfo info) { + if (info.raw.kind == PointerDeviceKind.touch) { + final rocket = children.whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.pull(); + } else { + final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; + focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveUp(); + } + } + + super.onTapDown(info); + } + + @override + void onTapUp(TapUpInfo info) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.release(); + } else { + _moveFlippersDown(); + } + super.onTapUp(info); + } + + @override + void onTapCancel() { + children.whereType().first.release(); + + _moveFlippersDown(); + super.onTapCancel(); + } + + void _moveFlippersDown() { + if (focusedBoardSide != null) { + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveDown(); + focusedBoardSide = null; + } + } } class _GameBallsController extends ComponentController - with BlocComponent, HasGameRef { + with BlocComponent { _GameBallsController(PinballGame game) : super(game); late final Plunger _plunger; @@ -82,9 +138,9 @@ class _GameBallsController extends ComponentController @override bool listenWhen(GameState? previousState, GameState newState) { final noBallsLeft = component.descendants().whereType().isEmpty; - final canBallRespawn = newState.balls > 0; + final notGameOver = !newState.isGameOver; - return noBallsLeft && canBallRespawn; + return noBallsLeft && notGameOver; } @override @@ -101,7 +157,7 @@ class _GameBallsController extends ComponentController void _spawnBall() { final ball = ControlledBall.launch( - characterTheme: gameRef.characterTheme, + characterTheme: component.characterTheme, )..initialPosition = Vector2( _plunger.body.position.x, _plunger.body.position.y - Ball.size.y, @@ -117,7 +173,7 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter { DebugPinballGame({ required CharacterTheme characterTheme, required PinballAudio audio, @@ -154,28 +210,20 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { @override void onTapUp(TapUpInfo info) { - add( - ControlledBall.debug()..initialPosition = info.eventPosition.game, - ); + super.onTapUp(info); + + if (info.raw.kind == PointerDeviceKind.mouse) { + add(ControlledBall.debug()..initialPosition = info.eventPosition.game); + } } } class _DebugGameBallsController extends _GameBallsController { _DebugGameBallsController(PinballGame game) : super(game); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - final noBallsLeft = component - .descendants() - .whereType() - .where((ball) => ball.controller is! DebugBallController) - .isEmpty; - final canBallRespawn = newState.balls > 0; - - return noBallsLeft && canBallRespawn; - } } +// TODO(wolfenrain): investigate this CI failure. +// coverage:ignore-start class _DebugInformation extends Component with HasGameRef { _DebugInformation() : super(priority: RenderPriority.debugInfo); @@ -207,3 +255,4 @@ class _DebugInformation extends Component with HasGameRef { _debugTextPaint.render(canvas, debugText, position); } } +// coverage:ignore-end diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 3623e21f..9cfb2d67 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -7,7 +7,7 @@ import 'package:pinball/theme/app_colors.dart'; /// {@template game_hud} /// Overlay on the [PinballGame]. /// -/// Displays the current [GameState.score], [GameState.balls] and animates when +/// Displays the current [GameState.score], [GameState.rounds] and animates when /// the player gets a [GameBonus]. /// {@endtemplate} class GameHud extends StatefulWidget { diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart index 98776764..30135cd2 100644 --- a/lib/game/view/widgets/round_count_display.dart +++ b/lib/game/view/widgets/round_count_display.dart @@ -14,9 +14,7 @@ class RoundCountDisplay extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - // TODO(arturplaczek): refactor when GameState handle balls and rounds and - // select state.rounds property instead of state.ball - final balls = context.select((GameBloc bloc) => bloc.state.balls); + final rounds = context.select((GameBloc bloc) => bloc.state.rounds); return Row( children: [ @@ -29,9 +27,9 @@ class RoundCountDisplay extends StatelessWidget { const SizedBox(width: 8), Row( children: [ - RoundIndicator(isActive: balls >= 1), - RoundIndicator(isActive: balls >= 2), - RoundIndicator(isActive: balls >= 3), + RoundIndicator(isActive: rounds >= 1), + RoundIndicator(isActive: rounds >= 2), + RoundIndicator(isActive: rounds >= 3), ], ), ], diff --git a/packages/pinball_components/assets/images/chrome_dino/head.png b/packages/pinball_components/assets/images/chrome_dino/head.png deleted file mode 100644 index 15be6fcd..00000000 Binary files a/packages/pinball_components/assets/images/chrome_dino/head.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/chrome_dino/mouth.png b/packages/pinball_components/assets/images/chrome_dino/mouth.png deleted file mode 100644 index 3d9caeae..00000000 Binary files a/packages/pinball_components/assets/images/chrome_dino/mouth.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dino/animatronic/head.png b/packages/pinball_components/assets/images/dino/animatronic/head.png new file mode 100644 index 00000000..87332679 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/animatronic/head.png differ diff --git a/packages/pinball_components/assets/images/dino/animatronic/mouth.png b/packages/pinball_components/assets/images/dino/animatronic/mouth.png new file mode 100644 index 00000000..4955bdf3 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/animatronic/mouth.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-bottom.png b/packages/pinball_components/assets/images/dino/bottom-wall.png similarity index 100% rename from packages/pinball_components/assets/images/dino/dino-land-bottom.png rename to packages/pinball_components/assets/images/dino/bottom-wall.png diff --git a/packages/pinball_components/assets/images/dino/dino-land-top.png b/packages/pinball_components/assets/images/dino/top-wall.png similarity index 100% rename from packages/pinball_components/assets/images/dino/dino-land-top.png rename to packages/pinball_components/assets/images/dino/top-wall.png diff --git a/packages/pinball_components/assets/images/spaceship/rail/exit.png b/packages/pinball_components/assets/images/spaceship/rail/exit.png new file mode 100644 index 00000000..80a819d0 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/rail/exit.png differ diff --git a/packages/pinball_components/assets/images/spaceship/rail/foreground.png b/packages/pinball_components/assets/images/spaceship/rail/foreground.png deleted file mode 100644 index 4d11e865..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/rail/foreground.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/spaceship/rail/main.png b/packages/pinball_components/assets/images/spaceship/rail/main.png index 4b299c2c..9291c784 100644 Binary files a/packages/pinball_components/assets/images/spaceship/rail/main.png and b/packages/pinball_components/assets/images/spaceship/rail/main.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 997c7855..c6d1bb14 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -16,8 +16,6 @@ class $AssetsImagesGen { $AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); - $AssetsImagesChromeDinoGen get chromeDino => - const $AssetsImagesChromeDinoGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); @@ -98,18 +96,6 @@ class $AssetsImagesBoundaryGen { const AssetGenImage('assets/images/boundary/outer.png'); } -class $AssetsImagesChromeDinoGen { - const $AssetsImagesChromeDinoGen(); - - /// File path: assets/images/chrome_dino/head.png - AssetGenImage get head => - const AssetGenImage('assets/images/chrome_dino/head.png'); - - /// File path: assets/images/chrome_dino/mouth.png - AssetGenImage get mouth => - const AssetGenImage('assets/images/chrome_dino/mouth.png'); -} - class $AssetsImagesDashGen { const $AssetsImagesDashGen(); @@ -123,13 +109,16 @@ class $AssetsImagesDashGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); - /// File path: assets/images/dino/dino-land-bottom.png - AssetGenImage get dinoLandBottom => - const AssetGenImage('assets/images/dino/dino-land-bottom.png'); + $AssetsImagesDinoAnimatronicGen get animatronic => + const $AssetsImagesDinoAnimatronicGen(); + + /// File path: assets/images/dino/bottom-wall.png + AssetGenImage get bottomWall => + const AssetGenImage('assets/images/dino/bottom-wall.png'); - /// File path: assets/images/dino/dino-land-top.png - AssetGenImage get dinoLandTop => - const AssetGenImage('assets/images/dino/dino-land-top.png'); + /// File path: assets/images/dino/top-wall.png + AssetGenImage get topWall => + const AssetGenImage('assets/images/dino/top-wall.png'); } class $AssetsImagesFlipperGen { @@ -319,12 +308,24 @@ class $AssetsImagesDashBumperGen { const $AssetsImagesDashBumperMainGen(); } +class $AssetsImagesDinoAnimatronicGen { + const $AssetsImagesDinoAnimatronicGen(); + + /// File path: assets/images/dino/animatronic/head.png + AssetGenImage get head => + const AssetGenImage('assets/images/dino/animatronic/head.png'); + + /// File path: assets/images/dino/animatronic/mouth.png + AssetGenImage get mouth => + const AssetGenImage('assets/images/dino/animatronic/mouth.png'); +} + class $AssetsImagesSpaceshipRailGen { const $AssetsImagesSpaceshipRailGen(); - /// File path: assets/images/spaceship/rail/foreground.png - AssetGenImage get foreground => - const AssetGenImage('assets/images/spaceship/rail/foreground.png'); + /// File path: assets/images/spaceship/rail/exit.png + AssetGenImage get exit => + const AssetGenImage('assets/images/spaceship/rail/exit.png'); /// File path: assets/images/spaceship/rail/main.png AssetGenImage get main => diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 59bacb08..3d0f9445 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -67,7 +67,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { _BottomBoundarySpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-5.4, 55.6), + position: Vector2(-5, 55.6), ); @override diff --git a/packages/pinball_components/lib/src/components/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart index 7846f140..e1a1a1fc 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino.dart @@ -1,31 +1,33 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template chrome_dino} -/// Dinosaur that gobbles up a [Ball], swivel his head around, and shoots it -/// back out. +/// Dino that swivels back and forth, opening its mouth to eat a [Ball]. +/// +/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in a +/// different direction. /// {@endtemplate} class ChromeDino extends BodyComponent with InitialPosition { /// {@macro chrome_dino} ChromeDino() : super( - // TODO(alestiago): Remove once sprites are defined. - paint: Paint()..color = Colors.blue, priority: RenderPriority.dino, + renderBody: false, ); /// The size of the dinosaur mouth. - static final size = Vector2(5, 2.5); + static final size = Vector2(5.5, 5); /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc /// motion. Future<_ChromeDinoJoint> _anchorToJoint() async { - final anchor = _ChromeDinoAnchor(); + // TODO(allisonryan0002): try moving to anchor after new body is defined. + final anchor = _ChromeDinoAnchor() + ..initialPosition = initialPosition + Vector2(9, -4); + await add(anchor); final jointDef = _ChromeDinoAnchorRevoluteJointDef( @@ -42,9 +44,11 @@ class ChromeDino extends BodyComponent with InitialPosition { Future onLoad() async { await super.onLoad(); final joint = await _anchorToJoint(); + const framesInAnimation = 98; + const animationFPS = 1 / 24; await add( TimerComponent( - period: 1, + period: (framesInAnimation / 2) * animationFPS, onTick: joint._swivel, repeat: true, ), @@ -54,44 +58,17 @@ class ChromeDino extends BodyComponent with InitialPosition { List _createFixtureDefs() { final fixtureDefs = []; - // TODO(alestiago): Subject to change when sprites are added. - final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); - final fixtureDef = FixtureDef( - box, - density: 999, - friction: 0.3, - restitution: 0.1, - isSensor: true, - ); - + // TODO(allisonryan0002): Update this shape to better match sprite. + final box = PolygonShape() + ..setAsBox( + size.x / 2, + size.y / 2, + initialPosition + Vector2(-4, 2), + -_ChromeDinoJoint._halfSweepingAngle, + ); + final fixtureDef = FixtureDef(box, density: 1); fixtureDefs.add(fixtureDef); - // FIXME(alestiago): Investigate why adding these fixtures is considered as - // an invalid contact type. - // final upperEdge = EdgeShape() - // ..set( - // Vector2(-size.x / 2, -size.y / 2), - // Vector2(size.x / 2, -size.y / 2), - // ); - // final upperEdgeDef = FixtureDef(upperEdge)..density = 0.5; - // fixtureDefs.add(upperEdgeDef); - - // final lowerEdge = EdgeShape() - // ..set( - // Vector2(-size.x / 2, size.y / 2), - // Vector2(size.x / 2, size.y / 2), - // ); - // final lowerEdgeDef = FixtureDef(lowerEdge)..density = 0.5; - // fixtureDefs.add(lowerEdgeDef); - - // final rightEdge = EdgeShape() - // ..set( - // Vector2(size.x / 2, -size.y / 2), - // Vector2(size.x / 2, size.y / 2), - // ); - // final rightEdgeDef = FixtureDef(rightEdge)..density = 0.5; - // fixtureDefs.add(rightEdgeDef); - return fixtureDefs; } @@ -110,13 +87,18 @@ class ChromeDino extends BodyComponent with InitialPosition { } } -/// {@template flipper_anchor} -/// [JointAnchor] positioned at the end of a [ChromeDino]. -/// {@endtemplate} class _ChromeDinoAnchor extends JointAnchor { - /// {@macro flipper_anchor} - _ChromeDinoAnchor() { - initialPosition = Vector2(ChromeDino.size.x / 2, 0); + _ChromeDinoAnchor(); + + // TODO(allisonryan0002): if these aren't moved when fixing the rendering, see + // if the joint can be created in onMount to resolve render syncing. + @override + Future onLoad() async { + await super.onLoad(); + await addAll([ + _ChromeDinoMouthSprite(), + _ChromeDinoHeadSprite(), + ]); } } @@ -135,22 +117,86 @@ class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { chromeDino.body.position + anchor.body.position, ); enableLimit = true; - // TODO(alestiago): Apply design angle value. - const angle = math.pi / 3.5; - lowerAngle = -angle / 2; - upperAngle = angle / 2; + lowerAngle = -_ChromeDinoJoint._halfSweepingAngle; + upperAngle = _ChromeDinoJoint._halfSweepingAngle; enableMotor = true; - // TODO(alestiago): Tune this values. - maxMotorTorque = motorSpeed = chromeDino.body.mass * 30; + maxMotorTorque = chromeDino.body.mass * 255; + motorSpeed = 2; } } class _ChromeDinoJoint extends RevoluteJoint { _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); + static const _halfSweepingAngle = 0.1143; + /// Sweeps the [ChromeDino] up and down repeatedly. void _swivel() { setMotorSpeed(-motorSpeed); } } + +class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoMouthSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: _ChromeDinoJoint._halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.mouth.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; + } +} + +class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoHeadSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: _ChromeDinoJoint._halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.head.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; + } +} diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 0e0e2efa..39824490 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -35,51 +35,46 @@ class _DinoTopWall extends BodyComponent with InitialPosition { List _createFixtureDefs() { final topStraightShape = EdgeShape() ..set( - Vector2(28.65, -35.1), - Vector2(29.5, -35.1), + Vector2(28.65, -34.3), + Vector2(29.5, -34.3), ); - final topStraightFixtureDef = FixtureDef(topStraightShape); final topCurveShape = BezierCurveShape( controlPoints: [ topStraightShape.vertex1, - Vector2(18.8, -27), - Vector2(26.6, -21), + Vector2(18.8, -26.2), + Vector2(26.6, -20.2), ], ); - final topCurveFixtureDef = FixtureDef(topCurveShape); final middleCurveShape = BezierCurveShape( controlPoints: [ topCurveShape.vertices.last, - Vector2(27.8, -20.1), - Vector2(26.8, -19.5), + Vector2(27.8, -19.3), + Vector2(26.8, -18.7), ], ); - final middleCurveFixtureDef = FixtureDef(middleCurveShape); final bottomCurveShape = BezierCurveShape( controlPoints: [ middleCurveShape.vertices.last, - Vector2(23, -15), - Vector2(27, -15), + Vector2(23, -14.2), + Vector2(27, -14.2), ], ); - final bottomCurveFixtureDef = FixtureDef(bottomCurveShape); final bottomStraightShape = EdgeShape() ..set( bottomCurveShape.vertices.last, - Vector2(31, -14.5), + Vector2(31, -13.7), ); - final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); return [ - topStraightFixtureDef, - topCurveFixtureDef, - middleCurveFixtureDef, - bottomCurveFixtureDef, - bottomStraightFixtureDef, + FixtureDef(topStraightShape), + FixtureDef(topCurveShape), + FixtureDef(middleCurveShape), + FixtureDef(bottomCurveShape), + FixtureDef(bottomStraightShape), ]; } @@ -109,12 +104,12 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.topWall.keyName, ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22.8, -38.9); + position = Vector2(22.8, -38.1); } } @@ -131,17 +126,11 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { ); List _createFixtureDefs() { - const restitution = 1.0; - final topStraightShape = EdgeShape() ..set( Vector2(32.4, -8.8), Vector2(25, -7.7), ); - final topStraightFixtureDef = FixtureDef( - topStraightShape, - restitution: restitution, - ); final topLeftCurveShape = BezierCurveShape( controlPoints: [ @@ -150,36 +139,24 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { Vector2(29.8, 13.8), ], ); - final topLeftCurveFixtureDef = FixtureDef( - topLeftCurveShape, - restitution: restitution, - ); final bottomLeftStraightShape = EdgeShape() ..set( topLeftCurveShape.vertices.last, Vector2(31.9, 44.1), ); - final bottomLeftStraightFixtureDef = FixtureDef( - bottomLeftStraightShape, - restitution: restitution, - ); final bottomStraightShape = EdgeShape() ..set( bottomLeftStraightShape.vertex2, Vector2(37.8, 44.1), ); - final bottomStraightFixtureDef = FixtureDef( - bottomStraightShape, - restitution: restitution, - ); return [ - topStraightFixtureDef, - topLeftCurveFixtureDef, - bottomLeftStraightFixtureDef, - bottomStraightFixtureDef, + FixtureDef(topStraightShape), + FixtureDef(topLeftCurveShape), + FixtureDef(bottomLeftStraightShape), + FixtureDef(bottomStraightShape), ]; } @@ -203,11 +180,11 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.dino.dinoLandBottom.keyName, + Assets.images.dino.bottomWall.keyName, ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(23.6, -9.5); + position = Vector2(23.8, -9.5); } } diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index dccd7ce7..bb982e96 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -55,7 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { ); final joint = _FlipperJoint(jointDef); world.createJoint(joint); - unawaited(mounted.whenComplete(joint.unlock)); } List _createFixtureDefs() { @@ -132,6 +131,15 @@ 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 { @@ -215,11 +223,8 @@ class _FlipperJoint extends RevoluteJoint { /// The joint is locked when initialized in order to force the [Flipper] /// at its resting position. void lock() { - const angle = _halfSweepingAngle; - setLimits( - angle * side.direction, - angle * side.direction, - ); + final angle = _halfSweepingAngle * side.direction; + setLimits(angle, angle); } /// Unlocks the [Flipper] from its resting position. diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart index 8ef950ed..0f530b64 100644 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -24,10 +24,10 @@ abstract class RenderPriority { static const int ballOnSpaceship = _above + spaceshipSaucer; /// Render priority for the [Ball] while it's on the [SpaceshipRail]. - static const int ballOnSpaceshipRail = _below + spaceshipSaucer; + static const int ballOnSpaceshipRail = _above + spaceshipRail; /// Render priority for the [Ball] while it's on the [LaunchRamp]. - static const int ballOnLaunchRamp = _above + launchRamp; + static const int ballOnLaunchRamp = launchRamp; // Background @@ -51,7 +51,7 @@ abstract class RenderPriority { static const int launchRamp = _above + outerBoundary; - static const int launchRampForegroundRailing = _below + ballOnBoard; + static const int launchRampForegroundRailing = ballOnBoard; static const int plunger = _above + launchRamp; @@ -87,9 +87,9 @@ abstract class RenderPriority { static const int spaceshipRail = _above + bottomGroup; - static const int spaceshipRailForeground = _above + spaceshipRail; + static const int spaceshipRailExit = _above + ballOnSpaceshipRail; - static const int spaceshipSaucer = _above + spaceshipRail; + static const int spaceshipSaucer = _above + ballOnSpaceshipRail; static const int spaceshipSaucerWall = _above + spaceshipSaucer; diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index 3dfd2c1c..91540c62 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -2,88 +2,71 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_rail} -/// A [Blueprint] for the spaceship drop tube. +/// A [Blueprint] for the rail exiting the [Spaceship]. /// {@endtemplate} class SpaceshipRail extends Blueprint { /// {@macro spaceship_rail} SpaceshipRail() : super( components: [ - _SpaceshipRailRamp(), + _SpaceshipRail(), _SpaceshipRailExit(), - _SpaceshipRailBase(radius: 0.55) - ..initialPosition = Vector2(-26.15, -18.65), - _SpaceshipRailBase(radius: 0.8) - ..initialPosition = Vector2(-25.5, 12.9), - _SpaceshipRailForeground() + _SpaceshipRailExitSpriteComponent() ], ); } -class _SpaceshipRailRamp extends BodyComponent with Layered { - _SpaceshipRailRamp() +class _SpaceshipRail extends BodyComponent with Layered { + _SpaceshipRail() : super( priority: RenderPriority.spaceshipRail, + children: [_SpaceshipRailSpriteComponent()], renderBody: false, - children: [_SpaceshipRailRampSpriteComponent()], ) { layer = Layer.spaceshipExitRail; } List _createFixtureDefs() { - final fixturesDefs = []; - final topArcShape = ArcShape( - center: Vector2(-35.5, -30.9), + center: Vector2(-35.1, -30.9), arcRadius: 2.5, angle: math.pi, rotation: 0.2, ); - final topArcFixtureDef = FixtureDef(topArcShape); - fixturesDefs.add(topArcFixtureDef); final topLeftCurveShape = BezierCurveShape( controlPoints: [ - Vector2(-37.9, -30.4), - Vector2(-38, -23.9), + Vector2(-37.6, -30.4), + Vector2(-37.8, -23.9), Vector2(-30.93, -18.2), ], ); - final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape); - fixturesDefs.add(topLeftCurveFixtureDef); final middleLeftCurveShape = BezierCurveShape( controlPoints: [ topLeftCurveShape.vertices.last, Vector2(-22.6, -10.3), - Vector2(-30, -0.2), + Vector2(-29.5, -0.2), ], ); - final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape); - fixturesDefs.add(middleLeftCurveFixtureDef); final bottomLeftCurveShape = BezierCurveShape( controlPoints: [ middleLeftCurveShape.vertices.last, - Vector2(-36, 8.6), - Vector2(-32.04, 18.3), + Vector2(-35.6, 8.6), + Vector2(-31.3, 18.3), ], ); - final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape); - fixturesDefs.add(bottomLeftCurveFixtureDef); final topRightStraightShape = EdgeShape() ..set( - Vector2(-33, -31.3), Vector2(-27.2, -21.3), + Vector2(-33, -31.3), ); - final topRightStraightFixtureDef = FixtureDef(topRightStraightShape); - fixturesDefs.add(topRightStraightFixtureDef); final middleRightCurveShape = BezierCurveShape( controlPoints: [ @@ -92,8 +75,6 @@ class _SpaceshipRailRamp extends BodyComponent with Layered { Vector2(-25.29, 1.7), ], ); - final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape); - fixturesDefs.add(middleRightCurveFixtureDef); final bottomRightCurveShape = BezierCurveShape( controlPoints: [ @@ -102,10 +83,16 @@ class _SpaceshipRailRamp extends BodyComponent with Layered { Vector2(-26.8, 15.7), ], ); - final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape); - fixturesDefs.add(bottomRightCurveFixtureDef); - return fixturesDefs; + return [ + FixtureDef(topArcShape), + FixtureDef(topLeftCurveShape), + FixtureDef(middleLeftCurveShape), + FixtureDef(bottomLeftCurveShape), + FixtureDef(topRightStraightShape), + FixtureDef(middleRightCurveShape), + FixtureDef(bottomRightCurveShape), + ]; } @override @@ -116,55 +103,47 @@ class _SpaceshipRailRamp extends BodyComponent with Layered { } } -class _SpaceshipRailRampSpriteComponent extends SpriteComponent - with HasGameRef { +class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef { + _SpaceshipRailSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-29.4, -5.7), + ); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.rail.main.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.spaceship.rail.main.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-29.4, -5.7); } } -class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { - _SpaceshipRailForeground() - : super(priority: RenderPriority.spaceshipRailForeground); +class _SpaceshipRailExitSpriteComponent extends SpriteComponent + with HasGameRef { + _SpaceshipRailExitSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-28, 19.4), + priority: RenderPriority.spaceshipRailExit, + ); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.rail.foreground.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.spaceship.rail.exit.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-28.5, 19.7); - } -} - -/// Represents the ground bases of the [_SpaceshipRailRamp]. -class _SpaceshipRailBase extends BodyComponent with InitialPosition { - _SpaceshipRailBase({required this.radius}) : super(renderBody: false); - - final double radius; - - @override - Body createBody() { - final shape = CircleShape()..radius = radius; - final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef( - position: initialPosition, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 19c169da..deab6d8f 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -50,6 +50,7 @@ flutter: - assets/images/baseboard/ - assets/images/boundary/ - assets/images/dino/ + - assets/images/dino/animatronic/ - assets/images/flipper/ - assets/images/launch_ramp/ - assets/images/dash/ @@ -60,7 +61,6 @@ flutter: - assets/images/spaceship/rail/ - assets/images/spaceship/ramp/ - assets/images/spaceship/ramp/arrow/ - - assets/images/chrome_dino/ - assets/images/kicker/ - assets/images/plunger/ - assets/images/slingshot/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 6261b494..5310f00e 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,7 +6,6 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; -import 'package:sandbox/stories/kicker/stories.dart'; import 'package:sandbox/stories/stories.dart'; void main() { @@ -15,11 +14,9 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); - addFlipperStories(dashbook); - addBaseboardStories(dashbook); addChromeDinoStories(dashbook); - addDashNestBumperStories(dashbook); - addKickerStories(dashbook); + addFlutterForestStories(dashbook); + addBottomGroupStories(dashbook); addPlungerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index 2a13fb5e..4bd067fa 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -12,6 +12,10 @@ class SpaceshipRailGame extends BallGame { color: Colors.blue, ballPriority: RenderPriority.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, + imagesFileNames: [ + Assets.images.spaceship.rail.main.keyName, + Assets.images.spaceship.rail.exit.keyName, + ], ); static const description = ''' diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart deleted file mode 100644 index b07e3a73..00000000 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/baseboard/baseboard_game.dart'; - -void addBaseboardStories(Dashbook dashbook) { - dashbook.storiesOf('Baseboard').addGame( - title: 'Traced', - description: BaseboardGame.description, - gameBuilder: (_) => BaseboardGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart new file mode 100644 index 00000000..d0cc7322 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart @@ -0,0 +1 @@ +export 'stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart new file mode 100644 index 00000000..7712ca79 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart @@ -0,0 +1,24 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/bottom_group/baseboard_game.dart'; +import 'package:sandbox/stories/bottom_group/flipper_game.dart'; +import 'package:sandbox/stories/bottom_group/kicker_game.dart'; + +void addBottomGroupStories(Dashbook dashbook) { + dashbook.storiesOf('Bottom Group') + ..addGame( + title: 'Flipper', + description: FlipperGame.description, + gameBuilder: (_) => FlipperGame(), + ) + ..addGame( + title: 'Kicker', + description: KickerGame.description, + gameBuilder: (_) => KickerGame(), + ) + ..addGame( + title: 'Baseboard', + description: BaseboardGame.description, + gameBuilder: (_) => BaseboardGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart index 2e6831e3..d6e7ef95 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart @@ -1,8 +1,22 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class ChromeDinoGame extends Forge2DGame { - static const description = 'Shows how a ChromeDino is rendered.'; +class ChromeDinoGame extends BallGame { + ChromeDinoGame() + : super( + imagesFileNames: [ + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + ], + ); + + static const description = ''' + Shows how ChromeDino is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; @override Future onLoad() async { @@ -10,5 +24,7 @@ class ChromeDinoGame extends Forge2DGame { camera.followVector2(Vector2.zero()); await add(ChromeDino()); + + await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart index 391cdca7..a4c70c03 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart @@ -4,7 +4,7 @@ import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart'; void addChromeDinoStories(Dashbook dashbook) { dashbook.storiesOf('Chrome Dino').addGame( - title: 'Trace', + title: 'Traced', description: ChromeDinoGame.description, gameBuilder: (_) => ChromeDinoGame(), ); diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart index b491d64b..a6987fcc 100644 --- a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart @@ -20,8 +20,8 @@ class DinoWallGame extends BallGame { await super.onLoad(); await images.loadAll([ - Assets.images.dino.dinoLandTop.keyName, - Assets.images.dino.dinoLandBottom.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, ]); await addFromBlueprint(DinoWalls()); diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart deleted file mode 100644 index 2ef2a4b6..00000000 --- a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/flipper/flipper_game.dart'; - -void addFlipperStories(Dashbook dashbook) { - dashbook.storiesOf('Flipper').addGame( - title: 'Traced', - description: FlipperGame.description, - gameBuilder: (_) => FlipperGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart index ef9c1ffb..dd557a27 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -5,7 +5,7 @@ import 'package:sandbox/stories/flutter_forest/signpost_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; -void addDashNestBumperStories(Dashbook dashbook) { +void addFlutterForestStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') ..addGame( title: 'Signpost', diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart deleted file mode 100644 index cfebb7e4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/kicker/kicker_game.dart'; - -void addKickerStories(Dashbook dashbook) { - dashbook.storiesOf('Kickers').addGame( - title: 'Traced', - description: KickerGame.description, - gameBuilder: (_) => KickerGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 8fab46cb..70eecd97 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,12 +1,11 @@ export 'android_acres/stories.dart'; export 'backboard/stories.dart'; export 'ball/stories.dart'; -export 'baseboard/stories.dart'; +export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; export 'dino_wall/stories.dart'; export 'effects/stories.dart'; -export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; diff --git a/packages/pinball_components/test/src/components/chrome_dino_test.dart b/packages/pinball_components/test/src/components/chrome_dino_test.dart index 8a0adb85..f97270b9 100644 --- a/packages/pinball_components/test/src/components/chrome_dino_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino_test.dart @@ -1,13 +1,19 @@ // ignore_for_file: cascade_invocations -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import '../../helpers/helpers.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final assets = [ + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group('ChromeDino', () { flameTester.test( @@ -20,19 +26,84 @@ void main() { }, ); - flameTester.test( - 'swivels', - (game) async { - // TODO(alestiago): Write golden tests to check the - // swivel animation. - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd(ChromeDino()); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + final sweepAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/up.png'), + ); - final previousPosition = chromeDino.body.position.clone(); - game.update(64); + game.update(sweepAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/middle.png'), + ); - expect(chromeDino.body.position, isNot(equals(previousPosition))); + game.update(sweepAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/down.png'), + ); }, ); + + group('swivels', () { + flameTester.test( + 'up', + (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + game.camera.followVector2(Vector2.zero()); + + final sweepAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + game.update(sweepAnimationDuration * 1.5); + + expect(chromeDino.body.angularVelocity, isPositive); + }, + ); + + flameTester.test( + 'down', + (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + game.camera.followVector2(Vector2.zero()); + + final sweepAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + game.update(sweepAnimationDuration * 0.5); + + expect(chromeDino.body.angularVelocity, isNegative); + }, + ); + }); }); } diff --git a/packages/pinball_components/test/src/components/dash_animatronic_test.dart b/packages/pinball_components/test/src/components/dash_animatronic_test.dart index d0707223..d64c3f07 100644 --- a/packages/pinball_components/test/src/components/dash_animatronic_test.dart +++ b/packages/pinball_components/test/src/components/dash_animatronic_test.dart @@ -45,6 +45,7 @@ void main() { ); }, ); + flameTester.test( 'loads correctly', (game) async { diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index 7ed97248..ff64fb00 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -12,8 +12,8 @@ void main() { group('DinoWalls', () { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.dino.dinoLandTop.keyName, - Assets.images.dino.dinoLandBottom.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index 01afebd0..9e9b5633 100644 Binary files a/packages/pinball_components/test/src/components/golden/boundaries.png and b/packages/pinball_components/test/src/components/golden/boundaries.png differ diff --git a/packages/pinball_components/test/src/components/golden/chrome_dino/down.png b/packages/pinball_components/test/src/components/golden/chrome_dino/down.png new file mode 100644 index 00000000..ef91da0a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/chrome_dino/down.png differ diff --git a/packages/pinball_components/test/src/components/golden/chrome_dino/middle.png b/packages/pinball_components/test/src/components/golden/chrome_dino/middle.png new file mode 100644 index 00000000..d4e6286a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/chrome_dino/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/chrome_dino/up.png b/packages/pinball_components/test/src/components/golden/chrome_dino/up.png new file mode 100644 index 00000000..042028d1 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/chrome_dino/up.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index 5956b43b..b47c453f 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship-rail.png b/packages/pinball_components/test/src/components/golden/spaceship-rail.png index d81f7dba..d8ce5fca 100644 Binary files a/packages/pinball_components/test/src/components/golden/spaceship-rail.png and b/packages/pinball_components/test/src/components/golden/spaceship-rail.png differ diff --git a/packages/pinball_components/test/src/components/spaceship_rail_test.dart b/packages/pinball_components/test/src/components/spaceship_rail_test.dart index d3242ff6..bc5a7f75 100644 --- a/packages/pinball_components/test/src/components/spaceship_rail_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_rail_test.dart @@ -11,13 +11,19 @@ import '../../helpers/helpers.dart'; void main() { group('SpaceshipRail', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.spaceship.rail.main.keyName, + Assets.images.spaceship.rail.exit.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); await game.addFromBlueprint(SpaceshipRail()); await game.ready(); + await tester.pump(); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 8; diff --git a/pubspec.lock b/pubspec.lock index 4a851209..9ee8ae6c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "39.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "2.8.0" args: dependency: transitive description: @@ -71,6 +71,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -316,7 +323,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.3" json_annotation: dependency: transitive description: @@ -351,7 +358,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.3" meta: dependency: transitive description: @@ -414,7 +421,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" path_provider: dependency: transitive description: @@ -587,7 +594,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -629,21 +636,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.9" typed_data: dependency: transitive description: @@ -664,7 +671,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.1" very_good_analysis: dependency: "direct dev" description: diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 37e14f73..3711105e 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -4,27 +4,69 @@ import 'package:pinball/game/game.dart'; void main() { group('GameBloc', () { - test('initial state has 3 balls and empty score', () { + test('initial state has 3 rounds and empty score', () { final gameBloc = GameBloc(); expect(gameBloc.state.score, equals(0)); - expect(gameBloc.state.balls, equals(3)); + expect(gameBloc.state.rounds, equals(3)); }); - group('LostBall', () { + group('RoundLost', () { blocTest( - 'decreases number of balls', + 'decreases number of rounds ' + 'when there are already available rounds', build: GameBloc.new, act: (bloc) { - bloc.add(const BallLost()); + bloc.add(const RoundLost()); }, expect: () => [ const GameState( score: 0, - balls: 2, + multiplier: 1, + rounds: 2, bonusHistory: [], ), ], ); + + blocTest( + 'apply multiplier to score ' + 'when round is lost', + build: GameBloc.new, + seed: () => const GameState( + score: 5, + multiplier: 3, + rounds: 2, + bonusHistory: [], + ), + act: (bloc) { + bloc.add(const RoundLost()); + }, + expect: () => [ + isA() + ..having((state) => state.score, 'score', 15) + ..having((state) => state.rounds, 'rounds', 1), + ], + ); + + blocTest( + 'resets multiplier ' + 'when round is lost', + build: GameBloc.new, + seed: () => const GameState( + score: 5, + multiplier: 3, + rounds: 2, + bonusHistory: [], + ), + act: (bloc) { + bloc.add(const RoundLost()); + }, + expect: () => [ + isA() + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.rounds, 'rounds', 1), + ], + ); }); group('Scored', () { @@ -36,16 +78,12 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState( - score: 2, - balls: 3, - bonusHistory: [], - ), - const GameState( - score: 5, - balls: 3, - bonusHistory: [], - ), + isA() + ..having((state) => state.score, 'score', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 5) + ..having((state) => state.isGameOver, 'isGameOver', false), ], ); @@ -54,27 +92,85 @@ void main() { 'when game is over', build: GameBloc.new, act: (bloc) { - for (var i = 0; i < bloc.state.balls; i++) { - bloc.add(const BallLost()); + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const RoundLost()); } bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState( - score: 0, - balls: 2, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 1, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 0, - bonusHistory: [], - ), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 0) + ..having((state) => state.isGameOver, 'isGameOver', true), + ], + ); + }); + + group('MultiplierIncreased', () { + blocTest( + 'increases multiplier ' + 'when multiplier is below 6 and game is not over', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const MultiplierIncreased()) + ..add(const MultiplierIncreased()), + expect: () => [ + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 3) + ..having((state) => state.isGameOver, 'isGameOver', false), + ], + ); + + blocTest( + "doesn't increase multiplier " + 'when multiplier is 6 and game is not over', + build: GameBloc.new, + seed: () => const GameState( + score: 0, + multiplier: 6, + rounds: 3, + bonusHistory: [], + ), + act: (bloc) => bloc..add(const MultiplierIncreased()), + expect: () => const [], + ); + + blocTest( + "doesn't increase multiplier " + 'when game is over', + build: GameBloc.new, + act: (bloc) { + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const RoundLost()); + } + bloc.add(const MultiplierIncreased()); + }, + expect: () => [ + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', true), ], ); }); @@ -88,17 +184,19 @@ void main() { act: (bloc) => bloc ..add(const BonusActivated(GameBonus.googleWord)) ..add(const BonusActivated(GameBonus.dashNest)), - expect: () => const [ - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.googleWord], - ), - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], - ), + expect: () => [ + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.googleWord], + ), + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.googleWord, GameBonus.dashNest], + ), ], ); }, @@ -109,12 +207,13 @@ void main() { 'adds game bonus', build: GameBloc.new, act: (bloc) => bloc..add(const SparkyTurboChargeActivated()), - expect: () => const [ - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.sparkyTurboCharge], - ), + expect: () => [ + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.sparkyTurboCharge], + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index d7d587bd..6a39bd67 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -5,15 +5,15 @@ import 'package:pinball/game/game.dart'; void main() { group('GameEvent', () { - group('BallLost', () { + group('RoundLost', () { test('can be instantiated', () { - expect(const BallLost(), isNotNull); + expect(const RoundLost(), isNotNull); }); test('supports value equality', () { expect( - BallLost(), - equals(const BallLost()), + RoundLost(), + equals(const RoundLost()), ); }); }); @@ -41,6 +41,19 @@ void main() { }); }); + group('MultiplierIncreased', () { + test('can be instantiated', () { + expect(const MultiplierIncreased(), isNotNull); + }); + + test('supports value equality', () { + expect( + MultiplierIncreased(), + equals(const MultiplierIncreased()), + ); + }); + }); + group('BonusActivated', () { test('can be instantiated', () { expect(const BonusActivated(GameBonus.dashNest), isNotNull); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 8170346f..add25e05 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -9,13 +9,15 @@ void main() { expect( GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: const [], ), equals( const GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ), ), @@ -27,7 +29,8 @@ void main() { expect( const GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ), isNotNull, @@ -37,12 +40,13 @@ void main() { test( 'throws AssertionError ' - 'when balls are negative', + 'when score is negative', () { expect( () => GameState( - balls: -1, - score: 0, + score: -1, + multiplier: 1, + rounds: 3, bonusHistory: const [], ), throwsAssertionError, @@ -52,12 +56,29 @@ void main() { test( 'throws AssertionError ' - 'when score is negative', + 'when multiplier is less than 1', () { expect( () => GameState( - balls: 0, - score: -1, + score: 1, + multiplier: 0, + rounds: 3, + bonusHistory: const [], + ), + throwsAssertionError, + ); + }, + ); + + test( + 'throws AssertionError ' + 'when rounds is negative', + () { + expect( + () => GameState( + score: 1, + multiplier: 1, + rounds: -1, bonusHistory: const [], ), throwsAssertionError, @@ -68,10 +89,11 @@ void main() { group('isGameOver', () { test( 'is true ' - 'when no balls are left', () { + 'when no rounds are left', () { const gameState = GameState( - balls: 0, score: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); expect(gameState.isGameOver, isTrue); @@ -79,10 +101,11 @@ void main() { test( 'is false ' - 'when one 1 ball left', () { + 'when one 1 round left', () { const gameState = GameState( - balls: 1, score: 0, + multiplier: 1, + rounds: 1, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); @@ -95,8 +118,9 @@ void main() { 'when scored is decreased', () { const gameState = GameState( - balls: 0, score: 2, + multiplier: 1, + rounds: 3, bonusHistory: [], ); expect( @@ -111,8 +135,9 @@ void main() { 'when no argument specified', () { const gameState = GameState( - balls: 0, score: 2, + multiplier: 1, + rounds: 3, bonusHistory: [], ); expect( @@ -128,12 +153,14 @@ void main() { () { const gameState = GameState( score: 2, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, - balls: gameState.balls + 1, + multiplier: gameState.multiplier + 1, + rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], ); expect(gameState, isNot(equals(otherGameState))); @@ -141,7 +168,8 @@ void main() { expect( gameState.copyWith( score: otherGameState.score, - balls: otherGameState.balls, + multiplier: otherGameState.multiplier, + rounds: otherGameState.rounds, bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), diff --git a/test/game/components/android_acres_test.dart b/test/game/components/android_acres_test.dart index 4b35d882..419524c6 100644 --- a/test/game/components/android_acres_test.dart +++ b/test/game/components/android_acres_test.dart @@ -21,6 +21,8 @@ void main() { Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.spaceship.ramp.arrow.active5.keyName, + Assets.images.spaceship.rail.main.keyName, + Assets.images.spaceship.rail.exit.keyName, Assets.images.androidBumper.a.lit.keyName, Assets.images.androidBumper.a.dimmed.keyName, Assets.images.androidBumper.b.lit.keyName, diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart deleted file mode 100644 index a73d7a50..00000000 --- a/test/game/components/board_test.dart +++ /dev/null @@ -1,122 +0,0 @@ -// 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.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); - - group('Board', () { - flameTester.test( - 'loads correctly', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - expect(game.contains(board), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'one left flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final leftFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isLeft, - ); - expect(leftFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'one right flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - final rightFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isRight, - ); - expect(rightFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'two Baseboards', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final baseboards = board.descendants().whereType(); - expect(baseboards.length, equals(2)); - }, - ); - - flameTester.test( - 'two Kickers', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final kickers = board.descendants().whereType(); - expect(kickers.length, equals(2)); - }, - ); - - flameTester.test( - 'one FlutterForest', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final flutterForest = board.descendants().whereType(); - expect(flutterForest.length, equals(1)); - }, - ); - - flameTester.test( - 'one ChromeDino', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final chromeDino = board.descendants().whereType(); - expect(chromeDino.length, equals(1)); - }, - ); - }); - }); -} diff --git a/test/game/components/bottom_group_test.dart b/test/game/components/bottom_group_test.dart new file mode 100644 index 00000000..3254f155 --- /dev/null +++ b/test/game/components/bottom_group_test.dart @@ -0,0 +1,86 @@ +// 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.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('BottomGroup', () { + flameTester.test( + 'loads correctly', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + expect(game.contains(bottomGroup), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'one left flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isLeft, + ); + expect(leftFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'one right flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final rightFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isRight, + ); + expect(rightFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'two Baseboards', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final basebottomGroups = + bottomGroup.descendants().whereType(); + expect(basebottomGroups.length, equals(2)); + }, + ); + + flameTester.test( + 'two Kickers', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final kickers = bottomGroup.descendants().whereType(); + expect(kickers.length, equals(2)); + }, + ); + }); + }); +} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index e615d508..c84ddaa7 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -53,16 +53,39 @@ void main() { }); flameBlocTester.testGameWidget( - 'lost adds BallLost to GameBloc', + "lost doesn't adds RoundLost to GameBloc " + 'when there are balls left', + setUp: (game, tester) async { + final controller = BallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + final otherBall = Ball(baseColor: const Color(0xFF00FFFF)); + final otherController = BallController(otherBall); + await otherBall.add(otherController); + await game.ensureAdd(otherBall); + + controller.lost(); + await game.ready(); + }, + verify: (game, tester) async { + verifyNever(() => gameBloc.add(const RoundLost())); + }, + ); + + flameBlocTester.testGameWidget( + 'lost adds RoundLost to GameBloc ' + 'when there are no balls left', setUp: (game, tester) async { final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); controller.lost(); + await game.ready(); }, verify: (game, tester) async { - verify(() => gameBloc.add(const BallLost())).called(1); + verify(() => gameBloc.add(const RoundLost())).called(1); }, ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 2f970254..36a8161b 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -25,7 +25,8 @@ void main() { final bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index eee2bcb0..a39bdef6 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -20,7 +20,8 @@ void main() { final bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/drain_test.dart b/test/game/components/drain_test.dart new file mode 100644 index 00000000..f1875a56 --- /dev/null +++ b/test/game/components/drain_test.dart @@ -0,0 +1,60 @@ +// 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/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Drain', () { + flameTester.test( + 'loads correctly', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(game.contains(drain), isTrue); + }, + ); + + flameTester.test( + 'body is static', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.bodyType, equals(BodyType.static)); + }, + ); + + flameTester.test( + 'is sensor', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.fixtures.first.isSensor, isTrue); + }, + ); + + test( + 'calls lost on contact with ball', + () async { + final drain = Drain(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + + drain.beginContact(ball, MockContact()); + + verify(controller.lost).called(1); + }, + ); + }); +} diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index 3de04b90..ef93892c 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -15,7 +15,8 @@ void main() { test('is true when the game over state has changed', () { final state = GameState( score: 10, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: const [], ); @@ -66,7 +67,8 @@ void main() { gameFlowController.onNewState( GameState( score: 10, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: const [], ), ); diff --git a/test/game/components/scoring_behavior_test.dart b/test/game/components/scoring_behavior_test.dart index d5e706b0..4fb07f40 100644 --- a/test/game/components/scoring_behavior_test.dart +++ b/test/game/components/scoring_behavior_test.dart @@ -43,7 +43,8 @@ void main() { bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart deleted file mode 100644 index 2905ab9a..00000000 --- a/test/game/components/wall_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -// 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/game/game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); - - group('Wall', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(game.contains(wall), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - game.contains(wall); - - expect(wall.body.position, Vector2.zero()); - }, - ); - - flameTester.test( - 'is static', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.restitution, greaterThan(0)); - }, - ); - - flameTester.test( - 'has no friction', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.friction, equals(0)); - }, - ); - }); - }); - - group( - 'BottomWall', - () { - group('removes ball on contact', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); - - flameBlocTester.testGameWidget( - 'when ball is launch', - setUp: (game, tester) async { - final ball = ControlledBall.launch( - characterTheme: game.characterTheme, - ); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'when ball is bonus', - setUp: (game, tester) async { - final ball = ControlledBall.bonus( - characterTheme: game.characterTheme, - ); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameTester.test( - 'when ball is debug', - (game) async { - final ball = ControlledBall.debug(); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - }); - }, - ); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index d7c384b1..2b2d68d8 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -3,6 +3,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -13,28 +14,56 @@ import '../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.androidBumper.a.lit.keyName, + Assets.images.androidBumper.a.dimmed.keyName, + Assets.images.androidBumper.b.lit.keyName, + Assets.images.androidBumper.b.dimmed.keyName, + Assets.images.backboard.backboardScores.keyName, + Assets.images.backboard.backboardGameOver.keyName, + Assets.images.backboard.display.keyName, + Assets.images.ball.ball.keyName, + Assets.images.ball.flameEffect.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.boundary.bottom.keyName, + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + Assets.images.dash.animatronic.keyName, Assets.images.dash.bumper.a.active.keyName, Assets.images.dash.bumper.a.inactive.keyName, Assets.images.dash.bumper.b.active.keyName, Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + Assets.images.googleWord.letter1.keyName, + Assets.images.googleWord.letter2.keyName, + Assets.images.googleWord.letter3.keyName, + Assets.images.googleWord.letter4.keyName, + Assets.images.googleWord.letter5.keyName, + Assets.images.googleWord.letter6.keyName, + Assets.images.kicker.left.keyName, + Assets.images.kicker.right.keyName, + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + Assets.images.plunger.plunger.keyName, + Assets.images.plunger.rocket.keyName, Assets.images.signpost.inactive.keyName, Assets.images.signpost.active1.keyName, Assets.images.signpost.active2.keyName, Assets.images.signpost.active3.keyName, - Assets.images.androidBumper.a.lit.keyName, - Assets.images.androidBumper.a.dimmed.keyName, - Assets.images.androidBumper.b.lit.keyName, - Assets.images.androidBumper.b.dimmed.keyName, - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, - Assets.images.sparky.animatronic.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + Assets.images.spaceship.saucer.keyName, + Assets.images.spaceship.bridge.keyName, Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.spaceship.ramp.railingBackground.keyName, @@ -45,20 +74,26 @@ void main() { Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.spaceship.ramp.arrow.active5.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - Assets.images.boundary.outer.keyName, - Assets.images.boundary.outerBottom.keyName, - Assets.images.boundary.bottom.keyName, - Assets.images.slingshot.upper.keyName, - Assets.images.slingshot.lower.keyName, - Assets.images.dino.dinoLandTop.keyName, - Assets.images.dino.dinoLandBottom.keyName, - Assets.images.multiball.lit.keyName, - Assets.images.multiball.dimmed.keyName, + Assets.images.spaceship.rail.main.keyName, + Assets.images.spaceship.rail.exit.keyName, + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, ]; + final flameTester = FlameTester( () => PinballTestGame(assets: assets), ); @@ -71,12 +106,23 @@ void main() { // TODO(alestiago): tests that Blueprints get added once the Blueprint // class is removed. flameTester.test( - 'has only one BottomWall', + 'has only one Drain', + (game) async { + await game.ready(); + expect( + game.children.whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'has only one BottomGroup', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, @@ -93,10 +139,10 @@ void main() { }, ); - flameTester.test('has one Board', (game) async { + flameTester.test('has one FlutterForest', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }); @@ -113,26 +159,23 @@ void main() { }, ); + flameTester.test( + 'one GoogleWord', + (game) async { + await game.ready(); + expect(game.children.whereType().length, equals(1)); + }, + ); + group('controller', () { - // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - // assets: assets, - ); - - flameBlocTester.testGameWidget( - 'listens when all balls are lost and there are more than 0 balls', + flameTester.testGameWidget( + 'listens when all balls are lost and there are more than 0 rounds', setUp: (game, tester) async { + // TODO(ruimiguel): check why testGameWidget doesn't add any ball + // to the game. Test needs to have no balls, so fortunately works. final newState = MockGameState(); - when(() => newState.balls).thenReturn(2); + when(() => newState.isGameOver).thenReturn(false); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); @@ -149,10 +192,10 @@ void main() { "doesn't listen when some balls are left", (game) async { final newState = MockGameState(); - when(() => newState.balls).thenReturn(1); + when(() => newState.isGameOver).thenReturn(false); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, greaterThan(0), ); expect( @@ -162,19 +205,20 @@ void main() { }, ); - flameBlocTester.test( - "doesn't listen when no balls left", - (game) async { + flameTester.testGameWidget( + "doesn't listen when game is over", + setUp: (game, tester) async { + // TODO(ruimiguel): check why testGameWidget doesn't add any ball + // to the game. Test needs to have no balls, so fortunately works. final newState = MockGameState(); - when(() => newState.balls).thenReturn(0); - + when(() => newState.isGameOver).thenReturn(true); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); await game.ready(); expect( - game.descendants().whereType().isEmpty, + game.descendants().whereType().isEmpty, isTrue, ); expect( @@ -191,14 +235,13 @@ void main() { flameTester.test( 'spawns a ball', (game) async { - await game.ready(); final previousBalls = - game.descendants().whereType().toList(); + game.descendants().whereType().toList(); game.controller.onNewState(MockGameState()); await game.ready(); final currentBalls = - game.descendants().whereType().toList(); + game.descendants().whereType().toList(); expect( currentBalls.length, @@ -210,60 +253,208 @@ void main() { ); }); }); - }); - group('DebugPinballGame', () { - debugModeFlameTester.test('adds a ball on tap up', (game) async { - await game.ready(); + group('flipper control', () { + flameTester.test('tap down moves left flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapUpEvent = MockTapUpInfo(); - when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); - final previousBalls = game.descendants().whereType().toList(); + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); - game.onTapUp(tapUpEvent); - await game.ready(); + game.onTapDown(tapDownEvent); - expect( - game.children.whereType().length, - equals(previousBalls.length + 1), - ); + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap down moves right flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(game.canvasSize); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.right, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap up moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + + flameTester.test('tap cancel moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + game.onTapCancel(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); }); - group('controller', () { - late GameBloc gameBloc; + group('plunger control', () { + flameTester.test('tap down moves plunger down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); - setUp(() { - gameBloc = GameBloc(); + final plunger = game.descendants().whereType().first; + + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); }); - final debugModeFlameBlocTester = - FlameBlocTester( - gameBuilder: DebugPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + flameTester.test('tap up releases plunger', (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - debugModeFlameBlocTester.testGameWidget( - 'ignores debug balls', - setUp: (game, tester) async { - final newState = MockGameState(); - when(() => newState.balls).thenReturn(1); + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - await game.ready(); - game.children.removeWhere((component) => component is Ball); - await game.ready(); - await game.ensureAdd(ControlledBall.debug()); + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); - expect( - game.controller.listenWhen(MockGameState(), newState), - isTrue, - ); - }, - ); + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + + flameTester.test('tap cancel releases plunger', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + game.onTapCancel(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); }); }); + + group('DebugPinballGame', () { + debugModeFlameTester.test( + 'adds a ball on tap up', + (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.all(10)); + + final raw = MockTapUpDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + when(() => tapUpEvent.raw).thenReturn(raw); + + final previousBalls = + game.descendants().whereType().toList(); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect( + game.children.whereType().length, + equals(previousBalls.length + 1), + ); + }, + ); + }); } diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index fe8bd092..d101d06e 100644 --- a/test/game/view/widgets/game_hud_test.dart +++ b/test/game/view/widgets/game_hud_test.dart @@ -28,7 +28,8 @@ void main() { const initialState = GameState( score: 1000, - balls: 2, + multiplier: 1, + rounds: 1, bonusHistory: [], ); diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index 8281ce83..dfa28869 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -11,7 +11,8 @@ void main() { late GameBloc gameBloc; const initialState = GameState( score: 0, - balls: 3, + multiplier: 1, + rounds: 3, bonusHistory: [], ); @@ -37,7 +38,7 @@ void main() { testWidgets('two active round indicator', (tester) async { final state = initialState.copyWith( - balls: 2, + rounds: 2, ); whenListen( gameBloc, @@ -68,7 +69,7 @@ void main() { testWidgets('one active round indicator', (tester) async { final state = initialState.copyWith( - balls: 1, + rounds: 1, ); whenListen( gameBloc, diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart index 0d3af694..63f7d1c5 100644 --- a/test/game/view/widgets/score_view_test.dart +++ b/test/game/view/widgets/score_view_test.dart @@ -15,7 +15,8 @@ void main() { const score = 123456789; const initialState = GameState( score: score, - balls: 1, + multiplier: 1, + rounds: 1, bonusHistory: [], ); @@ -46,7 +47,7 @@ void main() { stateController.add( initialState.copyWith( - balls: 0, + rounds: 0, ), ); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 5c052110..a6250356 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -15,9 +15,7 @@ import 'package:pinball_components/pinball_components.dart'; class MockPinballGame extends Mock implements PinballGame {} -class MockWall extends Mock implements Wall {} - -class MockBottomWall extends Mock implements BottomWall {} +class MockDrain extends Mock implements Drain {} class MockBody extends Mock implements Body {} @@ -55,8 +53,14 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class MockTapDownInfo extends Mock implements TapDownInfo {} + +class MockTapDownDetails extends Mock implements TapDownDetails {} + class MockTapUpInfo extends Mock implements TapUpInfo {} +class MockTapUpDetails extends Mock implements TapUpDetails {} + class MockEventPosition extends Mock implements EventPosition {} class MockFilter extends Mock implements Filter {}