mirror of https://github.com/flutter/pinball.git
commit
24edc0118a
@ -0,0 +1,178 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geometry/geometry.dart';
|
||||
|
||||
/// {@template pathway}
|
||||
/// [Pathway] creates lines of various shapes.
|
||||
///
|
||||
/// [BodyComponent]s such as a Ball can collide and move along a [Pathway].
|
||||
/// {@endtemplate}
|
||||
class Pathway extends BodyComponent {
|
||||
Pathway._({
|
||||
// TODO(ruialonso): remove color when assets added.
|
||||
Color? color,
|
||||
required Vector2 position,
|
||||
required List<List<Vector2>> paths,
|
||||
}) : _position = position,
|
||||
_paths = paths {
|
||||
paint = Paint()
|
||||
..color = color ?? const Color.fromARGB(0, 0, 0, 0)
|
||||
..style = PaintingStyle.stroke;
|
||||
}
|
||||
|
||||
/// Creates a uniform unidirectional (straight) [Pathway].
|
||||
///
|
||||
/// Does so with two [ChainShape] separated by a [width]. Placed
|
||||
/// at a [position] between [start] and [end] points. Can
|
||||
/// be rotated by a given [rotation] in radians.
|
||||
///
|
||||
/// If [singleWall] is true, just one [ChainShape] is created.
|
||||
factory Pathway.straight({
|
||||
Color? color,
|
||||
required Vector2 position,
|
||||
required Vector2 start,
|
||||
required Vector2 end,
|
||||
required double width,
|
||||
double rotation = 0,
|
||||
bool singleWall = false,
|
||||
}) {
|
||||
final paths = <List<Vector2>>[];
|
||||
|
||||
// TODO(ruialonso): Refactor repetitive logic
|
||||
final firstWall = [
|
||||
start.clone(),
|
||||
end.clone(),
|
||||
].map((vector) => vector..rotate(rotation)).toList();
|
||||
paths.add(firstWall);
|
||||
|
||||
if (!singleWall) {
|
||||
final secondWall = [
|
||||
start + Vector2(width, 0),
|
||||
end + Vector2(width, 0),
|
||||
].map((vector) => vector..rotate(rotation)).toList();
|
||||
paths.add(secondWall);
|
||||
}
|
||||
|
||||
return Pathway._(
|
||||
color: color,
|
||||
position: position,
|
||||
paths: paths,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates an arc [Pathway].
|
||||
///
|
||||
/// The [angle], in radians, specifies the size of the arc. For example, 2*pi
|
||||
/// returns a complete circumference and minor angles a semi circumference.
|
||||
///
|
||||
/// The center of the arc is placed at [position].
|
||||
///
|
||||
/// Does so with two [ChainShape] separated by a [width]. Which can be
|
||||
/// rotated by a given [rotation] in radians.
|
||||
///
|
||||
/// The outer radius is specified by [radius], whilst the inner one is
|
||||
/// equivalent to [radius] - [width].
|
||||
///
|
||||
/// If [singleWall] is true, just one [ChainShape] is created.
|
||||
factory Pathway.arc({
|
||||
Color? color,
|
||||
required Vector2 position,
|
||||
required double width,
|
||||
required double radius,
|
||||
required double angle,
|
||||
double rotation = 0,
|
||||
bool singleWall = false,
|
||||
}) {
|
||||
final paths = <List<Vector2>>[];
|
||||
|
||||
// TODO(ruialonso): Refactor repetitive logic
|
||||
final outerWall = calculateArc(
|
||||
center: position,
|
||||
radius: radius,
|
||||
angle: angle,
|
||||
offsetAngle: rotation,
|
||||
);
|
||||
paths.add(outerWall);
|
||||
|
||||
if (!singleWall) {
|
||||
final innerWall = calculateArc(
|
||||
center: position,
|
||||
radius: radius - width,
|
||||
angle: angle,
|
||||
offsetAngle: rotation,
|
||||
);
|
||||
paths.add(innerWall);
|
||||
}
|
||||
|
||||
return Pathway._(
|
||||
color: color,
|
||||
position: position,
|
||||
paths: paths,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a bezier curve [Pathway].
|
||||
///
|
||||
/// Does so with two [ChainShape] separated by a [width]. Which can be
|
||||
/// rotated by a given [rotation] in radians.
|
||||
///
|
||||
/// First and last [controlPoints] set the beginning and end of the curve,
|
||||
/// inner points between them set its final shape.
|
||||
///
|
||||
/// If [singleWall] is true, just one [ChainShape] is created.
|
||||
factory Pathway.bezierCurve({
|
||||
Color? color,
|
||||
required Vector2 position,
|
||||
required List<Vector2> controlPoints,
|
||||
required double width,
|
||||
double rotation = 0,
|
||||
bool singleWall = false,
|
||||
}) {
|
||||
final paths = <List<Vector2>>[];
|
||||
|
||||
// TODO(ruialonso): Refactor repetitive logic
|
||||
final firstWall = calculateBezierCurve(controlPoints: controlPoints)
|
||||
.map((vector) => vector..rotate(rotation))
|
||||
.toList();
|
||||
paths.add(firstWall);
|
||||
|
||||
if (!singleWall) {
|
||||
final secondWall = calculateBezierCurve(
|
||||
controlPoints: controlPoints
|
||||
.map((vector) => vector + Vector2(width, -width))
|
||||
.toList(),
|
||||
).map((vector) => vector..rotate(rotation)).toList();
|
||||
paths.add(secondWall);
|
||||
}
|
||||
|
||||
return Pathway._(
|
||||
color: color,
|
||||
position: position,
|
||||
paths: paths,
|
||||
);
|
||||
}
|
||||
|
||||
final Vector2 _position;
|
||||
final List<List<Vector2>> _paths;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final bodyDef = BodyDef()
|
||||
..type = BodyType.static
|
||||
..position = _position;
|
||||
|
||||
final body = world.createBody(bodyDef);
|
||||
for (final path in _paths) {
|
||||
final chain = ChainShape()
|
||||
..createChain(
|
||||
path.map(gameRef.screenToWorld).toList(),
|
||||
);
|
||||
final fixtureDef = FixtureDef(chain);
|
||||
|
||||
body.createFixture(fixtureDef);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
pubspec.lock
|
@ -0,0 +1,11 @@
|
||||
# geometry
|
||||
|
||||
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||
[![License: MIT][license_badge]][license_link]
|
||||
|
||||
Helper package to calculate points of lines, arcs and curves for the pathways of the ball.
|
||||
|
||||
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[license_link]: https://opensource.org/licenses/MIT
|
||||
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
|
||||
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
|
@ -0,0 +1 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
@ -0,0 +1,3 @@
|
||||
library geometry;
|
||||
|
||||
export 'src/geometry.dart';
|
@ -0,0 +1,19 @@
|
||||
name: geometry
|
||||
description: Helper package to calculate points of lines, arcs and curves for the pathways of the ball
|
||||
version: 1.0.0+1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.16.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flame: ^1.0.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mocktail: ^0.2.0
|
||||
test: ^1.19.2
|
||||
very_good_analysis: ^2.4.0
|
@ -0,0 +1,159 @@
|
||||
// ignore_for_file: prefer_const_constructors, cascade_invocations
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:geometry/geometry.dart';
|
||||
|
||||
class Binomial {
|
||||
Binomial({required this.n, required this.k});
|
||||
|
||||
final num n;
|
||||
final num k;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('calculateArc', () {
|
||||
test('returns by default 100 points as indicated by precision', () {
|
||||
final points = calculateArc(
|
||||
center: Vector2.zero(),
|
||||
radius: 100,
|
||||
angle: 90,
|
||||
);
|
||||
expect(points.length, 100);
|
||||
});
|
||||
|
||||
test('returns as many points as indicated by precision', () {
|
||||
final points = calculateArc(
|
||||
center: Vector2.zero(),
|
||||
radius: 100,
|
||||
angle: 90,
|
||||
precision: 50,
|
||||
);
|
||||
expect(points.length, 50);
|
||||
});
|
||||
});
|
||||
|
||||
group('calculateBezierCurve', () {
|
||||
test('fails if step not in range', () {
|
||||
expect(
|
||||
() => calculateBezierCurve(
|
||||
controlPoints: [
|
||||
Vector2(0, 0),
|
||||
Vector2(10, 10),
|
||||
],
|
||||
step: 2,
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if not enough control points', () {
|
||||
expect(
|
||||
() => calculateBezierCurve(controlPoints: [Vector2.zero()]),
|
||||
throwsAssertionError,
|
||||
);
|
||||
expect(
|
||||
() => calculateBezierCurve(controlPoints: []),
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns by default 1000 points as indicated by step', () {
|
||||
final points = calculateBezierCurve(
|
||||
controlPoints: [
|
||||
Vector2(0, 0),
|
||||
Vector2(10, 10),
|
||||
],
|
||||
);
|
||||
expect(points.length, 1000);
|
||||
});
|
||||
|
||||
test('returns as many points as indicated by step', () {
|
||||
final points = calculateBezierCurve(
|
||||
controlPoints: [
|
||||
Vector2(0, 0),
|
||||
Vector2(10, 10),
|
||||
],
|
||||
step: 0.01,
|
||||
);
|
||||
expect(points.length, 100);
|
||||
});
|
||||
});
|
||||
|
||||
group('binomial', () {
|
||||
test('fails if k is negative', () {
|
||||
expect(() => binomial(1, -1), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('fails if n is negative', () {
|
||||
expect(() => binomial(-1, 1), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('fails if n < k', () {
|
||||
expect(() => binomial(1, 2), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('for a specific input gives a correct value', () {
|
||||
final binomialInputsToExpected = {
|
||||
Binomial(n: 0, k: 0): 1,
|
||||
Binomial(n: 1, k: 0): 1,
|
||||
Binomial(n: 1, k: 1): 1,
|
||||
Binomial(n: 2, k: 0): 1,
|
||||
Binomial(n: 2, k: 1): 2,
|
||||
Binomial(n: 2, k: 2): 1,
|
||||
Binomial(n: 3, k: 0): 1,
|
||||
Binomial(n: 3, k: 1): 3,
|
||||
Binomial(n: 3, k: 2): 3,
|
||||
Binomial(n: 3, k: 3): 1,
|
||||
Binomial(n: 4, k: 0): 1,
|
||||
Binomial(n: 4, k: 1): 4,
|
||||
Binomial(n: 4, k: 2): 6,
|
||||
Binomial(n: 4, k: 3): 4,
|
||||
Binomial(n: 4, k: 4): 1,
|
||||
Binomial(n: 5, k: 0): 1,
|
||||
Binomial(n: 5, k: 1): 5,
|
||||
Binomial(n: 5, k: 2): 10,
|
||||
Binomial(n: 5, k: 3): 10,
|
||||
Binomial(n: 5, k: 4): 5,
|
||||
Binomial(n: 5, k: 5): 1,
|
||||
Binomial(n: 6, k: 0): 1,
|
||||
Binomial(n: 6, k: 1): 6,
|
||||
Binomial(n: 6, k: 2): 15,
|
||||
Binomial(n: 6, k: 3): 20,
|
||||
Binomial(n: 6, k: 4): 15,
|
||||
Binomial(n: 6, k: 5): 6,
|
||||
Binomial(n: 6, k: 6): 1,
|
||||
};
|
||||
binomialInputsToExpected.forEach((input, value) {
|
||||
expect(binomial(input.n, input.k), value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('factorial', () {
|
||||
test('fails if negative number', () {
|
||||
expect(() => factorial(-1), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('for a specific input gives a correct value', () {
|
||||
final factorialInputsToExpected = {
|
||||
0: 1,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 6,
|
||||
4: 24,
|
||||
5: 120,
|
||||
6: 720,
|
||||
7: 5040,
|
||||
8: 40320,
|
||||
9: 362880,
|
||||
10: 3628800,
|
||||
11: 39916800,
|
||||
12: 479001600,
|
||||
13: 6227020800,
|
||||
};
|
||||
factorialInputsToExpected.forEach((input, expected) {
|
||||
expect(factorial(input), expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
// ignore_for_file: cascade_invocations, prefer_const_constructors
|
||||
import 'dart:math' as math;
|
||||
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/game/game.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final flameTester = FlameTester(PinballGame.new);
|
||||
|
||||
group('Pathway', () {
|
||||
const width = 50.0;
|
||||
|
||||
group('straight', () {
|
||||
group('color', () {
|
||||
flameTester.test(
|
||||
'has transparent color by default when no color is specified',
|
||||
(game) async {
|
||||
final pathway = Pathway.straight(
|
||||
position: Vector2.zero(),
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(game.contains(pathway), isTrue);
|
||||
expect(pathway.paint, isNotNull);
|
||||
expect(
|
||||
pathway.paint.color,
|
||||
equals(Color.fromARGB(0, 0, 0, 0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'has a color when is specified',
|
||||
(game) async {
|
||||
const defaultColor = Colors.blue;
|
||||
|
||||
final pathway = Pathway.straight(
|
||||
color: defaultColor,
|
||||
position: Vector2.zero(),
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(game.contains(pathway), isTrue);
|
||||
expect(pathway.paint, isNotNull);
|
||||
expect(pathway.paint.color.value, equals(defaultColor.value));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'loads correctly',
|
||||
(game) async {
|
||||
final pathway = Pathway.straight(
|
||||
position: Vector2.zero(),
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(game.contains(pathway), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('body', () {
|
||||
flameTester.test(
|
||||
'positions correctly',
|
||||
(game) async {
|
||||
final position = Vector2.all(10);
|
||||
final pathway = Pathway.straight(
|
||||
position: position,
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
game.contains(pathway);
|
||||
expect(pathway.body.position, position);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'is static',
|
||||
(game) async {
|
||||
final pathway = Pathway.straight(
|
||||
position: Vector2.zero(),
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(pathway.body.bodyType, equals(BodyType.static));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('fixtures', () {
|
||||
flameTester.test(
|
||||
'has only one ChainShape when singleWall is true',
|
||||
(game) async {
|
||||
final pathway = Pathway.straight(
|
||||
position: Vector2.zero(),
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
singleWall: true,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(pathway.body.fixtures.length, 1);
|
||||
final fixture = pathway.body.fixtures[0];
|
||||
expect(fixture, isA<Fixture>());
|
||||
expect(fixture.shape.shapeType, equals(ShapeType.chain));
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'has two ChainShape when singleWall is false (default)',
|
||||
(game) async {
|
||||
final pathway = Pathway.straight(
|
||||
position: Vector2.zero(),
|
||||
start: Vector2(10, 10),
|
||||
end: Vector2(20, 20),
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(pathway.body.fixtures.length, 2);
|
||||
for (final fixture in pathway.body.fixtures) {
|
||||
expect(fixture, isA<Fixture>());
|
||||
expect(fixture.shape.shapeType, equals(ShapeType.chain));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('arc', () {
|
||||
flameTester.test(
|
||||
'loads correctly',
|
||||
(game) async {
|
||||
final pathway = Pathway.arc(
|
||||
position: Vector2.zero(),
|
||||
width: width,
|
||||
radius: 100,
|
||||
angle: math.pi / 2,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(game.contains(pathway), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('body', () {
|
||||
flameTester.test(
|
||||
'positions correctly',
|
||||
(game) async {
|
||||
final position = Vector2.all(10);
|
||||
final pathway = Pathway.arc(
|
||||
position: position,
|
||||
width: width,
|
||||
radius: 100,
|
||||
angle: math.pi / 2,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
game.contains(pathway);
|
||||
expect(pathway.body.position, position);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'is static',
|
||||
(game) async {
|
||||
final pathway = Pathway.arc(
|
||||
position: Vector2.zero(),
|
||||
width: width,
|
||||
radius: 100,
|
||||
angle: math.pi / 2,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(pathway.body.bodyType, equals(BodyType.static));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('bezier curve', () {
|
||||
final controlPoints = [
|
||||
Vector2(0, 0),
|
||||
Vector2(50, 0),
|
||||
Vector2(0, 50),
|
||||
Vector2(50, 50),
|
||||
];
|
||||
|
||||
flameTester.test(
|
||||
'loads correctly',
|
||||
(game) async {
|
||||
final pathway = Pathway.bezierCurve(
|
||||
position: Vector2.zero(),
|
||||
controlPoints: controlPoints,
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(game.contains(pathway), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('body', () {
|
||||
flameTester.test(
|
||||
'positions correctly',
|
||||
(game) async {
|
||||
final position = Vector2.all(10);
|
||||
final pathway = Pathway.bezierCurve(
|
||||
position: position,
|
||||
controlPoints: controlPoints,
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
game.contains(pathway);
|
||||
expect(pathway.body.position, position);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'is static',
|
||||
(game) async {
|
||||
final pathway = Pathway.bezierCurve(
|
||||
position: Vector2.zero(),
|
||||
controlPoints: controlPoints,
|
||||
width: width,
|
||||
);
|
||||
await game.ensureAdd(pathway);
|
||||
|
||||
expect(pathway.body.bodyType, equals(BodyType.static));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in new issue