mirror of https://github.com/flutter/pinball.git
refactor: splitting `Ball` business from UI logic (#86)
* feat: refactoring ball component to separate business from ui logic * fix: tests * fix: lint * fix: test coverage * Update lib/game/components/ball.dart Co-authored-by: Alejandro Santiago <dev@alestiago.com> Co-authored-by: Alejandro Santiago <dev@alestiago.com>pull/91/head
parent
394acd1802
commit
c8782cfbf4
@ -1 +1,4 @@
|
|||||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- lib/**/*.gen.dart
|
||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,68 @@
|
|||||||
|
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
/// *****************************************************
|
||||||
|
/// FlutterGen
|
||||||
|
/// *****************************************************
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class $AssetsImagesGen {
|
||||||
|
const $AssetsImagesGen();
|
||||||
|
|
||||||
|
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
class Assets {
|
||||||
|
Assets._();
|
||||||
|
|
||||||
|
static const $AssetsImagesGen images = $AssetsImagesGen();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetGenImage extends AssetImage {
|
||||||
|
const AssetGenImage(String assetName)
|
||||||
|
: super(assetName, package: 'pinball_components');
|
||||||
|
|
||||||
|
Image image({
|
||||||
|
Key? key,
|
||||||
|
ImageFrameBuilder? frameBuilder,
|
||||||
|
ImageLoadingBuilder? loadingBuilder,
|
||||||
|
ImageErrorWidgetBuilder? errorBuilder,
|
||||||
|
String? semanticLabel,
|
||||||
|
bool excludeFromSemantics = false,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
Color? color,
|
||||||
|
BlendMode? colorBlendMode,
|
||||||
|
BoxFit? fit,
|
||||||
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
|
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||||
|
Rect? centerSlice,
|
||||||
|
bool matchTextDirection = false,
|
||||||
|
bool gaplessPlayback = false,
|
||||||
|
bool isAntiAlias = false,
|
||||||
|
FilterQuality filterQuality = FilterQuality.low,
|
||||||
|
}) {
|
||||||
|
return Image(
|
||||||
|
key: key,
|
||||||
|
image: this,
|
||||||
|
frameBuilder: frameBuilder,
|
||||||
|
loadingBuilder: loadingBuilder,
|
||||||
|
errorBuilder: errorBuilder,
|
||||||
|
semanticLabel: semanticLabel,
|
||||||
|
excludeFromSemantics: excludeFromSemantics,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
fit: fit,
|
||||||
|
alignment: alignment,
|
||||||
|
repeat: repeat,
|
||||||
|
centerSlice: centerSlice,
|
||||||
|
matchTextDirection: matchTextDirection,
|
||||||
|
gaplessPlayback: gaplessPlayback,
|
||||||
|
isAntiAlias: isAntiAlias,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get path => assetName;
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
library pinball_components;
|
library pinball_components;
|
||||||
|
|
||||||
|
export 'gen/assets.gen.dart';
|
||||||
export 'src/pinball_components.dart';
|
export 'src/pinball_components.dart';
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
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
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Ball<T extends Forge2DGame> extends BodyComponent<T>
|
||||||
|
with Layered, InitialPosition {
|
||||||
|
/// {@macro ball_body}
|
||||||
|
Ball({
|
||||||
|
required this.baseColor,
|
||||||
|
}) {
|
||||||
|
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
|
||||||
|
// and default layer is Layer.all. But on final game Ball will be always be
|
||||||
|
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
|
||||||
|
// We need to see what happens if Ball appears from other place like nest
|
||||||
|
// bumper, it will need to explicit change layer to Layer.board then.
|
||||||
|
layer = Layer.board;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size of the [Ball]
|
||||||
|
final Vector2 size = Vector2.all(2);
|
||||||
|
|
||||||
|
/// The base [Color] used to tint this [Ball]
|
||||||
|
final Color baseColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final sprite = await gameRef.loadSprite(Assets.images.ball.keyName);
|
||||||
|
final tint = baseColor.withOpacity(0.5);
|
||||||
|
await add(
|
||||||
|
SpriteComponent(
|
||||||
|
sprite: sprite,
|
||||||
|
size: size,
|
||||||
|
anchor: Anchor.center,
|
||||||
|
)..tint(tint),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = CircleShape()..radius = size.x / 2;
|
||||||
|
|
||||||
|
final fixtureDef = FixtureDef(shape)..density = 1;
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()
|
||||||
|
..position = initialPosition
|
||||||
|
..userData = this
|
||||||
|
..type = BodyType.dynamic;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Immediatly and completly [stop]s the ball.
|
||||||
|
///
|
||||||
|
/// The [Ball] will no longer be affected by any forces, including it's
|
||||||
|
/// weight and those emitted from collisions.
|
||||||
|
void stop() {
|
||||||
|
body.setType(BodyType.static);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows the [Ball] to be affected by forces.
|
||||||
|
///
|
||||||
|
/// If previously [stop]ed, the previous ball's velocity is not kept.
|
||||||
|
void resume() {
|
||||||
|
body.setType(BodyType.dynamic);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export 'ball.dart';
|
||||||
|
export 'initial_position.dart';
|
||||||
|
export 'layer.dart';
|
@ -1,7 +1 @@
|
|||||||
/// {@template pinball_components}
|
export 'components/components.dart';
|
||||||
/// Package with the UI game components for the Pinball Game
|
|
||||||
/// {@endtemplate}
|
|
||||||
class PinballComponents {
|
|
||||||
/// {@macro pinball_components}
|
|
||||||
const PinballComponents();
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export 'test_game.dart';
|
@ -0,0 +1,7 @@
|
|||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
|
||||||
|
class TestGame extends Forge2DGame {
|
||||||
|
TestGame() {
|
||||||
|
images.prefix = '';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
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 '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(TestGame.new);
|
||||||
|
|
||||||
|
group('Ball', () {
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ready();
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
expect(game.contains(ball), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('body', () {
|
||||||
|
flameTester.test(
|
||||||
|
'is dynamic',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
expect(ball.body.bodyType, equals(BodyType.dynamic));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('can be moved', () {
|
||||||
|
flameTester.test('by its weight', (game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
game.update(1);
|
||||||
|
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('by applying velocity', (game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
ball.body.gravityScale = 0;
|
||||||
|
ball.body.linearVelocity.setValues(10, 10);
|
||||||
|
game.update(1);
|
||||||
|
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fixture', () {
|
||||||
|
flameTester.test(
|
||||||
|
'exists',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
expect(ball.body.fixtures[0], isA<Fixture>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is dense',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
final fixture = ball.body.fixtures[0];
|
||||||
|
expect(fixture.density, greaterThan(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'shape is circular',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
final fixture = ball.body.fixtures[0];
|
||||||
|
expect(fixture.shape.shapeType, equals(ShapeType.circle));
|
||||||
|
expect(fixture.shape.radius, equals(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has Layer.all as default filter maskBits',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ready();
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final fixture = ball.body.fixtures[0];
|
||||||
|
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('stop', () {
|
||||||
|
group("can't be moved", () {
|
||||||
|
flameTester.test('by its weight', (game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
ball.stop();
|
||||||
|
|
||||||
|
game.update(1);
|
||||||
|
expect(ball.body.position, equals(ball.initialPosition));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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', () {
|
||||||
|
group('can move', () {
|
||||||
|
flameTester.test(
|
||||||
|
'by its weight when previously stopped',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
ball.stop();
|
||||||
|
ball.resume();
|
||||||
|
|
||||||
|
game.update(1);
|
||||||
|
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'by applying velocity when previously stopped',
|
||||||
|
(game) async {
|
||||||
|
final ball = Ball(baseColor: Colors.blue);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
ball.stop();
|
||||||
|
ball.resume();
|
||||||
|
|
||||||
|
ball.body.gravityScale = 0;
|
||||||
|
ball.body.linearVelocity.setValues(10, 10);
|
||||||
|
game.update(1);
|
||||||
|
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
// ignore_for_file: prefer_const_constructors
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('PinballComponents', () {
|
|
||||||
test('can be instantiated', () {
|
|
||||||
expect(PinballComponents(), isNotNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in new issue