diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index 301bc61e..5900f2b3 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,5 +1,6 @@ export 'ball_spawning_behavior.dart'; export 'ball_theming_behavior.dart'; +export 'bonus_ball_spawning_behavior.dart'; export 'bonus_noise_behavior.dart'; export 'bumper_noise_behavior.dart'; export 'camera_focusing_behavior.dart'; diff --git a/lib/game/behaviors/bonus_ball_spawning_behavior.dart b/lib/game/behaviors/bonus_ball_spawning_behavior.dart new file mode 100644 index 00000000..26fe423d --- /dev/null +++ b/lib/game/behaviors/bonus_ball_spawning_behavior.dart @@ -0,0 +1,30 @@ +import 'package:flame/components.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template bonus_ball_spawning_behavior} +/// After a duration, spawns a bonus ball from the [DinoWalls] and boosts it +/// into the middle of the board. +/// {@endtemplate} +class BonusBallSpawningBehavior extends TimerComponent with HasGameRef { + /// {@macro bonus_ball_spawning_behavior} + BonusBallSpawningBehavior() + : super( + period: 5, + removeOnFinish: true, + ); + + @override + void onTick() { + final characterTheme = readBloc() + .state + .characterTheme; + gameRef.descendants().whereType().single.add( + Ball(assetPath: characterTheme.ball.keyName) + ..add(BallImpulsingBehavior(impulse: Vector2(-40, 0))) + ..initialPosition = Vector2(29.2, -24.5) + ..zIndex = ZIndexes.ballOnBoard, + ); + } +} diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index f37299c7..bf1b52c4 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -1,7 +1,7 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -22,7 +22,6 @@ class FlutterForestBonusBehavior extends Component final bumpers = parent.children.whereType(); final signpost = parent.firstChild()!; final animatronic = parent.firstChild()!; - final canvas = gameRef.descendants().whereType().single; for (final bumper in bumpers) { bumper.bloc.stream.listen((state) { @@ -38,15 +37,7 @@ class FlutterForestBonusBehavior extends Component if (signpost.bloc.isFullyProgressed()) { bloc.add(const BonusActivated(GameBonus.dashNest)); - final characterTheme = - readBloc() - .state - .characterTheme; - canvas.add( - Ball(assetPath: characterTheme.ball.keyName) - ..initialPosition = Vector2(29.2, -24.5) - ..zIndex = ZIndexes.ballOnBoard, - ); + add(BonusBallSpawningBehavior()); animatronic.playing = true; signpost.bloc.onProgressed(); } diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_impulsing_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_impulsing_behavior.dart new file mode 100644 index 00000000..d875ef7c --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_impulsing_behavior.dart @@ -0,0 +1,22 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ball_impulsing_behavior} +/// Impulses the [Ball] in a given direction. +/// {@endtemplate} +class BallImpulsingBehavior extends Component with ParentIsA { + /// {@macro ball_impulsing_behavior} + BallImpulsingBehavior({ + required Vector2 impulse, + }) : _impulse = impulse; + + final Vector2 _impulse; + + @override + Future onLoad() async { + await super.onLoad(); + parent.body.linearVelocity = _impulse; + shouldRemove = true; + } +} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart index 1068a20e..d2be36a9 100644 --- a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -1,3 +1,4 @@ export 'ball_gravitating_behavior.dart'; +export 'ball_impulsing_behavior.dart'; export 'ball_scaling_behavior.dart'; export 'ball_turbo_charging_behavior.dart'; diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_implusing_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_implusing_behavior_test.dart new file mode 100644 index 00000000..53ab4553 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_implusing_behavior_test.dart @@ -0,0 +1,53 @@ +// 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 'package:pinball_theme/pinball_theme.dart' as theme; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallImpulsingBehavior', + () { + final asset = theme.Assets.images.dash.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + test('can be instantiated', () { + expect( + BallImpulsingBehavior(impulse: Vector2.zero()), + isA(), + ); + }); + + flameTester.test( + 'impulses the ball with the given velocity when loaded ' + 'and then removes itself', + (game) async { + final ball = Ball.test(); + await game.ensureAdd(ball); + final impulse = Vector2.all(1); + final behavior = BallImpulsingBehavior(impulse: impulse); + await ball.ensureAdd(behavior); + + expect( + ball.body.linearVelocity.x, + equals(impulse.x), + ); + expect( + ball.body.linearVelocity.y, + equals(impulse.y), + ); + expect( + game.descendants().whereType().isEmpty, + isTrue, + ); + }, + ); + }, + ); +} diff --git a/test/game/behaviors/bonus_ball_spawning_behavior_test.dart b/test/game/behaviors/bonus_ball_spawning_behavior_test.dart new file mode 100644 index 00000000..1aacf506 --- /dev/null +++ b/test/game/behaviors/bonus_ball_spawning_behavior_test.dart @@ -0,0 +1,61 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + theme.Assets.images.dash.ball.keyName, + ]); + } + + Future pump(BonusBallSpawningBehavior child) async { + await ensureAdd( + FlameBlocProvider.value( + value: CharacterThemeCubit(), + children: [ + ZCanvasComponent( + children: [child], + ), + ], + ), + ); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('FlutterForestBonusBehavior', () { + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'adds a ball with a BallImpulsingBehavior to the game onTick ' + 'resulting in a -40 x impulse', + (game) async { + await game.onLoad(); + final behavior = BonusBallSpawningBehavior(); + + await game.pump(behavior); + + game.update(behavior.timer.limit); + await game.ready(); + + final ball = game.descendants().whereType().single; + + expect(ball.body.linearVelocity.x, equals(-40)); + expect(ball.body.linearVelocity.y, equals(0)); + }, + ); + }); +} diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index 0d058c70..0b75a61e 100644 --- a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart +++ b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart @@ -5,9 +5,9 @@ import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; @@ -27,13 +27,8 @@ class _TestGame extends Forge2DGame { required GameBloc gameBloc, }) async { await ensureAdd( - FlameMultiBlocProvider( - providers: [ - FlameBlocProvider.value(value: gameBloc), - FlameBlocProvider.value( - value: CharacterThemeCubit(), - ), - ], + FlameBlocProvider.value( + value: gameBloc, children: [ ZCanvasComponent( children: [child], @@ -94,7 +89,7 @@ void main() { ); flameTester.testGameWidget( - 'adds a new Ball to the game ' + 'adds BonusBallSpawningBehavior to the game ' 'when bumpers are activated three times', setUp: (game, tester) async { await game.onLoad(); @@ -121,7 +116,7 @@ void main() { await game.ready(); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, equals(1), ); },