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/assets/images/components/spaceship_railing_bg.png b/assets/images/components/spaceship_railing_bg.png new file mode 100644 index 00000000..2298f799 Binary files /dev/null and b/assets/images/components/spaceship_railing_bg.png differ diff --git a/assets/images/components/spaceship_railing_fg.png b/assets/images/components/spaceship_railing_fg.png new file mode 100644 index 00000000..e788fde0 Binary files /dev/null and b/assets/images/components/spaceship_railing_fg.png differ diff --git a/assets/images/components/spaceship_ramp.png b/assets/images/components/spaceship_ramp.png new file mode 100644 index 00000000..81498965 Binary files /dev/null and b/assets/images/components/spaceship_ramp.png differ diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart index 851028f0..1d6e0173 100644 --- a/lib/flame/component_controller.dart +++ b/lib/flame/component_controller.dart @@ -23,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]. diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 99b7e608..b5aadc97 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,10 +1,9 @@ export 'board.dart'; export 'bonus_word.dart'; export 'controlled_ball.dart'; -export 'flipper_controller.dart'; +export 'controlled_flipper.dart'; export 'flutter_forest.dart'; export 'jetpack_ramp.dart'; -export 'kicker.dart'; export 'launcher_ramp.dart'; export 'plunger.dart'; export 'score_points.dart'; diff --git a/lib/game/components/flipper_controller.dart b/lib/game/components/controlled_flipper.dart similarity index 100% rename from lib/game/components/flipper_controller.dart rename to lib/game/components/controlled_flipper.dart diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index aa5a9dbd..34b00dd4 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -2,45 +2,52 @@ import 'dart:math' as math; -import 'package:flame/extensions.dart'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; /// A [Blueprint] which creates the [JetpackRamp]. class Jetpack extends Forge2DBlueprint { + /// {@macro spaceship} + Jetpack(); + + static const int ballPriorityInsideRamp = 4; + @override void build(_) { - final position = Vector2( - BoardDimensions.bounds.left + 40.5, - BoardDimensions.bounds.top - 31.5, - ); - addAllContactCallback([ RampOpeningBallContactCallback<_JetpackRampOpening>(), ]); final rightOpening = _JetpackRampOpening( + // TODO(ruimiguel): set Board priority when defined. + outsidePriority: 1, rotation: math.pi, ) - ..initialPosition = position + Vector2(12.9, -20) + ..initialPosition = Vector2(1.7, 19) ..layer = Layer.opening; final leftOpening = _JetpackRampOpening( outsideLayer: Layer.spaceship, + outsidePriority: Spaceship.ballPriorityWhenOnSpaceship, rotation: math.pi, ) - ..initialPosition = position + Vector2(-2.5, -20) + ..initialPosition = Vector2(-13.7, 19) ..layer = Layer.jetpack; - final jetpackRamp = JetpackRamp() - ..initialPosition = position + Vector2(5, -20.2) - ..layer = Layer.jetpack; + final jetpackRamp = JetpackRamp(); + + final jetpackRampWallFg = _JetpackRampForegroundRailing(); + + final baseRight = _JetpackBase()..initialPosition = Vector2(1.7, 20); addAll([ rightOpening, leftOpening, + baseRight, jetpackRamp, + jetpackRampWallFg, ]); } } @@ -49,42 +56,129 @@ class Jetpack extends Forge2DBlueprint { /// Represents the upper left blue ramp of the [Board]. /// {@endtemplate} class JetpackRamp extends BodyComponent with InitialPosition, Layered { - JetpackRamp() : super(priority: 2) { + JetpackRamp() : super(priority: Jetpack.ballPriorityInsideRamp - 1) { layer = Layer.jetpack; - paint = Paint() - ..color = const Color.fromARGB(255, 8, 218, 241) - ..style = PaintingStyle.stroke; } - /// Radius of the external arc. - static const _externalRadius = 18.0; - /// Width between walls of the ramp. static const width = 5.0; List _createFixtureDefs() { final fixturesDef = []; - final externalCurveShape = ArcShape( - center: initialPosition, - arcRadius: _externalRadius, - angle: math.pi, - rotation: math.pi, + final outerLeftCurveShape = BezierCurveShape( + controlPoints: [ + Vector2(-30.95, 38), + Vector2(-32.5, 71.25), + Vector2(-14.2, 71.25), + ], + ); + + final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape); + fixturesDef.add(outerLeftCurveFixtureDef); + + final outerRightCurveShape = BezierCurveShape( + controlPoints: [ + outerLeftCurveShape.vertices.last, + Vector2(4.7, 71.25), + Vector2(6.3, 40), + ], + ); + + final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape); + fixturesDef.add(outerRightCurveFixtureDef); + + return fixturesDef; + } + + @override + Body createBody() { + renderBody = false; + + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprites(); + } + + Future _loadSprites() async { + final spriteRamp = await gameRef.loadSprite( + Assets.images.components.spaceshipRamp.path, + ); + + final spriteRampComponent = SpriteComponent( + sprite: spriteRamp, + size: Vector2(38.1, 33.8), + anchor: Anchor.center, + position: Vector2(-12.2, -53.5), + ); + + final spriteRailingBg = await gameRef.loadSprite( + Assets.images.components.spaceshipRailingBg.path, + ); + final spriteRailingBgComponent = SpriteComponent( + sprite: spriteRailingBg, + size: Vector2(38.3, 35.1), + anchor: Anchor.center, + position: spriteRampComponent.position + Vector2(0, -1), ); - final externalFixtureDef = FixtureDef(externalCurveShape); - fixturesDef.add(externalFixtureDef); - final internalCurveShape = externalCurveShape.copyWith( - arcRadius: _externalRadius - width, + await addAll([ + spriteRailingBgComponent, + spriteRampComponent, + ]); + } +} + +class _JetpackRampForegroundRailing extends BodyComponent + with InitialPosition, Layered { + _JetpackRampForegroundRailing() + : super(priority: Jetpack.ballPriorityInsideRamp + 1) { + layer = Layer.jetpack; + } + + List _createFixtureDefs() { + final fixturesDef = []; + + final innerLeftCurveShape = BezierCurveShape( + controlPoints: [ + Vector2(-24.5, 38), + Vector2(-26.3, 64), + Vector2(-13.8, 64.5), + ], ); - final internalFixtureDef = FixtureDef(internalCurveShape); - fixturesDef.add(internalFixtureDef); + + final innerLeftCurveFixtureDef = FixtureDef(innerLeftCurveShape); + fixturesDef.add(innerLeftCurveFixtureDef); + + final innerRightCurveShape = BezierCurveShape( + controlPoints: [ + innerLeftCurveShape.vertices.last, + Vector2(-1, 64.5), + Vector2(0.1, 39.5), + ], + ); + + final innerRightCurveFixtureDef = FixtureDef(innerRightCurveShape); + fixturesDef.add(innerRightCurveFixtureDef); return fixturesDef; } @override Body createBody() { + renderBody = false; + final bodyDef = BodyDef() ..userData = this ..position = initialPosition; @@ -94,6 +188,56 @@ class JetpackRamp extends BodyComponent with InitialPosition, Layered { return body; } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprites(); + } + + Future _loadSprites() async { + final spriteRailingFg = await gameRef.loadSprite( + Assets.images.components.spaceshipRailingFg.path, + ); + final spriteRailingFgComponent = SpriteComponent( + sprite: spriteRailingFg, + size: Vector2(26.1, 28.3), + anchor: Anchor.center, + position: Vector2(-12.2, -52.5), + ); + + await add(spriteRailingFgComponent); + } +} + +class _JetpackBase extends BodyComponent with InitialPosition, Layered { + _JetpackBase() { + layer = Layer.board; + } + + @override + Body createBody() { + renderBody = false; + + const baseWidth = 6; + final baseShape = BezierCurveShape( + controlPoints: [ + Vector2(initialPosition.x - baseWidth / 2, initialPosition.y), + Vector2(initialPosition.x - baseWidth / 2, initialPosition.y) + + Vector2(2, 2), + Vector2(initialPosition.x + baseWidth / 2, initialPosition.y) + + Vector2(-2, 2), + Vector2(initialPosition.x + baseWidth / 2, initialPosition.y) + ], + ); + final fixtureDef = FixtureDef(baseShape); + + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } } /// {@template jetpack_ramp_opening} @@ -104,12 +248,15 @@ class _JetpackRampOpening extends RampOpening { /// {@macro jetpack_ramp_opening} _JetpackRampOpening({ Layer? outsideLayer, + int? outsidePriority, required double rotation, }) : _rotation = rotation, super( insideLayer: Layer.jetpack, outsideLayer: outsideLayer, orientation: RampOrientation.down, + insidePriority: Jetpack.ballPriorityInsideRamp, + outsidePriority: outsidePriority, ); final double _rotation; @@ -117,11 +264,14 @@ class _JetpackRampOpening extends RampOpening { static final Vector2 _size = Vector2(JetpackRamp.width / 4, .1); @override - Shape get shape => PolygonShape() - ..setAsBox( - _size.x, - _size.y, - initialPosition, - _rotation, - ); + Shape get shape { + renderBody = false; + return PolygonShape() + ..setAsBox( + _size.x, + _size.y, + initialPosition, + _rotation, + ); + } } diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index c05f8aa2..5c071f4f 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -124,6 +124,7 @@ class _LauncherRampOpening extends RampOpening { super( insideLayer: Layer.launcher, orientation: RampOrientation.down, + insidePriority: 3, ); final double _rotation; diff --git a/lib/game/components/spaceship_exit_rail.dart b/lib/game/components/spaceship_exit_rail.dart index 4a6c44cd..e4be9b31 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} @@ -171,13 +191,20 @@ class SpaceshipExitRailEnd extends RampOpening { : super( insideLayer: Layer.spaceshipExitRail, orientation: RampOrientation.down, + insidePriority: 3, ) { + 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 +218,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/game_assets.dart b/lib/game/game_assets.dart index cc8aac9c..dcb740a1 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -22,6 +22,10 @@ extension PinballGameAssetsX on PinballGame { 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.spaceshipRamp.path), + images.load(Assets.images.components.spaceshipRailingBg.path), + images.load(Assets.images.components.spaceshipRailingFg.path), + images.load(Assets.images.components.spaceshipDropTube.path), ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index b5162053..095a5f53 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(), ), ); @@ -94,7 +92,9 @@ class PinballGame extends Forge2DGame } Future _addPaths() async { - unawaited(addFromBlueprint(Jetpack())); + unawaited( + addFromBlueprint(Jetpack()), + ); unawaited(addFromBlueprint(Launcher())); } diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 97be7f3e..7796df34 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -20,6 +20,22 @@ class $AssetsImagesComponentsGen { /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); + + /// File path: assets/images/components/spaceship-drop-tube.png + AssetGenImage get spaceshipDropTube => + const AssetGenImage('assets/images/components/spaceship-drop-tube.png'); + + /// File path: assets/images/components/spaceship_railing_bg.png + AssetGenImage get spaceshipRailingBg => + const AssetGenImage('assets/images/components/spaceship_railing_bg.png'); + + /// File path: assets/images/components/spaceship_railing_fg.png + AssetGenImage get spaceshipRailingFg => + const AssetGenImage('assets/images/components/spaceship_railing_fg.png'); + + /// File path: assets/images/components/spaceship_ramp.png + AssetGenImage get spaceshipRamp => + const AssetGenImage('assets/images/components/spaceship_ramp.png'); } class Assets { diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index e702f043..eb5e8d74 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -114,9 +114,9 @@ void main() { Vector2(0, 0), Vector2(10, 10), ], - step: 0.001, + step: 0.02, ); - expect(points.length, 1000); + expect(points.length, 50); }); }); 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 b6dd4f7c..87e8c054 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -10,6 +10,7 @@ export 'flipper.dart'; export 'flutter_sign_post.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; +export 'kicker.dart'; export 'layer.dart'; export 'ramp_opening.dart'; export 'shapes/shapes.dart'; 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/lib/game/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart similarity index 100% rename from lib/game/components/kicker.dart rename to packages/pinball_components/lib/src/components/kicker.dart diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 4d84eb68..588c0e33 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -185,8 +185,9 @@ class SpaceshipHole extends RampOpening { : super( insideLayer: Layer.spaceship, outsideLayer: outsideLayer, - outsidePriority: outsidePriority, orientation: RampOrientation.up, + insidePriority: 4, + outsidePriority: outsidePriority, ) { renderBody = false; layer = Layer.spaceship; @@ -195,8 +196,8 @@ class SpaceshipHole extends RampOpening { @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, ); 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/sandbox/lib/stories/flipper/basic.dart b/packages/pinball_components/sandbox/lib/stories/flipper/basic.dart index 0e5587ea..d31515de 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/basic.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/basic.dart @@ -1,21 +1,39 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; -class BasicFlipperGame extends BasicGame { +import 'package:sandbox/stories/ball/basic.dart'; + +class BasicFlipperGame extends BasicBallGame with KeyboardEvents { + BasicFlipperGame() : super(color: Colors.blue); + static const info = ''' Basic example of how a Flipper works. '''; + static const _leftFlipperKeys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]; + + static const _rightFlipperKeys = [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]; + + late Flipper leftFlipper; + late Flipper rightFlipper; + @override Future onLoad() async { await super.onLoad(); final center = screenToWorld(camera.viewport.canvasSize! / 2); - final leftFlipper = Flipper(side: BoardSide.left) + leftFlipper = Flipper(side: BoardSide.left) ..initialPosition = center - Vector2(Flipper.size.x, 0); - final rightFlipper = Flipper(side: BoardSide.right) + rightFlipper = Flipper(side: BoardSide.right) ..initialPosition = center + Vector2(Flipper.size.x, 0); await addAll([ @@ -23,4 +41,32 @@ class BasicFlipperGame extends BasicGame { rightFlipper, ]); } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + final movedLeftFlipper = _leftFlipperKeys.contains(event.logicalKey); + if (movedLeftFlipper) { + if (event is RawKeyDownEvent) { + leftFlipper.moveUp(); + } else if (event is RawKeyUpEvent) { + leftFlipper.moveDown(); + } + } + + final movedRightFlipper = _rightFlipperKeys.contains(event.logicalKey); + if (movedRightFlipper) { + if (event is RawKeyDownEvent) { + rightFlipper.moveUp(); + } else if (event is RawKeyUpEvent) { + rightFlipper.moveDown(); + } + } + + return movedLeftFlipper || movedRightFlipper + ? KeyEventResult.handled + : KeyEventResult.ignored; + } } diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart b/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart index d6c5d3df..9b5802f8 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart @@ -3,10 +3,9 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/flipper/basic.dart'; -class FlipperTracingGame extends BasicGame { +class FlipperTracingGame extends BasicFlipperGame { static const info = ''' Basic example of how the Flipper body overlays the sprite. '''; @@ -14,17 +13,6 @@ class FlipperTracingGame extends BasicGame { @override Future onLoad() async { await super.onLoad(); - final center = screenToWorld(camera.viewport.canvasSize! / 2); - - final leftFlipper = Flipper(side: BoardSide.left) - ..initialPosition = center - Vector2(Flipper.size.x, 0); - final rightFlipper = Flipper(side: BoardSide.right) - ..initialPosition = center + Vector2(Flipper.size.x, 0); - - await addAll([ - leftFlipper, - rightFlipper, - ]); leftFlipper.trace(); rightFlipper.trace(); } @@ -37,7 +25,6 @@ extension on BodyComponent { body.joints.whereType().forEach( (joint) => joint.setLimits(0, 0), ); - body.setType(BodyType.static); unawaited( mounted.whenComplete(() { diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index c36afff2..7771d1e1 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -1,12 +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 {} 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 7bc62212..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,43 +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/test/game/components/kicker_test.dart b/packages/pinball_components/test/src/components/kicker_test.dart similarity index 97% rename from test/game/components/kicker_test.dart rename to packages/pinball_components/test/src/components/kicker_test.dart index 333c7fbe..4af7b8b1 100644 --- a/test/game/components/kicker_test.dart +++ b/packages/pinball_components/test/src/components/kicker_test.dart @@ -3,7 +3,6 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; void main() { 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/flipper_controller_test.dart b/test/game/components/controlled_flipper_test.dart similarity index 100% rename from test/game/components/flipper_controller_test.dart rename to test/game/components/controlled_flipper_test.dart diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index a0e1b81f..33dbb991 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -86,12 +86,12 @@ void main() { group('controller', () { group('listenWhen', () { final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - game: TestGame.new, - gameBloc: () => gameBloc, + final flameBlocTester = FlameBlocTester( + gameBuilder: TestGame.new, + blocBuilder: () => gameBloc, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'listens when a Bonus.dashNest is added', verify: (game, tester) async { final flutterForest = FlutterForest(); @@ -145,12 +145,12 @@ void main() { ); }); - final tester = flameBlocTester( - game: PinballGameTest.create, - gameBloc: () => gameBloc, + final flameBlocTester = FlameBlocTester( + gameBuilder: PinballGameTest.create, + blocBuilder: () => gameBloc, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'add DashNestActivated event', setUp: (game, tester) async { await game.ready(); @@ -171,7 +171,7 @@ void main() { }, ); - tester.testGameWidget( + flameBlocTester.testGameWidget( 'add Scored event', setUp: (game, tester) async { final flutterForest = FlutterForest(); 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, + ), + ); + }, + ); }