diff --git a/lib/game/behaviors/scoring_behavior.dart b/lib/game/behaviors/scoring_behavior.dart index c48c13e2..a30c753b 100644 --- a/lib/game/behaviors/scoring_behavior.dart +++ b/lib/game/behaviors/scoring_behavior.dart @@ -1,11 +1,55 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +/// {@template scoring_behavior} +/// Adds [_points] to the score and shows a text effect. +/// +/// The behavior removes itself after the effect is completed. +/// {@endtemplate} +class ScoringBehavior extends Component with HasGameRef { + /// {@macto scoring_behavior} + ScoringBehavior({ + required Points points, + required Vector2 position, + double duration = 1, + }) : _points = points, + _position = position, + _effectController = EffectController( + duration: duration, + ); + + final Points _points; + final Vector2 _position; + + final EffectController _effectController; + + @override + void update(double dt) { + super.update(dt); + if (_effectController.completed) { + removeFromParent(); + } + } + + @override + Future onLoad() async { + gameRef.read().add(Scored(points: _points.value)); + await gameRef.firstChild()!.add( + ScoreComponent( + points: _points, + position: _position, + effectController: _effectController, + ), + ); + } +} + /// {@template scoring_contact_behavior} /// Adds points to the score when the [Ball] contacts the [parent]. /// {@endtemplate} @@ -23,12 +67,11 @@ class ScoringContactBehavior extends ContactBehavior super.beginContact(other, contact); if (other is! Ball) return; - gameRef.read().add(Scored(points: _points.value)); - gameRef.firstChild()!.add( - ScoreComponent( - points: _points, - position: other.body.position, - ), - ); + parent.add( + ScoringBehavior( + points: _points, + position: other.body.position, + ), + ); } } diff --git a/packages/pinball_components/lib/src/components/score_component.dart b/packages/pinball_components/lib/src/components/score_component.dart index 12d198cb..5f95878a 100644 --- a/packages/pinball_components/lib/src/components/score_component.dart +++ b/packages/pinball_components/lib/src/components/score_component.dart @@ -23,16 +23,20 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex { ScoreComponent({ required this.points, required Vector2 position, - }) : super( + required EffectController effectController, + }) : _effectController = effectController, + super( position: position, anchor: Anchor.center, ) { zIndex = ZIndexes.score; } + late Points points; + late final Effect _effect; - late Points points; + final EffectController _effectController; @override Future onLoad() async { @@ -46,7 +50,7 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex { await add( _effect = MoveEffect.by( Vector2(0, -5), - EffectController(duration: 1), + _effectController, ), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart index ce14d7b6..423dde98 100644 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart @@ -1,3 +1,4 @@ +import 'package:flame/effects.dart'; import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_theme/pinball_theme.dart'; @@ -52,6 +53,7 @@ class BackboardGameOverGame extends AssetsGame points: components.Points.values .firstWhere((element) => element.value == score), position: Vector2(0, 50), + effectController: EffectController(duration: 1), ), ); }, diff --git a/packages/pinball_components/sandbox/lib/stories/score/score_game.dart b/packages/pinball_components/sandbox/lib/stories/score/score_game.dart index 4bde5018..edb4fa36 100644 --- a/packages/pinball_components/sandbox/lib/stories/score/score_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/score/score_game.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:flame/effects.dart'; import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; @@ -38,6 +39,7 @@ class ScoreGame extends AssetsGame with TapDetector { ScoreComponent( points: score, position: info.eventPosition.game..multiply(Vector2(1, -1)), + effectController: EffectController(duration: 1), ), ); } diff --git a/packages/pinball_components/test/src/components/score_component_test.dart b/packages/pinball_components/test/src/components/score_component_test.dart index 69688874..f2bd52e3 100644 --- a/packages/pinball_components/test/src/components/score_component_test.dart +++ b/packages/pinball_components/test/src/components/score_component_test.dart @@ -28,6 +28,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); }, @@ -46,6 +47,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -67,6 +69,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -88,6 +91,7 @@ void main() { ScoreComponent( points: Points.fiveThousand, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -113,6 +117,7 @@ void main() { ScoreComponent( points: Points.twentyThousand, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -138,6 +143,7 @@ void main() { ScoreComponent( points: Points.twoHundredThousand, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -163,6 +169,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); diff --git a/test/game/behaviors/scoring_behavior_test.dart b/test/game/behaviors/scoring_behavior_test.dart index 4476f494..3e710641 100644 --- a/test/game/behaviors/scoring_behavior_test.dart +++ b/test/game/behaviors/scoring_behavior_test.dart @@ -34,80 +34,177 @@ void main() { Assets.images.score.oneMillion.keyName, ]; - group('ScoringContactBehavior', () { - group('beginContact', () { - late GameBloc bloc; - late Ball ball; - late BodyComponent parent; - - setUp(() { - ball = _MockBall(); - final ballBody = _MockBody(); - when(() => ball.body).thenReturn(ballBody); - when(() => ballBody.position).thenReturn(Vector2.all(4)); - - parent = _TestBodyComponent(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () { - bloc = _MockGameBloc(); - const state = GameState( - score: 0, - multiplier: 1, - rounds: 3, - bonusHistory: [], - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - assets: assets, - ); + late GameBloc bloc; + late Ball ball; + late BodyComponent parent; - flameBlocTester.testGameWidget( - 'emits Scored event with points', - setUp: (game, tester) async { - const points = Points.oneMillion; - final behavior = ScoringContactBehavior(points: points); - await parent.add(behavior); - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); - - behavior.beginContact(ball, _MockContact()); - - verify( - () => bloc.add( - Scored(points: points.value), - ), - ).called(1); - }, - ); + setUp(() { + ball = _MockBall(); + final ballBody = _MockBody(); + when(() => ball.body).thenReturn(ballBody); + when(() => ballBody.position).thenReturn(Vector2.all(4)); + + parent = _TestBodyComponent(); + }); - flameBlocTester.testGameWidget( - "adds a ScoreComponent at Ball's position with points", - setUp: (game, tester) async { - const points = Points.oneMillion; - final behavior = ScoringContactBehavior(points: points); - await parent.add(behavior); - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); - - behavior.beginContact(ball, _MockContact()); - await game.ready(); - - final scoreText = game.descendants().whereType(); - expect(scoreText.length, equals(1)); - expect( - scoreText.first.points, - equals(points), - ); - expect( - scoreText.first.position, - equals(ball.body.position), - ); - }, + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () { + bloc = _MockGameBloc(); + const state = GameState( + score: 0, + multiplier: 1, + rounds: 3, + bonusHistory: [], + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + assets: assets, + ); + + group('ScoringBehavior', () { + test('can be instantiated', () { + expect( + ScoringBehavior( + points: Points.fiveThousand, + position: Vector2.zero(), + ), + isA(), ); }); + + flameBlocTester.testGameWidget( + 'can be loaded', + setUp: (game, tester) async { + final canvas = ZCanvasComponent(children: [parent]); + final behavior = ScoringBehavior( + points: Points.fiveThousand, + position: Vector2.zero(), + ); + await parent.add(behavior); + await game.ensureAdd(canvas); + + expect( + parent.firstChild(), + equals(behavior), + ); + }, + ); + + flameBlocTester.testGameWidget( + 'emits Scored event with points when added', + setUp: (game, tester) async { + const points = Points.oneMillion; + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + final behavior = ScoringBehavior( + points: points, + position: Vector2(0, 0), + ); + await parent.ensureAdd(behavior); + + verify( + () => bloc.add( + Scored(points: points.value), + ), + ).called(1); + }, + ); + + flameBlocTester.testGameWidget( + 'correctly renders text', + setUp: (game, tester) async { + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + const points = Points.oneMillion; + final position = Vector2.all(1); + final behavior = ScoringBehavior( + points: points, + position: position, + ); + await parent.ensureAdd(behavior); + + final scoreText = game.descendants().whereType(); + expect(scoreText.length, equals(1)); + expect( + scoreText.first.points, + equals(points), + ); + expect( + scoreText.first.position, + equals(position), + ); + }, + ); + + flameBlocTester.testGameWidget( + 'is removed after duration', + setUp: (game, tester) async { + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + const duration = 2.0; + final behavior = ScoringBehavior( + points: Points.oneMillion, + position: Vector2(0, 0), + duration: duration, + ); + await parent.ensureAdd(behavior); + + game.update(duration); + game.update(0); + await tester.pump(); + }, + verify: (game, _) async { + expect( + game.descendants().whereType(), + isEmpty, + ); + }, + ); + }); + + group('ScoringContactBehavior', () { + flameBlocTester.testGameWidget( + 'beginContact adds a ScoringBehavior', + setUp: (game, tester) async { + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + final behavior = ScoringContactBehavior(points: Points.oneMillion); + await parent.ensureAdd(behavior); + + behavior.beginContact(ball, _MockContact()); + await game.ready(); + + expect( + parent.firstChild(), + isNotNull, + ); + }, + ); + + flameBlocTester.testGameWidget( + "beginContact positions text at contact's position", + setUp: (game, tester) async { + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + final behavior = ScoringContactBehavior(points: Points.oneMillion); + await parent.ensureAdd(behavior); + + behavior.beginContact(ball, _MockContact()); + await game.ready(); + + final scoreText = game.descendants().whereType(); + expect( + scoreText.first.position, + equals(ball.body.position), + ); + }, + ); }); }