From aafc254ad3a2a7477f85d799e96ba4df39f6659f Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Tue, 12 Apr 2022 17:18:34 +0200 Subject: [PATCH] feat: add alien bumper (#166) * feat: added alien bumpers * test: tests for alien bumpers * feat: sandbox for alien bumpers * refactor: changed alien bumper ellipses * feat: added alien bumpers zone to game * test: tests for alien zone * refactor: final size and positions for alien bumpers * feat: added new alien zone bumpers * test: changed tests for alien * chore: api doc * refactor: alien sandbox traceable * Update lib/game/components/alien_zone.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: moved alien from board to pinball and fixed sprites * test: fixed test for alien at pinball * Update packages/pinball_components/sandbox/lib/stories/alien_bumper/alien_bumper_game.dart Co-authored-by: Alejandro Santiago * test: clean flamebloc test * refactor: ControlledAlienBumper visible for testing * chore: removed unused file * test: fixed alien test in pinball * refactor: refactored alien dashbook stories * chore: analysis error Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Alejandro Santiago --- lib/game/components/alien_zone.dart | 95 +++++++++++++++ lib/game/components/components.dart | 1 + lib/game/pinball_game.dart | 1 + .../assets/images/alien_bumper/a/active.png | Bin 0 -> 8423 bytes .../assets/images/alien_bumper/a/inactive.png | Bin 0 -> 4856 bytes .../assets/images/alien_bumper/b/active.png | Bin 0 -> 6292 bytes .../assets/images/alien_bumper/b/inactive.png | Bin 0 -> 4586 bytes .../lib/gen/assets.gen.dart | 105 ++++++++++------ .../lib/src/components/alien_bumper.dart | 109 +++++++++++++++++ .../lib/src/components/components.dart | 1 + packages/pinball_components/pubspec.yaml | 2 + .../pinball_components/sandbox/lib/main.dart | 1 + .../alien_zone/alien_bumper_a_game.dart | 28 +++++ .../alien_zone/alien_bumper_b_game.dart | 28 +++++ .../lib/stories/alien_zone/stories.dart | 25 ++++ .../sandbox/lib/stories/stories.dart | 1 + .../src/components/alien_bumper_test.dart | 68 +++++++++++ test/game/components/alien_zone_test.dart | 115 ++++++++++++++++++ test/game/pinball_game_test.dart | 8 ++ 19 files changed, 552 insertions(+), 36 deletions(-) create mode 100644 lib/game/components/alien_zone.dart create mode 100644 packages/pinball_components/assets/images/alien_bumper/a/active.png create mode 100644 packages/pinball_components/assets/images/alien_bumper/a/inactive.png create mode 100644 packages/pinball_components/assets/images/alien_bumper/b/active.png create mode 100644 packages/pinball_components/assets/images/alien_bumper/b/inactive.png create mode 100644 packages/pinball_components/lib/src/components/alien_bumper.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart create mode 100644 packages/pinball_components/test/src/components/alien_bumper_test.dart create mode 100644 test/game/components/alien_zone_test.dart diff --git a/lib/game/components/alien_zone.dart b/lib/game/components/alien_zone.dart new file mode 100644 index 00000000..3d8b75ae --- /dev/null +++ b/lib/game/components/alien_zone.dart @@ -0,0 +1,95 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template alien_zone} +/// Area positioned below [Spaceship] where the [Ball] +/// can bounce off [AlienBumper]s. +/// +/// When a [Ball] hits [AlienBumper]s, they toggle between activated and +/// deactivated states. +/// {@endtemplate} +class AlienZone extends Component with HasGameRef { + /// {@macro alien_zone} + AlienZone(); + + @override + Future onLoad() async { + await super.onLoad(); + + gameRef.addContactCallback(_ControlledAlienBumperBallContactCallback()); + + final lowerBumper = ControlledAlienBumper.a() + ..initialPosition = Vector2(-32.52, 9.34); + final upperBumper = ControlledAlienBumper.b() + ..initialPosition = Vector2(-22.89, 17.43); + + await addAll([ + lowerBumper, + upperBumper, + ]); + } +} + +/// {@template controlled_alien_bumper} +/// [AlienBumper] with [_AlienBumperController] attached. +/// {@endtemplate} +@visibleForTesting +class ControlledAlienBumper extends AlienBumper + with Controls<_AlienBumperController>, ScorePoints { + /// {@macro controlled_alien_bumper} + ControlledAlienBumper.a() : super.a() { + controller = _AlienBumperController(this); + } + + /// {@macro controlled_alien_bumper} + ControlledAlienBumper.b() : super.b() { + controller = _AlienBumperController(this); + } + + @override + // TODO(ruimiguel): change points when get final points map. + int get points => 20; +} + +/// {@template alien_bumper_controller} +/// Controls a [AlienBumper]. +/// {@endtemplate} +class _AlienBumperController extends ComponentController + with HasGameRef { + /// {@macro alien_bumper_controller} + _AlienBumperController(AlienBumper alienBumper) : super(alienBumper); + + /// Flag for activated state of the [AlienBumper]. + /// + /// Used to toggle [AlienBumper]s' state between activated and deactivated. + bool isActivated = false; + + /// Registers when a [AlienBumper] is hit by a [Ball]. + void hit() { + if (isActivated) { + component.deactivate(); + } else { + component.activate(); + } + isActivated = !isActivated; + } +} + +/// Listens when a [Ball] bounces bounces against a [AlienBumper]. +class _ControlledAlienBumperBallContactCallback + extends ContactCallback, Ball> { + @override + void begin( + Controls<_AlienBumperController> controlledAlienBumper, + Ball _, + Contact __, + ) { + controlledAlienBumper.controller.hit(); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index a5540614..6a9d6a3d 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,3 +1,4 @@ +export 'alien_zone.dart'; export 'board.dart'; export 'bonus_word.dart'; export 'camera_controller.dart'; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index df602184..cad2eac5 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame await add(plunger); unawaited(add(Board())); + unawaited(add(AlienZone())); unawaited(add(SparkyFireZone())); unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); diff --git a/packages/pinball_components/assets/images/alien_bumper/a/active.png b/packages/pinball_components/assets/images/alien_bumper/a/active.png new file mode 100644 index 0000000000000000000000000000000000000000..92943dfc4f868e467f55c14a32cb83295ce63e98 GIT binary patch literal 8423 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 URwx6EN&o-=07*qoM6N<$g2(gi4*&oF literal 0 HcmV?d00001 diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 7b535ce8..63e28183 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -10,6 +10,8 @@ import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); + $AssetsImagesAlienBumperGen get alienBumper => + const $AssetsImagesAlienBumperGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); /// File path: assets/images/ball.png @@ -38,6 +40,13 @@ class $AssetsImagesGen { $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); } +class $AssetsImagesAlienBumperGen { + const $AssetsImagesAlienBumperGen(); + + $AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen(); + $AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen(); +} + class $AssetsImagesBackboardGen { const $AssetsImagesBackboardGen(); @@ -226,6 +235,30 @@ class $AssetsImagesSparkyGen { const $AssetsImagesSparkyComputerGen(); } +class $AssetsImagesAlienBumperAGen { + const $AssetsImagesAlienBumperAGen(); + + /// File path: assets/images/alien_bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/alien_bumper/a/active.png'); + + /// File path: assets/images/alien_bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/alien_bumper/a/inactive.png'); +} + +class $AssetsImagesAlienBumperBGen { + const $AssetsImagesAlienBumperBGen(); + + /// File path: assets/images/alien_bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/alien_bumper/b/active.png'); + + /// File path: assets/images/alien_bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/alien_bumper/b/inactive.png'); +} + class $AssetsImagesDashBumperGen { const $AssetsImagesDashBumperGen(); @@ -283,42 +316,6 @@ class $AssetsImagesSparkyComputerGen { const AssetGenImage('assets/images/sparky/computer/top.png'); } -class $AssetsImagesSparkyBumperAGen { - const $AssetsImagesSparkyBumperAGen(); - - /// File path: assets/images/sparky/bumper/a/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/a/active.png'); - - /// File path: assets/images/sparky/bumper/a/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); -} - -class $AssetsImagesSparkyBumperBGen { - const $AssetsImagesSparkyBumperBGen(); - - /// File path: assets/images/sparky/bumper/b/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/b/active.png'); - - /// File path: assets/images/sparky/bumper/b/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); -} - -class $AssetsImagesSparkyBumperCGen { - const $AssetsImagesSparkyBumperCGen(); - - /// File path: assets/images/sparky/bumper/c/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/c/active.png'); - - /// File path: assets/images/sparky/bumper/c/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); -} - class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); @@ -355,6 +352,42 @@ class $AssetsImagesDashBumperMainGen { const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); } +class $AssetsImagesSparkyBumperAGen { + const $AssetsImagesSparkyBumperAGen(); + + /// File path: assets/images/sparky/bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky/bumper/a/active.png'); + + /// File path: assets/images/sparky/bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); +} + +class $AssetsImagesSparkyBumperBGen { + const $AssetsImagesSparkyBumperBGen(); + + /// File path: assets/images/sparky/bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky/bumper/b/active.png'); + + /// File path: assets/images/sparky/bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); +} + +class $AssetsImagesSparkyBumperCGen { + const $AssetsImagesSparkyBumperCGen(); + + /// File path: assets/images/sparky/bumper/c/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky/bumper/c/active.png'); + + /// File path: assets/images/sparky/bumper/c/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); +} + class Assets { Assets._(); diff --git a/packages/pinball_components/lib/src/components/alien_bumper.dart b/packages/pinball_components/lib/src/components/alien_bumper.dart new file mode 100644 index 00000000..75b0560d --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper.dart @@ -0,0 +1,109 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template alien_bumper} +/// Bumper for Alien area. +/// {@endtemplate} +// TODO(ruimiguel): refactor later to unify with DashBumpers. +class AlienBumper extends BodyComponent with InitialPosition { + /// {@macro alien_bumper} + AlienBumper._({ + required double majorRadius, + required double minorRadius, + required String activeAssetPath, + required String inactiveAssetPath, + required SpriteComponent spriteComponent, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + _activeAssetPath = activeAssetPath, + _inactiveAssetPath = inactiveAssetPath, + _spriteComponent = spriteComponent; + + /// {@macro alien_bumper} + AlienBumper.a() + : this._( + majorRadius: 3.52, + minorRadius: 2.97, + activeAssetPath: Assets.images.alienBumper.a.active.keyName, + inactiveAssetPath: Assets.images.alienBumper.a.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.1), + ), + ); + + /// {@macro alien_bumper} + AlienBumper.b() + : this._( + majorRadius: 3.19, + minorRadius: 2.79, + activeAssetPath: Assets.images.alienBumper.b.active.keyName, + inactiveAssetPath: Assets.images.alienBumper.b.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.1), + ), + ); + + final double _majorRadius; + final double _minorRadius; + final String _activeAssetPath; + late final Sprite _activeSprite; + final String _inactiveAssetPath; + late final Sprite _inactiveSprite; + final SpriteComponent _spriteComponent; + + @override + Future onLoad() async { + await super.onLoad(); + renderBody = false; + + await _loadSprites(); + + deactivate(); + await add(_spriteComponent); + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(15.9 * math.pi / 180); + final fixtureDef = FixtureDef(shape) + ..friction = 0 + ..restitution = 4; + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + Future _loadSprites() async { + // TODO(alestiago): I think ideally we would like to do: + // Sprite(path).load so we don't require to store the activeAssetPath and + // the inactive assetPath. + _inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath); + _activeSprite = await gameRef.loadSprite(_activeAssetPath); + } + + /// Activates the [AlienBumper]. + void activate() { + _spriteComponent + ..sprite = _activeSprite + ..size = _activeSprite.originalSize / 10; + } + + /// Deactivates the [AlienBumper]. + void deactivate() { + _spriteComponent + ..sprite = _inactiveSprite + ..size = _inactiveSprite.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 0348e4df..acb8a7c5 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,3 +1,4 @@ +export 'alien_bumper.dart'; export 'backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index fc44a74a..1c54095a 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -52,6 +52,8 @@ flutter: - assets/images/kicker/ - assets/images/plunger/ - assets/images/slingshot/ + - assets/images/alien_bumper/a/ + - assets/images/alien_bumper/b/ - assets/images/sparky/computer/ - assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/b/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 1e4aab5e..0bfcd175 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -24,6 +24,7 @@ void main() { addPlungerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); + addAlienZoneStories(dashbook); addZoomStories(dashbook); addBoundariesStories(dashbook); addGoogleWordStories(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart new file mode 100644 index 00000000..007d67e1 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart @@ -0,0 +1,28 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AlienBumperAGame extends BasicBallGame { + AlienBumperAGame() : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a AlienBumperA is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + final alienBumperA = AlienBumper.a() + ..initialPosition = Vector2(center.x - 20, center.y - 20) + ..priority = 1; + await add(alienBumperA); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart new file mode 100644 index 00000000..fada1dd9 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart @@ -0,0 +1,28 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AlienBumperBGame extends BasicBallGame { + AlienBumperBGame() : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a AlienBumperB is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + final alienBumperB = AlienBumper.b() + ..initialPosition = Vector2(center.x - 10, center.y + 10) + ..priority = 1; + await add(alienBumperB); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart new file mode 100644 index 00000000..4bc758f9 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart @@ -0,0 +1,25 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart'; +import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart'; + +void addAlienZoneStories(Dashbook dashbook) { + dashbook.storiesOf('Alien Zone') + ..add( + 'Alien Bumper A', + (context) => GameWidget( + game: AlienBumperAGame()..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('alien_zone/alien_bumper_a.dart'), + info: AlienBumperAGame.info, + ) + ..add( + 'Alien Bumper B', + (context) => GameWidget( + game: AlienBumperBGame()..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('alien_zone/alien_bumper_b.dart'), + info: AlienBumperAGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 7dd02878..cdcf0825 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,3 +1,4 @@ +export 'alien_zone/stories.dart'; export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; diff --git a/packages/pinball_components/test/src/components/alien_bumper_test.dart b/packages/pinball_components/test/src/components/alien_bumper_test.dart new file mode 100644 index 00000000..cd55b62e --- /dev/null +++ b/packages/pinball_components/test/src/components/alien_bumper_test.dart @@ -0,0 +1,68 @@ +// ignore_for_file: cascade_invocations + +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 '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('AlienBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final bumper = AlienBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = AlienBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = AlienBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = AlienBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = AlienBumper.a(); + await game.ensureAdd(bumper); + + final spriteComponent = bumper.firstChild()!; + + final deactivatedSprite = spriteComponent.sprite; + bumper.activate(); + expect( + spriteComponent.sprite, + isNot(equals(deactivatedSprite)), + ); + + final activatedSprite = spriteComponent.sprite; + bumper.deactivate(); + expect( + spriteComponent.sprite, + isNot(equals(activatedSprite)), + ); + + expect( + activatedSprite, + isNot(equals(deactivatedSprite)), + ); + }); + }); +} diff --git a/test/game/components/alien_zone_test.dart b/test/game/components/alien_zone_test.dart new file mode 100644 index 00000000..68a2c2f1 --- /dev/null +++ b/test/game/components/alien_zone_test.dart @@ -0,0 +1,115 @@ +// 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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(EmptyPinballGameTest.new); + + group('AlienZone', () { + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final alienZone = AlienZone(); + await game.ensureAdd(alienZone); + + expect(game.contains(alienZone), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'two AlienBumper', + (game) async { + await game.ready(); + final alienZone = AlienZone(); + await game.ensureAdd(alienZone); + + expect( + alienZone.descendants().whereType().length, + equals(2), + ); + }, + ); + }); + + group('bumpers', () { + late ControlledAlienBumper controlledAlienBumper; + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + flameTester.testGameWidget( + 'activate when deactivated bumper is hit', + setUp: (game, tester) async { + controlledAlienBumper = ControlledAlienBumper.a(); + await game.ensureAdd(controlledAlienBumper); + + controlledAlienBumper.controller.hit(); + }, + verify: (game, tester) async { + expect(controlledAlienBumper.controller.isActivated, isTrue); + }, + ); + + flameTester.testGameWidget( + 'deactivate when activated bumper is hit', + setUp: (game, tester) async { + controlledAlienBumper = ControlledAlienBumper.a(); + await game.ensureAdd(controlledAlienBumper); + + controlledAlienBumper.controller.hit(); + controlledAlienBumper.controller.hit(); + }, + verify: (game, tester) async { + expect(controlledAlienBumper.controller.isActivated, isFalse); + }, + ); + + flameBlocTester.testGameWidget( + 'add Scored event', + setUp: (game, tester) async { + final ball = Ball(baseColor: const Color(0xFF00FFFF)); + final alienZone = AlienZone(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + + await game.ensureAdd(alienZone); + await game.ensureAdd(ball); + game.addContactCallback(BallScorePointsCallback(game)); + + final bumpers = alienZone.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + verify( + () => gameBloc.add( + Scored(points: bumper.points), + ), + ).called(1); + } + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2dfd5d76..ef55b399 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -70,6 +70,14 @@ void main() { }, ); + flameTester.test( + 'one AlienZone', + (game) async { + await game.ready(); + expect(game.children.whereType().length, equals(1)); + }, + ); + group('controller', () { // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () {