diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index a312daee..fc7d9b12 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -15,6 +15,7 @@ class Board extends Component { final bottomGroup = _BottomGroup(); final flutterForest = FlutterForest(); + final sparkyFireZone = SparkyFireZone(); // TODO(alestiago): adjust positioning to real design. final dino = ChromeDino() @@ -27,6 +28,7 @@ class Board extends Component { bottomGroup, dino, flutterForest, + sparkyFireZone, ]); } } diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index 9d88f0f5..cd945d58 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -1,21 +1,62 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -// TODO(ruimiguel): create and add SparkyFireZone component here in other PR. +/// {@template sparky_fire_zone} +/// Area positioned at the top left of the [Board] where the [Ball] +/// can bounce off [SparkyBumper]s. +// [Component]. +class SparkyFireZone extends Component with HasGameRef { + /// {@macro sparky_fire_zone} + SparkyFireZone(); -// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done -// ignore: public_member_api_docs + @override + Future onLoad() async { + await super.onLoad(); + + gameRef.addContactCallback(_ControlledSparkyBumperBallContactCallback()); + + final lowerLeftBumper = ControlledSparkyBumper.a() + ..initialPosition = Vector2(-23.15, 41.65); + final upperLeftBumper = ControlledSparkyBumper.b() + ..initialPosition = Vector2(-21.25, 58.15); + final rightBumper = ControlledSparkyBumper.c() + ..initialPosition = Vector2(-3.56, 53.051); + + await addAll([ + lowerLeftBumper, + upperLeftBumper, + rightBumper, + ]); + } +} + +/// {@template controlled_sparky_bumper} +/// [SparkyBumper] with [_SparkyBumperController] attached. +/// {@endtemplate} class ControlledSparkyBumper extends SparkyBumper - with Controls<_SparkyBumperController> { - // TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done - // ignore: public_member_api_docs - ControlledSparkyBumper() : super.a() { + with Controls<_SparkyBumperController>, ScorePoints { + ///{@macro controlled_sparky_bumper} + ControlledSparkyBumper.a() : super.a() { controller = _SparkyBumperController(this); } + + ///{@macro controlled_sparky_bumper} + ControlledSparkyBumper.b() : super.b() { + controller = _SparkyBumperController(this); + } + + ///{@macro controlled_sparky_bumper} + ControlledSparkyBumper.c() : super.c() { + controller = _SparkyBumperController(this); + } + + @override + int get points => 20; } /// {@template sparky_bumper_controller} @@ -42,3 +83,16 @@ class _SparkyBumperController extends ComponentController isActivated = !isActivated; } } + +/// Listens when a [Ball] bounces bounces against a [SparkyBumper]. +class _ControlledSparkyBumperBallContactCallback + extends ContactCallback, Ball> { + @override + void begin( + Controls<_SparkyBumperController> controlledSparkyBumper, + Ball _, + Contact __, + ) { + controlledSparkyBumper.controller.hit(); + } +} diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart index dceaa9cc..da8d8404 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_fire_zone_test.dart @@ -1,8 +1,13 @@ // ignore_for_file: cascade_invocations +import 'dart:ui'; + +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; @@ -11,13 +16,57 @@ void main() { final flameTester = FlameTester(EmptyPinballGameTest.new); group('SparkyFireZone', () { + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final sparkyFireZone = SparkyFireZone(); + await game.ensureAdd(sparkyFireZone); + + expect(game.contains(sparkyFireZone), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'three SparkyBumper', + (game) async { + await game.ready(); + final sparkyFireZone = SparkyFireZone(); + await game.ensureAdd(sparkyFireZone); + + expect( + sparkyFireZone.descendants().whereType().length, + equals(3), + ); + }, + ); + }); + group('bumpers', () { late ControlledSparkyBumper controlledSparkyBumper; + late Ball ball; + late GameBloc gameBloc; + + setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); flameTester.testGameWidget( 'activate when deactivated bumper is hit', setUp: (game, tester) async { - controlledSparkyBumper = ControlledSparkyBumper(); + controlledSparkyBumper = ControlledSparkyBumper.a(); await game.ensureAdd(controlledSparkyBumper); controlledSparkyBumper.controller.hit(); @@ -30,7 +79,7 @@ void main() { flameTester.testGameWidget( 'deactivate when activated bumper is hit', setUp: (game, tester) async { - controlledSparkyBumper = ControlledSparkyBumper(); + controlledSparkyBumper = ControlledSparkyBumper.a(); await game.ensureAdd(controlledSparkyBumper); controlledSparkyBumper.controller.hit(); @@ -40,6 +89,27 @@ void main() { expect(controlledSparkyBumper.controller.isActivated, isFalse); }, ); + + flameBlocTester.testGameWidget( + 'add Scored event', + setUp: (game, tester) async { + final sparkyFireZone = SparkyFireZone(); + await game.ensureAdd(sparkyFireZone); + await game.ensureAdd(ball); + game.addContactCallback(BallScorePointsCallback(game)); + + final bumpers = sparkyFireZone.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + verify( + () => gameBloc.add( + Scored(points: bumper.points), + ), + ).called(1); + } + }, + ); }); }); }