diff --git a/lib/game/behaviors/scoring_behavior.dart b/lib/game/behaviors/scoring_behavior.dart index 119efe6f..84597838 100644 --- a/lib/game/behaviors/scoring_behavior.dart +++ b/lib/game/behaviors/scoring_behavior.dart @@ -1,33 +1,77 @@ // 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 when the [Ball] contacts the [parent]. +/// Adds [_points] to the score and shows a text effect. +/// +/// The behavior removes itself after the duration. /// {@endtemplate} -class ScoringBehavior extends ContactBehavior with HasGameRef { - /// {@macro scoring_behavior} +class ScoringBehavior extends Component with HasGameRef { + /// {@macto scoring_behavior} ScoringBehavior({ required Points points, - }) : _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 beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! Ball) return; + void update(double dt) { + super.update(dt); + if (_effectController.completed) { + removeFromParent(); + } + } + @override + Future onLoad() async { gameRef.read().add(Scored(points: _points.value)); - gameRef.firstChild()!.add( + await gameRef.firstChild()!.add( ScoreComponent( points: _points, - position: other.body.position, + position: _position, + effectController: _effectController, ), ); } } + +/// {@template scoring_contact_behavior} +/// Adds points to the score when the [Ball] contacts the [parent]. +/// {@endtemplate} +class ScoringContactBehavior extends ContactBehavior + with HasGameRef { + /// {@macro scoring_contact_behavior} + ScoringContactBehavior({ + required Points points, + }) : _points = points; + + final Points _points; + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + parent.add( + ScoringBehavior( + points: _points, + position: other.body.position, + ), + ); + } +} diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 032c5b22..82b71741 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -20,24 +20,24 @@ class AndroidAcres extends Component { AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( children: [ - ScoringBehavior(points: Points.twoHundredThousand), + ScoringContactBehavior(points: Points.twoHundredThousand), ], )..initialPosition = Vector2(-26, -28.25), AndroidBumper.a( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-25, 1.3), AndroidBumper.b( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-32.8, -9.2), AndroidBumper.cow( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-20.5, -13.8), diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index 8def273f..d7856e48 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -52,7 +52,8 @@ class _BottomGroupSide extends Component { final kicker = Kicker( side: _side, children: [ - ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']), + ScoringContactBehavior(points: Points.fiveThousand) + ..applyTo(['bouncy_edge']), ], )..initialPosition = Vector2( (22.64 * direction) + centerXAdjustment, diff --git a/lib/game/components/dino_desert/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart index e415c173..9ba9c71b 100644 --- a/lib/game/components/dino_desert/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -17,7 +17,7 @@ class DinoDesert extends Component { children: [ ChromeDino( children: [ - ScoringBehavior(points: Points.twoHundredThousand) + ScoringContactBehavior(points: Points.twoHundredThousand) ..applyTo(['inside_mouth']), ], )..initialPosition = Vector2(12.6, -6.9), diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index f2b93d00..259b6bb2 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -18,25 +18,25 @@ class FlutterForest extends Component with ZIndex { children: [ Signpost( children: [ - ScoringBehavior(points: Points.fiveThousand), + ScoringContactBehavior(points: Points.fiveThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(8.35, -58.3), DashNestBumper.main( children: [ - ScoringBehavior(points: Points.twoHundredThousand), + ScoringContactBehavior(points: Points.twoHundredThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(22.3, -46.75), diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart index a2f6470a..76bac244 100644 --- a/lib/game/components/google_word/google_word.dart +++ b/lib/game/components/google_word/google_word.dart @@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex { children: [ GoogleLetter( 0, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(-13.1, 1.72), GoogleLetter( 1, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(-8.33, -0.75), GoogleLetter( 2, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(-2.88, -1.85), GoogleLetter( 3, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(2.88, -1.85), GoogleLetter( 4, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(8.33, -0.75), GoogleLetter( 5, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(13.1, 1.72), GoogleWordBonusBehavior(), ], diff --git a/lib/game/components/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart index 7ce83c7a..5a266b4e 100644 --- a/lib/game/components/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch.dart @@ -17,19 +17,19 @@ class SparkyScorch extends Component { children: [ SparkyBumper.a( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.b( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-21.25, -57.9), SparkyBumper.c( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-3.3, -52.55), @@ -51,7 +51,7 @@ class SparkyComputerSensor extends BodyComponent : super( renderBody: false, children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), ], ); 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 07c2753a..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('ScoringBehavior', () { - 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; + + setUp(() { + ball = _MockBall(); + final ballBody = _MockBody(); + when(() => ball.body).thenReturn(ballBody); + when(() => ballBody.position).thenReturn(Vector2.all(4)); + + parent = _TestBodyComponent(); + }); - flameBlocTester.testGameWidget( - 'emits Scored event with points', - setUp: (game, tester) async { - const points = Points.oneMillion; - final scoringBehavior = ScoringBehavior(points: points); - await parent.add(scoringBehavior); - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); - - scoringBehavior.beginContact(ball, _MockContact()); - - verify( - () => bloc.add( - Scored(points: points.value), - ), - ).called(1); - }, + 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, + ); - flameBlocTester.testGameWidget( - "adds a ScoreComponent at Ball's position with points", - setUp: (game, tester) async { - const points = Points.oneMillion; - final scoringBehavior = ScoringBehavior(points: points); - await parent.add(scoringBehavior); - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); - - scoringBehavior.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), - ); - }, + 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), + ); + }, + ); }); } diff --git a/test/game/components/dino_desert/dino_desert_test.dart b/test/game/components/dino_desert/dino_desert_test.dart index d4c39dbe..63e45e5b 100644 --- a/test/game/components/dino_desert/dino_desert_test.dart +++ b/test/game/components/dino_desert/dino_desert_test.dart @@ -68,13 +68,13 @@ void main() { group('adds', () { flameTester.test( - 'ScoringBehavior to ChromeDino', + 'ScoringContactBehavior to ChromeDino', (game) async { await game.ensureAdd(DinoDesert()); final chromeDino = game.descendants().whereType().single; expect( - chromeDino.firstChild(), + chromeDino.firstChild(), isNotNull, ); },