From 655007b2d28c7219cb495d1cc7e08c18f41dd11d Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 6 Apr 2022 19:06:14 +0100 Subject: [PATCH 1/3] feat: improved extra ball logic (#151) --- lib/flame/component_controller.dart | 2 +- lib/game/bloc/game_bloc.dart | 14 +- lib/game/bloc/game_state.dart | 5 +- lib/game/components/controlled_ball.dart | 60 ++----- lib/game/components/wall.dart | 10 +- lib/game/pinball_game.dart | 106 ++++++++++--- test/game/bloc/game_bloc_test.dart | 23 +-- .../game/components/controlled_ball_test.dart | 146 +----------------- test/game/components/flutter_forest_test.dart | 16 +- test/game/components/wall_test.dart | 90 +++++++---- test/game/pinball_game_test.dart | 143 ++++++++++++++++- test/helpers/extensions.dart | 2 + test/helpers/forge2d.dart | 13 ++ test/helpers/helpers.dart | 1 + test/helpers/mocks.dart | 2 + 15 files changed, 339 insertions(+), 294 deletions(-) create mode 100644 test/helpers/forge2d.dart diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart index 1d6e0173..b9568348 100644 --- a/lib/flame/component_controller.dart +++ b/lib/flame/component_controller.dart @@ -33,7 +33,7 @@ abstract class ComponentController extends Component { /// Mixin that attaches a single [ComponentController] to a [Component]. mixin Controls on Component { /// The [ComponentController] attached to this [Component]. - late final T controller; + late T controller; @override @mustCallSuper diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index c02417a7..7c1b4f44 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -19,9 +19,7 @@ class GameBloc extends Bloc { static const bonusWordScore = 10000; void _onBallLost(BallLost event, Emitter emit) { - if (state.balls > 0) { - emit(state.copyWith(balls: state.balls - 1)); - } + emit(state.copyWith(balls: state.balls - 1)); } void _onScored(Scored event, Emitter emit) { @@ -36,7 +34,8 @@ class GameBloc extends Bloc { event.letterIndex, ]; - if (newBonusLetters.length == bonusWord.length) { + final achievedBonus = newBonusLetters.length == bonusWord.length; + if (achievedBonus) { emit( state.copyWith( activatedBonusLetters: [], @@ -55,15 +54,16 @@ class GameBloc extends Bloc { } void _onDashNestActivated(DashNestActivated event, Emitter emit) { - const nestsRequiredForBonus = 3; - final newNests = { ...state.activatedDashNests, event.nestId, }; - if (newNests.length == nestsRequiredForBonus) { + + final achievedBonus = newNests.length == 3; + if (achievedBonus) { emit( state.copyWith( + balls: state.balls + 1, activatedDashNests: {}, bonusHistory: [ ...state.bonusHistory, diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index d08ba04b..bbaa4cd8 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -5,11 +5,10 @@ part of 'game_bloc.dart'; /// Defines bonuses that a player can gain during a PinballGame. enum GameBonus { /// Bonus achieved when the user activate all of the bonus - /// letters on the board, forming the bonus word + /// letters on the board, forming the bonus word. word, - /// Bonus achieved when the user activates all of the Dash - /// nests on the board, adding a new ball to the board. + /// Bonus achieved when the user activates all dash nest bumpers. dashNest, } diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 1981e39c..cef076d8 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,5 +1,4 @@ import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; import 'package:pinball/flame/flame.dart'; @@ -18,7 +17,7 @@ class ControlledBall extends Ball with Controls { ControlledBall.launch({ required PinballTheme theme, }) : super(baseColor: theme.characterTheme.ballColor) { - controller = LaunchedBallController(this); + controller = BallController(this); } /// {@template bonus_ball} @@ -29,74 +28,43 @@ class ControlledBall extends Ball with Controls { ControlledBall.bonus({ required PinballTheme theme, }) : super(baseColor: theme.characterTheme.ballColor) { - controller = BonusBallController(this); + controller = BallController(this); } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = BonusBallController(this); + controller = DebugBallController(this); } } /// {@template ball_controller} /// Controller attached to a [Ball] that handles its game related logic. /// {@endtemplate} -abstract class BallController extends ComponentController { +class BallController extends ComponentController + with HasGameRef { /// {@macro ball_controller} BallController(Ball ball) : super(ball); /// Removes the [Ball] from a [PinballGame]. /// - /// {@template ball_controller_lost} /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// a [BottomWall]. - /// {@endtemplate} - void lost(); -} - -/// {@template bonus_ball_controller} -/// {@macro ball_controller} -/// -/// A [BonusBallController] doesn't change the [GameState.balls] count. -/// {@endtemplate} -class BonusBallController extends BallController { - /// {@macro bonus_ball_controller} - BonusBallController(Ball component) : super(component); - - @override void lost() { component.shouldRemove = true; } -} - -/// {@template launched_ball_controller} -/// {@macro ball_controller} -/// -/// A [LaunchedBallController] changes the [GameState.balls] count. -/// {@endtemplate} -class LaunchedBallController extends BallController - with HasGameRef, BlocComponent { - /// {@macro launched_ball_controller} - LaunchedBallController(Ball ball) : super(ball); @override - bool listenWhen(GameState? previousState, GameState newState) { - return (previousState?.balls ?? 0) > newState.balls; + void onRemove() { + super.onRemove(); + gameRef.read().add(const BallLost()); } +} - @override - void onNewState(GameState state) { - super.onNewState(state); - component.shouldRemove = true; - if (state.balls > 0) gameRef.spawnBall(); - } +/// {@macro ball_controller} +class DebugBallController extends BallController { + /// {@macro ball_controller} + DebugBallController(Ball component) : super(component); - /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if - /// any are left. - /// - /// {@macro ball_controller_lost} @override - void lost() { - gameRef.read().add(const BallLost()); - } + void onRemove() {} } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 030edc50..ba8af5e7 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -71,12 +71,12 @@ class BottomWall extends Wall { } /// {@template bottom_wall_ball_contact_callback} -/// Listens when a [Ball] falls into a [BottomWall]. +/// Listens when a [ControlledBall] falls into a [BottomWall]. /// {@endtemplate} -class BottomWallBallContactCallback extends ContactCallback { +class BottomWallBallContactCallback + extends ContactCallback { @override - void begin(Ball ball, BottomWall wall, Contact contact) { - // TODO(alestiago): replace with .firstChild when available. - ball.children.whereType().first.lost(); + void begin(ControlledBall ball, BottomWall wall, Contact contact) { + ball.controller.lost(); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index a6eb0884..7a0e6823 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -5,6 +5,7 @@ import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -12,29 +13,38 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets; class PinballGame extends Forge2DGame - with FlameBloc, HasKeyboardHandlerComponents { - PinballGame({required this.theme, required this.audio}) { + with + FlameBloc, + HasKeyboardHandlerComponents, + Controls<_GameBallsController> { + PinballGame({ + required this.theme, + required this.audio, + }) { images.prefix = ''; + controller = _GameBallsController(this); } final PinballTheme theme; final PinballAudio audio; - @override - void onAttach() { - super.onAttach(); - spawnBall(); - } - @override Future onLoad() async { _addContactCallbacks(); + // Fix camera on the center of the board. + camera + ..followVector2(Vector2(0, -7.8)) + ..zoom = size.y / 16; await _addGameBoundaries(); unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(LaunchRamp())); - unawaited(_addPlunger()); + + final plunger = Plunger(compressionDistance: 29) + ..initialPosition = Vector2(38, -19); + await add(plunger); + unawaited(add(Board())); unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); @@ -52,10 +62,8 @@ class PinballGame extends Forge2DGame ), ); - // Fix camera on the center of the board. - camera - ..followVector2(Vector2(0, -7.8)) - ..zoom = size.y / 16; + controller.attachTo(plunger); + await super.onLoad(); } void _addContactCallbacks() { @@ -69,12 +77,6 @@ class PinballGame extends Forge2DGame createBoundaries(this).forEach(add); } - Future _addPlunger() async { - final plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(38, -19); - await add(plunger); - } - Future _addBonusWord() async { await add( BonusWord( @@ -85,13 +87,49 @@ class PinballGame extends Forge2DGame ), ); } +} - Future spawnBall() async { - // TODO(alestiago): Remove once this logic is moved to controller. +class _GameBallsController extends ComponentController + with BlocComponent, HasGameRef { + _GameBallsController(PinballGame game) : super(game); + + late final Plunger _plunger; + + @override + bool listenWhen(GameState? previousState, GameState newState) { + final noBallsLeft = component.descendants().whereType().isEmpty; + final canBallRespawn = newState.balls > 0; + + return noBallsLeft && canBallRespawn; + } + + @override + void onNewState(GameState state) { + super.onNewState(state); + _spawnBall(); + } + + @override + Future onLoad() async { + await super.onLoad(); + _spawnBall(); + } + + void _spawnBall() { final ball = ControlledBall.launch( - theme: theme, - )..initialPosition = Vector2(38, -19 + Ball.size.y); - await add(ball); + theme: gameRef.theme, + )..initialPosition = Vector2( + _plunger.body.position.x, + _plunger.body.position.y + Ball.size.y, + ); + component.add(ball); + } + + /// Attaches the controller to the plunger. + // TODO(alestiago): Remove this method and use onLoad instead. + // ignore: use_setters_to_change_properties + void attachTo(Plunger plunger) { + _plunger = plunger; } } @@ -102,7 +140,9 @@ class DebugPinballGame extends PinballGame with TapDetector { }) : super( theme: theme, audio: audio, - ); + ) { + controller = _DebugGameBallsController(this); + } @override Future onLoad() async { @@ -134,3 +174,19 @@ class DebugPinballGame extends PinballGame with TapDetector { ); } } + +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; + } +} diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index f4b79001..8ec53106 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -12,13 +12,10 @@ void main() { group('LostBall', () { blocTest( - "doesn't decrease ball " - 'when no balls left', + 'decreases number of balls', build: GameBloc.new, act: (bloc) { - for (var i = 0; i <= bloc.state.balls; i++) { - bloc.add(const BallLost()); - } + bloc.add(const BallLost()); }, expect: () => [ const GameState( @@ -28,20 +25,6 @@ void main() { activatedDashNests: {}, bonusHistory: [], ), - const GameState( - score: 0, - balls: 1, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), ], ); }); @@ -230,7 +213,7 @@ void main() { ), GameState( score: 0, - balls: 3, + balls: 4, activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 05056484..53847b3c 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -13,42 +13,12 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); - group('BonusBallController', () { - late Ball ball; - - setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); - }); - - test('can be instantiated', () { - expect( - BonusBallController(ball), - isA(), - ); - }); - - flameTester.test( - 'lost removes ball', - (game) async { - await game.add(ball); - final controller = BonusBallController(ball); - await ball.ensureAdd(controller); - - controller.lost(); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - }); - - group('LaunchedBallController', () { + group('BallController', () { test('can be instantiated', () { expect( - LaunchedBallController(MockBall()), - isA(), + BallController(MockBall()), + isA(), ); }); @@ -74,7 +44,7 @@ void main() { flameBlocTester.testGameWidget( 'lost adds BallLost to GameBloc', setUp: (game, tester) async { - final controller = LaunchedBallController(ball); + final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); @@ -84,114 +54,6 @@ void main() { verify(() => gameBloc.add(const BallLost())).called(1); }, ); - - group('listenWhen', () { - flameBlocTester.testGameWidget( - 'listens when a ball has been lost', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - - await ball.add(controller); - await game.ensureAdd(ball); - }, - verify: (game, tester) async { - final controller = - game.descendants().whereType().first; - - final previousState = MockGameState(); - final newState = MockGameState(); - when(() => previousState.balls).thenReturn(3); - when(() => newState.balls).thenReturn(2); - - expect(controller.listenWhen(previousState, newState), isTrue); - }, - ); - - flameBlocTester.testGameWidget( - 'does not listen when a ball has not been lost', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - - await ball.add(controller); - await game.ensureAdd(ball); - }, - verify: (game, tester) async { - final controller = - game.descendants().whereType().first; - - final previousState = MockGameState(); - final newState = MockGameState(); - when(() => previousState.balls).thenReturn(3); - when(() => newState.balls).thenReturn(3); - - expect(controller.listenWhen(previousState, newState), isFalse); - }, - ); - }); - - group('onNewState', () { - flameBlocTester.testGameWidget( - 'removes ball', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final state = MockGameState(); - when(() => state.balls).thenReturn(1); - controller.onNewState(state); - await game.ready(); - }, - verify: (game, tester) async { - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'spawns a new ball when the ball is not the last one', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final state = MockGameState(); - when(() => state.balls).thenReturn(1); - - final previousBalls = game.descendants().whereType().toList(); - controller.onNewState(state); - await game.ready(); - - final currentBalls = game.descendants().whereType().toList(); - - expect(currentBalls.contains(ball), isFalse); - expect(currentBalls.length, equals(previousBalls.length)); - }, - ); - - flameBlocTester.testGameWidget( - 'does not spawn a new ball is the last one', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final state = MockGameState(); - when(() => state.balls).thenReturn(0); - - final previousBalls = game.descendants().whereType().toList(); - controller.onNewState(state); - await game.ready(); - - final currentBalls = game.descendants().whereType(); - - expect(currentBalls.contains(ball), isFalse); - expect( - currentBalls.length, - equals((previousBalls..remove(ball)).length), - ); - }, - ); - }); }); }); } diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 60c55be9..d85fe54f 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -1,7 +1,6 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,18 +10,6 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; -void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { - assert( - bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty, - 'Bodies require fixtures to contact each other.', - ); - - final fixtureA = bodyA.body.fixtures.first; - final fixtureB = bodyB.body.fixtures.first; - final contact = Contact.init(fixtureA, 0, fixtureB, 0); - game.world.contactManager.contactListener?.beginContact(contact); -} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballGameTest.new); @@ -92,7 +79,7 @@ void main() { ); flameBlocTester.testGameWidget( - 'listens when a Bonus.dashNest is added', + 'listens when a Bonus.dashNest and a bonusBall is added', verify: (game, tester) async { final flutterForest = FlutterForest(); @@ -103,6 +90,7 @@ void main() { activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], ); + expect( flutterForest.controller .listenWhen(const GameState.initial(), state), diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 18c7ea5b..f8e7483c 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -3,40 +3,15 @@ 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(Forge2DGame.new); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('Wall', () { - group('BottomWallBallContactCallback', () { - test( - 'removes the ball on begin contact when the wall is a bottom one', - () { - final wall = MockBottomWall(); - final ballController = MockBallController(); - final ball = MockBall(); - final componentSet = MockComponentSet(); - - when(() => componentSet.whereType()) - .thenReturn([ballController]); - when(() => ball.children).thenReturn(componentSet); - - BottomWallBallContactCallback() - // Remove once https://github.com/flame-engine/flame/pull/1415 - // is merged - ..end(MockBall(), MockBottomWall(), MockContact()) - ..begin(ball, wall, MockContact()); - - verify(ballController.lost).called(1); - }, - ); - }); - flameTester.test( 'loads correctly', (game) async { @@ -123,4 +98,67 @@ void main() { ); }); }); + + group( + 'BottomWall', + () { + group('removes ball on contact', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'when ball is launch', + setUp: (game, tester) async { + final ball = ControlledBall.launch(theme: game.theme); + final wall = BottomWall(); + await game.ensureAddAll([ball, wall]); + game.addContactCallback(BottomWallBallContactCallback()); + + 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(theme: game.theme); + final wall = BottomWall(); + await game.ensureAddAll([ball, wall]); + game.addContactCallback(BottomWallBallContactCallback()); + + 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]); + game.addContactCallback(BottomWallBallContactCallback()); + + 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 f418bad0..d83bb396 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -10,11 +11,11 @@ import 'package:pinball_components/pinball_components.dart'; import '../helpers/helpers.dart'; void main() { - group('PinballGame', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.new); - final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGameTest.new); + final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); + group('PinballGame', () { // TODO(alestiago): test if [PinballGame] registers // [BallScorePointsCallback] once the following issue is resolved: // https://github.com/flame-engine/flame/issues/1416 @@ -60,8 +61,106 @@ void main() { equals(1), ); }); + + group('controller', () { + // TODO(alestiago): Write test to be controller agnostic. + group('listenWhen', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'listens when all balls are lost and there are more than 0 balls', + setUp: (game, tester) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(2); + game.descendants().whereType().forEach( + (ball) => ball.controller.lost(), + ); + await game.ready(); + + expect( + game.controller.listenWhen(MockGameState(), newState), + isTrue, + ); + }, + ); + + flameTester.test( + "doesn't listen when some balls are left", + (game) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(1); + + expect( + game.descendants().whereType().length, + greaterThan(0), + ); + expect( + game.controller.listenWhen(MockGameState(), newState), + isFalse, + ); + }, + ); + + flameBlocTester.test( + "doesn't listen when no balls left", + (game) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(0); + + game.descendants().whereType().forEach( + (ball) => ball.controller.lost(), + ); + await game.ready(); + + expect( + game.descendants().whereType().isEmpty, + isTrue, + ); + expect( + game.controller.listenWhen(MockGameState(), newState), + isFalse, + ); + }, + ); + }); + + group( + 'onNewState', + () { + flameTester.test( + 'spawns a ball', + (game) async { + await game.ready(); + final previousBalls = + game.descendants().whereType().toList(); + + game.controller.onNewState(MockGameState()); + await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + + expect( + currentBalls.length, + equals(previousBalls.length + 1), + ); + }, + ); + }, + ); + }); }); + }); + group('DebugPinballGame', () { debugModeFlameTester.test('adds a ball on tap up', (game) async { await game.ready(); @@ -71,12 +170,46 @@ void main() { final tapUpEvent = MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + final previousBalls = game.descendants().whereType().toList(); + game.onTapUp(tapUpEvent); await game.ready(); expect( game.children.whereType().length, - equals(1), + equals(previousBalls.length + 1), + ); + }); + + group('controller', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final debugModeFlameBlocTester = + FlameBlocTester( + gameBuilder: DebugPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + debugModeFlameBlocTester.testGameWidget( + 'ignores debug balls', + setUp: (game, tester) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(1); + + await game.ready(); + game.children.removeWhere((component) => component is Ball); + await game.ready(); + await game.ensureAdd(ControlledBall.debug()); + + expect( + game.controller.listenWhen(MockGameState(), newState), + isTrue, + ); + }, ); }); }); diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index 4731eec4..8e054fe0 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,3 +1,5 @@ +// ignore_for_file: must_call_super + import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; diff --git a/test/helpers/forge2d.dart b/test/helpers/forge2d.dart new file mode 100644 index 00000000..f000d404 --- /dev/null +++ b/test/helpers/forge2d.dart @@ -0,0 +1,13 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { + assert( + bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty, + 'Bodies require fixtures to contact each other.', + ); + + final fixtureA = bodyA.body.fixtures.first; + final fixtureB = bodyB.body.fixtures.first; + final contact = Contact.init(fixtureA, 0, fixtureB, 0); + game.world.contactManager.contactListener?.beginContact(contact); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index d9dc2a17..4b6c29f1 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -7,6 +7,7 @@ export 'builders.dart'; export 'extensions.dart'; export 'fakes.dart'; +export 'forge2d.dart'; export 'key_testers.dart'; export 'mocks.dart'; export 'navigator.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index c0dec5f5..748b48f3 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -21,6 +21,8 @@ class MockBody extends Mock implements Body {} class MockBall extends Mock implements Ball {} +class MockControlledBall extends Mock implements ControlledBall {} + class MockBallController extends Mock implements BallController {} class MockContact extends Mock implements Contact {} From 2f40dcc971ffae367817bd7ec75912ee2bc8d9d5 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:22:38 -0500 Subject: [PATCH 2/3] feat: add slingshots (#148) * feat: add slingshots * test: slingshot * feat: add slingshot to sandbox * chore: add todo --- lib/game/game_assets.dart | 4 + lib/game/pinball_game.dart | 1 + .../assets/images/slingshot/left_lower.png | Bin 0 -> 6458 bytes .../assets/images/slingshot/left_upper.png | Bin 0 -> 6061 bytes .../assets/images/slingshot/right_lower.png | Bin 0 -> 6542 bytes .../assets/images/slingshot/right_upper.png | Bin 0 -> 6002 bytes .../lib/gen/assets.gen.dart | 21 +++ .../lib/src/components/components.dart | 1 + .../lib/src/components/slingshot.dart | 138 ++++++++++++++++++ packages/pinball_components/pubspec.yaml | 1 + .../pinball_components/sandbox/lib/main.dart | 1 + .../lib/stories/slingshot/slingshot_game.dart | 66 +++++++++ .../lib/stories/slingshot/stories.dart | 17 +++ .../sandbox/lib/stories/stories.dart | 1 + .../test/src/components/golden/slingshots.png | Bin 0 -> 49944 bytes .../test/src/components/slingshot_test.dart | 97 ++++++++++++ 16 files changed, 348 insertions(+) create mode 100644 packages/pinball_components/assets/images/slingshot/left_lower.png create mode 100644 packages/pinball_components/assets/images/slingshot/left_upper.png create mode 100644 packages/pinball_components/assets/images/slingshot/right_lower.png create mode 100644 packages/pinball_components/assets/images/slingshot/right_upper.png create mode 100644 packages/pinball_components/lib/src/components/slingshot.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart create mode 100644 packages/pinball_components/test/src/components/golden/slingshots.png create mode 100644 packages/pinball_components/test/src/components/slingshot_test.dart diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 47175c32..050b2cd3 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -15,6 +15,10 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.baseboard.right.keyName), images.load(components.Assets.images.kicker.left.keyName), images.load(components.Assets.images.kicker.right.keyName), + images.load(components.Assets.images.slingshot.leftUpper.keyName), + images.load(components.Assets.images.slingshot.leftLower.keyName), + images.load(components.Assets.images.slingshot.rightUpper.keyName), + images.load(components.Assets.images.slingshot.rightLower.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName), images.load( components.Assets.images.launchRamp.foregroundRailing.keyName, diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 7a0e6823..2ccf8fe8 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -46,6 +46,7 @@ class PinballGame extends Forge2DGame await add(plunger); unawaited(add(Board())); + unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); unawaited(addFromBlueprint(SpaceshipRamp())); diff --git a/packages/pinball_components/assets/images/slingshot/left_lower.png b/packages/pinball_components/assets/images/slingshot/left_lower.png new file mode 100644 index 0000000000000000000000000000000000000000..b44b58fbca8025771379cb8ac421e789a896c9ee GIT binary patch literal 6458 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 OAs7vVn>A{7k^cjqC8WIo literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/slingshot/right_lower.png b/packages/pinball_components/assets/images/slingshot/right_lower.png new file mode 100644 index 0000000000000000000000000000000000000000..71a6a277828fd5d0f99dc5cae819e067e5d3df90 GIT binary patch literal 6542 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 UBvxtz{=tIK(=yU5*KmmXA3&?lwg3PC literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/slingshot/right_upper.png b/packages/pinball_components/assets/images/slingshot/right_upper.png new file mode 100644 index 0000000000000000000000000000000000000000..e6b42ded4f2a513501042baaba58a1a7174ac402 GIT binary patch literal 6002 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 F{|64fsRRH3 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index de59219e..518d3237 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -29,6 +29,7 @@ class $AssetsImagesGen { $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSparkyBumperGen get sparkyBumper => const $AssetsImagesSparkyBumperGen(); @@ -127,6 +128,26 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesSlingshotGen { + const $AssetsImagesSlingshotGen(); + + /// File path: assets/images/slingshot/left_lower.png + AssetGenImage get leftLower => + const AssetGenImage('assets/images/slingshot/left_lower.png'); + + /// File path: assets/images/slingshot/left_upper.png + AssetGenImage get leftUpper => + const AssetGenImage('assets/images/slingshot/left_upper.png'); + + /// File path: assets/images/slingshot/right_lower.png + AssetGenImage get rightLower => + const AssetGenImage('assets/images/slingshot/right_lower.png'); + + /// File path: assets/images/slingshot/right_upper.png + AssetGenImage get rightUpper => + const AssetGenImage('assets/images/slingshot/right_upper.png'); +} + class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 6b0c2ef5..14d657d5 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -16,6 +16,7 @@ export 'launch_ramp.dart'; export 'layer.dart'; export 'ramp_opening.dart'; export 'shapes/shapes.dart'; +export 'slingshot.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; diff --git a/packages/pinball_components/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart new file mode 100644 index 00000000..0ebe13ce --- /dev/null +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -0,0 +1,138 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template slingshots} +/// A [Blueprint] which creates the left and right pairs of [Slingshot]s. +/// {@endtemplate} +class Slingshots extends Forge2DBlueprint { + @override + void build(_) { + // TODO(allisonryan0002): use radians values instead of converting degrees. + final leftUpperSlingshot = Slingshot( + length: 5.66, + angle: -1.5 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftUpper.keyName, + )..initialPosition = Vector2(-29, 1.5); + + final leftLowerSlingshot = Slingshot( + length: 3.54, + angle: -29.1 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftLower.keyName, + )..initialPosition = Vector2(-31, -6.2); + + final rightUpperSlingshot = Slingshot( + length: 5.64, + angle: 1 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightUpper.keyName, + )..initialPosition = Vector2(22.3, 1.58); + + final rightLowerSlingshot = Slingshot( + length: 3.46, + angle: 26.8 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightLower.keyName, + )..initialPosition = Vector2(24.7, -6.2); + + addAll([ + leftUpperSlingshot, + leftLowerSlingshot, + rightUpperSlingshot, + rightLowerSlingshot, + ]); + } +} + +/// {@template slingshot} +/// Elastic bumper that bounces the [Ball] off of its straight sides. +/// {@endtemplate} +class Slingshot extends BodyComponent with InitialPosition { + /// {@macro slingshot} + Slingshot({ + required double length, + required double angle, + required String spritePath, + }) : _length = length, + _angle = angle, + _spritePath = spritePath, + super(priority: 1); + + final double _length; + + final double _angle; + + final String _spritePath; + + List _createFixtureDefs() { + final fixturesDef = []; + const circleRadius = 1.55; + + final topCircleShape = CircleShape()..radius = circleRadius; + topCircleShape.position.setValues(0, _length / 2); + final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0; + fixturesDef.add(topCircleFixtureDef); + + final bottomCircleShape = CircleShape()..radius = circleRadius; + bottomCircleShape.position.setValues(0, -_length / 2); + final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0; + fixturesDef.add(bottomCircleFixtureDef); + + final leftEdgeShape = EdgeShape() + ..set( + Vector2(circleRadius, _length / 2), + Vector2(circleRadius, -_length / 2), + ); + final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape) + ..friction = 0 + ..restitution = 5; + fixturesDef.add(leftEdgeShapeFixtureDef); + + final rightEdgeShape = EdgeShape() + ..set( + Vector2(-circleRadius, _length / 2), + Vector2(-circleRadius, -_length / 2), + ); + final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape) + ..friction = 0 + ..restitution = 5; + fixturesDef.add(rightEdgeShapeFixtureDef); + + return fixturesDef; + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition + ..angle = _angle; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + renderBody = false; + } + + Future _loadSprite() async { + final sprite = await gameRef.loadSprite(_spritePath); + + await add( + SpriteComponent( + sprite: sprite, + size: sprite.originalSize / 10, + anchor: Anchor.center, + angle: _angle, + ), + ); + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 312e01f3..b6f71b8b 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -39,6 +39,7 @@ flutter: - assets/images/spaceship/ramp/ - assets/images/chrome_dino/ - assets/images/kicker/ + - assets/images/slingshot/ - assets/images/sparky_bumper/a/ - assets/images/sparky_bumper/b/ - assets/images/sparky_bumper/c/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 88b86da6..481ca781 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -21,6 +21,7 @@ void main() { addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); addKickerStories(dashbook); + addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart new file mode 100644 index 00000000..c02689ca --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart @@ -0,0 +1,66 @@ +import 'dart:math' as math; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SlingshotGame extends BasicBallGame { + SlingshotGame({ + required this.trace, + }) : super(color: const Color(0xFFFF0000)); + + static const info = ''' + Shows how Slingshots are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + final bool trace; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + + final leftUpperSlingshot = Slingshot( + length: 5.66, + angle: -1.5 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftUpper.keyName, + )..initialPosition = center + Vector2(-29, 1.5); + + final leftLowerSlingshot = Slingshot( + length: 3.54, + angle: -29.1 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftLower.keyName, + )..initialPosition = center + Vector2(-31, -6.2); + + final rightUpperSlingshot = Slingshot( + length: 5.64, + angle: 1 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightUpper.keyName, + )..initialPosition = center + Vector2(22.3, 1.58); + + final rightLowerSlingshot = Slingshot( + length: 3.46, + angle: 26.8 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightLower.keyName, + )..initialPosition = center + Vector2(24.7, -6.2); + + await addAll([ + leftUpperSlingshot, + leftLowerSlingshot, + rightUpperSlingshot, + rightLowerSlingshot, + ]); + + if (trace) { + leftUpperSlingshot.trace(); + leftLowerSlingshot.trace(); + rightUpperSlingshot.trace(); + rightLowerSlingshot.trace(); + } + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart new file mode 100644 index 00000000..6e985d32 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart @@ -0,0 +1,17 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/slingshot/slingshot_game.dart'; + +void addSlingshotStories(Dashbook dashbook) { + dashbook.storiesOf('Slingshots').add( + 'Basic', + (context) => GameWidget( + game: SlingshotGame( + trace: context.boolProperty('Trace', true), + ), + ), + codeLink: buildSourceLink('slingshot_game/basic.dart'), + info: SlingshotGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 746d83d6..c5d60a8d 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -5,5 +5,6 @@ export 'dash_nest_bumper/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'layer/stories.dart'; +export 'slingshot/stories.dart'; export 'spaceship/stories.dart'; export 'sparky_bumper/stories.dart'; diff --git a/packages/pinball_components/test/src/components/golden/slingshots.png b/packages/pinball_components/test/src/components/golden/slingshots.png new file mode 100644 index 0000000000000000000000000000000000000000..2e4ada7b6a610b9dce69f08eabff4dc68bc8c29c GIT binary patch literal 49944 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, + // matchesGoldenFile('golden/slingshots.png'), + // ); + // }, + ); + + flameTester.test( + 'loads correctly', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + expect(game.contains(slingshot), isTrue); + }, + ); + + flameTester.test( + 'body is static', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + expect(slingshot.body.bodyType, equals(BodyType.static)); + }, + ); + + flameTester.test( + 'has restitution', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + final totalRestitution = slingshot.body.fixtures.fold( + 0, + (total, fixture) => total + fixture.restitution, + ); + expect(totalRestitution, greaterThan(0)); + }, + ); + + flameTester.test( + 'has no friction', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + final totalFriction = slingshot.body.fixtures.fold( + 0, + (total, fixture) => total + fixture.friction, + ); + expect(totalFriction, equals(0)); + }, + ); + }); +} From daebb0b749f2888f47ff24040e1862921fac62c0 Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 6 Apr 2022 15:51:54 -0300 Subject: [PATCH 3/3] feat: adding camera zoom effect (#153) * feat: adding camera zoom effect * testing ci * testing ci * Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * testing ci * testing ci * ci * feat: pr suggestion * fix: lint * fix: lint * Apply suggestions from code review Co-authored-by: Alejandro Santiago Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Alejandro Santiago --- .github/workflows/pinball_components.yaml | 2 +- .../lib/src/components/camera_zoom.dart | 56 ++++++++++++ .../lib/src/components/components.dart | 1 + .../pinball_components/sandbox/lib/main.dart | 1 + .../sandbox/lib/stories/stories.dart | 1 + .../lib/stories/zoom/basic_zoom_game.dart | 37 ++++++++ .../sandbox/lib/stories/zoom/stories.dart | 15 ++++ .../test/src/components/camera_zoom_test.dart | 85 ++++++++++++++++++ .../golden/camera_zoom/finished.png | Bin 0 -> 34758 bytes .../golden/camera_zoom/in_between.png | Bin 0 -> 34158 bytes .../components/golden/camera_zoom/no_zoom.png | Bin 0 -> 31706 bytes 11 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 packages/pinball_components/lib/src/components/camera_zoom.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/zoom/stories.dart create mode 100644 packages/pinball_components/test/src/components/camera_zoom_test.dart create mode 100644 packages/pinball_components/test/src/components/golden/camera_zoom/finished.png create mode 100644 packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png create mode 100644 packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png diff --git a/.github/workflows/pinball_components.yaml b/.github/workflows/pinball_components.yaml index bf1907f8..e4154059 100644 --- a/.github/workflows/pinball_components.yaml +++ b/.github/workflows/pinball_components.yaml @@ -13,7 +13,7 @@ on: jobs: build: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@b075749771679a5baa4c90d36ad2e8580bbf273b with: working_directory: packages/pinball_components coverage_excludes: "lib/gen/*.dart" diff --git a/packages/pinball_components/lib/src/components/camera_zoom.dart b/packages/pinball_components/lib/src/components/camera_zoom.dart new file mode 100644 index 00000000..a3da382e --- /dev/null +++ b/packages/pinball_components/lib/src/components/camera_zoom.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flutter/material.dart'; + +/// {@template camera_zoom} +/// Applies zoom to the camera of the game where this is added to +/// {@endtemplate} +class CameraZoom extends Effect with HasGameRef { + /// {@macro camera_zoom} + CameraZoom({ + required this.value, + }) : super( + EffectController( + duration: 0.4, + curve: Curves.easeOut, + ), + ); + + /// The total zoom value to be applied to the camera + final double value; + + late final Tween _tween; + + final Completer _completer = Completer(); + + @override + Future onLoad() async { + _tween = Tween( + begin: gameRef.camera.zoom, + end: value, + ); + } + + @override + void apply(double progress) { + gameRef.camera.zoom = _tween.transform(progress); + } + + /// Returns a [Future] that completes once the zoom is finished + Future get completed { + if (controller.completed) { + return Future.value(); + } + + return _completer.future; + } + + @override + void onRemove() { + _completer.complete(); + + super.onRemove(); + } +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 14d657d5..8ac1a0f9 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -3,6 +3,7 @@ export 'baseboard.dart'; export 'board_dimensions.dart'; export 'board_side.dart'; export 'boundaries.dart'; +export 'camera_zoom.dart'; export 'chrome_dino.dart'; export 'dash_nest_bumper.dart'; export 'dino_walls.dart'; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 481ca781..3d65dbe2 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -23,5 +23,6 @@ void main() { addKickerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); + addZoomStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index c5d60a8d..d7409e87 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -8,3 +8,4 @@ export 'layer/stories.dart'; export 'slingshot/stories.dart'; export 'spaceship/stories.dart'; export 'sparky_bumper/stories.dart'; +export 'zoom/stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart b/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart new file mode 100644 index 00000000..276dd39c --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BasicCameraZoomGame extends BasicGame with TapDetector { + static const info = ''' + Simple game to demonstrate how the CameraZoom can be used. + Tap to zoom in/out + '''; + + bool zoomApplied = false; + + @override + Future onLoad() async { + final sprite = await loadSprite(Assets.images.flutterSignPost.keyName); + + await add( + SpriteComponent( + sprite: sprite, + size: Vector2(4, 8), + anchor: Anchor.center, + ), + ); + + camera.followVector2(Vector2.zero()); + } + + @override + void onTap() { + if (firstChild() == null) { + final zoom = CameraZoom(value: zoomApplied ? 30 : 10); + add(zoom); + zoomApplied = !zoomApplied; + } + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart b/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart new file mode 100644 index 00000000..653d5491 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart @@ -0,0 +1,15 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/zoom/basic_zoom_game.dart'; + +void addZoomStories(Dashbook dashbook) { + dashbook.storiesOf('CameraZoom').add( + 'Basic', + (context) => GameWidget( + game: BasicCameraZoomGame(), + ), + codeLink: buildSourceLink('zoom/basic_zoom_game.dart'), + info: BasicCameraZoomGame.info, + ); +} diff --git a/packages/pinball_components/test/src/components/camera_zoom_test.dart b/packages/pinball_components/test/src/components/camera_zoom_test.dart new file mode 100644 index 00000000..00f43847 --- /dev/null +++ b/packages/pinball_components/test/src/components/camera_zoom_test.dart @@ -0,0 +1,85 @@ +// ignore_for_file: cascade_invocations + +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() { + group('CameraZoom', () { + final tester = FlameTester(TestGame.new); + + tester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 10; + final sprite = await game.loadSprite( + Assets.images.flutterSignPost.keyName, + ); + + await game.add( + SpriteComponent( + sprite: sprite, + size: Vector2(4, 8), + anchor: Anchor.center, + ), + ); + + await game.add(CameraZoom(value: 40)); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/camera_zoom/no_zoom.png'), + ); + + game.update(0.2); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/camera_zoom/in_between.png'), + ); + + game.update(0.4); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/camera_zoom/finished.png'), + ); + game.update(0.1); + await tester.pump(); + + expect(game.firstChild(), isNull); + }, + ); + + tester.test( + 'completes when checked after it is finished', + (game) async { + await game.add(CameraZoom(value: 40)); + game.update(10); + final cameraZoom = game.firstChild(); + final future = cameraZoom!.completed; + + expect(future, completes); + }, + ); + + tester.test( + 'completes when checked before it is finished', + (game) async { + final zoom = CameraZoom(value: 40); + final future = zoom.completed; + + await game.add(zoom); + game.update(10); + game.update(0); + + expect(future, completes); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png new file mode 100644 index 0000000000000000000000000000000000000000..be784ada97497fb7eea0808c5b4af683fd8cfae2 GIT binary patch literal 34758 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png new file mode 100644 index 0000000000000000000000000000000000000000..3809f0d045dfc8fbfa34a5d954e0ae90b05b3546 GIT binary patch literal 34158 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