diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart index 94fa7c5c..a5194c60 100644 --- a/lib/flame/component_controller.dart +++ b/lib/flame/component_controller.dart @@ -23,9 +23,23 @@ abstract class ComponentController extends Component { ); await super.addToParent(parent); } +} + +/// {@template controller} +/// Mixin that attaches a single [ComponentController] to a [Component]. +/// {@endtemplate} +mixin Controls on Component { + /// Builds a [ComponentController] for this [Component]. + /// + /// **Note**: This method should not be directly called. + T controllerBuilder(); - /// Ads the [ComponentController] to its [component]. - Future attach() async { - if (parent == null) await component.add(this); + /// The [ComponentController] attached to this [Component]. + late final T controller; + + @override + Future onLoad() async { + await super.onLoad(); + await add(controller = controllerBuilder()); } } diff --git a/lib/game/components/ball_controller.dart b/lib/game/components/ball_controller.dart deleted file mode 100644 index ffc8d5f3..00000000 --- a/lib/game/components/ball_controller.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; -import 'package:pinball/flame/flame.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template ball_controller} -/// Controller attached to a [Ball] that handles its game related logic. -/// {@endtemplate} -class BallController extends ComponentController { - /// {@macro ball_controller} - BallController(Ball ball) : super(ball); - - /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if - /// any are left. - /// - /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into - /// a [BottomWall]. - void lost() { - component.shouldRemove = true; - } -} - -/// {@macro ball_controller} -class PlungerBallController extends BallController - with HasGameRef { - /// {@macro ball_controller} - PlungerBallController(Ball ball) : super(ball); - - @override - void lost() { - super.lost(); - final bloc = gameRef.read()..add(const BallLost()); - final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; - - if (shouldBallRespwan) gameRef.spawnBall(); - } -} diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index c634960b..f022862c 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -94,10 +94,9 @@ class _BottomGroupSide extends Component { Future onLoad() async { final direction = _side.direction; - final flipper = Flipper( + final flipper = ControlledFlipper( side: _side, )..initialPosition = _position; - await FlipperController(flipper).attach(); final baseboard = Baseboard(side: _side) ..initialPosition = _position + diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 3fad782b..1f1f1ce5 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,8 +1,8 @@ -export 'ball_controller.dart'; export 'baseboard.dart'; export 'board.dart'; export 'bonus_word.dart'; export 'chrome_dino.dart'; +export 'controlled_ball.dart'; export 'flipper_controller.dart'; export 'flutter_forest.dart'; export 'jetpack_ramp.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart new file mode 100644 index 00000000..55b676ba --- /dev/null +++ b/lib/game/components/controlled_ball.dart @@ -0,0 +1,97 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template controlled_ball} +/// A [Ball] with a [BallController] attached. +/// {@endtemplate} +abstract class _ControlledBall extends Ball + with Controls { + _ControlledBall({required Color baseColor}) : super(baseColor: baseColor); +} + +/// {@template plunger_ball} +/// A [Ball] that starts at the [Plunger]. +/// +/// When a [PlungerBall] is lost, it will decrease the [GameState.balls] count, +/// and a new [PlungerBall] is spawned if it's possible. +/// {@endtemplate} +class PlungerBall extends _ControlledBall { + // TODO(alestiago): Work on a better solution than passing the game. + /// {@macro plunger_ball} + PlungerBall( + PinballGame game, { + required Plunger plunger, + }) : super(baseColor: game.theme.characterTheme.ballColor) { + // TODO(alestiago): Dicuss if this is a good idea. + initialPosition = Vector2( + plunger.body.position.x, + plunger.body.position.y + Ball.size.y, + ); + } + + @override + PlungerBallController controllerBuilder() => PlungerBallController(this); +} + +/// {@template bonus_ball} +/// {@macro controlled_ball} +/// +/// When a [BonusBall] is lost, the [GameState.balls] doesn't change. +/// {@endtemplate} +class BonusBall extends _ControlledBall { + /// {@macro bonus_ball} + BonusBall(PinballGame game) + : super(baseColor: game.theme.characterTheme.ballColor); + + @override + BallController controllerBuilder() => BallController(this); +} + +/// {@template debug_ball} +/// [Ball] used in [DebugPinballGame]. +/// {@endtemplate} +class DebugBall extends _ControlledBall { + /// {@macro debug_ball} + DebugBall() : super(baseColor: const Color(0xFFFF0000)); + + @override + BallController controllerBuilder() => BallController(this); +} + +/// {@template ball_controller} +/// Controller attached to a [Ball] that handles its game related logic. +/// {@endtemplate} +class BallController extends ComponentController { + /// {@macro ball_controller} + BallController(Ball ball) : super(ball); + + /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if + /// any are left. + /// + /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into + /// a [BottomWall]. + void lost() { + component.shouldRemove = true; + } +} + +/// {@macro ball_controller} +class PlungerBallController extends BallController + with HasGameRef { + /// {@macro ball_controller} + PlungerBallController(Ball ball) : super(ball); + + @override + void lost() { + super.lost(); + final bloc = gameRef.read()..add(const BallLost()); + final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; + + if (shouldBallRespwan) gameRef.spawnBall(); + } +} diff --git a/lib/game/components/flipper_controller.dart b/lib/game/components/flipper_controller.dart index 1330c70d..9153034a 100644 --- a/lib/game/components/flipper_controller.dart +++ b/lib/game/components/flipper_controller.dart @@ -3,6 +3,17 @@ import 'package:flutter/services.dart'; import 'package:pinball/flame/flame.dart'; import 'package:pinball_components/pinball_components.dart'; +/// {@template controlled_flipper} +/// A [Flipper] with a [FlipperController] attached. +/// {@endtemplate} +class ControlledFlipper extends Flipper with Controls { + /// {@macro controlled_flipper} + ControlledFlipper({required BoardSide side}) : super(side: side); + + @override + FlipperController controllerBuilder() => FlipperController(this); +} + /// {@template flipper_controller} /// A [ComponentController] that controls a [Flipper]s movement. /// {@endtemplate} diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index bf28ebe3..7cb8f96d 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -32,11 +32,9 @@ class FlutterForest extends Component void onNewState(GameState state) { super.onNewState(state); - final ball = Ball( - baseColor: gameRef.theme.characterTheme.ballColor, - )..initialPosition = Vector2(17.2, 52.7); - BallController(ball).attach(); - gameRef.add(ball); + add( + BonusBall(gameRef)..initialPosition = Vector2(17.2, 52.7), + ); } @override diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index ec88328b..bc0b931d 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -101,13 +101,7 @@ class PinballGame extends Forge2DGame } void spawnBall() { - final ball = Ball( - baseColor: theme.characterTheme.ballColor, - )..initialPosition = Vector2( - plunger.body.position.x, - plunger.body.position.y + Ball.size.y, - ); - PlungerBallController(ball).attach(); + final ball = PlungerBall(this, plunger: plunger); add(ball); } } @@ -140,9 +134,8 @@ class DebugPinballGame extends PinballGame with TapDetector { @override void onTapUp(TapUpInfo info) { - final ball = Ball(baseColor: const Color(0xFFFF0000)) - ..initialPosition = info.eventPosition.game; - BallController(ball).attach(); - add(ball); + add( + DebugBall()..initialPosition = info.eventPosition.game, + ); } } diff --git a/test/flame/component_controller_test.dart b/test/flame/component_controller_test.dart index dbaf9cd4..ab9bddd0 100644 --- a/test/flame/component_controller_test.dart +++ b/test/flame/component_controller_test.dart @@ -37,36 +37,5 @@ void main() { ); }, ); - - group('attach', () { - flameTester.test( - 'adds component controller to controlled component', - (game) async { - final component = Component(); - final controller = TestComponentController(component); - - await controller.attach(); - await game.add(component); - await game.ready(); - - expect(component.contains(controller), isTrue); - }, - ); - - flameTester.test( - "doesn't fail when already has a parent", - (game) async { - final component = Component(); - final controller = TestComponentController(component); - - await controller.attach(); - - await expectLater( - () async => controller.attach(), - returnsNormally, - ); - }, - ); - }); }); } diff --git a/test/game/components/ball_controller_test.dart b/test/game/components/controlled_ball_test.dart similarity index 95% rename from test/game/components/ball_controller_test.dart rename to test/game/components/controlled_ball_test.dart index 22473099..03efa12c 100644 --- a/test/game/components/ball_controller_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -27,7 +27,7 @@ void main() { (game) async { await game.add(ball); final controller = BallController(ball); - await controller.attach(); + await ball.add(controller); await game.ready(); controller.lost(); @@ -63,7 +63,7 @@ void main() { (game, tester) async { await game.add(ball); final controller = PlungerBallController(ball); - await controller.attach(); + await ball.add(controller); await game.ready(); controller.lost(); @@ -77,7 +77,7 @@ void main() { 'adds BallLost to GameBloc', (game, tester) async { final controller = PlungerBallController(ball); - await controller.attach(); + await ball.add(controller); await game.add(ball); await game.ready(); @@ -91,7 +91,7 @@ void main() { 'adds a new ball if the game is not over', (game, tester) async { final controller = PlungerBallController(ball); - await controller.attach(); + await ball.add(controller); await game.add(ball); await game.ready(); @@ -122,7 +122,7 @@ void main() { ), ); final controller = BallController(ball); - await controller.attach(); + await ball.add(controller); await game.add(ball); await game.ready();