From 413900e89fc1e5c90e80955c0f7727e8ed528821 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 15 Mar 2022 13:52:48 +0000 Subject: [PATCH] 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)); + }, + ); + }); + }); +}