From b93e104b0818964d0d7eed0af5a8c9d3b28393b8 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 15 Mar 2022 12:17:49 +0000 Subject: [PATCH 1/3] feat: include boardSide.direction (#46) * feat: implemented BoardSide.direction * docs: included doc comment --- lib/game/components/board_side.dart | 5 +++++ test/game/components/board_side_test.dart | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/lib/game/components/board_side.dart b/lib/game/components/board_side.dart index 611f70b8..ea8fb1cf 100644 --- a/lib/game/components/board_side.dart +++ b/lib/game/components/board_side.dart @@ -19,4 +19,9 @@ extension BoardSideX on BoardSide { /// Whether this side is [BoardSide.right]. bool get isRight => this == BoardSide.right; + + /// Direction of the [BoardSide]. + /// + /// Represents the path which the [BoardSide] moves along. + int get direction => isLeft ? -1 : 1; } diff --git a/test/game/components/board_side_test.dart b/test/game/components/board_side_test.dart index 3d6d3fa1..ba201065 100644 --- a/test/game/components/board_side_test.dart +++ b/test/game/components/board_side_test.dart @@ -23,5 +23,12 @@ void main() { expect(side.isLeft, isFalse); expect(side.isRight, isTrue); }); + + test('direction is correct', () { + const side = BoardSide.left; + expect(side.direction, equals(-1)); + const side2 = BoardSide.right; + expect(side2.direction, equals(1)); + }); }); } From 6a68bf1ed79884c6c218a09e2380cbabeac7f0da Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 15 Mar 2022 08:34:31 -0500 Subject: [PATCH 2/3] feat: add round bumper (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add round bumper * refactor: PR suggestions * fix: remove unused import 🤦‍♀️ --- lib/game/components/components.dart | 1 + lib/game/components/round_bumper.dart | 41 +++++++ test/game/components/round_bumper_test.dart | 124 ++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 lib/game/components/round_bumper.dart create mode 100644 test/game/components/round_bumper_test.dart diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 71108fcc..f50b62d2 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -6,5 +6,6 @@ export 'flipper.dart'; export 'joint_anchor.dart'; export 'pathway.dart'; export 'plunger.dart'; +export 'round_bumper.dart'; export 'score_points.dart'; export 'wall.dart'; diff --git a/lib/game/components/round_bumper.dart b/lib/game/components/round_bumper.dart new file mode 100644 index 00000000..753ed15b --- /dev/null +++ b/lib/game/components/round_bumper.dart @@ -0,0 +1,41 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; + +/// {@template round_bumper} +/// Circular body that repels a [Ball] on contact, increasing the score. +/// {@endtemplate} +class RoundBumper extends BodyComponent with ScorePoints { + /// {@macro round_bumper} + RoundBumper({ + required Vector2 position, + required double radius, + required int points, + }) : _position = position, + _radius = radius, + _points = points; + + /// The position of the [RoundBumper] body. + final Vector2 _position; + + /// The radius of the [RoundBumper]. + final double _radius; + + /// Points awarded from hitting this [RoundBumper]. + final int _points; + + @override + int get points => _points; + + @override + Body createBody() { + final shape = CircleShape()..radius = _radius; + + final fixtureDef = FixtureDef(shape)..restitution = 1; + + final bodyDef = BodyDef() + ..position = _position + ..type = BodyType.static; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/test/game/components/round_bumper_test.dart b/test/game/components/round_bumper_test.dart new file mode 100644 index 00000000..c780dd0b --- /dev/null +++ b/test/game/components/round_bumper_test.dart @@ -0,0 +1,124 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RoundBumper', () { + final flameTester = FlameTester(Forge2DGame.new); + const radius = 1.0; + const points = 1; + + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(game.contains(roundBumper), isTrue); + }, + ); + + flameTester.test( + 'has points', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(roundBumper.points, equals(points)); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final roundBumper = RoundBumper( + position: position, + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + game.contains(roundBumper); + + expect(roundBumper.body.position, equals(position)); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(roundBumper.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixture', () { + flameTester.test( + 'exists', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + expect(roundBumper.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'has restitution', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + final fixture = roundBumper.body.fixtures[0]; + expect(fixture.restitution, greaterThan(0)); + }, + ); + + flameTester.test( + 'shape is circular', + (game) async { + final roundBumper = RoundBumper( + position: Vector2.zero(), + radius: radius, + points: points, + ); + await game.ensureAdd(roundBumper); + + final fixture = roundBumper.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.circle)); + expect(fixture.shape.radius, equals(1)); + }, + ); + }); + }); +} From 413900e89fc1e5c90e80955c0f7727e8ed528821 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 15 Mar 2022 13:52:48 +0000 Subject: [PATCH 3/3] feat: define SlingShot component (#39) * feat: created sling-short.dart * refactor: used appropiate file name * chore: included sling_shot export * refactor: simplified _createFixtureDefs * doc: included SlingShot in doc comment * feat: implemented basic SlingShot * feat: used EdgeShape instead of PolygonShape * feat: implemented _addSlingShot method * feat: adding placeholder art for the flippers * Update lib/game/components/flipper.dart Co-authored-by: Alejandro Santiago * docs: included missing documentation (#29) * chore: ignored lint rue * docs: documented ball.dart * docs: ignored lint rule * docs: documented wall.dart * docs: documented game_over_dialog.dart * docs: fixed typo * docs: included TODO comments * fix: misisng doc * chore: add code owners (#31) * feat: add character selection (#20) * chore: lock file * feat: character selection page * fix: ignore generated asset coverage * chore: add suggestions * feat: tint ball with theme color * refactor: decrease theme cubit scope * chore: minimize changes * chore: typos and readability * refactor: use extension for initial pinball game * fix: tests from merge * refactor: ignore docs for views * refactor: revert to ignoring for file * fix: todo analyzer warning * refactor: remove Flutter dep from geometry (#27) * fix: removed flutter dependency * test: fixed tests for assertions * test: check assertion with isA * ci: added geometry workflow file * refactor: changed flame dep to vector_math for vector2 * fix: changed import for vector to vector_math_64 * Update .github/workflows/geometry.yaml Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * chore: rename pinball game test extension (#33) * chore: rename pinball game test extension * refactor: initial to create * docs: small change * chore: removed unecessary end callback (#30) * feat: adding ball spawning upon click on debug mode (#28) * feat: adding ball spawming upon click on debug mode * PR suggestions * fix: coverage * fix: rebase * feat: rebase fixes * feat: moved triangle to centroid * feat: made SlingShot a PositionBodyComponent * feat: removed PositionBodyComponent * refactor: moved centroid function * refactor: simplified centroid function * docs: typo in macro * feat: modified restitution value * refactor: added variable for incline * docs: included TODO comment * feat: included tests * feat: removed friction from SlingShot * feat: removed adding slinghsots * refactor: used variables for fixtures * feat: included side in SlingShot * feat: included different shapes test * docs: fixed typo * refactor: removed unused import * refactor: used centroid from geometry package * docs: fixed typo * refactor: improved triangleVertices readability * refactor: removed EmptyGame class Co-authored-by: Erick Zanardo Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Rui Miguel Alonso --- lib/game/components/board_side.dart | 2 +- lib/game/components/components.dart | 1 + lib/game/components/sling_shot.dart | 96 ++++++++++++ test/game/components/ball_test.dart | 1 + test/game/components/sling_shot_test.dart | 180 ++++++++++++++++++++++ 5 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 lib/game/components/sling_shot.dart create mode 100644 test/game/components/sling_shot_test.dart diff --git a/lib/game/components/board_side.dart b/lib/game/components/board_side.dart index ea8fb1cf..f7587f47 100644 --- a/lib/game/components/board_side.dart +++ b/lib/game/components/board_side.dart @@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart'; /// Indicates a side of the board. /// /// Usually used to position or mirror elements of a [PinballGame]; such as a -/// [Flipper]. +/// [Flipper] or [SlingShot]. enum BoardSide { /// The left side of the board. left, diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index f50b62d2..d5c479c7 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -8,4 +8,5 @@ export 'pathway.dart'; export 'plunger.dart'; export 'round_bumper.dart'; export 'score_points.dart'; +export 'sling_shot.dart'; export 'wall.dart'; diff --git a/lib/game/components/sling_shot.dart b/lib/game/components/sling_shot.dart new file mode 100644 index 00000000..dacf1ee5 --- /dev/null +++ b/lib/game/components/sling_shot.dart @@ -0,0 +1,96 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:geometry/geometry.dart' show centroid; +import 'package:pinball/game/game.dart'; + +/// {@template sling_shot} +/// Triangular [BodyType.static] body that propels the [Ball] towards the +/// opposite side. +/// +/// [SlingShot]s are usually positioned above each [Flipper]. +/// {@endtemplate sling_shot} +class SlingShot extends BodyComponent { + /// {@macro sling_shot} + SlingShot({ + required Vector2 position, + required BoardSide side, + }) : _position = position, + _side = side { + // TODO(alestiago): Use sprite instead of color when provided. + paint = Paint() + ..color = const Color(0xFF00FF00) + ..style = PaintingStyle.fill; + } + + /// The initial position of the [SlingShot] body. + final Vector2 _position; + + /// Whether the [SlingShot] is on the left or right side of the board. + /// + /// A [SlingShot] with [BoardSide.left] propels the [Ball] to the right, + /// whereas a [SlingShot] with [BoardSide.right] propels the [Ball] to the + /// left. + final BoardSide _side; + + List _createFixtureDefs() { + final fixtures = []; + + // TODO(alestiago): Use size from PositionedBodyComponent instead, + // once a sprite is given. + final size = Vector2(10, 10); + + // TODO(alestiago): This magic number can be deduced by specifying the + // angle and using polar coordinate system to place the bottom right + // vertex. + // Something as: y = -size.y * math.cos(angle) + const additionalIncrement = 2; + final triangleVertices = _side.isLeft + ? [ + Vector2(0, 0), + Vector2(0, -size.y), + Vector2( + size.x, + -size.y - additionalIncrement, + ), + ] + : [ + Vector2(size.x, 0), + Vector2(size.x, -size.y), + Vector2( + 0, + -size.y - additionalIncrement, + ), + ]; + final triangleCentroid = centroid(triangleVertices); + for (final vertex in triangleVertices) { + vertex.setFrom(vertex - triangleCentroid); + } + + final triangle = PolygonShape()..set(triangleVertices); + final triangleFixtureDef = FixtureDef(triangle)..friction = 0; + fixtures.add(triangleFixtureDef); + + final kicker = EdgeShape() + ..set( + triangleVertices.first, + triangleVertices.last, + ); + // TODO(alestiago): Play with restitution value once game is bundled. + final kickerFixtureDef = FixtureDef(kicker) + ..restitution = 20.0 + ..friction = 0; + fixtures.add(kickerFixtureDef); + + return fixtures; + } + + @override + Body createBody() { + final bodyDef = BodyDef()..position = _position; + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index e83dd619..e6172d6d 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -18,6 +18,7 @@ void main() { 'loads correctly', (game) async { final ball = Ball(position: Vector2.zero()); + await game.ready(); await game.ensureAdd(ball); expect(game.contains(ball), isTrue); diff --git a/test/game/components/sling_shot_test.dart b/test/game/components/sling_shot_test.dart new file mode 100644 index 00000000..e7e89ead --- /dev/null +++ b/test/game/components/sling_shot_test.dart @@ -0,0 +1,180 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + group('SlingShot', () { + final flameTester = FlameTester(Forge2DGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(game.contains(slingShot), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final slingShot = SlingShot( + position: position, + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.position, equals(position)); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'shape is triangular', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.polygon)); + expect((fixture.shape as PolygonShape).vertices.length, equals(3)); + }, + ); + + flameTester.test( + 'triangular shapes are different ' + 'when side is left or right', + (game) async { + final leftSlingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + final rightSlingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.right, + ); + + await game.ensureAdd(leftSlingShot); + await game.ensureAdd(rightSlingShot); + + final rightShape = + rightSlingShot.body.fixtures[0].shape as PolygonShape; + final leftShape = + leftSlingShot.body.fixtures[0].shape as PolygonShape; + + expect(rightShape.vertices, isNot(equals(leftShape.vertices))); + }, + ); + + flameTester.test( + 'has no friction', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[0]; + expect(fixture.friction, equals(0)); + }, + ); + }); + + group('second fixture', () { + flameTester.test( + 'exists', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + expect(slingShot.body.fixtures[1], isA()); + }, + ); + + flameTester.test( + 'shape is edge', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[1]; + expect(fixture.shape.shapeType, equals(ShapeType.edge)); + }, + ); + + flameTester.test( + 'has restitution', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[1]; + expect(fixture.restitution, greaterThan(0)); + }, + ); + + flameTester.test( + 'has no friction', + (game) async { + final slingShot = SlingShot( + position: Vector2.zero(), + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); + + final fixture = slingShot.body.fixtures[1]; + expect(fixture.friction, equals(0)); + }, + ); + }); + }); +}