From e2c67571d5538b4f4ff7474ac598bb95406d7fc2 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Thu, 24 Mar 2022 10:01:52 -0500 Subject: [PATCH 1/3] feat: reshape `Baseboards` (#85) * refactor: removed findNested extensions (#77) * refactor: baseboard shape and position * chore: remove unused asset * test: update fixture test Co-authored-by: Alejandro Santiago --- assets/images/components/sauce.png | Bin 7862 -> 0 bytes lib/game/components/baseboard.dart | 85 +++++++++++++++-------- lib/game/components/board.dart | 4 +- lib/gen/assets.gen.dart | 2 - test/game/components/baseboard_test.dart | 4 +- 5 files changed, 60 insertions(+), 35 deletions(-) delete mode 100644 assets/images/components/sauce.png diff --git a/assets/images/components/sauce.png b/assets/images/components/sauce.png deleted file mode 100644 index 743a920a29d76879989b94e50cabf907247a9435..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7862 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 _createFixtureDefs() { final fixturesDef = []; + final direction = _side.direction; + final arcsAngle = -1.11 * direction; + const arcsRotation = math.pi / 2.08; + + final topCircleShape = CircleShape()..radius = 0.7; + topCircleShape.position.setValues(11.39 * direction, 6.05); + final topCircleFixtureDef = FixtureDef(topCircleShape); + fixturesDef.add(topCircleFixtureDef); + + final innerEdgeShape = EdgeShape() + ..set( + Vector2(10.86 * direction, 6.45), + Vector2(6.96 * direction, 0.25), + ); + final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape); + fixturesDef.add(innerEdgeShapeFixtureDef); + + final outerEdgeShape = EdgeShape() + ..set( + Vector2(11.96 * direction, 5.85), + Vector2(5.48 * direction, -4.85), + ); + final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape); + fixturesDef.add(outerEdgeShapeFixtureDef); + + final upperArcFixtureDefs = Pathway.arc( + center: Vector2(1.76 * direction, 3.25), + width: 0, + radius: 6.1, + angle: arcsAngle, + rotation: arcsRotation, + singleWall: true, + ).createFixtureDefs(); + fixturesDef.addAll(upperArcFixtureDefs); + + final lowerArcFixtureDefs = Pathway.arc( + center: Vector2(1.85 * direction, -2.15), + width: 0, + radius: 4.5, + angle: arcsAngle, + rotation: arcsRotation, + singleWall: true, + ).createFixtureDefs(); + fixturesDef.addAll(lowerArcFixtureDefs); - final circleShape1 = CircleShape()..radius = Baseboard.height / 2; - circleShape1.position.setValues( - -(Baseboard.width / 2) + circleShape1.radius, - 0, - ); - final circle1FixtureDef = FixtureDef(circleShape1); - fixturesDef.add(circle1FixtureDef); - - final circleShape2 = CircleShape()..radius = Baseboard.height / 2; - circleShape2.position.setValues( - (Baseboard.width / 2) - circleShape2.radius, - 0, - ); - final circle2FixtureDef = FixtureDef(circleShape2); - fixturesDef.add(circle2FixtureDef); - - final rectangle = PolygonShape() - ..setAsBoxXY( - (Baseboard.width - Baseboard.height) / 2, - Baseboard.height / 2, + final bottomRectangle = PolygonShape() + ..setAsBox( + 7, + 2, + Vector2(-5.14 * direction, -4.75), + 0, ); - final rectangleFixtureDef = FixtureDef(rectangle); - fixturesDef.add(rectangleFixtureDef); + final bottomRectangleFixtureDef = FixtureDef(bottomRectangle); + fixturesDef.add(bottomRectangleFixtureDef); return fixturesDef; } @@ -56,7 +83,7 @@ class Baseboard extends BodyComponent with InitialPosition { Body createBody() { // TODO(allisonryan0002): share sweeping angle with flipper when components // are grouped. - const angle = math.pi / 7; + const angle = math.pi / 5; final bodyDef = BodyDef() ..position = initialPosition diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 6e895d6e..adfbef49 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -134,8 +134,8 @@ class _BottomGroupSide extends Component { final baseboard = Baseboard(side: _side) ..initialPosition = _position + Vector2( - (Flipper.size.x * direction) - direction, - Flipper.size.y, + (Baseboard.size.x / 1.6 * direction), + Baseboard.size.y - 2, ); final kicker = Kicker( side: _side, diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index ba75412b..6e81fe77 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -17,8 +17,6 @@ class $AssetsImagesComponentsGen { AssetGenImage get flipper => const AssetGenImage('assets/images/components/flipper.png'); - AssetGenImage get sauce => - const AssetGenImage('assets/images/components/sauce.png'); $AssetsImagesComponentsSpaceshipGen get spaceship => const $AssetsImagesComponentsSpaceshipGen(); } diff --git a/test/game/components/baseboard_test.dart b/test/game/components/baseboard_test.dart index 75cc62cc..f834a41e 100644 --- a/test/game/components/baseboard_test.dart +++ b/test/game/components/baseboard_test.dart @@ -61,14 +61,14 @@ void main() { group('fixtures', () { flameTester.test( - 'has three', + 'has six', (game) async { final baseboard = Baseboard( side: BoardSide.left, ); await game.ensureAdd(baseboard); - expect(baseboard.body.fixtures.length, equals(3)); + expect(baseboard.body.fixtures.length, equals(6)); }, ); }); From cfd4e790faf9e1024bfcf10bba902f323c2c4970 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 24 Mar 2022 16:51:19 +0000 Subject: [PATCH 2/3] feat: bumping all `DashNestBumper`s summons a new `Ball` (#89) * refactor: rename RoundBumper to DashNestBumper * feat: implemented DashNestBumper * refactor: renamed files and tests * chore: removed unused import * refactor: renamed file to be a test file * feat: added a new Ball on new state Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/board.dart | 47 +------ lib/game/components/bonus_word.dart | 8 +- lib/game/components/components.dart | 2 +- lib/game/components/flutter_forest.dart | 131 ++++++++++++++++++ lib/game/components/round_bumper.dart | 35 ----- test/game/components/board_test.dart | 6 +- test/game/components/bonus_word_test.dart | 3 +- test/game/components/flutter_forest_test.dart | 125 +++++++++++++++++ test/game/components/round_bumper_test.dart | 102 -------------- test/helpers/mocks.dart | 2 + 10 files changed, 269 insertions(+), 192 deletions(-) create mode 100644 lib/game/components/flutter_forest.dart delete mode 100644 lib/game/components/round_bumper.dart create mode 100644 test/game/components/flutter_forest_test.dart delete mode 100644 test/game/components/round_bumper_test.dart diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index adfbef49..c771b9d8 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -1,11 +1,9 @@ import 'package:flame/components.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; /// {@template board} -/// The main flat surface of the [PinballGame], where the [Flipper]s, -/// [RoundBumper]s, [Kicker]s are arranged. -/// {entemplate} +/// The main flat surface of the [PinballGame]. +/// {endtemplate} class Board extends Component { /// {@macro board} Board(); @@ -21,7 +19,7 @@ class Board extends Component { spacing: 2, ); - final dashForest = _FlutterForest( + final flutterForest = FlutterForest( position: Vector2( PinballGame.boardBounds.center.dx + 20, PinballGame.boardBounds.center.dy + 48, @@ -30,44 +28,7 @@ class Board extends Component { await addAll([ bottomGroup, - dashForest, - ]); - } -} - -/// {@template flutter_forest} -/// Area positioned at the top right of the [Board] where the [Ball] -/// can bounce off [RoundBumper]s. -/// {@endtemplate} -class _FlutterForest extends Component { - /// {@macro flutter_forest} - _FlutterForest({ - required this.position, - }); - - final Vector2 position; - - @override - Future onLoad() async { - // TODO(alestiago): adjust positioning once sprites are added. - // TODO(alestiago): Use [NestBumper] instead of [RoundBumper] once provided. - final smallLeftNest = RoundBumper( - radius: 1, - points: 10, - )..initialPosition = position + Vector2(-4.8, 2.8); - final smallRightNest = RoundBumper( - radius: 1, - points: 10, - )..initialPosition = position + Vector2(0.5, -5.5); - final bigNest = RoundBumper( - radius: 2, - points: 20, - )..initialPosition = position; - - await addAll([ - smallLeftNest, - smallRightNest, - bigNest, + flutterForest, ]); } } diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart index d97a9bd1..9dc9b0b0 100644 --- a/lib/game/components/bonus_word.dart +++ b/lib/game/components/bonus_word.dart @@ -21,13 +21,9 @@ class BonusWord extends Component with BlocComponent { @override bool listenWhen(GameState? previousState, GameState newState) { - if ((previousState?.bonusHistory.length ?? 0) < + return (previousState?.bonusHistory.length ?? 0) < newState.bonusHistory.length && - newState.bonusHistory.last == GameBonus.word) { - return true; - } - - return false; + newState.bonusHistory.last == GameBonus.word; } @override diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 1ed293da..07b036f6 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -4,6 +4,7 @@ export 'board.dart'; export 'board_side.dart'; export 'bonus_word.dart'; export 'flipper.dart'; +export 'flutter_forest.dart'; export 'jetpack_ramp.dart'; export 'joint_anchor.dart'; export 'kicker.dart'; @@ -11,7 +12,6 @@ export 'launcher_ramp.dart'; export 'pathway.dart'; export 'plunger.dart'; export 'ramp_opening.dart'; -export 'round_bumper.dart'; export 'score_points.dart'; export 'spaceship.dart'; export 'wall.dart'; diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart new file mode 100644 index 00000000..51dcd90a --- /dev/null +++ b/lib/game/components/flutter_forest.dart @@ -0,0 +1,131 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/flame/blueprint.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template flutter_forest} +/// Area positioned at the top right of the [Board] where the [Ball] +/// can bounce off [DashNestBumper]s. +/// +/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] +/// is awarded, and the [BigDashNestBumper] releases a new [Ball]. +/// {@endtemplate} +// TODO(alestiago): Make a [Blueprint] once nesting [Blueprint] is implemented. +class FlutterForest extends Component + with HasGameRef, BlocComponent { + /// {@macro flutter_forest} + FlutterForest({ + required this.position, + }); + + /// The position of the [FlutterForest] on the [Board]. + final Vector2 position; + + @override + bool listenWhen(GameState? previousState, GameState newState) { + return (previousState?.bonusHistory.length ?? 0) < + newState.bonusHistory.length && + newState.bonusHistory.last == GameBonus.dashNest; + } + + @override + void onNewState(GameState state) { + super.onNewState(state); + gameRef.addFromBlueprint(BallBlueprint(position: position)); + } + + @override + Future onLoad() async { + gameRef.addContactCallback(DashNestBumperBallContactCallback()); + + // TODO(alestiago): adjust positioning once sprites are added. + final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') + ..initialPosition = position + Vector2(-4.8, 2.8); + final smallRightNest = SmallDashNestBumper(id: 'small_right_nest') + ..initialPosition = position + Vector2(0.5, -5.5); + final bigNest = BigDashNestBumper(id: 'big_nest') + ..initialPosition = position; + + await addAll([ + smallLeftNest, + smallRightNest, + bigNest, + ]); + } +} + +/// {@template dash_nest_bumper} +/// Bumper located in the [FlutterForest]. +/// {@endtemplate} +@visibleForTesting +abstract class DashNestBumper extends BodyComponent + with ScorePoints, InitialPosition { + /// {@macro dash_nest_bumper} + DashNestBumper({required this.id}); + + /// Unique identifier for this [DashNestBumper]. + /// + /// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. + final String id; +} + +/// Listens when a [Ball] bounces bounces against a [DashNestBumper]. +@visibleForTesting +class DashNestBumperBallContactCallback + extends ContactCallback { + @override + void begin(DashNestBumper dashNestBumper, Ball ball, Contact _) { + dashNestBumper.gameRef.read().add( + DashNestActivated(dashNestBumper.id), + ); + } +} + +/// {@macro dash_nest_bumper} +@visibleForTesting +class BigDashNestBumper extends DashNestBumper { + /// {@macro dash_nest_bumper} + BigDashNestBumper({required String id}) : super(id: id); + + @override + int get points => 20; + + @override + Body createBody() { + final shape = CircleShape()..radius = 2.5; + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +/// {@macro dash_nest_bumper} +@visibleForTesting +class SmallDashNestBumper extends DashNestBumper { + /// {@macro dash_nest_bumper} + SmallDashNestBumper({required String id}) : super(id: id); + + @override + int get points => 10; + + @override + Body createBody() { + final shape = CircleShape()..radius = 1; + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/lib/game/components/round_bumper.dart b/lib/game/components/round_bumper.dart deleted file mode 100644 index 969bddbe..00000000 --- a/lib/game/components/round_bumper.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template round_bumper} -/// Circular body that repels a [Ball] on contact, increasing the score. -/// {@endtemplate} -class RoundBumper extends BodyComponent with ScorePoints, InitialPosition { - /// {@macro round_bumper} - RoundBumper({ - required double radius, - required int points, - }) : _radius = radius, - _points = points; - - /// The radius of the [RoundBumper]. - final double _radius; - - /// Points awarded from hitting this [RoundBumper]. - final int _points; - - @override - int get points => _points; - - @override - Body createBody() { - final shape = CircleShape()..radius = _radius; - - final fixtureDef = FixtureDef(shape)..restitution = 1; - - final bodyDef = BodyDef()..position = initialPosition; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index f0cd0e16..5a4b95dc 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -75,15 +75,15 @@ void main() { ); flameTester.test( - 'has three RoundBumpers', + 'has one FlutterForest', (game) async { // TODO(alestiago): change to [NestBumpers] once provided. final board = Board(); await game.ready(); await game.ensureAdd(board); - final roundBumpers = board.descendants().whereType(); - expect(roundBumpers.length, equals(3)); + final flutterForest = board.descendants().whereType(); + expect(flutterForest.length, equals(1)); }, ); }); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index a12a5a74..afd69935 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -194,6 +194,7 @@ void main() { group('bonus letter activation', () { final gameBloc = MockGameBloc(); + final tester = flameBlocTester(gameBloc: () => gameBloc); BonusLetter _getBonusLetter(PinballGame game) { return game.children @@ -212,8 +213,6 @@ void main() { ); }); - final tester = flameBlocTester(gameBloc: () => gameBloc); - tester.widgetTest( 'adds BonusLetterActivated to GameBloc when not activated', (game, tester) async { diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart new file mode 100644 index 00000000..f960796c --- /dev/null +++ b/test/game/components/flutter_forest_test.dart @@ -0,0 +1,125 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.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(PinballGameTest.create); + + group('FlutterForest', () { + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(position: Vector2(0, 0)); + await game.ensureAdd(flutterForest); + + expect(game.contains(flutterForest), isTrue); + }, + ); + + flameTester.test( + 'onNewState adds a new ball', + (game) async { + final flutterForest = FlutterForest(position: Vector2(0, 0)); + await game.ready(); + await game.ensureAdd(flutterForest); + + final previousBalls = game.descendants().whereType().length; + flutterForest.onNewState(MockGameState()); + await game.ready(); + + expect( + game.descendants().whereType().length, + greaterThan(previousBalls), + ); + }, + ); + + group('listenWhen', () { + final gameBloc = MockGameBloc(); + final tester = flameBlocTester(gameBloc: () => gameBloc); + + setUp(() { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + tester.widgetTest( + 'listens when a Bonus.dashNest is added', + (game, tester) async { + await game.ready(); + final flutterForest = + game.descendants().whereType().first; + + const state = GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {}, + bonusHistory: [GameBonus.dashNest], + ); + + expect( + flutterForest.listenWhen(const GameState.initial(), state), + isTrue, + ); + }, + ); + }); + }); + + group('DashNestBumperBallContactCallback', () { + final gameBloc = MockGameBloc(); + final tester = flameBlocTester(gameBloc: () => gameBloc); + + setUp(() { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + tester.widgetTest( + 'adds a DashNestActivated event with DashNestBumper.id', + (game, tester) async { + final contactCallback = DashNestBumperBallContactCallback(); + const id = '0'; + final dashNestBumper = MockDashNestBumper(); + when(() => dashNestBumper.id).thenReturn(id); + when(() => dashNestBumper.gameRef).thenReturn(game); + + contactCallback.begin(dashNestBumper, MockBall(), MockContact()); + + verify(() => gameBloc.add(DashNestActivated(dashNestBumper.id))) + .called(1); + }, + ); + }); + + group('BigDashNestBumper', () { + test('has points', () { + final dashNestBumper = BigDashNestBumper(id: ''); + expect(dashNestBumper.points, greaterThan(0)); + }); + }); + + group('SmallDashNestBumper', () { + test('has points', () { + final dashNestBumper = SmallDashNestBumper(id: ''); + expect(dashNestBumper.points, greaterThan(0)); + }); + }); +} diff --git a/test/game/components/round_bumper_test.dart b/test/game/components/round_bumper_test.dart deleted file mode 100644 index 437167ad..00000000 --- a/test/game/components/round_bumper_test.dart +++ /dev/null @@ -1,102 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('RoundBumper', () { - final flameTester = FlameTester(Forge2DGame.new); - const radius = 1.0; - const points = 1; - - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final roundBumper = RoundBumper( - radius: radius, - points: points, - ); - await game.ensureAdd(roundBumper); - - expect(game.contains(roundBumper), isTrue); - }, - ); - - flameTester.test( - 'has points', - (game) async { - final roundBumper = RoundBumper( - radius: radius, - points: points, - ); - await game.ensureAdd(roundBumper); - - expect(roundBumper.points, equals(points)); - }, - ); - - group('body', () { - flameTester.test( - 'is static', - (game) async { - final roundBumper = RoundBumper( - radius: radius, - points: points, - ); - await game.ensureAdd(roundBumper); - - expect(roundBumper.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final roundBumper = RoundBumper( - radius: radius, - points: points, - ); - await game.ensureAdd(roundBumper); - - expect(roundBumper.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { - final roundBumper = RoundBumper( - radius: radius, - points: points, - ); - await game.ensureAdd(roundBumper); - - final fixture = roundBumper.body.fixtures[0]; - expect(fixture.restitution, greaterThan(0)); - }, - ); - - flameTester.test( - 'shape is circular', - (game) async { - final roundBumper = RoundBumper( - radius: radius, - points: points, - ); - await game.ensureAdd(roundBumper); - - final fixture = roundBumper.body.fixtures[0]; - expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(1)); - }, - ); - }); - }); -} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index bd9f82cf..8ddab690 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -68,3 +68,5 @@ class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} class MockSpaceshipHole extends Mock implements SpaceshipHole {} class MockComponentSet extends Mock implements ComponentSet {} + +class MockDashNestBumper extends Mock implements DashNestBumper {} From cf92856dc1ba7ae3c34e2b3019ce83c13d0f3421 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 24 Mar 2022 14:36:57 -0300 Subject: [PATCH 3/3] feat: implementing composable blueprints (#92) * feat: implementing composable blueprints * fix: coverage --- lib/flame/blueprint.dart | 22 +++++++++++++++++- test/flame/blueprint_test.dart | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/flame/blueprint.dart b/lib/flame/blueprint.dart index d536d650..57af7d6d 100644 --- a/lib/flame/blueprint.dart +++ b/lib/flame/blueprint.dart @@ -14,6 +14,8 @@ const _attachedErrorMessage = "Can't add to attached Blueprints"; /// the [FlameGame] level. abstract class Blueprint { final List _components = []; + final List _blueprints = []; + bool _isAttached = false; /// Called before the the [Component]s managed @@ -25,7 +27,10 @@ abstract class Blueprint { @mustCallSuper Future attach(T game) async { build(game); - await game.addAll(_components); + await Future.wait([ + game.addAll(_components), + ..._blueprints.map(game.addFromBlueprint).toList(), + ]); _isAttached = true; } @@ -41,8 +46,23 @@ abstract class Blueprint { _components.add(component); } + /// Adds a list of [Blueprint]s to this blueprint. + void addAllBlueprints(List blueprints) { + assert(!_isAttached, _attachedErrorMessage); + _blueprints.addAll(blueprints); + } + + /// Adds a single [Blueprint] to this blueprint. + void addBlueprint(Blueprint blueprint) { + assert(!_isAttached, _attachedErrorMessage); + _blueprints.add(blueprint); + } + /// Returns a copy of the components built by this blueprint List get components => List.unmodifiable(_components); + + /// Returns a copy of the children blueprints + List get blueprints => List.unmodifiable(_blueprints); } /// A [Blueprint] that provides additional diff --git a/test/flame/blueprint_test.dart b/test/flame/blueprint_test.dart index 3a9f5ed3..e5fc2c4f 100644 --- a/test/flame/blueprint_test.dart +++ b/test/flame/blueprint_test.dart @@ -14,6 +14,28 @@ class MyBlueprint extends Blueprint { } } +class MyOtherBlueprint extends Blueprint { + @override + void build(_) { + add(Component()); + } +} + +class YetMyOtherBlueprint extends Blueprint { + @override + void build(_) { + add(Component()); + } +} + +class MyComposedBlueprint extends Blueprint { + @override + void build(_) { + addBlueprint(MyBlueprint()); + addAllBlueprints([MyOtherBlueprint(), YetMyOtherBlueprint()]); + } +} + class MyForge2dBlueprint extends Forge2DBlueprint { @override void build(_) { @@ -24,12 +46,23 @@ class MyForge2dBlueprint extends Forge2DBlueprint { void main() { group('Blueprint', () { + setUpAll(() { + registerFallbackValue(MyBlueprint()); + registerFallbackValue(Component()); + }); + test('components can be added to it', () { final blueprint = MyBlueprint()..build(MockPinballGame()); expect(blueprint.components.length, equals(3)); }); + test('blueprints can be added to it', () { + final blueprint = MyComposedBlueprint()..build(MockPinballGame()); + + expect(blueprint.blueprints.length, equals(3)); + }); + test('adds the components to a game on attach', () { final mockGame = MockPinballGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); @@ -38,6 +71,14 @@ void main() { verify(() => mockGame.addAll(any())).called(1); }); + test('adds components from a child Blueprint the to a game on attach', () { + final mockGame = MockPinballGame(); + when(() => mockGame.addAll(any())).thenAnswer((_) async {}); + MyComposedBlueprint().attach(mockGame); + + verify(() => mockGame.addAll(any())).called(4); + }); + test( 'throws assertion error when adding to an already attached blueprint', () async {