diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 4103bb81..9dc81135 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -67,7 +67,9 @@ class BallController extends ComponentController const Duration(milliseconds: 2583), ); component.resume(); - await component.boost(Vector2(40, 110)); + await component.add( + BallTurboChargingBehavior(impulse: Vector2(40, 110)), + ); } @override diff --git a/packages/pinball_components/lib/src/components/ball/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart index 12b1c877..456dd4f1 100644 --- a/packages/pinball_components/lib/src/components/ball/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,14 +1,13 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_components/src/components/ball/behaviors/ball_gravitating_behavior.dart'; -import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'behaviors/behaviors.dart'; + /// {@template ball} /// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} @@ -81,12 +80,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { void resume() { body.gravityScale = Vector2(1, 1); } - - /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. - Future boost(Vector2 impulse) async { - body.linearVelocity = impulse; - await add(_TurboChargeSpriteAnimationComponent()); - } } class _BallSpriteComponent extends SpriteComponent with HasGameRef { @@ -101,55 +94,3 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef { anchor = Anchor.center; } } - -class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef, ZIndex { - _TurboChargeSpriteAnimationComponent() - : super( - anchor: const Anchor(0.53, 0.72), - removeOnFinish: true, - ) { - zIndex = ZIndexes.turboChargeFlame; - } - - late final Vector2 _textureSize; - - @override - Future onLoad() async { - await super.onLoad(); - - final spriteSheet = await gameRef.images.load( - Assets.images.ball.flameEffect.keyName, - ); - - const amountPerRow = 8; - const amountPerColumn = 4; - _textureSize = Vector2( - spriteSheet.width / amountPerRow, - spriteSheet.height / amountPerColumn, - ); - - animation = SpriteAnimation.fromFrameData( - spriteSheet, - SpriteAnimationData.sequenced( - amount: amountPerRow * amountPerColumn, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: _textureSize, - loop: false, - ), - ); - } - - @override - void update(double dt) { - super.update(dt); - - if (parent != null) { - final body = (parent! as BodyComponent).body; - final direction = -body.linearVelocity.normalized(); - angle = math.atan2(direction.x, -direction.y); - size = (_textureSize / 45) * body.fixtures.first.shape.radius; - } - } -} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart new file mode 100644 index 00000000..f1e5a855 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart @@ -0,0 +1,81 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ball_turbo_charging_behavior} +/// Puts the [Ball] in flames and [_impulse]s it. +/// {@endtemplate} +class BallTurboChargingBehavior extends TimerComponent with ParentIsA { + /// {@macro ball_turbo_charging_behavior} + BallTurboChargingBehavior({ + required Vector2 impulse, + }) : _impulse = impulse, + super(period: 5, removeOnFinish: true); + + final Vector2 _impulse; + + @override + Future onLoad() async { + await super.onLoad(); + + parent.body.linearVelocity = _impulse; + await parent.add(_TurboChargeSpriteAnimationComponent()); + } + + @override + void onRemove() { + parent + .firstChild<_TurboChargeSpriteAnimationComponent>()! + .removeFromParent(); + super.onRemove(); + } +} + +class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef, ZIndex, ParentIsA { + _TurboChargeSpriteAnimationComponent() + : super( + anchor: const Anchor(0.53, 0.72), + ) { + zIndex = ZIndexes.turboChargeFlame; + } + + late final Vector2 _textureSize; + + @override + void update(double dt) { + super.update(dt); + + final direction = -parent.body.linearVelocity.normalized(); + angle = math.atan2(direction.x, -direction.y); + size = (_textureSize / 45) * parent.body.fixtures.first.shape.radius; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = await gameRef.images.load( + Assets.images.ball.flameEffect.keyName, + ); + + const amountPerRow = 8; + const amountPerColumn = 4; + _textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: _textureSize, + ), + ); + } +} 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 038b7833..1068a20e 100644 --- a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -1,2 +1,3 @@ export 'ball_gravitating_behavior.dart'; export 'ball_scaling_behavior.dart'; +export 'ball_turbo_charging_behavior.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart index 7f07de97..a66459a6 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart @@ -13,8 +13,9 @@ class BallBoosterGame extends LineGame { @override void onLine(Vector2 line) { final ball = Ball(baseColor: Colors.transparent); - add(ball); + final impulse = line * -1 * 20; + ball.add(BallTurboChargingBehavior(impulse: impulse)); - ball.mounted.then((value) => ball.boost(line * -1 * 20)); + add(ball); } } diff --git a/packages/pinball_components/test/src/components/ball/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart index 02175f16..655836a0 100644 --- a/packages/pinball_components/test/src/components/ball/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -1,12 +1,10 @@ // ignore_for_file: cascade_invocations -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; import '../../../helpers/helpers.dart'; @@ -180,50 +178,5 @@ void main() { ); }); }); - - group('boost', () { - flameTester.test('applies an impulse to the ball', (game) async { - final ball = Ball(baseColor: baseColor); - await game.ensureAdd(ball); - - expect(ball.body.linearVelocity, equals(Vector2.zero())); - - await ball.boost(Vector2.all(10)); - expect(ball.body.linearVelocity.x, greaterThan(0)); - expect(ball.body.linearVelocity.y, greaterThan(0)); - }); - - flameTester.test('adds TurboChargeSpriteAnimation', (game) async { - final ball = Ball(baseColor: baseColor); - await game.ensureAdd(ball); - - await ball.boost(Vector2.all(10)); - game.update(0); - - expect( - ball.children.whereType().single, - isNotNull, - ); - }); - - flameTester.test('removes TurboChargeSpriteAnimation after it finishes', - (game) async { - final ball = Ball(baseColor: baseColor); - await game.ensureAdd(ball); - - await ball.boost(Vector2.all(10)); - game.update(0); - - final turboChargeSpriteAnimation = - ball.children.whereType().single; - - expect(ball.contains(turboChargeSpriteAnimation), isTrue); - - game.update(turboChargeSpriteAnimation.animation!.totalDuration()); - game.update(0.1); - - expect(ball.contains(turboChargeSpriteAnimation), isFalse); - }); - }); }); } diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart index de291f21..d78df37a 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart @@ -6,7 +6,6 @@ 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_components/src/components/ball/behaviors/behaviors.dart'; import '../../../../helpers/helpers.dart'; diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index cd0a0486..0aeeda98 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -6,7 +6,6 @@ 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_components/src/components/ball/behaviors/behaviors.dart'; import '../../../../helpers/helpers.dart'; diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart new file mode 100644 index 00000000..00f34832 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart @@ -0,0 +1,94 @@ +// ignore_for_file: cascade_invocations + +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallTurboChargingBehavior', + () { + final assets = [Assets.images.ball.ball.keyName]; + final flameTester = FlameTester(() => TestGame(assets)); + const baseColor = Color(0xFFFFFFFF); + + test('can be instantiated', () { + expect( + BallTurboChargingBehavior(impulse: Vector2.zero()), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test( + 'impulses the ball velocity when loaded', + (game) async { + final ball = Ball.test(baseColor: baseColor); + await game.ensureAdd(ball); + final impulse = Vector2.all(1); + final behavior = BallTurboChargingBehavior(impulse: impulse); + await ball.ensureAdd(behavior); + + expect( + ball.body.linearVelocity.x, + equals(impulse.x), + ); + expect( + ball.body.linearVelocity.y, + equals(impulse.y), + ); + }, + ); + + flameTester.test('adds sprite', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + + await ball.ensureAdd( + BallTurboChargingBehavior(impulse: Vector2.zero()), + ); + + expect( + ball.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('removes sprite after it finishes', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + + final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); + await ball.ensureAdd(behavior); + + final turboChargeSpriteAnimation = + ball.children.whereType().single; + + expect(ball.contains(turboChargeSpriteAnimation), isTrue); + + game.update(behavior.timer.limit); + game.update(0.1); + + expect(ball.contains(turboChargeSpriteAnimation), isFalse); + }); + }, + ); +} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index cfb3e157..d8d31b4e 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -99,6 +100,7 @@ void main() { group('turboCharge', () { setUpAll(() { registerFallbackValue(Vector2.zero()); + registerFallbackValue(Component()); }); flameBlocTester.testGameWidget( @@ -124,7 +126,7 @@ void main() { final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); - when(() => ball.boost(any())).thenAnswer((_) async {}); + when(() => ball.add(any())).thenAnswer((_) async {}); await controller.turboCharge(); @@ -140,29 +142,13 @@ void main() { final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); - when(() => ball.boost(any())).thenAnswer((_) async {}); + when(() => ball.add(any())).thenAnswer((_) async {}); await controller.turboCharge(); verify(ball.resume).called(1); }, ); - - flameBlocTester.test( - 'boosts the ball', - (game) async { - final gameRef = _MockPinballGame(); - final ball = _MockControlledBall(); - final controller = _WrappedBallController(ball, gameRef); - when(() => gameRef.read()).thenReturn(gameBloc); - when(() => ball.controller).thenReturn(controller); - when(() => ball.boost(any())).thenAnswer((_) async {}); - - await controller.turboCharge(); - - verify(() => ball.boost(any())).called(1); - }, - ); }); }); }