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
|
||||
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;
|
||||
|
||||
export 'gen/assets.gen.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}
|
||||
/// Package with the UI game components for the Pinball Game
|
||||
/// {@endtemplate}
|
||||
class PinballComponents {
|
||||
/// {@macro pinball_components}
|
||||
const PinballComponents();
|
||||
}
|
||||
export 'components/components.dart';
|
||||
|
@ -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