diff --git a/assets/images/components/spaceship-drop-tube.png b/assets/images/components/spaceship-drop-tube.png new file mode 100644 index 00000000..4b299c2c Binary files /dev/null and b/assets/images/components/spaceship-drop-tube.png differ diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart index 2bbf5ca9..1d6e0173 100644 --- a/lib/flame/component_controller.dart +++ b/lib/flame/component_controller.dart @@ -1,5 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flutter/foundation.dart'; /// {@template component_controller} /// A [ComponentController] is a [Component] in charge of handling the logic @@ -22,6 +23,11 @@ abstract class ComponentController extends Component { ); await super.addToParent(parent); } + + @override + Future add(Component component) { + throw Exception('ComponentController cannot add other components.'); + } } /// Mixin that attaches a single [ComponentController] to a [Component]. @@ -30,6 +36,7 @@ mixin Controls on Component { late final T controller; @override + @mustCallSuper Future onLoad() async { await super.onLoad(); await add(controller); diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index 6eb3ce7d..2d7bdf33 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -1,11 +1,10 @@ // ignore_for_file: avoid_renaming_method_parameters -import 'dart:math' as math; - 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/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -16,10 +15,47 @@ import 'package:pinball_components/pinball_components.dart'; /// 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 { +// TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from +// [Component]. +class FlutterForest extends Component with Controls<_FlutterForestController> { /// {@macro flutter_forest} + FlutterForest() { + controller = _FlutterForestController(this); + } + + @override + Future onLoad() async { + await super.onLoad(); + final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); + + final bigNest = _ControlledBigDashNestBumper( + id: 'big_nest_bumper', + )..initialPosition = Vector2(18.55, 59.35); + final smallLeftNest = _ControlledSmallDashNestBumper.a( + id: 'small_nest_bumper_a', + )..initialPosition = Vector2(8.95, 51.95); + final smallRightNest = _ControlledSmallDashNestBumper.b( + id: 'small_nest_bumper_b', + )..initialPosition = Vector2(23.3, 46.75); + + await addAll([ + signPost, + smallLeftNest, + smallRightNest, + bigNest, + ]); + } +} + +class _FlutterForestController extends ComponentController + with BlocComponent, HasGameRef { + _FlutterForestController(FlutterForest flutterForest) : super(flutterForest); + + @override + Future onLoad() async { + await super.onLoad(); + gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback()); + } @override bool listenWhen(GameState? previousState, GameState newState) { @@ -32,117 +68,90 @@ class FlutterForest extends Component void onNewState(GameState state) { super.onNewState(state); - add( - ControlledBall.bonus( - theme: gameRef.theme, - )..initialPosition = Vector2(17.2, 52.7), + component.add( + ControlledBall.bonus(theme: gameRef.theme) + ..initialPosition = Vector2(17.2, 52.7), ); } +} - @override - Future onLoad() async { - gameRef.addContactCallback(DashNestBumperBallContactCallback()); +class _ControlledBigDashNestBumper extends BigDashNestBumper + with Controls, ScorePoints { + _ControlledBigDashNestBumper({required String id}) : super() { + controller = DashNestBumperController(this, id: id); + } - final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); + @override + int get points => 20; +} - // TODO(alestiago): adjust positioning once sprites are added. - final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') - ..initialPosition = Vector2(8.95, 51.95); - final smallRightNest = SmallDashNestBumper(id: 'small_right_nest') - ..initialPosition = Vector2(23.3, 46.75); - final bigNest = BigDashNestBumper(id: 'big_nest') - ..initialPosition = Vector2(18.55, 59.35); +class _ControlledSmallDashNestBumper extends SmallDashNestBumper + with Controls, ScorePoints { + _ControlledSmallDashNestBumper.a({required String id}) : super.a() { + controller = DashNestBumperController(this, id: id); + } - await addAll([ - signPost, - smallLeftNest, - smallRightNest, - bigNest, - ]); + _ControlledSmallDashNestBumper.b({required String id}) : super.b() { + controller = DashNestBumperController(this, id: id); } + + @override + int get points => 10; } -/// {@template dash_nest_bumper} -/// Bumper located in the [FlutterForest]. +/// {@template dash_nest_bumper_controller} +/// Controls a [DashNestBumper]. /// {@endtemplate} @visibleForTesting -abstract class DashNestBumper extends BodyComponent - with ScorePoints, InitialPosition { - /// {@macro dash_nest_bumper} - DashNestBumper({required this.id}) { - paint = Paint() - ..color = Colors.blue.withOpacity(0.5) - ..style = PaintingStyle.fill; - } - - /// Unique identifier for this [DashNestBumper]. +class DashNestBumperController extends ComponentController + with BlocComponent, HasGameRef { + /// {@macro dash_nest_bumper_controller} + DashNestBumperController( + DashNestBumper dashNestBumper, { + required this.id, + }) : super(dashNestBumper); + + /// Unique identifier for the controlled [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), - ); - } -} + bool listenWhen(GameState? previousState, GameState newState) { + final wasActive = previousState?.activatedDashNests.contains(id) ?? false; + final isActive = newState.activatedDashNests.contains(id); -/// {@macro dash_nest_bumper} -@visibleForTesting -class BigDashNestBumper extends DashNestBumper { - /// {@macro dash_nest_bumper} - BigDashNestBumper({required String id}) : super(id: id); + return wasActive != isActive; + } @override - int get points => 20; + void onNewState(GameState state) { + super.onNewState(state); - @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 4.85, - minorRadius: 3.95, - )..rotate(math.pi / 2); - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); + if (state.activatedDashNests.contains(id)) { + component.activate(); + } else { + component.deactivate(); + } } -} -/// {@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; + /// Registers when a [DashNestBumper] is hit by a [Ball]. + /// + /// Triggered by [_ControlledDashNestBumperBallContactCallback]. + void hit() { + gameRef.read().add(DashNestActivated(id)); + } +} +/// Listens when a [Ball] bounces bounces against a [DashNestBumper]. +class _ControlledDashNestBumperBallContactCallback + extends ContactCallback, Ball> { @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 3, - minorRadius: 2.25, - )..rotate(math.pi / 2); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); + void begin( + Controls controlledDashNestBumper, + Ball _, + Contact __, + ) { + controlledDashNestBumper.controller.hit(); } } diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index 4c4c8be9..aa5a9dbd 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -107,7 +107,7 @@ class _JetpackRampOpening extends RampOpening { required double rotation, }) : _rotation = rotation, super( - pathwayLayer: Layer.jetpack, + insideLayer: Layer.jetpack, outsideLayer: outsideLayer, orientation: RampOrientation.down, ); diff --git a/lib/game/components/spaceship_exit_rail.dart b/lib/game/components/spaceship_exit_rail.dart index 0dc38322..fed97e0e 100644 --- a/lib/game/components/spaceship_exit_rail.dart +++ b/lib/game/components/spaceship_exit_rail.dart @@ -1,10 +1,10 @@ // ignore_for_file: avoid_renaming_method_parameters import 'dart:math' as math; -import 'dart:ui'; -import 'package:flame/extensions.dart'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template spaceship_exit_rail} @@ -12,10 +12,10 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@endtemplate} class SpaceshipExitRail extends Forge2DBlueprint { /// {@macro spaceship_exit_rail} - SpaceshipExitRail({required this.position}); + SpaceshipExitRail(); - /// The [position] where the elements will be created - final Vector2 position; + /// Base priority for wall while be on jetpack ramp. + static const ballPriorityWhenOnSpaceshipExitRail = 2; @override void build(_) { @@ -23,127 +23,101 @@ class SpaceshipExitRail extends Forge2DBlueprint { SpaceshipExitRailEndBallContactCallback(), ]); - final spaceshipExitRailRamp = _SpaceshipExitRailRamp() - ..initialPosition = position; - final exitRail = SpaceshipExitRailEnd() - ..initialPosition = position + _SpaceshipExitRailRamp.exitPoint; + final exitRailRamp = _SpaceshipExitRailRamp(); + final exitRailEnd = SpaceshipExitRailEnd(); + final topBase = _SpaceshipExitRailBase(radius: 0.55) + ..initialPosition = Vector2(-26.15, 18.65); + final bottomBase = _SpaceshipExitRailBase(radius: 0.8) + ..initialPosition = Vector2(-25.5, -12.9); addAll([ - spaceshipExitRailRamp, - exitRail, + exitRailRamp, + exitRailEnd, + topBase, + bottomBase, ]); } } class _SpaceshipExitRailRamp extends BodyComponent with InitialPosition, Layered { - _SpaceshipExitRailRamp() : super(priority: 2) { + _SpaceshipExitRailRamp() + : super( + priority: SpaceshipExitRail.ballPriorityWhenOnSpaceshipExitRail - 1, + ) { + renderBody = false; layer = Layer.spaceshipExitRail; - // TODO(ruimiguel): remove color once asset is placed. - paint = Paint() - ..color = const Color.fromARGB(255, 249, 65, 3) - ..style = PaintingStyle.stroke; } - static final exitPoint = Vector2(9.2, -48.5); - List _createFixtureDefs() { - const entranceRotationAngle = 175 * math.pi / 180; - const curveRotationAngle = 275 * math.pi / 180; - const exitRotationAngle = 340 * math.pi / 180; - const width = 5.5; - final fixturesDefs = []; - final entranceWall = ArcShape( - center: Vector2(width / 2, 0), - arcRadius: width / 2, + final topArcShape = ArcShape( + center: Vector2(-35.5, 30.9), + arcRadius: 2.5, angle: math.pi, - rotation: entranceRotationAngle, + rotation: 2.9, ); - final entranceFixtureDef = FixtureDef(entranceWall); - fixturesDefs.add(entranceFixtureDef); - - final topLeftControlPoints = [ - Vector2(0, 0), - Vector2(10, .5), - Vector2(7, 4), - Vector2(15.5, 8.3), - ]; + final topArcFixtureDef = FixtureDef(topArcShape); + fixturesDefs.add(topArcFixtureDef); + final topLeftCurveShape = BezierCurveShape( - controlPoints: topLeftControlPoints, - )..rotate(curveRotationAngle); - final topLeftFixtureDef = FixtureDef(topLeftCurveShape); - fixturesDefs.add(topLeftFixtureDef); - - final topRightControlPoints = [ - Vector2(0, width), - Vector2(10, 6.5), - Vector2(7, 10), - Vector2(15.5, 13.2), - ]; - final topRightCurveShape = BezierCurveShape( - controlPoints: topRightControlPoints, - )..rotate(curveRotationAngle); - final topRightFixtureDef = FixtureDef(topRightCurveShape); - fixturesDefs.add(topRightFixtureDef); - - final mediumLeftControlPoints = [ - topLeftControlPoints.last, - Vector2(21, 12.9), - Vector2(30, 7.1), - Vector2(32, 4.8), - ]; - final mediumLeftCurveShape = BezierCurveShape( - controlPoints: mediumLeftControlPoints, - )..rotate(curveRotationAngle); - final mediumLeftFixtureDef = FixtureDef(mediumLeftCurveShape); - fixturesDefs.add(mediumLeftFixtureDef); - - final mediumRightControlPoints = [ - topRightControlPoints.last, - Vector2(21, 17.2), - Vector2(30, 12.1), - Vector2(32, 10.2), - ]; - final mediumRightCurveShape = BezierCurveShape( - controlPoints: mediumRightControlPoints, - )..rotate(curveRotationAngle); - final mediumRightFixtureDef = FixtureDef(mediumRightCurveShape); - fixturesDefs.add(mediumRightFixtureDef); - - final bottomLeftControlPoints = [ - mediumLeftControlPoints.last, - Vector2(40, -1), - Vector2(48, 1.9), - Vector2(50.5, 2.5), - ]; + controlPoints: [ + Vector2(-37.9, 30.4), + Vector2(-38, 23.9), + Vector2(-30.93, 18.2), + ], + ); + final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape); + fixturesDefs.add(topLeftCurveFixtureDef); + + final middleLeftCurveShape = BezierCurveShape( + controlPoints: [ + Vector2(-30.93, 18.2), + Vector2(-22.6, 10.3), + Vector2(-30, 0.2), + ], + ); + final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape); + fixturesDefs.add(middleLeftCurveFixtureDef); + final bottomLeftCurveShape = BezierCurveShape( - controlPoints: bottomLeftControlPoints, - )..rotate(curveRotationAngle); - final bottomLeftFixtureDef = FixtureDef(bottomLeftCurveShape); - fixturesDefs.add(bottomLeftFixtureDef); - - final bottomRightControlPoints = [ - mediumRightControlPoints.last, - Vector2(40, 4), - Vector2(46, 6.5), - Vector2(48.8, 7.6), - ]; + controlPoints: [ + Vector2(-30, 0.2), + Vector2(-36, -8.6), + Vector2(-32.04, -18.3), + ], + ); + final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape); + fixturesDefs.add(bottomLeftCurveFixtureDef); + + final topRightStraightShape = EdgeShape() + ..set( + Vector2(-33, 31.3), + Vector2(-27.2, 21.3), + ); + final topRightStraightFixtureDef = FixtureDef(topRightStraightShape); + fixturesDefs.add(topRightStraightFixtureDef); + + final middleRightCurveShape = BezierCurveShape( + controlPoints: [ + Vector2(-27.2, 21.3), + Vector2(-16.5, 11.4), + Vector2(-25.29, -1.7), + ], + ); + final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape); + fixturesDefs.add(middleRightCurveFixtureDef); + final bottomRightCurveShape = BezierCurveShape( - controlPoints: bottomRightControlPoints, - )..rotate(curveRotationAngle); - final bottomRightFixtureDef = FixtureDef(bottomRightCurveShape); - fixturesDefs.add(bottomRightFixtureDef); - - final exitWall = ArcShape( - center: exitPoint, - arcRadius: width / 2, - angle: math.pi, - rotation: exitRotationAngle, + controlPoints: [ + Vector2(-25.29, -1.7), + Vector2(-29.91, -8.5), + Vector2(-26.8, -15.7), + ], ); - final exitFixtureDef = FixtureDef(exitWall); - fixturesDefs.add(exitFixtureDef); + final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape); + fixturesDefs.add(bottomRightCurveFixtureDef); return fixturesDefs; } @@ -159,6 +133,52 @@ class _SpaceshipExitRailRamp extends BodyComponent return body; } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + } + + Future _loadSprite() async { + final sprite = await gameRef.loadSprite( + Assets.images.components.spaceshipDropTube.path, + ); + final spriteComponent = SpriteComponent( + sprite: sprite, + size: Vector2(17.5, 55.7), + anchor: Anchor.center, + position: Vector2(-29.4, -5.7), + ); + + await add(spriteComponent); + } +} + +class _SpaceshipExitRailBase extends BodyComponent + with InitialPosition, Layered { + _SpaceshipExitRailBase({required this.radius}) + : super( + priority: SpaceshipExitRail.ballPriorityWhenOnSpaceshipExitRail + 1, + ) { + renderBody = false; + layer = Layer.board; + } + + final double radius; + + @override + Body createBody() { + final shape = CircleShape()..radius = radius; + + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } } /// {@template spaceship_exit_rail_end} @@ -169,15 +189,21 @@ class SpaceshipExitRailEnd extends RampOpening { /// {@macro spaceship_exit_rail_end} SpaceshipExitRailEnd() : super( - pathwayLayer: Layer.spaceshipExitRail, + insideLayer: Layer.spaceshipExitRail, orientation: RampOrientation.down, ) { + renderBody = false; layer = Layer.spaceshipExitRail; } @override Shape get shape { - return CircleShape()..radius = 1; + return ArcShape( + center: Vector2(-29, -17.8), + arcRadius: 2.5, + angle: math.pi * 0.8, + rotation: -0.16, + ); } } @@ -191,8 +217,7 @@ class SpaceshipExitRailEndBallContactCallback @override void begin(SpaceshipExitRailEnd exitRail, Ball ball, _) { ball - ..priority = 1 - ..gameRef.reorderChildren() + ..sendTo(exitRail.outsidePriority) ..layer = exitRail.outsideLayer; } } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index f5d03c80..030edc50 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -2,8 +2,8 @@ import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/components/components.dart'; -import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template wall} /// A continuous generic and [BodyType.static] barrier that divides a game area. diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 6c7dab2d..fb4804e1 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -19,9 +19,16 @@ extension PinballGameAssetsX on PinballGame { images.load( components.Assets.images.launchRamp.foregroundRailing.keyName, ), + images.load(components.Assets.images.dino.dinoLandTop.keyName), + images.load(components.Assets.images.dino.dinoLandBottom.keyName), + images.load(components.Assets.images.dashBumper.a.active.keyName), + images.load(components.Assets.images.dashBumper.a.inactive.keyName), + images.load(components.Assets.images.dashBumper.b.active.keyName), + images.load(components.Assets.images.dashBumper.b.inactive.keyName), + images.load(components.Assets.images.dashBumper.main.active.keyName), + images.load(components.Assets.images.dashBumper.main.inactive.keyName), images.load(Assets.images.components.background.path), - images.load(Assets.images.components.launchRamp.launchRamp.path), - images.load(Assets.images.components.launchRamp.launchRailFG.path), + images.load(Assets.images.components.spaceshipDropTube.path), ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index b2e375de..4a1d91da 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -45,9 +45,7 @@ class PinballGame extends Forge2DGame ); unawaited( addFromBlueprint( - SpaceshipExitRail( - position: Vector2(-34.3, 23.8), - ), + SpaceshipExitRail(), ), ); @@ -66,6 +64,13 @@ class PinballGame extends Forge2DGame Future _addGameBoundaries() async { await add(BottomWall()); createBoundaries(this).forEach(add); + unawaited( + addFromBlueprint( + DinoWalls( + position: Vector2(-2.4, 0), + ), + ), + ); } Future _addPlunger() async { diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index b3589cb3..2fe23925 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,6 +3,8 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -15,23 +17,13 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); + /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); - $AssetsImagesComponentsLaunchRampGen get launchRamp => - const $AssetsImagesComponentsLaunchRampGen(); -} - -class $AssetsImagesComponentsLaunchRampGen { - const $AssetsImagesComponentsLaunchRampGen(); - - /// File path: assets/images/components/launch_ramp/launch-ramp.png - AssetGenImage get launchRamp => const AssetGenImage( - 'assets/images/components/launch_ramp/launch-ramp.png'); - - /// File path: assets/images/components/launch_ramp/launch-rail-FG.png - AssetGenImage get launchRailFG => const AssetGenImage( - 'assets/images/components/launch_ramp/launch-rail-FG.png'); + /// File path: assets/images/components/spaceship-drop-tube.png + AssetGenImage get spaceshipDropTube => + const AssetGenImage('assets/images/components/spaceship-drop-tube.png'); } class Assets { diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index 6975f8cb..edc176e7 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -82,7 +82,7 @@ List calculateEllipse({ /// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve List calculateBezierCurve({ required List controlPoints, - double step = 0.001, + double step = 0.01, }) { assert( 0 <= step && step <= 1, diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index 7a49b2b2..eb5e8d74 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -98,14 +98,14 @@ void main() { ); }); - test('returns by default 1000 points as indicated by step', () { + test('returns by default 100 points as indicated by step', () { final points = calculateBezierCurve( controlPoints: [ Vector2(0, 0), Vector2(10, 10), ], ); - expect(points.length, 1000); + expect(points.length, 100); }); test('returns as many points as indicated by step', () { @@ -114,9 +114,9 @@ void main() { Vector2(0, 0), Vector2(10, 10), ], - step: 0.01, + step: 0.02, ); - expect(points.length, 100); + expect(points.length, 50); }); }); diff --git a/packages/pinball_components/assets/images/dash_bumper/a/active.png b/packages/pinball_components/assets/images/dash_bumper/a/active.png new file mode 100644 index 00000000..feeee11f Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/a/inactive.png b/packages/pinball_components/assets/images/dash_bumper/a/inactive.png new file mode 100644 index 00000000..58ab8c56 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/b/active.png b/packages/pinball_components/assets/images/dash_bumper/b/active.png new file mode 100644 index 00000000..4bc2897f Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/b/inactive.png b/packages/pinball_components/assets/images/dash_bumper/b/inactive.png new file mode 100644 index 00000000..eddc7693 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/main/active.png b/packages/pinball_components/assets/images/dash_bumper/main/active.png new file mode 100644 index 00000000..bef56684 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/main/active.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/main/inactive.png b/packages/pinball_components/assets/images/dash_bumper/main/inactive.png new file mode 100644 index 00000000..e6f15b38 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/main/inactive.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-bottom.png b/packages/pinball_components/assets/images/dino/dino-land-bottom.png new file mode 100644 index 00000000..1839dda3 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/dino-land-bottom.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-top.png b/packages/pinball_components/assets/images/dino/dino-land-top.png new file mode 100644 index 00000000..85c2619a Binary files /dev/null and b/packages/pinball_components/assets/images/dino/dino-land-top.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 2ddd26d3..0e47c425 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -14,7 +14,9 @@ class $AssetsImagesGen { AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); - + $AssetsImagesDashBumperGen get dashBumper => + const $AssetsImagesDashBumperGen(); + $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); /// File path: assets/images/flutter_sign_post.png @@ -45,6 +47,27 @@ class $AssetsImagesBaseboardGen { const AssetGenImage('assets/images/baseboard/right.png'); } +class $AssetsImagesDashBumperGen { + const $AssetsImagesDashBumperGen(); + + $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); + $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); + $AssetsImagesDashBumperMainGen get main => + const $AssetsImagesDashBumperMainGen(); +} + +class $AssetsImagesDinoGen { + const $AssetsImagesDinoGen(); + + /// File path: assets/images/dino/dino-land-bottom.png + AssetGenImage get dinoLandBottom => + const AssetGenImage('assets/images/dino/dino-land-bottom.png'); + + /// File path: assets/images/dino/dino-land-top.png + AssetGenImage get dinoLandTop => + const AssetGenImage('assets/images/dino/dino-land-top.png'); +} + class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); @@ -69,6 +92,42 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesDashBumperAGen { + const $AssetsImagesDashBumperAGen(); + + /// File path: assets/images/dash_bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash_bumper/a/active.png'); + + /// File path: assets/images/dash_bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); +} + +class $AssetsImagesDashBumperBGen { + const $AssetsImagesDashBumperBGen(); + + /// File path: assets/images/dash_bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash_bumper/b/active.png'); + + /// File path: assets/images/dash_bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); +} + +class $AssetsImagesDashBumperMainGen { + const $AssetsImagesDashBumperMainGen(); + + /// File path: assets/images/dash_bumper/main/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash_bumper/main/active.png'); + + /// File path: assets/images/dash_bumper/main/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); +} + class Assets { Assets._(); diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index b62ceeba..892936f9 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -83,8 +83,9 @@ class Ball extends BodyComponent final direction = body.linearVelocity.normalized(); final effect = FireEffect( burstPower: _boostTimer, - direction: direction, - position: body.position, + direction: -direction, + position: Vector2(body.position.x, -body.position.y), + priority: priority - 1, ); unawaited(gameRef.add(effect)); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index a0a0ab14..ce3ea7cb 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -2,6 +2,8 @@ export 'ball.dart'; export 'baseboard.dart'; export 'board_dimensions.dart'; export 'board_side.dart'; +export 'dash_nest_bumper.dart'; +export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; export 'flutter_sign_post.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart new file mode 100644 index 00000000..a2b9b982 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart @@ -0,0 +1,142 @@ +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 dash_nest_bumper} +/// Bumper with a nest appearance. +/// {@endtemplate} +abstract class DashNestBumper extends BodyComponent with InitialPosition { + /// {@macro dash_nest_bumper} + DashNestBumper._({ + required String activeAssetPath, + required String inactiveAssetPath, + required SpriteComponent spriteComponent, + }) : _activeAssetPath = activeAssetPath, + _inactiveAssetPath = inactiveAssetPath, + _spriteComponent = spriteComponent; + + final String _activeAssetPath; + late final Sprite _activeSprite; + final String _inactiveAssetPath; + late final Sprite _inactiveSprite; + final SpriteComponent _spriteComponent; + + 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 [DashNestBumper]. + void activate() { + _spriteComponent + ..sprite = _activeSprite + ..size = _activeSprite.originalSize / 10; + } + + /// Deactivates the [DashNestBumper]. + void deactivate() { + _spriteComponent + ..sprite = _inactiveSprite + ..size = _inactiveSprite.originalSize / 10; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprites(); + + // TODO(erickzanardo): Look into using onNewState instead. + // Currently doing: onNewState(gameRef.read()) will throw an + // `Exception: build context is not available yet` + deactivate(); + await add(_spriteComponent); + } +} + +/// {@macro dash_nest_bumper} +class BigDashNestBumper extends DashNestBumper { + /// {@macro dash_nest_bumper} + BigDashNestBumper() + : super._( + activeAssetPath: Assets.images.dashBumper.main.active.keyName, + inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + ), + ); + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 4.85, + minorRadius: 3.95, + )..rotate(math.pi / 2); + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +/// {@macro dash_nest_bumper} +class SmallDashNestBumper extends DashNestBumper { + /// {@macro dash_nest_bumper} + SmallDashNestBumper._({ + required String activeAssetPath, + required String inactiveAssetPath, + required SpriteComponent spriteComponent, + }) : super._( + activeAssetPath: activeAssetPath, + inactiveAssetPath: inactiveAssetPath, + spriteComponent: spriteComponent, + ); + + /// {@macro dash_nest_bumper} + SmallDashNestBumper.a() + : this._( + activeAssetPath: Assets.images.dashBumper.a.active.keyName, + inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0.35, -1.2), + ), + ); + + /// {@macro dash_nest_bumper} + SmallDashNestBumper.b() + : this._( + activeAssetPath: Assets.images.dashBumper.b.active.keyName, + inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0.35, -1.2), + ), + ); + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 3, + minorRadius: 2.25, + )..rotate(math.pi / 2); + final fixtureDef = FixtureDef(shape) + ..friction = 0 + ..restitution = 4; + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart new file mode 100644 index 00000000..13f56ff3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -0,0 +1,225 @@ +// ignore_for_file: comment_references, avoid_renaming_method_parameters + +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; + +/// {@template dinowalls} +/// A [Blueprint] which creates walls for the [ChromeDino]. +/// {@endtemplate} +class DinoWalls extends Forge2DBlueprint { + /// {@macro dinowalls} + DinoWalls({required this.position}); + + /// The [position] where the elements will be created + final Vector2 position; + + @override + void build(_) { + addAll([ + _DinoTopWall()..initialPosition = position, + _DinoBottomWall()..initialPosition = position, + ]); + } +} + +/// {@template dino_top_wall} +/// Wall segment located above [ChromeDino]. +/// {@endtemplate} +class _DinoTopWall extends BodyComponent with InitialPosition { + ///{@macro dino_top_wall} + _DinoTopWall() : super(priority: 2); + + List _createFixtureDefs() { + final fixturesDef = []; + + final topStraightShape = EdgeShape() + ..set( + Vector2(29.5, 35.1), + Vector2(28.4, 35.1), + ); + final topStraightFixtureDef = FixtureDef(topStraightShape); + fixturesDef.add(topStraightFixtureDef); + + final topCurveShape = BezierCurveShape( + controlPoints: [ + topStraightShape.vertex1, + Vector2(17.4, 26.38), + Vector2(25.5, 20.7), + ], + ); + fixturesDef.add(FixtureDef(topCurveShape)); + + final middleCurveShape = BezierCurveShape( + controlPoints: [ + topCurveShape.vertices.last, + Vector2(27.8, 20.1), + Vector2(26.8, 19.5), + ], + ); + fixturesDef.add(FixtureDef(middleCurveShape)); + + final bottomCurveShape = BezierCurveShape( + controlPoints: [ + middleCurveShape.vertices.last, + Vector2(21.15, 16), + Vector2(25.6, 15.2), + ], + ); + fixturesDef.add(FixtureDef(bottomCurveShape)); + + final bottomStraightShape = EdgeShape() + ..set( + bottomCurveShape.vertices.last, + Vector2(31, 14.5), + ); + final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); + fixturesDef.add(bottomStraightFixtureDef); + + return fixturesDef; + } + + @override + Body createBody() { + renderBody = false; + + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition + ..type = BodyType.static; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach( + (fixture) => body.createFixture( + fixture + ..restitution = 0.1 + ..friction = 0, + ), + ); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + } + + Future _loadSprite() async { + final sprite = await gameRef.loadSprite( + Assets.images.dino.dinoLandTop.keyName, + ); + final spriteComponent = SpriteComponent( + sprite: sprite, + size: Vector2(10.6, 27.7), + anchor: Anchor.center, + position: Vector2(27, -28.2), + ); + + await add(spriteComponent); + } +} + +/// {@template dino_bottom_wall} +/// Wall segment located below [ChromeDino]. +/// {@endtemplate} +class _DinoBottomWall extends BodyComponent with InitialPosition { + ///{@macro dino_top_wall} + _DinoBottomWall() : super(priority: 2); + + List _createFixtureDefs() { + final fixturesDef = []; + + final topStraightControlPoints = [ + Vector2(32.4, 8.3), + Vector2(25, 7.7), + ]; + final topStraightShape = EdgeShape() + ..set( + topStraightControlPoints.first, + topStraightControlPoints.last, + ); + final topStraightFixtureDef = FixtureDef(topStraightShape); + fixturesDef.add(topStraightFixtureDef); + + final topLeftCurveControlPoints = [ + topStraightControlPoints.last, + Vector2(21.8, 7), + Vector2(29.5, -13.8), + ]; + final topLeftCurveShape = BezierCurveShape( + controlPoints: topLeftCurveControlPoints, + ); + fixturesDef.add(FixtureDef(topLeftCurveShape)); + + final bottomLeftStraightControlPoints = [ + topLeftCurveControlPoints.last, + Vector2(31.8, -44.1), + ]; + final bottomLeftStraightShape = EdgeShape() + ..set( + bottomLeftStraightControlPoints.first, + bottomLeftStraightControlPoints.last, + ); + final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape); + fixturesDef.add(bottomLeftStraightFixtureDef); + + final bottomStraightControlPoints = [ + bottomLeftStraightControlPoints.last, + Vector2(37.8, -44.1), + ]; + final bottomStraightShape = EdgeShape() + ..set( + bottomStraightControlPoints.first, + bottomStraightControlPoints.last, + ); + final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); + fixturesDef.add(bottomStraightFixtureDef); + + return fixturesDef; + } + + @override + Body createBody() { + renderBody = false; + + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition + ..type = BodyType.static; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach( + (fixture) => body.createFixture( + fixture + ..restitution = 0.1 + ..friction = 0, + ), + ); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + } + + Future _loadSprite() async { + final sprite = await gameRef.loadSprite( + Assets.images.dino.dinoLandBottom.keyName, + ); + final spriteComponent = SpriteComponent( + sprite: sprite, + size: Vector2(15.6, 54.8), + anchor: Anchor.center, + )..position = Vector2(31.7, 18); + + await add(spriteComponent); + } +} diff --git a/packages/pinball_components/lib/src/components/fire_effect.dart b/packages/pinball_components/lib/src/components/fire_effect.dart index 0a7cef2b..cf8c3707 100644 --- a/packages/pinball_components/lib/src/components/fire_effect.dart +++ b/packages/pinball_components/lib/src/components/fire_effect.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame/particles.dart'; import 'package:flame_forge2d/flame_forge2d.dart' hide Particle; @@ -19,33 +20,24 @@ const _particleRadius = 0.25; /// A [BodyComponent] which creates a fire trail effect using the given /// parameters /// {@endtemplate} -class FireEffect extends BodyComponent { +class FireEffect extends ParticleSystemComponent { /// {@macro fire_effect} FireEffect({ required this.burstPower, - required this.position, required this.direction, - }); + Vector2? position, + int? priority, + }) : super( + position: position, + priority: priority, + ); /// A [double] value that will define how "strong" the burst of particles - /// will be + /// will be. final double burstPower; - /// The position of the burst - final Vector2 position; - - /// Which direction the burst will aim + /// Which direction the burst will aim. final Vector2 direction; - late Particle _particle; - - @override - Body createBody() { - final bodyDef = BodyDef()..position = position; - - final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } @override Future onLoad() async { @@ -71,15 +63,15 @@ class FireEffect extends BodyComponent { ); }), ]; - final rng = math.Random(); + final random = math.Random(); final spreadTween = Tween(begin: -0.2, end: 0.2); - _particle = Particle.generate( - count: (rng.nextDouble() * (burstPower * 10)).toInt(), + particle = Particle.generate( + count: math.max((random.nextDouble() * (burstPower * 10)).toInt(), 1), generator: (_) { final spread = Vector2( - spreadTween.transform(rng.nextDouble()), - spreadTween.transform(rng.nextDouble()), + spreadTween.transform(random.nextDouble()), + spreadTween.transform(random.nextDouble()), ); final finalDirection = Vector2(direction.x, -direction.y) + spread; final speed = finalDirection * (burstPower * 20); @@ -88,26 +80,9 @@ class FireEffect extends BodyComponent { lifespan: 5 / burstPower, position: Vector2.zero(), speed: speed, - child: children[rng.nextInt(children.length)], + child: children[random.nextInt(children.length)], ); }, ); } - - @override - void update(double dt) { - super.update(dt); - _particle.update(dt); - - if (_particle.shouldRemove) { - removeFromParent(); - } - } - - @override - void render(Canvas canvas) { - super.render(canvas); - - _particle.render(canvas); - } } diff --git a/packages/pinball_components/lib/src/components/ramp_opening.dart b/packages/pinball_components/lib/src/components/ramp_opening.dart index 8f33e813..cb6066f2 100644 --- a/packages/pinball_components/lib/src/components/ramp_opening.dart +++ b/packages/pinball_components/lib/src/components/ramp_opening.dart @@ -20,28 +20,41 @@ enum RampOrientation { /// [RampOpeningBallContactCallback] detects when a [Ball] passes /// through this opening. /// -/// By default the base [layer] is set to [Layer.board]. +/// By default the base [layer] is set to [Layer.board] and the +/// [outsidePriority] is set to the lowest possible [Layer]. /// {@endtemplate} // TODO(ruialonso): Consider renaming the class. abstract class RampOpening extends BodyComponent with InitialPosition, Layered { /// {@macro ramp_opening} RampOpening({ - required Layer pathwayLayer, + required Layer insideLayer, Layer? outsideLayer, + int? insidePriority, + int? outsidePriority, required this.orientation, - }) : _pathwayLayer = pathwayLayer, - _outsideLayer = outsideLayer ?? Layer.board { - layer = Layer.board; + }) : _insideLayer = insideLayer, + _outsideLayer = outsideLayer ?? Layer.board, + _insidePriority = insidePriority ?? 0, + _outsidePriority = outsidePriority ?? 0 { + layer = Layer.opening; } - final Layer _pathwayLayer; + final Layer _insideLayer; final Layer _outsideLayer; + final int _insidePriority; + final int _outsidePriority; - /// Mask of category bits for collision inside pathway. - Layer get pathwayLayer => _pathwayLayer; + /// Mask of category bits for collision inside ramp. + Layer get insideLayer => _insideLayer; - /// Mask of category bits for collision outside pathway. + /// Mask of category bits for collision outside ramp. Layer get outsideLayer => _outsideLayer; + /// Priority for the [Ball] inside ramp. + int get insidePriority => _insidePriority; + + /// Priority for the [Ball] outside ramp. + int get outsidePriority => _outsidePriority; + /// The [Shape] of the [RampOpening]. Shape get shape; @@ -64,8 +77,7 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered { } /// {@template ramp_opening_ball_contact_callback} -/// Detects when a [Ball] enters or exits a pathway ramp through a -/// [RampOpening]. +/// Detects when a [Ball] enters or exits a ramp through a [RampOpening]. /// /// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is /// outside or inside a ramp. @@ -80,9 +92,11 @@ class RampOpeningBallContactCallback Layer layer; if (!_ballsInside.contains(ball)) { - layer = opening.pathwayLayer; + layer = opening.insideLayer; _ballsInside.add(ball); - ball.layer = layer; + ball + ..sendTo(opening.insidePriority) + ..layer = layer; } else { _ballsInside.remove(ball); } @@ -103,7 +117,9 @@ class RampOpeningBallContactCallback ball.body.linearVelocity.y > 0); if (isBallOutsideOpening) { - ball.layer = opening.outsideLayer; + ball + ..sendTo(opening.outsidePriority) + ..layer = opening.outsideLayer; _ballsInside.remove(ball); } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 7e9d097e..59f1037a 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -21,6 +21,9 @@ class Spaceship extends Forge2DBlueprint { /// The [position] where the elements will be created final Vector2 position; + /// Base priority for wall while be on spaceship. + static const ballPriorityWhenOnSpaceship = 4; + @override void build(_) { addAllContactCallback([ @@ -33,8 +36,8 @@ class Spaceship extends Forge2DBlueprint { SpaceshipEntrance()..initialPosition = position, AndroidHead()..initialPosition = position, SpaceshipHole( - onExitLayer: Layer.spaceshipExitRail, - onExitElevation: 2, + outsideLayer: Layer.spaceshipExitRail, + outsidePriority: 2, )..initialPosition = position - Vector2(5.2, 4.8), SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipWall()..initialPosition = position, @@ -47,8 +50,8 @@ class Spaceship extends Forge2DBlueprint { /// {@endtemplate} class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_saucer} - // TODO(ruimiguel): apply Elevated when PR merged. - SpaceshipSaucer() : super(priority: 3) { + SpaceshipSaucer() + : super(priority: Spaceship.ballPriorityWhenOnSpaceship - 1) { layer = Layer.spaceship; } @@ -92,7 +95,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@endtemplate} class AndroidHead extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_bridge} - AndroidHead() : super(priority: 4) { + AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -147,16 +150,13 @@ class SpaceshipEntrance extends RampOpening { /// {@macro spaceship_entrance} SpaceshipEntrance() : super( - pathwayLayer: Layer.spaceship, + insideLayer: Layer.spaceship, orientation: RampOrientation.up, + insidePriority: Spaceship.ballPriorityWhenOnSpaceship, ) { layer = Layer.spaceship; } - /// Priority order for [SpaceshipHole] on enter. - // TODO(ruimiguel): apply Elevated when PR merged. - final int onEnterElevation = 4; - @override Shape get shape { renderBody = false; @@ -181,24 +181,22 @@ class SpaceshipEntrance extends RampOpening { /// {@endtemplate} class SpaceshipHole extends RampOpening { /// {@macro spaceship_hole} - SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1}) + SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1}) : super( - pathwayLayer: Layer.spaceship, - outsideLayer: onExitLayer, + insideLayer: Layer.spaceship, + outsideLayer: outsideLayer, + outsidePriority: outsidePriority, orientation: RampOrientation.up, ) { + renderBody = false; layer = Layer.spaceship; } - /// Priority order for [SpaceshipHole] on exit. - // TODO(ruimiguel): apply Elevated when PR merged. - final int onExitElevation; - @override Shape get shape { return ArcShape( - center: Vector2(0, 4.2), - arcRadius: 6, + center: Vector2(0, 3.2), + arcRadius: 5, angle: 1, rotation: 60 * pi / 180, ); @@ -235,8 +233,7 @@ class _SpaceshipWallShape extends ChainShape { /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_wall} - // TODO(ruimiguel): apply Elevated when PR merged - SpaceshipWall() : super(priority: 4) { + SpaceshipWall() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -269,9 +266,7 @@ class SpaceshipEntranceBallContactCallback @override void begin(SpaceshipEntrance entrance, Ball ball, _) { ball - // TODO(ruimiguel): apply Elevated when PR merged. - ..priority = entrance.onEnterElevation - ..gameRef.reorderChildren() + ..sendTo(entrance.insidePriority) ..layer = Layer.spaceship; } } @@ -279,16 +274,14 @@ class SpaceshipEntranceBallContactCallback /// [ContactCallback] that handles the contact between the [Ball] /// and a [SpaceshipHole]. /// -/// It sets the [Ball] priority and filter data so it will "be back" on the -/// board. +/// It sets the [Ball] priority and filter data so it will outside of the +/// [Spaceship]. class SpaceshipHoleBallContactCallback extends ContactCallback { @override void begin(SpaceshipHole hole, Ball ball, _) { ball - // TODO(ruimiguel): apply Elevated when PR merged. - ..priority = hole.onExitElevation - ..gameRef.reorderChildren() + ..sendTo(hole.outsidePriority) ..layer = hole.outsideLayer; } } diff --git a/packages/pinball_components/lib/src/flame/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart index c46a6fd2..9af8dba6 100644 --- a/packages/pinball_components/lib/src/flame/flame.dart +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -1 +1,2 @@ export 'blueprint.dart'; +export 'priority.dart'; diff --git a/packages/pinball_components/lib/src/flame/priority.dart b/packages/pinball_components/lib/src/flame/priority.dart new file mode 100644 index 00000000..f4dccabf --- /dev/null +++ b/packages/pinball_components/lib/src/flame/priority.dart @@ -0,0 +1,39 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; + +/// Helper methods to change the [priority] of a [Component]. +extension ComponentPriorityX on Component { + static const _lowestPriority = 0; + + /// Changes the priority to a specific one. + void sendTo(int destinationPriority) { + if (priority != destinationPriority) { + priority = math.max(destinationPriority, _lowestPriority); + reorderChildren(); + } + } + + /// Changes the priority to the lowest possible. + void sendToBack() { + if (priority != _lowestPriority) { + priority = _lowestPriority; + reorderChildren(); + } + } + + /// Decreases the priority to be lower than another [Component]. + void showBehindOf(Component other) { + if (priority >= other.priority) { + priority = math.max(other.priority - 1, _lowestPriority); + reorderChildren(); + } + } + + /// Increases the priority to be higher than another [Component]. + void showInFrontOf(Component other) { + if (priority <= other.priority) { + priority = other.priority + 1; + reorderChildren(); + } + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index ee832b67..5e0985cc 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -27,8 +27,12 @@ flutter: assets: - assets/images/ - assets/images/baseboard/ + - assets/images/dino/ - assets/images/flipper/ - assets/images/launch_ramp/ + - assets/images/dash_bumper/a/ + - assets/images/dash_bumper/b/ + - assets/images/dash_bumper/main/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart index 9f066952..1262af11 100644 --- a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect.dart @@ -34,7 +34,6 @@ class _EffectEmitter extends Component { add( FireEffect( burstPower: (_timer / _timerLimit) * _force, - position: Vector2.zero(), direction: _direction, ), ); diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 8d6f45b3..7771d1e1 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -1,11 +1,8 @@ -import 'dart:ui'; - +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; -class MockCanvas extends Mock implements Canvas {} - class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} @@ -24,3 +21,5 @@ class MockContact extends Mock implements Contact {} class MockContactCallback extends Mock implements ContactCallback {} + +class MockComponent extends Mock implements Component {} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart new file mode 100644 index 00000000..2c6bb00c --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart @@ -0,0 +1,116 @@ +// 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('BigDashNestBumper', () { + flameTester.test('loads correctly', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = BigDashNestBumper(); + 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)), + ); + }); + }); + + group('SmallDashNestBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = SmallDashNestBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = SmallDashNestBumper.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/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart new file mode 100644 index 00000000..af80444b --- /dev/null +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -0,0 +1,28 @@ +// 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_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('DinoWalls', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final dinoWalls = DinoWalls(position: Vector2.zero()); + await game.addFromBlueprint(dinoWalls); + await game.ready(); + + for (final wall in dinoWalls.components) { + expect(game.contains(wall), isTrue); + } + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/fire_effect_test.dart b/packages/pinball_components/test/src/components/fire_effect_test.dart index bc6baa4b..2c404747 100644 --- a/packages/pinball_components/test/src/components/fire_effect_test.dart +++ b/packages/pinball_components/test/src/components/fire_effect_test.dart @@ -1,11 +1,8 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; @@ -14,42 +11,16 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); - setUpAll(() { - registerFallbackValue(Offset.zero); - registerFallbackValue(Paint()); - }); - - group('FireEffect', () { - flameTester.test('is removed once its particles are done', (game) async { - await game.ensureAdd( - FireEffect( - burstPower: 1, - position: Vector2.zero(), - direction: Vector2.all(2), - ), - ); - await game.ready(); - expect(game.children.whereType().length, equals(1)); - game.update(5); - - await game.ready(); - expect(game.children.whereType().length, equals(0)); - }); - - flameTester.test('render circles on the canvas', (game) async { - final effect = FireEffect( + flameTester.test( + 'loads correctly', + (game) async { + final fireEffect = FireEffect( burstPower: 1, - position: Vector2.zero(), - direction: Vector2.all(2), + direction: Vector2.zero(), ); - await game.ensureAdd(effect); - await game.ready(); - - final canvas = MockCanvas(); - effect.render(canvas); + await game.ensureAdd(fireEffect); - verify(() => canvas.drawCircle(any(), any(), any())) - .called(greaterThan(0)); - }); - }); + expect(game.contains(fireEffect), isTrue); + }, + ); } diff --git a/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png b/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png new file mode 100644 index 00000000..14ae9c0d Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-top-wall.png b/packages/pinball_components/test/src/components/golden/dino-top-wall.png new file mode 100644 index 00000000..0d434d69 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dino-top-wall.png differ diff --git a/packages/pinball_components/test/src/components/ramp_opening_test.dart b/packages/pinball_components/test/src/components/ramp_opening_test.dart index c49e9164..cb42203a 100644 --- a/packages/pinball_components/test/src/components/ramp_opening_test.dart +++ b/packages/pinball_components/test/src/components/ramp_opening_test.dart @@ -12,7 +12,7 @@ class TestRampOpening extends RampOpening { required RampOrientation orientation, required Layer pathwayLayer, }) : super( - pathwayLayer: pathwayLayer, + insideLayer: pathwayLayer, orientation: orientation, ); @@ -129,14 +129,12 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); }); flameTester.test( @@ -152,14 +150,12 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); }); flameTester.test( @@ -174,15 +170,13 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); @@ -200,15 +194,13 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); @@ -226,21 +218,19 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verifyNever(() => ball.layer = Layer.board); callback.begin(ball, area, MockContact()); - verifyNever(() => ball.layer = area.pathwayLayer); + verifyNever(() => ball.layer = area.insideLayer); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart index f89408f7..4d980c69 100644 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -59,7 +59,8 @@ void main() { group('SpaceshipEntranceBallContactCallback', () { test('changes the ball priority on contact', () { - when(() => entrance.onEnterElevation).thenReturn(3); + when(() => ball.priority).thenReturn(2); + when(() => entrance.insidePriority).thenReturn(3); SpaceshipEntranceBallContactCallback().begin( entrance, @@ -67,39 +68,15 @@ void main() { MockContact(), ); - verify(() => ball.priority = entrance.onEnterElevation).called(1); - }); - - test('re order the game children', () { - when(() => entrance.onEnterElevation).thenReturn(3); - - SpaceshipEntranceBallContactCallback().begin( - entrance, - ball, - MockContact(), - ); - - verify(game.reorderChildren).called(1); + verify(() => ball.sendTo(entrance.insidePriority)).called(1); }); }); group('SpaceshipHoleBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => ball.priority).thenReturn(2); when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.onExitElevation).thenReturn(1); - - SpaceshipHoleBallContactCallback().begin( - hole, - ball, - MockContact(), - ); - - verify(() => ball.priority = hole.onExitElevation).called(1); - }); - - test('re order the game children', () { - when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.onExitElevation).thenReturn(1); + when(() => hole.outsidePriority).thenReturn(1); SpaceshipHoleBallContactCallback().begin( hole, @@ -107,7 +84,7 @@ void main() { MockContact(), ); - verify(game.reorderChildren).called(1); + verify(() => ball.sendTo(hole.outsidePriority)).called(1); }); }); }); diff --git a/packages/pinball_components/test/src/flame/priority_test.dart b/packages/pinball_components/test/src/flame/priority_test.dart new file mode 100644 index 00000000..231c7744 --- /dev/null +++ b/packages/pinball_components/test/src/flame/priority_test.dart @@ -0,0 +1,221 @@ +// 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:mocktail/mocktail.dart'; +import 'package:pinball_components/src/flame/priority.dart'; + +import '../../helpers/helpers.dart'; + +class TestBodyComponent extends BodyComponent { + @override + Body createBody() { + final fixtureDef = FixtureDef(CircleShape()); + return world.createBody(BodyDef())..createFixture(fixtureDef); + } +} + +void main() { + final flameTester = FlameTester(Forge2DGame.new); + + group('ComponentPriorityX', () { + group('sendTo', () { + flameTester.test( + 'changes the priority correctly to other level', + (game) async { + const newPriority = 5; + final component = TestBodyComponent()..priority = 4; + + component.sendTo(newPriority); + + expect(component.priority, equals(newPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the new priority is different', + (game) async { + const newPriority = 5; + final component = MockComponent(); + when(() => component.priority).thenReturn(4); + + component.sendTo(newPriority); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is the same", + (game) async { + const newPriority = 5; + final component = MockComponent(); + when(() => component.priority).thenReturn(newPriority); + + component.sendTo(newPriority); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('sendToBack', () { + flameTester.test( + 'changes the priority correctly to board level', + (game) async { + final component = TestBodyComponent()..priority = 4; + + component.sendToBack(); + + expect(component.priority, equals(0)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is greater than lowest level', + (game) async { + final component = MockComponent(); + when(() => component.priority).thenReturn(4); + + component.sendToBack(); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is the lowest level", + (game) async { + final component = MockComponent(); + when(() => component.priority).thenReturn(0); + + component.sendToBack(); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('showBehindOf', () { + flameTester.test( + 'changes the priority if it is greater than other component', + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority - 1; + + component.showBehindOf(otherComponent); + + expect(component.priority, equals(otherComponent.priority - 1)); + }, + ); + + flameTester.test( + "doesn't change the priority if it is lower than other component", + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority + 1; + + component.showBehindOf(otherComponent); + + expect(component.priority, equals(startPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is greater than other component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority - 1); + + component.showBehindOf(otherComponent); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is lower than other " + 'component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority + 1); + + component.showBehindOf(otherComponent); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('showInFrontOf', () { + flameTester.test( + 'changes the priority if it is lower than other component', + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority + 1; + + component.showInFrontOf(otherComponent); + + expect(component.priority, equals(otherComponent.priority + 1)); + }, + ); + + flameTester.test( + "doesn't change the priority if it is greater than other component", + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority - 1; + + component.showInFrontOf(otherComponent); + + expect(component.priority, equals(startPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is lower than other component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority + 1); + + component.showInFrontOf(otherComponent); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is greater than other " + 'component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority - 1); + + component.showInFrontOf(otherComponent); + + verifyNever(component.reorderChildren); + }, + ); + }); + }); +} diff --git a/test/flame/component_controller_test.dart b/test/flame/component_controller_test.dart index 4e5da210..e1973274 100644 --- a/test/flame/component_controller_test.dart +++ b/test/flame/component_controller_test.dart @@ -31,6 +31,7 @@ void main() { ); }, ); + flameTester.test( 'throws AssertionError when not attached to controlled component', (game) async { @@ -44,6 +45,35 @@ void main() { ); }, ); + + flameTester.test( + 'throws Exception when adding a component', + (game) async { + final component = ControlledComponent(); + final controller = TestComponentController(component); + + await expectLater( + () async => controller.add(Component()), + throwsException, + ); + }, + ); + + flameTester.test( + 'throws Exception when adding multiple components', + (game) async { + final component = ControlledComponent(); + final controller = TestComponentController(component); + + await expectLater( + () async => controller.addAll([ + Component(), + Component(), + ]), + throwsException, + ); + }, + ); }); group('Controls', () { diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index f48d60ee..7d73b6bc 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -196,10 +196,10 @@ void main() { group('bonus letter activation', () { late GameBloc gameBloc; - final tester = flameBlocTester( + final flameBlocTester = FlameBlocTester( // TODO(alestiago): Use TestGame once BonusLetter has controller. - game: PinballGameTest.create, - gameBloc: () => gameBloc, + gameBuilder: PinballGameTest.create, + blocBuilder: () => gameBloc, ); setUp(() { @@ -211,7 +211,7 @@ void main() { ); }); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'adds BonusLetterActivated to GameBloc when not activated', setUp: (game, tester) async { await game.ready(); @@ -225,7 +225,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( "doesn't add BonusLetterActivated to GameBloc when already activated", setUp: (game, tester) async { const state = GameState( @@ -253,7 +253,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'adds a ColorEffect', setUp: (game, tester) async { const state = GameState( @@ -284,7 +284,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'only listens when there is a change on the letter status', setUp: (game, tester) async { await game.ready(); diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index dcd075ca..8417aa25 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -66,12 +66,12 @@ void main() { ); }); - final tester = flameBlocTester( - game: PinballGameTest.create, - gameBloc: () => gameBloc, + final flameBlocTester = FlameBlocTester( + gameBuilder: PinballGameTest.create, + blocBuilder: () => gameBloc, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'lost adds BallLost to GameBloc', setUp: (game, tester) async { final controller = LaunchedBallController(ball); @@ -86,7 +86,7 @@ void main() { ); group('listenWhen', () { - tester.testGameWidget( + flameBlocTester.testGameWidget( 'listens when a ball has been lost', setUp: (game, tester) async { final controller = LaunchedBallController(ball); @@ -107,7 +107,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'does not listen when a ball has not been lost', setUp: (game, tester) async { final controller = LaunchedBallController(ball); @@ -130,7 +130,7 @@ void main() { }); group('onNewState', () { - tester.testGameWidget( + flameBlocTester.testGameWidget( 'removes ball', setUp: (game, tester) async { final controller = LaunchedBallController(ball); @@ -147,7 +147,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'spawns a new ball when the ball is not the last one', setUp: (game, tester) async { final controller = LaunchedBallController(ball); @@ -168,7 +168,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'does not spawn a new ball is the last one', setUp: (game, tester) async { final controller = LaunchedBallController(ball); diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 48586895..33dbb991 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -1,7 +1,9 @@ // 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/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -9,6 +11,18 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { + assert( + bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty, + 'Bodies require fixtures to contact each other.', + ); + + final fixtureA = bodyA.body.fixtures.first; + final fixtureB = bodyB.body.fixtures.first; + final contact = Contact.init(fixtureA, 0, fixtureB, 0); + game.world.contactManager.contactListener?.beginContact(contact); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); @@ -30,13 +44,73 @@ void main() { 'a FlutterSignPost', (game) async { await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a BigDashNestBumper', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); expect( - game.descendants().whereType().length, + flutterForest.descendants().whereType().length, equals(1), ); }, ); + + flameTester.test( + 'two SmallDashNestBumper', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(2), + ); + }, + ); + }); + + group('controller', () { + group('listenWhen', () { + final gameBloc = MockGameBloc(); + final flameBlocTester = FlameBlocTester( + gameBuilder: TestGame.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'listens when a Bonus.dashNest is added', + verify: (game, tester) async { + final flutterForest = FlutterForest(); + + const state = GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {}, + bonusHistory: [GameBonus.dashNest], + ); + expect( + flutterForest.controller + .listenWhen(const GameState.initial(), state), + isTrue, + ); + }, + ); + }); }); flameTester.test( @@ -47,7 +121,7 @@ void main() { await game.ensureAdd(flutterForest); final previousBalls = game.descendants().whereType().length; - flutterForest.onNewState(MockGameState()); + flutterForest.controller.onNewState(MockGameState()); await game.ready(); expect( @@ -57,14 +131,13 @@ void main() { }, ); - group('listenWhen', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - game: TestGame.new, - gameBloc: () => gameBloc, - ); + group('bumpers', () { + late Ball ball; + late GameBloc gameBloc; setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -72,73 +145,167 @@ void main() { ); }); - tester.testGameWidget( - 'listens when a Bonus.dashNest is added', - verify: (game, tester) async { + final flameBlocTester = FlameBlocTester( + gameBuilder: PinballGameTest.create, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'add DashNestActivated event', + setUp: (game, tester) async { + await game.ready(); + final flutterForest = + game.descendants().whereType().first; + await game.ensureAdd(ball); + + final bumpers = + flutterForest.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + final controller = bumper.firstChild()!; + verify( + () => gameBloc.add(DashNestActivated(controller.id)), + ).called(1); + } + }, + ); + + flameBlocTester.testGameWidget( + 'add Scored event', + setUp: (game, tester) async { final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + await game.ensureAdd(ball); - const state = GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.dashNest], - ); - expect( - flutterForest.listenWhen(const GameState.initial(), state), - isTrue, - ); + final bumpers = + flutterForest.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + final points = (bumper as ScorePoints).points; + verify( + () => gameBloc.add(Scored(points: points)), + ).called(1); + } }, ); }); }); - group('DashNestBumperBallContactCallback', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - // TODO(alestiago): Use TestGame.new once a controller is implemented. - game: PinballGameTest.create, - gameBloc: () => gameBloc, - ); + group('DashNestBumperController', () { + late DashNestBumper dashNestBumper; setUp(() { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); + dashNestBumper = MockDashNestBumper(); }); - final dashNestBumper = MockDashNestBumper(); - tester.testGameWidget( - 'adds a DashNestActivated event with DashNestBumper.id', - setUp: (game, tester) async { - const id = '0'; - when(() => dashNestBumper.id).thenReturn(id); - when(() => dashNestBumper.gameRef).thenReturn(game); - }, - verify: (game, tester) async { - final contactCallback = DashNestBumperBallContactCallback(); - contactCallback.begin(dashNestBumper, MockBall(), MockContact()); + group( + 'listensWhen', + () { + late GameState previousState; + late GameState newState; + + setUp( + () { + previousState = MockGameState(); + newState = MockGameState(); + }, + ); + + test('listens when the id is added to activatedDashNests', () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({}); + when(() => newState.activatedDashNests).thenReturn({id}); + + expect(controller.listenWhen(previousState, newState), isTrue); + }); + + test('listens when the id is removed from activatedDashNests', () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({id}); + when(() => newState.activatedDashNests).thenReturn({}); + + expect(controller.listenWhen(previousState, newState), isTrue); + }); + + test("doesn't listen when the id is never in activatedDashNests", () { + final controller = DashNestBumperController( + dashNestBumper, + id: '', + ); - verify( - () => gameBloc.add(DashNestActivated(dashNestBumper.id)), - ).called(1); + when(() => previousState.activatedDashNests).thenReturn({}); + when(() => newState.activatedDashNests).thenReturn({}); + + expect(controller.listenWhen(previousState, newState), isFalse); + }); + + test("doesn't listen when the id still in activatedDashNests", () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({id}); + when(() => newState.activatedDashNests).thenReturn({id}); + + expect(controller.listenWhen(previousState, newState), isFalse); + }); }, ); - }); - group('BigDashNestBumper', () { - test('has points', () { - final dashNestBumper = BigDashNestBumper(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); - }); + group( + 'onNewState', + () { + late GameState state; - group('SmallDashNestBumper', () { - test('has points', () { - final dashNestBumper = SmallDashNestBumper(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); + setUp(() { + state = MockGameState(); + }); + + test( + 'activates the bumper when id in activatedDashNests', + () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => state.activatedDashNests).thenReturn({id}); + controller.onNewState(state); + + verify(() => dashNestBumper.activate()).called(1); + }, + ); + + test( + 'deactivates the bumper when id not in activatedDashNests', + () { + final controller = DashNestBumperController( + dashNestBumper, + id: '', + ); + + when(() => state.activatedDashNests).thenReturn({}); + controller.onNewState(state); + + verify(() => dashNestBumper.deactivate()).called(1); + }, + ); + }, + ); }); } diff --git a/test/game/components/spaceship_exit_rail_test.dart b/test/game/components/spaceship_exit_rail_test.dart index 99afc808..edd81aab 100644 --- a/test/game/components/spaceship_exit_rail_test.dart +++ b/test/game/components/spaceship_exit_rail_test.dart @@ -31,29 +31,33 @@ void main() { when(() => fixture.filterData).thenReturn(filterData); }); + // TODO(alestiago): Make ContactCallback private and use `beginContact` + // instead. group('SpaceshipExitHoleBallContactCallback', () { - test('changes the ball priority on contact', () { + setUp(() { + when(() => ball.priority).thenReturn(1); when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); + when(() => exitRailEnd.outsidePriority).thenReturn(0); + }); + test('changes the ball priority on contact', () { SpaceshipExitRailEndBallContactCallback().begin( exitRailEnd, ball, MockContact(), ); - verify(() => ball.priority = 1).called(1); + verify(() => ball.sendTo(exitRailEnd.outsidePriority)).called(1); }); - test('reorders the game children', () { - when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); - + test('changes the ball layer on contact', () { SpaceshipExitRailEndBallContactCallback().begin( exitRailEnd, ball, MockContact(), ); - verify(game.reorderChildren).called(1); + verify(() => ball.layer = exitRailEnd.outsideLayer).called(1); }); }); }); diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index 970dd12b..f78aebe7 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -1,21 +1,21 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/src/game/flame_game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/game/game.dart'; -FlameTester flameBlocTester({ - required T Function() game, - required GameBloc Function() gameBloc, -}) { - return FlameTester( - game, - pumpWidget: (gameWidget, tester) async { - await tester.pumpWidget( - BlocProvider.value( - value: gameBloc(), - child: gameWidget, - ), - ); - }, - ); +class FlameBlocTester> + extends FlameTester { + FlameBlocTester({ + required GameCreateFunction gameBuilder, + required B Function() blocBuilder, + }) : super( + gameBuilder, + pumpWidget: (gameWidget, tester) async { + await tester.pumpWidget( + BlocProvider.value( + value: blocBuilder(), + child: gameWidget, + ), + ); + }, + ); } diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 9453c93a..fbe8edfb 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -66,10 +66,6 @@ class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} -class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} - -class MockSpaceshipHole extends Mock implements SpaceshipHole {} - class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} class MockComponentSet extends Mock implements ComponentSet {}