diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index c771b9d8..635a8d13 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -1,5 +1,6 @@ import 'package:flame/components.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; /// {@template board} /// The main flat surface of the [PinballGame]. @@ -92,12 +93,15 @@ class _BottomGroupSide extends Component { final flipper = Flipper( side: _side, )..initialPosition = _position; + await flipper.add(FlipperController(flipper)); + final baseboard = Baseboard(side: _side) ..initialPosition = _position + Vector2( (Baseboard.size.x / 1.6 * direction), Baseboard.size.y - 2, ); + final kicker = Kicker( side: _side, )..initialPosition = _position + diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 07b036f6..6513e363 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,12 +1,10 @@ export 'ball.dart'; export 'baseboard.dart'; export 'board.dart'; -export 'board_side.dart'; export 'bonus_word.dart'; export 'flipper.dart'; export 'flutter_forest.dart'; export 'jetpack_ramp.dart'; -export 'joint_anchor.dart'; export 'kicker.dart'; export 'launcher_ramp.dart'; export 'pathway.dart'; diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 6e64c781..946cfd49 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -1,166 +1,22 @@ -import 'dart:async'; -import 'dart:math' as math; - import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/services.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -const _leftFlipperKeys = [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, -]; - -const _rightFlipperKeys = [ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, -]; - -/// {@template flipper} -/// A bat, typically found in pairs at the bottom of the board. -/// -/// [Flipper] can be controlled by the player in an arc motion. -/// {@endtemplate flipper} -class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { - /// {@macro flipper} - Flipper({ - required this.side, - }) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys; +import 'package:pinball_components/pinball_components.dart'; - /// The size of the [Flipper]. - static final size = Vector2(12, 2.8); +/// {@template flipper_controller} +/// A [Component] that controls the [Flipper]s movement. +/// {@endtemplate} +class FlipperController extends Component with KeyboardHandler { + /// {@macro flipper_controller} + FlipperController(this.flipper) : _keys = flipper.side.flipperKeys; - /// The speed required to move the [Flipper] to its highest position. - /// - /// The higher the value, the faster the [Flipper] will move. - static const double _speed = 60; - - /// Whether the [Flipper] is on the left or right side of the board. - /// - /// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion, - /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion. - final BoardSide side; + /// The [Flipper] this controller is controlling. + final Flipper flipper; /// The [LogicalKeyboardKey]s that will control the [Flipper]. /// /// [onKeyEvent] method listens to when one of these keys is pressed. final List _keys; - /// Applies downward linear velocity to the [Flipper], moving it to its - /// resting position. - void _moveDown() { - body.linearVelocity = Vector2(0, -_speed); - } - - /// Applies upward linear velocity to the [Flipper], moving it to its highest - /// position. - void _moveUp() { - body.linearVelocity = Vector2(0, _speed); - } - - /// Loads the sprite that renders with the [Flipper]. - Future _loadSprite() async { - final sprite = await gameRef.loadSprite( - Assets.images.components.flipper.path, - ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: size, - anchor: Anchor.center, - ); - - if (side.isRight) { - spriteComponent.flipHorizontally(); - } - - await add(spriteComponent); - } - - /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. - Future _anchorToJoint() async { - final anchor = _FlipperAnchor(flipper: this); - await add(anchor); - - final jointDef = _FlipperAnchorRevoluteJointDef( - flipper: this, - anchor: anchor, - ); - final joint = _FlipperJoint(jointDef)..create(world); - - // FIXME(erickzanardo): when mounted the initial position is not fully - // reached. - unawaited( - mounted.whenComplete(joint.unlock), - ); - } - - List _createFixtureDefs() { - final fixturesDef = []; - final direction = side.direction; - - final bigCircleShape = CircleShape()..radius = 1.75; - bigCircleShape.position.setValues( - ((size.x / 2) * direction) + (bigCircleShape.radius * -direction), - 0, - ); - final bigCircleFixtureDef = FixtureDef(bigCircleShape); - fixturesDef.add(bigCircleFixtureDef); - - final smallCircleShape = CircleShape()..radius = 0.9; - smallCircleShape.position.setValues( - ((size.x / 2) * -direction) + (smallCircleShape.radius * direction), - 0, - ); - final smallCircleFixtureDef = FixtureDef(smallCircleShape); - fixturesDef.add(smallCircleFixtureDef); - - final trapeziumVertices = side.isLeft - ? [ - Vector2(bigCircleShape.position.x, bigCircleShape.radius), - Vector2(smallCircleShape.position.x, smallCircleShape.radius), - Vector2(smallCircleShape.position.x, -smallCircleShape.radius), - Vector2(bigCircleShape.position.x, -bigCircleShape.radius), - ] - : [ - Vector2(smallCircleShape.position.x, smallCircleShape.radius), - Vector2(bigCircleShape.position.x, bigCircleShape.radius), - Vector2(bigCircleShape.position.x, -bigCircleShape.radius), - Vector2(smallCircleShape.position.x, -smallCircleShape.radius), - ]; - final trapezium = PolygonShape()..set(trapeziumVertices); - final trapeziumFixtureDef = FixtureDef(trapezium) - ..density = 50.0 // TODO(alestiago): Use a proper density. - ..friction = .1; // TODO(alestiago): Use a proper friction. - fixturesDef.add(trapeziumFixtureDef); - - return fixturesDef; - } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await Future.wait([ - _loadSprite(), - _anchorToJoint(), - ]); - } - - @override - Body createBody() { - final bodyDef = BodyDef() - ..position = initialPosition - ..gravityScale = 0 - ..type = BodyType.dynamic; - final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); - - return body; - } - @override bool onKeyEvent( RawKeyEvent event, @@ -169,80 +25,28 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { - _moveUp(); + flipper.moveUp(); } else if (event is RawKeyUpEvent) { - _moveDown(); + flipper.moveDown(); } return false; } } -/// {@template flipper_anchor} -/// [JointAnchor] positioned at the end of a [Flipper]. -/// -/// The end of a [Flipper] depends on its [Flipper.side]. -/// {@endtemplate} -class _FlipperAnchor extends JointAnchor { - /// {@macro flipper_anchor} - _FlipperAnchor({ - required Flipper flipper, - }) { - initialPosition = Vector2( - flipper.body.position.x + ((Flipper.size.x * flipper.side.direction) / 2), - flipper.body.position.y, - ); - } -} - -/// {@template flipper_anchor_revolute_joint_def} -/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion. -/// {@endtemplate} -class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { - /// {@macro flipper_anchor_revolute_joint_def} - _FlipperAnchorRevoluteJointDef({ - required Flipper flipper, - required _FlipperAnchor anchor, - }) : side = flipper.side { - initialize( - flipper.body, - anchor.body, - anchor.body.position, - ); - - enableLimit = true; - final angle = (_sweepingAngle * -side.direction) / 2; - lowerAngle = upperAngle = angle; - } - - /// The total angle of the arc motion. - static const _sweepingAngle = math.pi / 3.5; - - final BoardSide side; -} - -class _FlipperJoint extends RevoluteJoint { - _FlipperJoint(_FlipperAnchorRevoluteJointDef def) - : side = def.side, - super(def); - - final BoardSide side; - - // TODO(alestiago): Remove once Forge2D supports custom joints. - void create(World world) { - world.joints.add(this); - bodyA.joints.add(this); - bodyB.joints.add(this); - } - - /// Unlocks the [Flipper] from its resting position. - /// - /// The [Flipper] is locked when initialized in order to force it to be at - /// its resting position. - void unlock() { - setLimits( - lowerLimit * side.direction, - -upperLimit * side.direction, - ); +extension on BoardSide { + List get flipperKeys { + switch (this) { + case BoardSide.left: + return [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]; + case BoardSide.right: + return [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]; + } } } diff --git a/lib/game/components/kicker.dart b/lib/game/components/kicker.dart index dc55a52f..d9eb7932 100644 --- a/lib/game/components/kicker.dart +++ b/lib/game/components/kicker.dart @@ -4,7 +4,6 @@ import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template kicker} diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 2ceb56d7..9a2da898 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -6,7 +6,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template ball} -/// A solid, [BodyType.dynamic] sphere that rolls and bounces around +/// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} class Ball extends BodyComponent with Layered, InitialPosition { @@ -90,7 +90,7 @@ class Ball extends BodyComponent } } - /// Applies a boost on this [Ball] + /// Applies a boost on this [Ball]. void boost(Vector2 impulse) { body.applyLinearImpulse(impulse); _boostTimer = _boostDuration; diff --git a/lib/game/components/board_side.dart b/packages/pinball_components/lib/src/components/board_side.dart similarity index 77% rename from lib/game/components/board_side.dart rename to packages/pinball_components/lib/src/components/board_side.dart index 2ef8d651..ac530567 100644 --- a/lib/game/components/board_side.dart +++ b/packages/pinball_components/lib/src/components/board_side.dart @@ -1,4 +1,8 @@ -import 'package:pinball/game/game.dart'; +// ignore_for_file: comment_references +// TODO(alestiago): Revisit ignore lint rule once Kicker is moved to this +// package. + +import 'package:pinball_components/pinball_components.dart'; /// Indicates a side of the board. /// diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index c1ef3e14..83673205 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,4 +1,7 @@ export 'ball.dart'; +export 'board_side.dart'; export 'fire_effect.dart'; +export 'flipper.dart'; export 'initial_position.dart'; +export 'joint_anchor.dart'; export 'layer.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart new file mode 100644 index 00000000..9f04940f --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -0,0 +1,210 @@ +import 'dart:async'; +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 flipper} +/// A bat, typically found in pairs at the bottom of the board. +/// +/// [Flipper] can be controlled by the player in an arc motion. +/// {@endtemplate flipper} +class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { + /// {@macro flipper} + Flipper({ + required this.side, + }); + + /// The size of the [Flipper]. + static final size = Vector2(12, 2.8); + + /// The speed required to move the [Flipper] to its highest position. + /// + /// The higher the value, the faster the [Flipper] will move. + static const double _speed = 60; + + /// Whether the [Flipper] is on the left or right side of the board. + /// + /// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion, + /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion. + final BoardSide side; + + /// Applies downward linear velocity to the [Flipper], moving it to its + /// resting position. + void moveDown() { + body.linearVelocity = Vector2(0, -_speed); + } + + /// Applies upward linear velocity to the [Flipper], moving it to its highest + /// position. + void moveUp() { + body.linearVelocity = Vector2(0, _speed); + } + + /// Loads the sprite that renders with the [Flipper]. + Future _loadSprite() async { + final asset = Assets.images.flipper; + final spritePath = (side.isLeft ? asset.left : asset.right).keyName; + final sprite = await gameRef.loadSprite(spritePath); + final spriteComponent = SpriteComponent( + sprite: sprite, + size: size, + anchor: Anchor.center, + ); + + await add(spriteComponent); + } + + /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. + Future _anchorToJoint() async { + final anchor = _FlipperAnchor(flipper: this); + await add(anchor); + + final jointDef = _FlipperAnchorRevoluteJointDef( + flipper: this, + anchor: anchor, + ); + final joint = _FlipperJoint(jointDef)..create(world); + + // FIXME(erickzanardo): when mounted the initial position is not fully + // reached. + unawaited( + mounted.whenComplete(joint.unlock), + ); + } + + List _createFixtureDefs() { + final fixturesDef = []; + final direction = side.direction; + + final bigCircleShape = CircleShape()..radius = 1.75; + bigCircleShape.position.setValues( + ((size.x / 2) * direction) + (bigCircleShape.radius * -direction), + 0, + ); + final bigCircleFixtureDef = FixtureDef(bigCircleShape); + fixturesDef.add(bigCircleFixtureDef); + + final smallCircleShape = CircleShape()..radius = 0.9; + smallCircleShape.position.setValues( + ((size.x / 2) * -direction) + (smallCircleShape.radius * direction), + 0, + ); + final smallCircleFixtureDef = FixtureDef(smallCircleShape); + fixturesDef.add(smallCircleFixtureDef); + + final trapeziumVertices = side.isLeft + ? [ + Vector2(bigCircleShape.position.x, bigCircleShape.radius), + Vector2(smallCircleShape.position.x, smallCircleShape.radius), + Vector2(smallCircleShape.position.x, -smallCircleShape.radius), + Vector2(bigCircleShape.position.x, -bigCircleShape.radius), + ] + : [ + Vector2(smallCircleShape.position.x, smallCircleShape.radius), + Vector2(bigCircleShape.position.x, bigCircleShape.radius), + Vector2(bigCircleShape.position.x, -bigCircleShape.radius), + Vector2(smallCircleShape.position.x, -smallCircleShape.radius), + ]; + final trapezium = PolygonShape()..set(trapeziumVertices); + final trapeziumFixtureDef = FixtureDef(trapezium) + ..density = 50.0 // TODO(alestiago): Use a proper density. + ..friction = .1; // TODO(alestiago): Use a proper friction. + fixturesDef.add(trapeziumFixtureDef); + + return fixturesDef; + } + + @override + Future onLoad() async { + await super.onLoad(); + renderBody = false; + + await Future.wait([ + _loadSprite(), + _anchorToJoint(), + ]); + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..position = initialPosition + ..gravityScale = 0 + ..type = BodyType.dynamic; + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +/// {@template flipper_anchor} +/// [JointAnchor] positioned at the end of a [Flipper]. +/// +/// The end of a [Flipper] depends on its [Flipper.side]. +/// {@endtemplate} +class _FlipperAnchor extends JointAnchor { + /// {@macro flipper_anchor} + _FlipperAnchor({ + required Flipper flipper, + }) { + initialPosition = Vector2( + flipper.body.position.x + ((Flipper.size.x * flipper.side.direction) / 2), + flipper.body.position.y, + ); + } +} + +/// {@template flipper_anchor_revolute_joint_def} +/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion. +/// {@endtemplate} +class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { + /// {@macro flipper_anchor_revolute_joint_def} + _FlipperAnchorRevoluteJointDef({ + required Flipper flipper, + required _FlipperAnchor anchor, + }) : side = flipper.side { + initialize( + flipper.body, + anchor.body, + anchor.body.position, + ); + + enableLimit = true; + final angle = (_sweepingAngle * -side.direction) / 2; + lowerAngle = upperAngle = angle; + } + + /// The total angle of the arc motion. + static const _sweepingAngle = math.pi / 3.5; + + final BoardSide side; +} + +class _FlipperJoint extends RevoluteJoint { + _FlipperJoint(_FlipperAnchorRevoluteJointDef def) + : side = def.side, + super(def); + + final BoardSide side; + + // TODO(alestiago): Remove once Forge2D supports custom joints. + void create(World world) { + world.joints.add(this); + bodyA.joints.add(this); + bodyB.joints.add(this); + } + + /// Unlocks the [Flipper] from its resting position. + /// + /// The [Flipper] is locked when initialized in order to force it to be at + /// its resting position. + void unlock() { + setLimits( + lowerLimit * side.direction, + -upperLimit * side.direction, + ); + } +} diff --git a/lib/game/components/joint_anchor.dart b/packages/pinball_components/lib/src/components/joint_anchor.dart similarity index 100% rename from lib/game/components/joint_anchor.dart rename to packages/pinball_components/lib/src/components/joint_anchor.dart diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index 3e6429df..ac2c32d8 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -7,7 +7,6 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart';