From c67db0e55c9c1835f7b5a18d4a041320742f116e Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 3 May 2022 18:54:11 +0100 Subject: [PATCH] refactor: moved scaling logic to `BallScalingBehavior` (#313) --- .../lib/src/components/{ => ball}/ball.dart | 29 +++--- .../ball/behaviors/ball_scaling_behavior.dart | 24 +++++ .../components/ball/behaviors/behaviors.dart | 1 + .../lib/src/components/components.dart | 2 +- .../src/components/{ => ball}/ball_test.dart | 63 ++++++------ .../behaviors/ball_scaling_behavior_test.dart | 99 +++++++++++++++++++ 6 files changed, 171 insertions(+), 47 deletions(-) rename packages/pinball_components/lib/src/components/{ => ball}/ball.dart (88%) create mode 100644 packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart rename packages/pinball_components/test/src/components/{ => ball}/ball_test.dart (78%) create mode 100644 packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart similarity index 88% rename from packages/pinball_components/lib/src/components/ball.dart rename to packages/pinball_components/lib/src/components/ball/ball.dart index 7469396a..4f913c2c 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:math' as math; -import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template ball} @@ -20,6 +20,7 @@ class Ball extends BodyComponent renderBody: false, children: [ _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + BallScalingBehavior(), ], ) { // TODO(ruimiguel): while developing Ball can be launched by clicking mouse, @@ -30,6 +31,15 @@ class Ball extends BodyComponent layer = Layer.board; } + /// Creates a [Ball] without any behaviors. + /// + /// This can be used for testing [Ball]'s behaviors in isolation. + @visibleForTesting + Ball.test({required this.baseColor}) + : super( + children: [_BallSpriteComponent()], + ); + /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); @@ -81,26 +91,9 @@ class Ball extends BodyComponent void update(double dt) { super.update(dt); - _rescaleSize(); _setPositionalGravity(); } - void _rescaleSize() { - final boardHeight = BoardDimensions.bounds.height; - const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; - - final standardizedYPosition = body.position.y + (boardHeight / 2); - - final scaleFactor = maxShrinkValue + - ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); - - body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; - - // TODO(alestiago): Revisit and see if there's a better way to do this. - final spriteComponent = firstChild<_BallSpriteComponent>(); - spriteComponent?.scale = Vector2.all(scaleFactor); - } - void _setPositionalGravity() { final defaultGravity = gameRef.world.gravity.y; final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2; diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart new file mode 100644 index 00000000..7fc06fb1 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Scales the ball's body and sprite according to its position on the board. +class BallScalingBehavior extends Component with ParentIsA { + @override + void update(double dt) { + super.update(dt); + final boardHeight = BoardDimensions.bounds.height; + const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; + + final standardizedYPosition = parent.body.position.y + (boardHeight / 2); + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); + + parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor; + + parent.firstChild()!.scale.setValues( + scaleFactor, + scaleFactor, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart new file mode 100644 index 00000000..22928734 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'ball_scaling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index c5ea7f9f..6e79ac56 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -2,7 +2,7 @@ export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; export 'android_spaceship/android_spaceship.dart'; export 'backboard/backboard.dart'; -export 'ball.dart'; +export 'ball/ball.dart'; export 'baseboard.dart'; export 'board_background_sprite_component.dart'; export 'board_dimensions.dart'; diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart similarity index 78% rename from packages/pinball_components/test/src/components/ball_test.dart rename to packages/pinball_components/test/src/components/ball/ball_test.dart index 26a03886..321e137b 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -6,18 +6,29 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); group('Ball', () { + const baseColor = Color(0xFFFFFFFF); + + test( + 'can be instantiated', + () { + expect(Ball(baseColor: baseColor), isA()); + expect(Ball.test(baseColor: baseColor), isA()); + }, + ); + flameTester.test( 'loads correctly', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ready(); await game.ensureAdd(ball); @@ -25,11 +36,20 @@ void main() { }, ); + flameTester.test('add a BallScalingBehavior', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + expect( + ball.descendants().whereType().length, + equals(1), + ); + }); + group('body', () { flameTester.test( 'is dynamic', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.bodyType, equals(BodyType.dynamic)); @@ -38,7 +58,7 @@ void main() { group('can be moved', () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); game.update(1); @@ -46,7 +66,7 @@ void main() { }); flameTester.test('by applying velocity', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.body.gravityScale = Vector2.zero(); @@ -61,7 +81,7 @@ void main() { flameTester.test( 'exists', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.fixtures[0], isA()); @@ -71,7 +91,7 @@ void main() { flameTester.test( 'is dense', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -82,7 +102,7 @@ void main() { flameTester.test( 'shape is circular', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -94,7 +114,7 @@ void main() { flameTester.test( 'has Layer.all as default filter maskBits', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ready(); await game.ensureAdd(ball); await game.ready(); @@ -108,7 +128,7 @@ void main() { group('stop', () { group("can't be moved", () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); @@ -116,19 +136,6 @@ void main() { expect(ball.body.position, equals(ball.initialPosition)); }); }); - - // TODO(allisonryan0002): delete or retest this if/when solution is added - // to prevent forces on a ball while stopped. - - // flameTester.test('by applying velocity', (game) async { - // final ball = Ball(baseColor: Colors.blue); - // await game.ensureAdd(ball); - // ball.stop(); - - // ball.body.linearVelocity.setValues(10, 10); - // game.update(1); - // expect(ball.body.position, equals(ball.initialPosition)); - // }); }); group('resume', () { @@ -136,7 +143,7 @@ void main() { flameTester.test( 'by its weight when previously stopped', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -149,7 +156,7 @@ void main() { flameTester.test( 'by applying velocity when previously stopped', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -165,7 +172,7 @@ void main() { group('boost', () { flameTester.test('applies an impulse to the ball', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.linearVelocity, equals(Vector2.zero())); @@ -176,7 +183,7 @@ void main() { }); flameTester.test('adds TurboChargeSpriteAnimation', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); await ball.boost(Vector2.all(10)); @@ -190,7 +197,7 @@ void main() { flameTester.test('removes TurboChargeSpriteAnimation after it finishes', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); await ball.boost(Vector2.all(10)); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart new file mode 100644 index 00000000..42cd9073 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -0,0 +1,99 @@ +// 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:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final asset = Assets.images.ball.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + group('BallScalingBehavior', () { + const baseColor = Color(0xFFFFFFFF); + test('can be instantiated', () { + expect( + BallScalingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallScalingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallScalingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test('scales the shape radius', (game) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, 10); + await ball1.add(BallScalingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, -10); + await ball2.add(BallScalingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + final shape1 = ball1.body.fixtures.first.shape; + final shape2 = ball2.body.fixtures.first.shape; + expect( + shape1.radius, + greaterThan(shape2.radius), + ); + }); + + flameTester.testGameWidget( + 'scales the sprite', + setUp: (game, tester) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, 10); + await ball1.add(BallScalingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, -10); + await ball2.add(BallScalingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + await tester.pump(); + await game.ready(); + + final sprite1 = ball1.firstChild()!; + final sprite2 = ball2.firstChild()!; + expect( + sprite1.scale.x, + greaterThan(sprite2.scale.x), + ); + expect( + sprite1.scale.y, + greaterThan(sprite2.scale.y), + ); + }, + ); + }); +}