From 32529e42bea5fd8db7a8f711d84f951a6b24a062 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Mon, 21 Mar 2022 15:20:38 +0000 Subject: [PATCH] feat: made`SlingShot` rounded (#63) * feat: made SlingShot rounded * feat: updated SlingShot tests * refactor: defined Shape extension * refactor: fixed test name typo Co-authored-by: Rui Miguel Alonso --- lib/game/components/sling_shot.dart | 124 +++++++++++----- test/game/components/ball_test.dart | 1 + test/game/components/sling_shot_test.dart | 164 +++++----------------- 3 files changed, 127 insertions(+), 162 deletions(-) diff --git a/lib/game/components/sling_shot.dart b/lib/game/components/sling_shot.dart index a39d130e..53160213 100644 --- a/lib/game/components/sling_shot.dart +++ b/lib/game/components/sling_shot.dart @@ -1,7 +1,9 @@ +import 'dart:math' as math; + 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:geometry/geometry.dart' as geometry show centroid; import 'package:pinball/game/game.dart'; /// {@template sling_shot} @@ -31,54 +33,88 @@ class SlingShot extends BodyComponent with InitialPosition { /// The size of the [SlingShot] body. // TODO(alestiago): Use size from PositionedBodyComponent instead, // once a sprite is given. - static final Vector2 size = Vector2(6, 8); + static final Vector2 size = Vector2(4, 10); List _createFixtureDefs() { - final fixturesDef = []; - - // 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 = 3; - 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), + final fixturesDefs = []; + final direction = _side.direction; + const quarterPi = math.pi / 4; + + final upperCircle = CircleShape()..radius = 1.45; + upperCircle.position.setValues(0, -upperCircle.radius / 2); + final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0; + fixturesDefs.add(upperCircleFixtureDef); + + final lowerCircle = CircleShape()..radius = 1.45; + lowerCircle.position.setValues( + size.x * -direction, + -size.y, + ); + final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0; + fixturesDefs.add(lowerCircleFixtureDef); + + final wallFacingEdge = EdgeShape() + ..set( + upperCircle.position + Vector2( + upperCircle.radius * direction, 0, - -size.y - additionalIncrement, ), - ]; - final triangleCentroid = centroid(triangleVertices); - for (final vertex in triangleVertices) { - vertex.setFrom(vertex - triangleCentroid); - } + // TODO(alestiago): Use values from design. + Vector2(2.0 * direction, -size.y + 2), + ); + final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0; + fixturesDefs.add(wallFacingLineFixtureDef); - final triangle = PolygonShape()..set(triangleVertices); - final triangleFixtureDef = FixtureDef(triangle)..friction = 0; - fixturesDef.add(triangleFixtureDef); + final bottomEdge = EdgeShape() + ..set( + wallFacingEdge.vertex2, + lowerCircle.position + + Vector2( + lowerCircle.radius * math.cos(quarterPi) * direction, + -lowerCircle.radius * math.sin(quarterPi), + ), + ); + final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0; + fixturesDefs.add(bottomLineFixtureDef); - final kicker = EdgeShape() + final kickerEdge = EdgeShape() ..set( - triangleVertices.first, - triangleVertices.last, + upperCircle.position + + Vector2( + upperCircle.radius * math.cos(quarterPi) * -direction, + upperCircle.radius * math.sin(quarterPi), + ), + lowerCircle.position + + Vector2( + lowerCircle.radius * math.cos(quarterPi) * -direction, + lowerCircle.radius * math.sin(quarterPi), + ), ); - // TODO(alestiago): Play with restitution value once game is bundled. - final kickerFixtureDef = FixtureDef(kicker) + + final kickerFixtureDef = FixtureDef(kickerEdge) + // TODO(alestiago): Play with restitution value once game is bundled. ..restitution = 10.0 ..friction = 0; - fixturesDef.add(kickerFixtureDef); + fixturesDefs.add(kickerFixtureDef); - return fixturesDef; + // TODO(alestiago): Evaluate if there is value on centering the fixtures. + final centroid = geometry.centroid( + [ + upperCircle.position + Vector2(0, -upperCircle.radius), + lowerCircle.position + + Vector2( + lowerCircle.radius * math.cos(quarterPi) * -direction, + -lowerCircle.radius * math.sin(quarterPi), + ), + wallFacingEdge.vertex2, + ], + ); + for (final fixtureDef in fixturesDefs) { + fixtureDef.shape.moveBy(-centroid); + } + + return fixturesDefs; } @override @@ -90,3 +126,17 @@ class SlingShot extends BodyComponent with InitialPosition { return body; } } + +// TODO(alestiago): Evaluate if there's value on generalising this to +// all shapes. +extension on Shape { + void moveBy(Vector2 offset) { + if (this is CircleShape) { + final circle = this as CircleShape; + circle.position.setFrom(circle.position + offset); + } else if (this is EdgeShape) { + final edge = this as EdgeShape; + edge.set(edge.vertex1 + offset, edge.vertex2 + offset); + } + } +} diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index cb6231fa..01668d1a 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -75,6 +75,7 @@ void main() { 'has Layer.all as default filter maskBits', (game) async { final ball = Ball(); + await game.ready(); await game.ensureAdd(ball); await game.ready(); diff --git a/test/game/components/sling_shot_test.dart b/test/game/components/sling_shot_test.dart index e7c796a1..2bfb2355 100644 --- a/test/game/components/sling_shot_test.dart +++ b/test/game/components/sling_shot_test.dart @@ -7,6 +7,7 @@ import 'package:pinball/game/game.dart'; void main() { group('SlingShot', () { + // TODO(alestiago): Include golden tests for left and right. final flameTester = FlameTester(Forge2DGame.new); flameTester.test( @@ -21,135 +22,48 @@ void main() { }, ); - group('body', () { - flameTester.test( - 'is static', - (game) async { - final slingShot = SlingShot( - 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( - side: BoardSide.left, - ); - await game.ensureAdd(slingShot); - - expect(slingShot.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'shape is triangular', - (game) async { - final slingShot = SlingShot( - 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( - side: BoardSide.left, - ); - final rightSlingShot = SlingShot( - 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( - 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( - side: BoardSide.left, - ); - await game.ensureAdd(slingShot); - - expect(slingShot.body.fixtures[1], isA()); - }, - ); - - flameTester.test( - 'shape is edge', - (game) async { - final slingShot = SlingShot( - side: BoardSide.left, - ); - await game.ensureAdd(slingShot); + flameTester.test( + 'body is static', + (game) async { + final slingShot = SlingShot( + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); - final fixture = slingShot.body.fixtures[1]; - expect(fixture.shape.shapeType, equals(ShapeType.edge)); - }, - ); + expect(slingShot.body.bodyType, equals(BodyType.static)); + }, + ); - flameTester.test( - 'has restitution', - (game) async { - final slingShot = SlingShot( - side: BoardSide.left, - ); - await game.ensureAdd(slingShot); + flameTester.test( + 'has restitution', + (game) async { + final slingShot = SlingShot( + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); - final fixture = slingShot.body.fixtures[1]; - expect(fixture.restitution, greaterThan(0)); - }, - ); + final totalRestitution = slingShot.body.fixtures.fold( + 0, + (total, fixture) => total + fixture.restitution, + ); + expect(totalRestitution, greaterThan(0)); + }, + ); - flameTester.test( - 'has no friction', - (game) async { - final slingShot = SlingShot( - side: BoardSide.left, - ); - await game.ensureAdd(slingShot); + flameTester.test( + 'has no friction', + (game) async { + final slingShot = SlingShot( + side: BoardSide.left, + ); + await game.ensureAdd(slingShot); - final fixture = slingShot.body.fixtures[1]; - expect(fixture.friction, equals(0)); - }, - ); - }); + final totalFriction = slingShot.body.fixtures.fold( + 0, + (total, fixture) => total + fixture.friction, + ); + expect(totalFriction, equals(0)); + }, + ); }); }