mirror of https://github.com/flutter/pinball.git
commit
0c24fd9fc1
@ -0,0 +1,22 @@
|
|||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
/// Indicates a side of the board.
|
||||||
|
///
|
||||||
|
/// Usually used to position or mirror elements of a [PinballGame]; such as a
|
||||||
|
/// [Flipper].
|
||||||
|
enum BoardSide {
|
||||||
|
/// The left side of the board.
|
||||||
|
left,
|
||||||
|
|
||||||
|
/// The right side of the board.
|
||||||
|
right,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility methods for [BoardSide].
|
||||||
|
extension BoardSideX on BoardSide {
|
||||||
|
/// Whether this side is [BoardSide.left].
|
||||||
|
bool get isLeft => this == BoardSide.left;
|
||||||
|
|
||||||
|
/// Whether this side is [BoardSide.right].
|
||||||
|
bool get isRight => this == BoardSide.right;
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
export 'anchor.dart';
|
export 'anchor.dart';
|
||||||
export 'ball.dart';
|
export 'ball.dart';
|
||||||
|
export 'board_side.dart';
|
||||||
|
export 'flipper.dart';
|
||||||
|
export 'pathway.dart';
|
||||||
export 'plunger.dart';
|
export 'plunger.dart';
|
||||||
export 'score_points.dart';
|
export 'score_points.dart';
|
||||||
export 'wall.dart';
|
export 'wall.dart';
|
||||||
|
@ -0,0 +1,241 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
/// {@template flipper}
|
||||||
|
/// A bat, typically found in pairs at the bottom of the board.
|
||||||
|
///
|
||||||
|
/// [Flipper] can be controlled by the player in an arc motion.
|
||||||
|
/// {@endtemplate flipper}
|
||||||
|
class Flipper extends BodyComponent with KeyboardHandler {
|
||||||
|
/// {@macro flipper}
|
||||||
|
Flipper._({
|
||||||
|
required Vector2 position,
|
||||||
|
required this.side,
|
||||||
|
required List<LogicalKeyboardKey> keys,
|
||||||
|
}) : _position = position,
|
||||||
|
_keys = keys {
|
||||||
|
// TODO(alestiago): Use sprite instead of color when provided.
|
||||||
|
paint = Paint()
|
||||||
|
..color = const Color(0xFF00FF00)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A left positioned [Flipper].
|
||||||
|
Flipper.left({
|
||||||
|
required Vector2 position,
|
||||||
|
}) : this._(
|
||||||
|
position: position,
|
||||||
|
side: BoardSide.left,
|
||||||
|
keys: [
|
||||||
|
LogicalKeyboardKey.arrowLeft,
|
||||||
|
LogicalKeyboardKey.keyA,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A right positioned [Flipper].
|
||||||
|
Flipper.right({
|
||||||
|
required Vector2 position,
|
||||||
|
}) : this._(
|
||||||
|
position: position,
|
||||||
|
side: BoardSide.right,
|
||||||
|
keys: [
|
||||||
|
LogicalKeyboardKey.arrowRight,
|
||||||
|
LogicalKeyboardKey.keyD,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The width of the [Flipper].
|
||||||
|
static const width = 12.0;
|
||||||
|
|
||||||
|
/// The height of the [Flipper].
|
||||||
|
static const height = 2.8;
|
||||||
|
|
||||||
|
/// The speed required to move the [Flipper] to its highest position.
|
||||||
|
///
|
||||||
|
/// The higher the value, the faster the [Flipper] will move.
|
||||||
|
static const double _speed = 60;
|
||||||
|
|
||||||
|
/// Whether the [Flipper] is on the left or right side of the board.
|
||||||
|
///
|
||||||
|
/// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion,
|
||||||
|
/// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion.
|
||||||
|
final BoardSide side;
|
||||||
|
|
||||||
|
/// The initial position of the [Flipper] body.
|
||||||
|
final Vector2 _position;
|
||||||
|
|
||||||
|
/// The [LogicalKeyboardKey]s that will control the [Flipper].
|
||||||
|
///
|
||||||
|
/// [onKeyEvent] method listens to when one of these keys is pressed.
|
||||||
|
final List<LogicalKeyboardKey> _keys;
|
||||||
|
|
||||||
|
/// Applies downward linear velocity to the [Flipper], moving it to its
|
||||||
|
/// resting position.
|
||||||
|
void _moveDown() {
|
||||||
|
body.linearVelocity = Vector2(0, -_speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies upward linear velocity to the [Flipper], moving it to its highest
|
||||||
|
/// position.
|
||||||
|
void _moveUp() {
|
||||||
|
body.linearVelocity = Vector2(0, _speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FixtureDef> _createFixtureDefs() {
|
||||||
|
final fixtures = <FixtureDef>[];
|
||||||
|
final isLeft = side.isLeft;
|
||||||
|
|
||||||
|
final bigCircleShape = CircleShape()..radius = height / 2;
|
||||||
|
bigCircleShape.position.setValues(
|
||||||
|
isLeft
|
||||||
|
? -(width / 2) + bigCircleShape.radius
|
||||||
|
: (width / 2) - bigCircleShape.radius,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
|
||||||
|
fixtures.add(bigCircleFixtureDef);
|
||||||
|
|
||||||
|
final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2;
|
||||||
|
smallCircleShape.position.setValues(
|
||||||
|
isLeft
|
||||||
|
? (width / 2) - smallCircleShape.radius
|
||||||
|
: -(width / 2) + smallCircleShape.radius,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
|
||||||
|
fixtures.add(smallCircleFixtureDef);
|
||||||
|
|
||||||
|
final trapeziumVertices = isLeft
|
||||||
|
? [
|
||||||
|
Vector2(bigCircleShape.position.x, bigCircleShape.radius),
|
||||||
|
Vector2(smallCircleShape.position.x, smallCircleShape.radius),
|
||||||
|
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
|
||||||
|
Vector2(bigCircleShape.position.x, -bigCircleShape.radius),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
Vector2(smallCircleShape.position.x, smallCircleShape.radius),
|
||||||
|
Vector2(bigCircleShape.position.x, bigCircleShape.radius),
|
||||||
|
Vector2(bigCircleShape.position.x, -bigCircleShape.radius),
|
||||||
|
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
|
||||||
|
];
|
||||||
|
final trapezium = PolygonShape()..set(trapeziumVertices);
|
||||||
|
final trapeziumFixtureDef = FixtureDef(trapezium)
|
||||||
|
..density = 50.0 // TODO(alestiago): Use a proper density.
|
||||||
|
..friction = .1; // TODO(alestiago): Use a proper friction.
|
||||||
|
fixtures.add(trapeziumFixtureDef);
|
||||||
|
|
||||||
|
return fixtures;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final bodyDef = BodyDef()
|
||||||
|
..gravityScale = 0
|
||||||
|
..type = BodyType.dynamic
|
||||||
|
..position = _position;
|
||||||
|
|
||||||
|
final body = world.createBody(bodyDef);
|
||||||
|
_createFixtureDefs().forEach(body.createFixture);
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(erickzanardo): Remove this once the issue is solved:
|
||||||
|
// https://github.com/flame-engine/flame/issues/1417
|
||||||
|
final Completer hasMounted = Completer<void>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onMount() {
|
||||||
|
super.onMount();
|
||||||
|
hasMounted.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
// TODO(alestiago): Check why false cancels the event for other components.
|
||||||
|
// Investigate why return is of type [bool] expected instead of a type
|
||||||
|
// [KeyEventResult].
|
||||||
|
if (!_keys.contains(event.logicalKey)) return true;
|
||||||
|
|
||||||
|
if (event is RawKeyDownEvent) {
|
||||||
|
_moveUp();
|
||||||
|
} else if (event is RawKeyUpEvent) {
|
||||||
|
_moveDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template flipper_anchor}
|
||||||
|
/// [Anchor] positioned at the end of a [Flipper].
|
||||||
|
///
|
||||||
|
/// The end of a [Flipper] depends on its [Flipper.side].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FlipperAnchor extends Anchor {
|
||||||
|
/// {@macro flipper_anchor}
|
||||||
|
FlipperAnchor({
|
||||||
|
required Flipper flipper,
|
||||||
|
}) : super(
|
||||||
|
position: Vector2(
|
||||||
|
flipper.side.isLeft
|
||||||
|
? flipper.body.position.x - Flipper.width / 2
|
||||||
|
: flipper.body.position.x + Flipper.width / 2,
|
||||||
|
flipper.body.position.y,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template flipper_anchor_revolute_joint_def}
|
||||||
|
/// Hinges one end of [Flipper] to a [Anchor] to achieve an arc motion.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
|
||||||
|
/// {@macro flipper_anchor_revolute_joint_def}
|
||||||
|
FlipperAnchorRevoluteJointDef({
|
||||||
|
required Flipper flipper,
|
||||||
|
required Anchor anchor,
|
||||||
|
}) {
|
||||||
|
initialize(
|
||||||
|
flipper.body,
|
||||||
|
anchor.body,
|
||||||
|
anchor.body.position,
|
||||||
|
);
|
||||||
|
enableLimit = true;
|
||||||
|
|
||||||
|
final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2;
|
||||||
|
lowerAngle = upperAngle = angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The total angle of the arc motion.
|
||||||
|
static const _sweepingAngle = math.pi / 3.5;
|
||||||
|
|
||||||
|
/// Unlocks the [Flipper] from its resting position.
|
||||||
|
///
|
||||||
|
/// The [Flipper] is locked when initialized in order to force it to be at
|
||||||
|
/// its resting position.
|
||||||
|
// TODO(alestiago): consider refactor once the issue is solved:
|
||||||
|
// https://github.com/flame-engine/forge2d/issues/36
|
||||||
|
static void unlock(RevoluteJoint joint, BoardSide side) {
|
||||||
|
late final double upperLimit, lowerLimit;
|
||||||
|
switch (side) {
|
||||||
|
case BoardSide.left:
|
||||||
|
lowerLimit = -joint.lowerLimit;
|
||||||
|
upperLimit = joint.upperLimit;
|
||||||
|
break;
|
||||||
|
case BoardSide.right:
|
||||||
|
lowerLimit = joint.lowerLimit;
|
||||||
|
upperLimit = -joint.upperLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
joint.setLimits(lowerLimit, upperLimit);
|
||||||
|
}
|
||||||
|
}
|
@ -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,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
/// {@template game_hud}
|
||||||
|
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
|
||||||
|
/// [GameState.balls].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class GameHud extends StatelessWidget {
|
||||||
|
/// {@macro game_hud}
|
||||||
|
const GameHud({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = context.watch<GameBloc>().state;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${state.score}',
|
||||||
|
style: Theme.of(context).textTheme.headline3,
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < state.balls; i++)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 6, right: 6),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 8,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
export 'game_hud.dart';
|
||||||
export 'pinball_game_page.dart';
|
export 'pinball_game_page.dart';
|
||||||
export 'widgets/widgets.dart';
|
export 'widgets/widgets.dart';
|
||||||
|
@ -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,27 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group(
|
||||||
|
'BoardSide',
|
||||||
|
() {
|
||||||
|
test('has two values', () {
|
||||||
|
expect(BoardSide.values.length, equals(2));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('BoardSideX', () {
|
||||||
|
test('isLeft is correct', () {
|
||||||
|
const side = BoardSide.left;
|
||||||
|
expect(side.isLeft, isTrue);
|
||||||
|
expect(side.isRight, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isRight is correct', () {
|
||||||
|
const side = BoardSide.right;
|
||||||
|
expect(side.isLeft, isFalse);
|
||||||
|
expect(side.isRight, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,401 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(PinballGame.new);
|
||||||
|
group(
|
||||||
|
'Flipper',
|
||||||
|
() {
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
final leftFlipper = Flipper.left(position: Vector2.zero());
|
||||||
|
final rightFlipper = Flipper.right(position: Vector2.zero());
|
||||||
|
await game.ensureAddAll([leftFlipper, rightFlipper]);
|
||||||
|
|
||||||
|
expect(game.contains(leftFlipper), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('constructor', () {
|
||||||
|
test('sets BoardSide', () {
|
||||||
|
final leftFlipper = Flipper.left(position: Vector2.zero());
|
||||||
|
expect(leftFlipper.side, equals(leftFlipper.side));
|
||||||
|
|
||||||
|
final rightFlipper = Flipper.right(position: Vector2.zero());
|
||||||
|
expect(rightFlipper.side, equals(rightFlipper.side));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('body', () {
|
||||||
|
flameTester.test(
|
||||||
|
'positions correctly',
|
||||||
|
(game) async {
|
||||||
|
final position = Vector2.all(10);
|
||||||
|
final flipper = Flipper.left(position: position);
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
game.contains(flipper);
|
||||||
|
|
||||||
|
expect(flipper.body.position, position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is dynamic',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
expect(flipper.body.bodyType, equals(BodyType.dynamic));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'ignores gravity',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
expect(flipper.body.gravityScale, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has greater mass than Ball',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
final ball = Ball(position: Vector2.zero());
|
||||||
|
|
||||||
|
await game.ensureAddAll([flipper, ball]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
flipper.body.getMassData().mass,
|
||||||
|
greaterThan(ball.body.getMassData().mass),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fixtures', () {
|
||||||
|
flameTester.test(
|
||||||
|
'has three',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
expect(flipper.body.fixtures.length, equals(3));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has density',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final fixtures = flipper.body.fixtures;
|
||||||
|
final density = fixtures.fold<double>(
|
||||||
|
0,
|
||||||
|
(sum, fixture) => sum + fixture.density,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(density, greaterThan(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('onKeyEvent', () {
|
||||||
|
final leftKeys = UnmodifiableListView([
|
||||||
|
LogicalKeyboardKey.arrowLeft,
|
||||||
|
LogicalKeyboardKey.keyA,
|
||||||
|
]);
|
||||||
|
final rightKeys = UnmodifiableListView([
|
||||||
|
LogicalKeyboardKey.arrowRight,
|
||||||
|
LogicalKeyboardKey.keyD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
group('and Flipper is left', () {
|
||||||
|
late Flipper flipper;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyDownEvents(leftKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'moves upwards '
|
||||||
|
'when ${event.logicalKey.keyLabel} is pressed',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isPositive);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyUpEvents(leftKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'moves downwards '
|
||||||
|
'when ${event.logicalKey.keyLabel} is released',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isNegative);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyUpEvents(rightKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'does nothing '
|
||||||
|
'when ${event.logicalKey.keyLabel} is released',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isZero);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyDownEvents(rightKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'does nothing '
|
||||||
|
'when ${event.logicalKey.keyLabel} is pressed',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isZero);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('and Flipper is right', () {
|
||||||
|
late Flipper flipper;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
flipper = Flipper.right(position: Vector2.zero());
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyDownEvents(rightKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'moves upwards '
|
||||||
|
'when ${event.logicalKey.keyLabel} is pressed',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isPositive);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyUpEvents(rightKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'moves downwards '
|
||||||
|
'when ${event.logicalKey.keyLabel} is released',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isNegative);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyUpEvents(leftKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'does nothing '
|
||||||
|
'when ${event.logicalKey.keyLabel} is released',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isZero);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testRawKeyDownEvents(leftKeys, (event) {
|
||||||
|
flameTester.test(
|
||||||
|
'does nothing '
|
||||||
|
'when ${event.logicalKey.keyLabel} is pressed',
|
||||||
|
(game) async {
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
flipper.onKeyEvent(event, {});
|
||||||
|
|
||||||
|
expect(flipper.body.linearVelocity.y, isZero);
|
||||||
|
expect(flipper.body.linearVelocity.x, isZero);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'FlipperAnchor',
|
||||||
|
() {
|
||||||
|
flameTester.test(
|
||||||
|
'position is at the left of the left Flipper',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'position is at the right of the right Flipper',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.right(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
expect(flipperAnchor.body.position.x, equals(Flipper.width / 2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('FlipperAnchorRevoluteJointDef', () {
|
||||||
|
group('initializes with', () {
|
||||||
|
flameTester.test(
|
||||||
|
'limits enabled',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
final jointDef = FlipperAnchorRevoluteJointDef(
|
||||||
|
flipper: flipper,
|
||||||
|
anchor: flipperAnchor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(jointDef.enableLimit, isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('equal upper and lower limits', () {
|
||||||
|
flameTester.test(
|
||||||
|
'when Flipper is left',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
final jointDef = FlipperAnchorRevoluteJointDef(
|
||||||
|
flipper: flipper,
|
||||||
|
anchor: flipperAnchor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(jointDef.lowerAngle, equals(jointDef.upperAngle));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'when Flipper is right',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.right(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
final jointDef = FlipperAnchorRevoluteJointDef(
|
||||||
|
flipper: flipper,
|
||||||
|
anchor: flipperAnchor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(jointDef.lowerAngle, equals(jointDef.upperAngle));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group(
|
||||||
|
'unlocks',
|
||||||
|
() {
|
||||||
|
flameTester.test(
|
||||||
|
'when Flipper is left',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
final jointDef = FlipperAnchorRevoluteJointDef(
|
||||||
|
flipper: flipper,
|
||||||
|
anchor: flipperAnchor,
|
||||||
|
);
|
||||||
|
final joint = game.world.createJoint(jointDef) as RevoluteJoint;
|
||||||
|
|
||||||
|
FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
joint.upperLimit,
|
||||||
|
isNot(equals(joint.lowerLimit)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'when Flipper is right',
|
||||||
|
(game) async {
|
||||||
|
final flipper = Flipper.right(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(flipper);
|
||||||
|
|
||||||
|
final flipperAnchor = FlipperAnchor(flipper: flipper);
|
||||||
|
await game.ensureAdd(flipperAnchor);
|
||||||
|
|
||||||
|
final jointDef = FlipperAnchorRevoluteJointDef(
|
||||||
|
flipper: flipper,
|
||||||
|
anchor: flipperAnchor,
|
||||||
|
);
|
||||||
|
final joint = game.world.createJoint(jointDef) as RevoluteJoint;
|
||||||
|
|
||||||
|
FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
joint.upperLimit,
|
||||||
|
isNot(equals(joint.lowerLimit)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -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));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,9 +1,54 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('PinballGame', () {
|
group('PinballGame', () {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(PinballGame.new);
|
||||||
|
|
||||||
// TODO(alestiago): test if [PinballGame] registers
|
// TODO(alestiago): test if [PinballGame] registers
|
||||||
// [BallScorePointsCallback] once the following issue is resolved:
|
// [BallScorePointsCallback] once the following issue is resolved:
|
||||||
// https://github.com/flame-engine/flame/issues/1416
|
// https://github.com/flame-engine/flame/issues/1416
|
||||||
|
group(
|
||||||
|
'components',
|
||||||
|
() {
|
||||||
|
group('Flippers', () {
|
||||||
|
bool Function(Component) flipperSelector(BoardSide side) =>
|
||||||
|
(component) => component is Flipper && component.side == side;
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has only one left Flipper',
|
||||||
|
(game) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => game.children.singleWhere(
|
||||||
|
flipperSelector(BoardSide.left),
|
||||||
|
),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has only one right Flipper',
|
||||||
|
(game) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => game.children.singleWhere(
|
||||||
|
flipperSelector(BoardSide.right),
|
||||||
|
),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameHud', () {
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
const initialState = GameState(score: 10, balls: 2, bonusLetters: []);
|
||||||
|
|
||||||
|
void _mockState(GameState state) {
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpHud(WidgetTester tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
GameHud(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = MockGameBloc();
|
||||||
|
_mockState(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'renders the current score',
|
||||||
|
(tester) async {
|
||||||
|
await _pumpHud(tester);
|
||||||
|
expect(find.text(initialState.score.toString()), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'renders the current ball number',
|
||||||
|
(tester) async {
|
||||||
|
await _pumpHud(tester);
|
||||||
|
expect(
|
||||||
|
find.byType(CircleAvatar),
|
||||||
|
findsNWidgets(initialState.balls),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('updates the score', (tester) async {
|
||||||
|
await _pumpHud(tester);
|
||||||
|
expect(find.text(initialState.score.toString()), findsOneWidget);
|
||||||
|
|
||||||
|
_mockState(initialState.copyWith(score: 20));
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('20'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('updates the ball number', (tester) async {
|
||||||
|
await _pumpHud(tester);
|
||||||
|
expect(
|
||||||
|
find.byType(CircleAvatar),
|
||||||
|
findsNWidgets(initialState.balls),
|
||||||
|
);
|
||||||
|
|
||||||
|
_mockState(initialState.copyWith(balls: 1));
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
find.byType(CircleAvatar),
|
||||||
|
findsNWidgets(1),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'helpers.dart';
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
void testRawKeyUpEvents(
|
||||||
|
List<LogicalKeyboardKey> keys,
|
||||||
|
Function(RawKeyUpEvent) test,
|
||||||
|
) {
|
||||||
|
for (final key in keys) {
|
||||||
|
test(_mockKeyUpEvent(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RawKeyUpEvent _mockKeyUpEvent(LogicalKeyboardKey key) {
|
||||||
|
final event = MockRawKeyUpEvent();
|
||||||
|
when(() => event.logicalKey).thenReturn(key);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
void testRawKeyDownEvents(
|
||||||
|
List<LogicalKeyboardKey> keys,
|
||||||
|
Function(RawKeyDownEvent) test,
|
||||||
|
) {
|
||||||
|
for (final key in keys) {
|
||||||
|
test(_mockKeyDownEvent(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RawKeyDownEvent _mockKeyDownEvent(LogicalKeyboardKey key) {
|
||||||
|
final event = MockRawKeyDownEvent();
|
||||||
|
when(() => event.logicalKey).thenReturn(key);
|
||||||
|
return event;
|
||||||
|
}
|
Loading…
Reference in new issue