diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 659dd994..27a339c8 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -66,9 +66,8 @@ class BallController extends ComponentController // given animations. component.stop(); await Future.delayed(const Duration(seconds: 1)); - component - ..resume() - ..boost(Vector2(200, 500)); + component.resume(); + await component.boost(Vector2(40, 110)); } @override diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index e7c7a343..a480e38f 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -7,7 +7,8 @@ extension PinballGameAssetsX on PinballGame { /// Returns a list of assets to be loaded List preLoadAssets() { return [ - images.load(components.Assets.images.ball.keyName), + images.load(components.Assets.images.ball.ball.keyName), + images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.flipper.left.keyName), images.load(components.Assets.images.flipper.right.keyName), diff --git a/packages/pinball_components/assets/images/ball.png b/packages/pinball_components/assets/images/ball/ball.png similarity index 100% rename from packages/pinball_components/assets/images/ball.png rename to packages/pinball_components/assets/images/ball/ball.png diff --git a/packages/pinball_components/assets/images/ball/flame_effect.png b/packages/pinball_components/assets/images/ball/flame_effect.png new file mode 100644 index 00000000..03a6fca6 Binary files /dev/null and b/packages/pinball_components/assets/images/ball/flame_effect.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 4b1dbc96..0f08c3d8 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -13,10 +13,7 @@ class $AssetsImagesGen { $AssetsImagesAlienBumperGen get alienBumper => const $AssetsImagesAlienBumperGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); - - /// File path: assets/images/ball.png - AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); - + $AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => @@ -63,6 +60,17 @@ class $AssetsImagesBackboardGen { const AssetGenImage('assets/images/backboard/display.png'); } +class $AssetsImagesBallGen { + const $AssetsImagesBallGen(); + + /// File path: assets/images/ball/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/ball/ball.png'); + + /// File path: assets/images/ball/flame_effect.png + AssetGenImage get flameEffect => + const AssetGenImage('assets/images/ball/flame_effect.png'); +} + class $AssetsImagesBaseboardGen { const $AssetsImagesBaseboardGen(); diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 36059cfd..abbfefc8 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template ball} @@ -49,9 +50,6 @@ class Ball extends BodyComponent /// The base [Color] used to tint this [Ball]. final Color baseColor; - double _boostTimer = 0; - static const _boostDuration = 2.0; - @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; @@ -87,32 +85,20 @@ class Ball extends BodyComponent body.gravityScale = Vector2(0, 1); } + /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. + Future boost(Vector2 impulse) async { + body.linearVelocity = impulse; + await add(_TurboChargeSpriteAnimationComponent()); + } + @override void update(double dt) { super.update(dt); - if (_boostTimer > 0) { - _boostTimer -= dt; - final direction = body.linearVelocity.normalized(); - final effect = FireEffect( - burstPower: _boostTimer, - direction: direction, - position: Vector2(body.position.x, body.position.y), - priority: priority - 1, - ); - - unawaited(gameRef.add(effect)); - } _rescaleSize(); _setPositionalGravity(); } - /// Applies a boost on this [Ball]. - void boost(Vector2 impulse) { - body.linearVelocity = impulse; - _boostTimer = _boostDuration; - } - void _rescaleSize() { final boardHeight = BoardDimensions.bounds.height; const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; @@ -153,10 +139,61 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef { Future onLoad() async { await super.onLoad(); final sprite = await gameRef.loadSprite( - Assets.images.ball.keyName, + Assets.images.ball.ball.keyName, ); this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; } } + +class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef { + _TurboChargeSpriteAnimationComponent() + : super( + anchor: const Anchor(0.53, 0.72), + priority: Ball.boardPriority + 1, + removeOnFinish: true, + ); + + 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/pubspec.yaml b/packages/pinball_components/pubspec.yaml index be06949c..da4446c1 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -39,6 +39,7 @@ flutter: assets: - assets/images/ + - assets/images/ball/ - assets/images/baseboard/ - assets/images/boundary/ - assets/images/dino/ diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball_test.dart index a0a73e2b..26a03886 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball_test.dart @@ -1,5 +1,6 @@ // 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'; @@ -169,20 +170,41 @@ void main() { expect(ball.body.linearVelocity, equals(Vector2.zero())); - ball.boost(Vector2.all(10)); + await ball.boost(Vector2.all(10)); expect(ball.body.linearVelocity.x, greaterThan(0)); expect(ball.body.linearVelocity.y, greaterThan(0)); }); - flameTester.test('adds fire effect components to the game', (game) async { + flameTester.test('adds TurboChargeSpriteAnimation', (game) async { final ball = Ball(baseColor: Colors.blue); await game.ensureAdd(ball); - ball.boost(Vector2.all(10)); + await ball.boost(Vector2.all(10)); game.update(0); - await game.ready(); - expect(game.children.whereType().length, greaterThan(0)); + expect( + ball.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('removes TurboChargeSpriteAnimation after it finishes', + (game) async { + final ball = Ball(baseColor: Colors.blue); + 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/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 96c67dd4..e615d508 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -94,6 +94,7 @@ void main() { final controller = WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); + when(() => ball.boost(any())).thenAnswer((_) async {}); await controller.turboCharge(); @@ -109,6 +110,7 @@ void main() { final controller = WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); + when(() => ball.boost(any())).thenAnswer((_) async {}); await controller.turboCharge(); @@ -124,6 +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 {}); await controller.turboCharge();