Merge remote-tracking branch 'origin' into feat/sling-shot-component

pull/39/head
alestiago 4 years ago
commit 6caec0222d

@ -15,6 +15,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
static const bonusWord = 'GOOGLE'; static const bonusWord = 'GOOGLE';
static const bonusWordScore = 10000;
void _onBallLost(BallLost event, Emitter emit) { void _onBallLost(BallLost event, Emitter emit) {
if (state.balls > 0) { if (state.balls > 0) {
@ -44,6 +45,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
], ],
), ),
); );
add(const Scored(points: bonusWordScore));
} else { } else {
emit( emit(
state.copyWith(activatedBonusLetters: newBonusLetters), state.copyWith(activatedBonusLetters: newBonusLetters),

@ -49,6 +49,10 @@ class GameState extends Equatable {
/// Determines when the player has only one ball left. /// Determines when the player has only one ball left.
bool get isLastBall => balls == 1; bool get isLastBall => balls == 1;
/// Shortcut method to check if the given [i]
/// is activated.
bool isLetterActivated(int i) => activatedBonusLetters.contains(i);
GameState copyWith({ GameState copyWith({
int? score, int? score,
int? balls, int? balls,

@ -6,16 +6,18 @@ import 'package:pinball/game/game.dart';
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the /// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame]. /// [PinballGame].
/// {@endtemplate} /// {@endtemplate}
class Ball extends PositionBodyComponent<PinballGame, SpriteComponent> { class Ball extends BodyComponent<PinballGame> {
/// {@macro ball} /// {@macro ball}
Ball({ Ball({
required Vector2 position, required Vector2 position,
}) : _position = position, }) : _position = position;
super(size: Vector2.all(2));
/// The initial position of the [Ball] body. /// The initial position of the [Ball] body.
final Vector2 _position; final Vector2 _position;
/// The size of the [Ball]
final Vector2 size = Vector2.all(2);
/// Asset location of the sprite that renders with the [Ball]. /// Asset location of the sprite that renders with the [Ball].
/// ///
/// Sprite is preloaded by [PinballGameAssetsX]. /// Sprite is preloaded by [PinballGameAssetsX].
@ -26,7 +28,13 @@ class Ball extends PositionBodyComponent<PinballGame, SpriteComponent> {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath); final sprite = await gameRef.loadSprite(spritePath);
final tint = gameRef.theme.characterTheme.ballColor.withOpacity(0.5); final tint = gameRef.theme.characterTheme.ballColor.withOpacity(0.5);
positionComponent = SpriteComponent(sprite: sprite, size: size)..tint(tint); await add(
SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
)..tint(tint),
);
} }
@override @override

@ -0,0 +1,91 @@
import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
/// {@template baseboard}
/// Straight, angled board piece to corral the [Ball] towards the [Flipper]s.
/// {@endtemplate}
class Baseboard extends BodyComponent {
/// {@macro baseboard}
Baseboard._({
required Vector2 position,
required BoardSide side,
}) : _position = position,
_side = side;
/// A left positioned [Baseboard].
Baseboard.left({
required Vector2 position,
}) : this._(
position: position,
side: BoardSide.left,
);
/// A right positioned [Baseboard].
Baseboard.right({
required Vector2 position,
}) : this._(
position: position,
side: BoardSide.right,
);
/// The width of the [Baseboard].
static const width = 10.0;
/// The height of the [Baseboard].
static const height = 2.0;
/// The position of the [Baseboard] body.
final Vector2 _position;
/// Whether the [Baseboard] is on the left or right side of the board.
final BoardSide _side;
List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[];
final circleShape1 = CircleShape()..radius = Baseboard.height / 2;
circleShape1.position.setValues(
-(Baseboard.width / 2) + circleShape1.radius,
0,
);
final circle1FixtureDef = FixtureDef(circleShape1);
fixtures.add(circle1FixtureDef);
final circleShape2 = CircleShape()..radius = Baseboard.height / 2;
circleShape2.position.setValues(
(Baseboard.width / 2) - circleShape2.radius,
0,
);
final circle2FixtureDef = FixtureDef(circleShape2);
fixtures.add(circle2FixtureDef);
final rectangle = PolygonShape()
..setAsBoxXY(
(Baseboard.width - Baseboard.height) / 2,
Baseboard.height / 2,
);
final rectangleFixtureDef = FixtureDef(rectangle);
fixtures.add(rectangleFixtureDef);
return fixtures;
}
@override
Body createBody() {
// TODO(allisonryan0002): share sweeping angle with flipper when components
// are grouped.
const angle = math.pi / 7;
final bodyDef = BodyDef()
..position = _position
..type = BodyType.static
..angle = _side.isLeft ? -angle : angle;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}

@ -0,0 +1,181 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
/// {@template bonus_word}
/// Loads all [BonusLetter]s to compose a [BonusWord].
/// {@endtemplate}
class BonusWord extends Component with BlocComponent<GameBloc, GameState> {
/// {@macro bonus_word}
BonusWord({required Vector2 position}) : _position = position;
final Vector2 _position;
@override
bool listenWhen(GameState? previousState, GameState newState) {
if ((previousState?.bonusHistory.length ?? 0) <
newState.bonusHistory.length &&
newState.bonusHistory.last == GameBonus.word) {
return true;
}
return false;
}
@override
void onNewState(GameState state) {
if (state.bonusHistory.last == GameBonus.word) {
final letters = children.whereType<BonusLetter>().toList();
for (var i = 0; i < letters.length; i++) {
final letter = letters[i];
letter.add(
SequenceEffect(
[
ColorEffect(
i.isOdd ? BonusLetter._activeColor : BonusLetter._disableColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
ColorEffect(
i.isOdd ? BonusLetter._disableColor : BonusLetter._activeColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
],
repeatCount: 4,
)..onFinishCallback = () {
letter.add(
ColorEffect(
BonusLetter._disableColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
);
},
);
}
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
final letters = GameBloc.bonusWord.split('');
for (var i = 0; i < letters.length; i++) {
unawaited(
add(
BonusLetter(
position: _position - Vector2(16 - (i * 6), -30),
letter: letters[i],
index: i,
),
),
);
}
}
}
/// {@template bonus_letter}
/// [BodyType.static] sensor component, part of a word bonus,
/// which will activate its letter after contact with a [Ball].
/// {@endtemplate}
class BonusLetter extends BodyComponent<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@macro bonus_letter}
BonusLetter({
required Vector2 position,
required String letter,
required int index,
}) : _position = position,
_letter = letter,
_index = index {
paint = Paint()..color = _disableColor;
}
/// The area size of this [BonusLetter].
static final areaSize = Vector2.all(4);
static const _activeColor = Colors.green;
static const _disableColor = Colors.red;
final Vector2 _position;
final String _letter;
final int _index;
@override
Future<void> onLoad() async {
await super.onLoad();
await add(
TextComponent(
position: Vector2(-1, -1),
text: _letter,
textRenderer: TextPaint(
style: const TextStyle(fontSize: 2, color: Colors.white),
),
),
);
}
@override
Body createBody() {
final shape = CircleShape()..radius = areaSize.x / 2;
final fixtureDef = FixtureDef(shape)..isSensor = true;
final bodyDef = BodyDef()
..userData = this
..position = _position
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
bool listenWhen(GameState? previousState, GameState newState) {
final wasActive = previousState?.isLetterActivated(_index) ?? false;
final isActive = newState.isLetterActivated(_index);
return wasActive != isActive;
}
@override
void onNewState(GameState state) {
final isActive = state.isLetterActivated(_index);
add(
ColorEffect(
isActive ? _activeColor : _disableColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
);
}
/// Activates this [BonusLetter], if it's not already activated.
void activate() {
final isActive = state?.isLetterActivated(_index) ?? false;
if (!isActive) {
gameRef.read<GameBloc>().add(BonusLetterActivated(_index));
}
}
}
/// Triggers [BonusLetter.activate] method when a [BonusLetter] and a [Ball]
/// come in contact.
class BonusLetterBallContactCallback
extends ContactCallback<Ball, BonusLetter> {
@override
void begin(Ball ball, BonusLetter bonusLetter, Contact contact) {
bonusLetter.activate();
}
}

@ -1,7 +1,9 @@
export 'anchor.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'bonus_word.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'joint_anchor.dart';
export 'pathway.dart'; export 'pathway.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'score_points.dart'; export 'score_points.dart';

@ -1,25 +1,29 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart' show PositionComponent, SpriteComponent; import 'package:flame/components.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
/// {@template flipper_group} /// {@template flipper_group}
/// Loads a [Flipper.right] and a [Flipper.left]. /// Loads a [Flipper.right] and a [Flipper.left].
/// {@endtemplate} /// {@endtemplate}
class FlipperGroup extends PositionComponent { class FlipperGroup extends Component {
/// {@macro flipper_group} /// {@macro flipper_group}
FlipperGroup({ FlipperGroup({
required Vector2 position, required this.position,
required this.spacing, required this.spacing,
}) : super(position: position); });
/// The amount of space between the [Flipper.right] and [Flipper.left]. /// The amount of space between the [Flipper.right] and [Flipper.left].
final double spacing; final double spacing;
/// The position of this [FlipperGroup]
final Vector2 position;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final leftFlipper = Flipper.left( final leftFlipper = Flipper.left(
@ -45,15 +49,14 @@ class FlipperGroup extends PositionComponent {
/// ///
/// [Flipper] can be controlled by the player in an arc motion. /// [Flipper] can be controlled by the player in an arc motion.
/// {@endtemplate flipper} /// {@endtemplate flipper}
class Flipper extends PositionBodyComponent with KeyboardHandler { class Flipper extends BodyComponent with KeyboardHandler {
/// {@macro flipper} /// {@macro flipper}
Flipper._({ Flipper._({
required Vector2 position, required Vector2 position,
required this.side, required this.side,
required List<LogicalKeyboardKey> keys, required List<LogicalKeyboardKey> keys,
}) : _position = position, }) : _position = position,
_keys = keys, _keys = keys;
super(size: Vector2(width, height));
/// A left positioned [Flipper]. /// A left positioned [Flipper].
Flipper.left({ Flipper.left({
@ -124,14 +127,17 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
/// Loads the sprite that renders with the [Flipper]. /// Loads the sprite that renders with the [Flipper].
Future<void> _loadSprite() async { Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(spritePath); final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent( final spriteComponent = SpriteComponent(
sprite: sprite, sprite: sprite,
size: size, size: Vector2(width, height),
anchor: Anchor.center,
); );
if (side.isRight) { if (side.isRight) {
positionComponent!.flipHorizontally(); spriteComponent.flipHorizontally();
} }
await add(spriteComponent);
} }
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
@ -160,11 +166,11 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
final fixtures = <FixtureDef>[]; final fixtures = <FixtureDef>[];
final isLeft = side.isLeft; final isLeft = side.isLeft;
final bigCircleShape = CircleShape()..radius = size.y / 2; final bigCircleShape = CircleShape()..radius = height / 2;
bigCircleShape.position.setValues( bigCircleShape.position.setValues(
isLeft isLeft
? -(size.x / 2) + bigCircleShape.radius ? -(width / 2) + bigCircleShape.radius
: (size.x / 2) - bigCircleShape.radius, : (width / 2) - bigCircleShape.radius,
0, 0,
); );
final bigCircleFixtureDef = FixtureDef(bigCircleShape); final bigCircleFixtureDef = FixtureDef(bigCircleShape);
@ -173,8 +179,8 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
isLeft isLeft
? (size.x / 2) - smallCircleShape.radius ? (width / 2) - smallCircleShape.radius
: -(size.x / 2) + smallCircleShape.radius, : -(width / 2) + smallCircleShape.radius,
0, 0,
); );
final smallCircleFixtureDef = FixtureDef(smallCircleShape); final smallCircleFixtureDef = FixtureDef(smallCircleShape);
@ -205,6 +211,7 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
paint = Paint()..color = Colors.transparent;
await Future.wait([ await Future.wait([
_loadSprite(), _loadSprite(),
_anchorToJoint(), _anchorToJoint(),
@ -249,15 +256,15 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
/// ///
/// The end of a [Flipper] depends on its [Flipper.side]. /// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate} /// {@endtemplate}
class FlipperAnchor extends Anchor { class FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor} /// {@macro flipper_anchor}
FlipperAnchor({ FlipperAnchor({
required Flipper flipper, required Flipper flipper,
}) : super( }) : super(
position: Vector2( position: Vector2(
flipper.side.isLeft flipper.side.isLeft
? flipper.body.position.x - flipper.size.x / 2 ? flipper.body.position.x - Flipper.width / 2
: flipper.body.position.x + flipper.size.x / 2, : flipper.body.position.x + Flipper.width / 2,
flipper.body.position.y, flipper.body.position.y,
), ),
); );

@ -1,6 +1,6 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
/// {@template anchor} /// {@template joint_anchor}
/// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s /// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s
/// with this [BodyType.static]. /// with this [BodyType.static].
/// ///
@ -15,9 +15,9 @@ import 'package:flame_forge2d/flame_forge2d.dart';
/// ); /// );
/// ``` /// ```
/// {@endtemplate} /// {@endtemplate}
class Anchor extends BodyComponent { class JointAnchor extends BodyComponent {
/// {@macro anchor} /// {@macro joint_anchor}
Anchor({ JointAnchor({
required Vector2 position, required Vector2 position,
}) : _position = position; }) : _position = position;

@ -1,7 +1,7 @@
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart' show Anchor; import 'package:pinball/game/game.dart';
/// {@template plunger} /// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the /// [Plunger] serves as a spring, that shoots the ball on the right side of the
@ -74,12 +74,31 @@ class Plunger extends BodyComponent with KeyboardHandler {
return true; return true;
} }
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical
/// motion.
Future<void> _anchorToJoint() async {
final anchor = PlungerAnchor(plunger: this);
await add(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: this,
anchor: anchor,
);
world.createJoint(jointDef);
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
} }
/// {@template plunger_anchor} /// {@template plunger_anchor}
/// [Anchor] positioned below a [Plunger]. /// [JointAnchor] positioned below a [Plunger].
/// {@endtemplate} /// {@endtemplate}
class PlungerAnchor extends Anchor { class PlungerAnchor extends JointAnchor {
/// {@macro plunger_anchor} /// {@macro plunger_anchor}
PlungerAnchor({ PlungerAnchor({
required Plunger plunger, required Plunger plunger,
@ -92,11 +111,11 @@ class PlungerAnchor extends Anchor {
} }
/// {@template plunger_anchor_prismatic_joint_def} /// {@template plunger_anchor_prismatic_joint_def}
/// [PrismaticJointDef] between a [Plunger] and an [Anchor] with motion on /// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on
/// the vertical axis. /// the vertical axis.
/// ///
/// The [Plunger] is constrained vertically between its starting position and /// The [Plunger] is constrained vertically between its starting position and
/// the [Anchor]. The [Anchor] must be below the [Plunger]. /// the [JointAnchor]. The [JointAnchor] must be below the [Plunger].
/// {@endtemplate} /// {@endtemplate}
class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def} /// {@macro plunger_anchor_prismatic_joint_def}

@ -48,6 +48,25 @@ class PinballGame extends Forge2DGame
), ),
); );
unawaited(_addFlippers());
unawaited(_addBonusWord());
}
Future<void> _addBonusWord() async {
await add(
BonusWord(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y - 50,
),
),
),
);
}
Future<void> _addFlippers() async {
final flippersPosition = screenToWorld( final flippersPosition = screenToWorld(
Vector2( Vector2(
camera.viewport.effectiveSize.x / 2, camera.viewport.effectiveSize.x / 2,
@ -63,6 +82,7 @@ class PinballGame extends Forge2DGame
), ),
), ),
); );
unawaited(_addBaseboards());
} }
void spawnBall() { void spawnBall() {
@ -72,6 +92,7 @@ class PinballGame extends Forge2DGame
void _addContactCallbacks() { void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback()); addContactCallback(BallScorePointsCallback());
addContactCallback(BottomWallBallContactCallback()); addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback());
} }
Future<void> _addGameBoundaries() async { Future<void> _addGameBoundaries() async {
@ -80,7 +101,6 @@ class PinballGame extends Forge2DGame
} }
Future<void> _addPlunger() async { Future<void> _addPlunger() async {
late PlungerAnchor plungerAnchor;
final compressionDistance = camera.viewport.effectiveSize.y / 12; final compressionDistance = camera.viewport.effectiveSize.y / 12;
await add( await add(
@ -94,14 +114,31 @@ class PinballGame extends Forge2DGame
compressionDistance: compressionDistance, compressionDistance: compressionDistance,
), ),
); );
await add(plungerAnchor = PlungerAnchor(plunger: plunger)); }
world.createJoint( Future<void> _addBaseboards() async {
PlungerAnchorPrismaticJointDef( final spaceBetweenBaseboards = camera.viewport.effectiveSize.x / 2;
plunger: plunger, final baseboardY = camera.viewport.effectiveSize.y / 1.12;
anchor: plungerAnchor,
final leftBaseboard = Baseboard.left(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2 - (spaceBetweenBaseboards / 2),
baseboardY,
),
),
);
await add(leftBaseboard);
final rightBaseboard = Baseboard.right(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2 + (spaceBetweenBaseboards / 2),
baseboardY,
),
), ),
); );
await add(rightBaseboard);
} }
} }

@ -63,6 +63,8 @@ class _PinballGameViewState extends State<PinballGameView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocListener<GameBloc, GameState>( return BlocListener<GameBloc, GameState>(
listenWhen: (previous, current) =>
previous.isGameOver != current.isGameOver,
listener: (context, state) { listener: (context, state) {
if (state.isGameOver) { if (state.isGameOver) {
showDialog<void>( showDialog<void>(

@ -4,6 +4,18 @@
"@play": { "@play": {
"description": "Text displayed on the landing page play button" "description": "Text displayed on the landing page play button"
}, },
"howToPlay": "How to Play",
"@howToPlay": {
"description": "Text displayed on the landing page how to play button"
},
"launchControls": "Launch Controls",
"@launchControls": {
"description": "Text displayed on the how to play dialog with the launch controls"
},
"flipperControls": "Flipper Controls",
"@flipperControls": {
"description": "Text displayed on the how to play dialog with the flipper controls"
},
"start": "Start", "start": "Start",
"@start": { "@start": {
"description": "Text displayed on the character selection page start button" "description": "Text displayed on the character selection page start button"

@ -13,12 +13,182 @@ class LandingPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: Center( body: Center(
child: TextButton( child: Column(
onPressed: () => mainAxisAlignment: MainAxisAlignment.center,
Navigator.of(context).push<void>(CharacterSelectionPage.route()), children: [
child: Text(l10n.play), TextButton(
onPressed: () => Navigator.of(context).push<void>(
CharacterSelectionPage.route(),
),
child: Text(l10n.play),
),
TextButton(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => const _HowToPlayDialog(),
),
child: Text(l10n.howToPlay),
),
],
), ),
), ),
); );
} }
} }
class _HowToPlayDialog extends StatelessWidget {
const _HowToPlayDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
const spacing = SizedBox(height: 16);
return Dialog(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.howToPlay),
spacing,
const _LaunchControls(),
spacing,
const _FlipperControls(),
],
),
),
);
}
}
class _LaunchControls extends StatelessWidget {
const _LaunchControls({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
const spacing = SizedBox(width: 10);
return Column(
children: [
Text(l10n.launchControls),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down),
spacing,
KeyIndicator.fromKeyName(keyName: 'SPACE'),
spacing,
KeyIndicator.fromKeyName(keyName: 'S'),
],
)
],
);
}
}
class _FlipperControls extends StatelessWidget {
const _FlipperControls({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
const rowSpacing = SizedBox(width: 20);
return Column(
children: [
Text(l10n.flipperControls),
const SizedBox(height: 10),
Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left),
rowSpacing,
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right),
],
),
const SizedBox(height: 8),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
KeyIndicator.fromKeyName(keyName: 'A'),
rowSpacing,
KeyIndicator.fromKeyName(keyName: 'D'),
],
)
],
)
],
);
}
}
// TODO(allisonryan0002): remove visibility when adding final UI.
@visibleForTesting
class KeyIndicator extends StatelessWidget {
const KeyIndicator._({
Key? key,
required String keyName,
required IconData keyIcon,
required bool fromIcon,
}) : _keyName = keyName,
_keyIcon = keyIcon,
_fromIcon = fromIcon,
super(key: key);
const KeyIndicator.fromKeyName({Key? key, required String keyName})
: this._(
key: key,
keyName: keyName,
keyIcon: Icons.keyboard_arrow_down,
fromIcon: false,
);
const KeyIndicator.fromIcon({Key? key, required IconData keyIcon})
: this._(
key: key,
keyName: '',
keyIcon: keyIcon,
fromIcon: true,
);
final String _keyName;
final IconData _keyIcon;
final bool _fromIcon;
@override
Widget build(BuildContext context) {
const iconPadding = EdgeInsets.all(15);
const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22);
final boarderColor = Colors.blue.withOpacity(0.5);
final color = Colors.blue.withOpacity(0.7);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: boarderColor,
width: 3,
),
),
child: _fromIcon
? Padding(
padding: iconPadding,
child: Icon(_keyIcon, color: color),
)
: Padding(
padding: textPadding,
child: Text(_keyName, style: TextStyle(color: color)),
),
);
}
}

@ -3,7 +3,7 @@
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] [![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link] [![License: MIT][license_badge]][license_link]
Helper package to calculate points of lines, arcs and curves for the pathways of the ball. Provides a set of helpers for working with 2D geometry.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT [license_link]: https://opensource.org/licenses/MIT

@ -106,3 +106,12 @@ num factorial(num n) {
return n * factorial(n - 1); return n * factorial(n - 1);
} }
} }
/// Arithmetic mean position of all the [Vector2]s in a polygon.
///
/// For more information read: https://en.wikipedia.org/wiki/Centroid
Vector2 centroid(List<Vector2> vertices) {
assert(vertices.isNotEmpty, 'Vertices must not be empty');
final sum = vertices.reduce((a, b) => a + b);
return sum / vertices.length.toDouble();
}

@ -1,5 +1,5 @@
name: geometry name: geometry
description: Helper package to calculate points of lines, arcs and curves for the pathways of the ball description: Provides a set of helpers for working with 2D geometry.
version: 1.0.0+1 version: 1.0.0+1
publish_to: none publish_to: none

@ -166,4 +166,29 @@ void main() {
}); });
}); });
}); });
group('centroid', () {
test('throws AssertionError when vertices are empty', () {
expect(() => centroid([]), throwsA(isA<AssertionError>()));
});
test('is correct when one vertex is given', () {
expect(centroid([Vector2.zero()]), Vector2.zero());
});
test('is correct when two vertex are given', () {
expect(centroid([Vector2.zero(), Vector2(1, 1)]), Vector2(0.5, 0.5));
});
test('is correct when three vertex are given', () {
expect(
centroid([
Vector2.zero(),
Vector2(1, 1),
Vector2(2, 2),
]),
Vector2(1, 1),
);
});
});
} }

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "30.0.0" version: "31.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.7.0" version: "2.8.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -35,14 +35,14 @@ packages:
name: bloc name: bloc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.0.2" version: "8.0.3"
bloc_test: bloc_test:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: bloc_test name: bloc_test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.0.2" version: "9.0.3"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -140,21 +140,21 @@ packages:
name: flame name: flame
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-releasecandidate.2" version: "1.1.0-releasecandidate.5"
flame_bloc: flame_bloc:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_bloc name: flame_bloc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-releasecandidate.2" version: "1.2.0-releasecandidate.5"
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_forge2d name: flame_forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0-releasecandidate.2" version: "0.9.0-releasecandidate.5"
flame_test: flame_test:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -190,7 +190,7 @@ packages:
name: forge2d name: forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.1" version: "0.8.2"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -218,7 +218,7 @@ packages:
name: http_multi_server name: http_multi_server
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.2.0"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -246,7 +246,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.4"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -351,14 +351,14 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.1" version: "6.0.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
name: pub_semver name: pub_semver
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -489,7 +489,7 @@ packages:
name: vm_service name: vm_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.3.0" version: "7.5.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

@ -9,9 +9,9 @@ environment:
dependencies: dependencies:
bloc: ^8.0.2 bloc: ^8.0.2
equatable: ^2.0.3 equatable: ^2.0.3
flame: ^1.1.0-releasecandidate.2 flame: ^1.1.0-releasecandidate.5
flame_bloc: ^1.2.0-releasecandidate.2 flame_bloc: ^1.2.0-releasecandidate.5
flame_forge2d: ^0.9.0-releasecandidate.2 flame_forge2d: ^0.9.0-releasecandidate.5
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1

@ -177,6 +177,12 @@ void main() {
activatedBonusLetters: [], activatedBonusLetters: [],
bonusHistory: [GameBonus.word], bonusHistory: [GameBonus.word],
), ),
GameState(
score: GameBloc.bonusWordScore,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [GameBonus.word],
),
], ],
); );
}); });

@ -126,6 +126,34 @@ void main() {
); );
}); });
group('isLetterActivated', () {
test(
'is true when the letter is activated',
() {
const gameState = GameState(
balls: 3,
score: 0,
activatedBonusLetters: [1],
bonusHistory: [],
);
expect(gameState.isLetterActivated(1), isTrue);
},
);
test(
'is false when the letter is not activated',
() {
const gameState = GameState(
balls: 3,
score: 0,
activatedBonusLetters: [1],
bonusHistory: [],
);
expect(gameState.isLetterActivated(0), isFalse);
},
);
});
group('copyWith', () { group('copyWith', () {
test( test(
'throws AssertionError ' 'throws AssertionError '

@ -11,10 +11,9 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('Ball', () { group('Ball', () {
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
@ -90,9 +89,10 @@ void main() {
}); });
group('resetting a ball', () { group('resetting a ball', () {
final gameBloc = MockGameBloc(); late GameBloc gameBloc;
setUp(() { setUp(() {
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -100,7 +100,7 @@ void main() {
); );
}); });
final tester = flameBlocTester(gameBloc: gameBloc); final tester = flameBlocTester(gameBloc: () => gameBloc);
tester.widgetTest( tester.widgetTest(
'adds BallLost to GameBloc', 'adds BallLost to GameBloc',
@ -119,7 +119,7 @@ void main() {
(game, tester) async { (game, tester) async {
await game.ready(); await game.ready();
game.children.whereType<Ball>().first.removeFromParent(); game.children.whereType<Ball>().first.lost();
await game.ready(); // Making sure that all additions are done await game.ready(); // Making sure that all additions are done
expect( expect(

@ -0,0 +1,74 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group('Baseboard', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final leftBaseboard = Baseboard.left(position: Vector2.zero());
final rightBaseboard = Baseboard.right(position: Vector2.zero());
await game.ensureAddAll([leftBaseboard, rightBaseboard]);
expect(game.contains(leftBaseboard), isTrue);
expect(game.contains(rightBaseboard), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final baseboard = Baseboard.left(position: position);
await game.ensureAdd(baseboard);
game.contains(baseboard);
expect(baseboard.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
final baseboard = Baseboard.left(position: Vector2.zero());
await game.ensureAdd(baseboard);
expect(baseboard.body.bodyType, equals(BodyType.static));
},
);
flameTester.test(
'is at an angle',
(game) async {
final leftBaseboard = Baseboard.left(position: Vector2.zero());
final rightBaseboard = Baseboard.right(position: Vector2.zero());
await game.ensureAddAll([leftBaseboard, rightBaseboard]);
expect(leftBaseboard.body.angle, isNegative);
expect(rightBaseboard.body.angle, isPositive);
},
);
});
group('fixtures', () {
flameTester.test(
'has three',
(game) async {
final baseboard = Baseboard.left(position: Vector2.zero());
await game.ensureAdd(baseboard);
expect(baseboard.body.fixtures.length, equals(3));
},
);
});
});
}

@ -0,0 +1,332 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('BonusWord', () {
flameTester.test(
'loads the letters correctly',
(game) async {
await game.ready();
final bonusWord = game.children.whereType<BonusWord>().first;
final letters = bonusWord.children.whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length));
},
);
group('listenWhen', () {
final previousState = MockGameState();
final currentState = MockGameState();
test(
'returns true when there is a new word bonus awarded',
() {
when(() => previousState.bonusHistory).thenReturn([]);
when(() => currentState.bonusHistory).thenReturn([GameBonus.word]);
expect(
BonusWord(position: Vector2.zero()).listenWhen(
previousState,
currentState,
),
isTrue,
);
},
);
test(
'returns false when there is no new word bonus awarded',
() {
when(() => previousState.bonusHistory).thenReturn([GameBonus.word]);
when(() => currentState.bonusHistory).thenReturn([GameBonus.word]);
expect(
BonusWord(position: Vector2.zero()).listenWhen(
previousState,
currentState,
),
isFalse,
);
},
);
});
group('onNewState', () {
final state = MockGameState();
flameTester.test(
'adds sequence effect to the letters when the player receives a bonus',
(game) async {
when(() => state.bonusHistory).thenReturn([GameBonus.word]);
final bonusWord = BonusWord(position: Vector2.zero());
await game.ensureAdd(bonusWord);
await game.ready();
bonusWord.onNewState(state);
game.update(0); // Run one frame so the effects are added
final letters = bonusWord.children.whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length));
for (final letter in letters) {
expect(
letter.children.whereType<SequenceEffect>().length,
equals(1),
);
}
},
);
flameTester.test(
'adds a color effect to reset the color when the sequence is finished',
(game) async {
when(() => state.bonusHistory).thenReturn([GameBonus.word]);
final bonusWord = BonusWord(position: Vector2.zero());
await game.ensureAdd(bonusWord);
await game.ready();
bonusWord.onNewState(state);
// Run the amount of time necessary for the animation to finish
game.update(3);
game.update(0); // Run one additional frame so the effects are added
final letters = bonusWord.children.whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length));
for (final letter in letters) {
expect(
letter.children.whereType<ColorEffect>().length,
equals(1),
);
}
},
);
});
});
group('BonusLetter', () {
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
(game) async {
final bonusLetter = BonusLetter(
position: Vector2.zero(),
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
await game.ready();
expect(game.contains(bonusLetter), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final bonusLetter = BonusLetter(
position: position,
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
game.contains(bonusLetter);
expect(bonusLetter.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
final bonusLetter = BonusLetter(
position: Vector2.zero(),
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
expect(bonusLetter.body.bodyType, equals(BodyType.static));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final bonusLetter = BonusLetter(
position: Vector2.zero(),
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
expect(bonusLetter.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'is sensor',
(game) async {
final bonusLetter = BonusLetter(
position: Vector2.zero(),
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
final fixture = bonusLetter.body.fixtures[0];
expect(fixture.isSensor, isTrue);
},
);
flameTester.test(
'shape is circular',
(game) async {
final bonusLetter = BonusLetter(
position: Vector2.zero(),
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
final fixture = bonusLetter.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(2));
},
);
});
group('bonus letter activation', () {
final gameBloc = MockGameBloc();
BonusLetter _getBonusLetter(PinballGame game) {
return game.children
.whereType<BonusWord>()
.first
.children
.whereType<BonusLetter>()
.first;
}
setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final tester = flameBlocTester(gameBloc: () => gameBloc);
tester.widgetTest(
'adds BonusLetterActivated to GameBloc when not activated',
(game, tester) async {
await game.ready();
_getBonusLetter(game).activate();
await tester.pump();
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);
},
);
tester.widgetTest(
"doesn't add BonusLetterActivated to GameBloc when already activated",
(game, tester) async {
const state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
bonusHistory: [],
);
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
await game.ready();
_getBonusLetter(game).activate();
await game.ready(); // Making sure that all additions are done
verifyNever(() => gameBloc.add(const BonusLetterActivated(0)));
},
);
tester.widgetTest(
'adds a ColorEffect',
(game, tester) async {
await game.ready();
const state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
bonusHistory: [],
);
final bonusLetter = _getBonusLetter(game);
bonusLetter.onNewState(state);
await tester.pump();
expect(
bonusLetter.children.whereType<ColorEffect>().length,
equals(1),
);
},
);
tester.widgetTest(
'only listens when there is a change on the letter status',
(game, tester) async {
await game.ready();
const state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
bonusHistory: [],
);
final bonusLetter = _getBonusLetter(game);
expect(
bonusLetter.listenWhen(const GameState.initial(), state),
isTrue,
);
},
);
});
group('BonusLetterBallContactCallback', () {
test('calls ball.activate', () {
final ball = MockBall();
final bonusLetter = MockBonusLetter();
final contactCallback = BonusLetterBallContactCallback();
contactCallback.begin(ball, bonusLetter, MockContact());
verify(bonusLetter.activate).called(1);
});
});
});
}

@ -103,9 +103,7 @@ void main() {
) as Flipper; ) as Flipper;
expect( expect(
leftFlipper.body.position.x + leftFlipper.body.position.x + Flipper.width + flipperGroup.spacing,
leftFlipper.size.x +
flipperGroup.spacing,
equals(rightFlipper.body.position.x), equals(rightFlipper.body.position.x),
); );
}, },
@ -178,6 +176,7 @@ void main() {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.left(position: Vector2.zero());
final ball = Ball(position: Vector2.zero()); final ball = Ball(position: Vector2.zero());
await game.ready();
await game.ensureAddAll([flipper, ball]); await game.ensureAddAll([flipper, ball]);
expect( expect(

@ -5,18 +5,15 @@ 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'; import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('Anchor', () { group('JointAnchor', () {
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final anchor = Anchor(position: Vector2.zero()); final anchor = JointAnchor(position: Vector2.zero());
await game.ready(); await game.ready();
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
@ -30,7 +27,7 @@ void main() {
(game) async { (game) async {
await game.ready(); await game.ready();
final position = Vector2.all(10); final position = Vector2.all(10);
final anchor = Anchor(position: position); final anchor = JointAnchor(position: position);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
game.contains(anchor); game.contains(anchor);
@ -42,7 +39,7 @@ void main() {
'is static', 'is static',
(game) async { (game) async {
await game.ready(); await game.ready();
final anchor = Anchor(position: Vector2.zero()); final anchor = JointAnchor(position: Vector2.zero());
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
expect(anchor.body.bodyType, equals(BodyType.static)); expect(anchor.body.bodyType, equals(BodyType.static));
@ -54,7 +51,7 @@ void main() {
flameTester.test( flameTester.test(
'has none', 'has none',
(game) async { (game) async {
final anchor = Anchor(position: Vector2.zero()); final anchor = JointAnchor(position: Vector2.zero());
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
expect(anchor.body.fixtures, isEmpty); expect(anchor.body.fixtures, isEmpty);

@ -6,11 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(Forge2DGame.new);
group('Pathway', () { group('Pathway', () {
const width = 50.0; const width = 50.0;

@ -13,7 +13,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(Forge2DGame.new);
group('Plunger', () { group('Plunger', () {
const compressionDistance = 0.0; const compressionDistance = 0.0;
@ -227,7 +227,7 @@ void main() {
); );
}); });
final flameTester = flameBlocTester(gameBloc: gameBloc); final flameTester = flameBlocTester(gameBloc: () => gameBloc);
group('initializes with', () { group('initializes with', () {
flameTester.test( flameTester.test(

@ -10,6 +10,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('Wall', () { group('Wall', () {
group('BottomWallBallContactCallback', () { group('BottomWallBallContactCallback', () {
@ -32,7 +33,6 @@ void main() {
}, },
); );
}); });
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',

@ -18,18 +18,13 @@ void main() {
// [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('components', () {
bool Function(Component) componentSelector<T>() =>
(component) => component is T;
flameTester.test( flameTester.test(
'has three Walls', 'has three Walls',
(game) async { (game) async {
await game.ready(); await game.ready();
final walls = game.children final walls = game.children.where(
.where( (component) => component is Wall && component is! BottomWall,
(component) => component is Wall && component is! BottomWall, );
)
.toList();
// TODO(allisonryan0002): expect 3 when launch track is added and // TODO(allisonryan0002): expect 3 when launch track is added and
// temporary wall is removed. // temporary wall is removed.
expect(walls.length, 4); expect(walls.length, 4);
@ -42,10 +37,8 @@ void main() {
await game.ready(); await game.ready();
expect( expect(
() => game.children.singleWhere( game.children.whereType<BottomWall>().length,
componentSelector<BottomWall>(), equals(1),
),
returnsNormally,
); );
}, },
); );
@ -54,26 +47,29 @@ void main() {
'has only one Plunger', 'has only one Plunger',
(game) async { (game) async {
await game.ready(); await game.ready();
expect( expect(
() => game.children.singleWhere( game.children.whereType<Plunger>().length,
(component) => component is Plunger, equals(1),
),
returnsNormally,
); );
}, },
); );
flameTester.test('has only one FlipperGroup', (game) async { flameTester.test('has only one FlipperGroup', (game) async {
await game.ready(); await game.ready();
expect( expect(
() => game.children.singleWhere( game.children.whereType<FlipperGroup>().length,
(component) => component is FlipperGroup, equals(1),
),
returnsNormally,
); );
}); });
flameTester.test(
'has two Baseboards',
(game) async {
await game.ready();
final baseboards = game.children.whereType<Baseboard>();
expect(baseboards.length, 2);
},
);
}); });
debugModeFlameTester.test('adds a ball on tap up', (game) async { debugModeFlameTester.test('adds a ball on tap up', (game) async {

@ -94,7 +94,7 @@ void main() {
whenListen( whenListen(
gameBloc, gameBloc,
Stream.value(state), Stream.value(state),
initialState: state, initialState: GameState.initial(),
); );
await tester.pumpApp( await tester.pumpApp(

@ -5,14 +5,14 @@ import 'package:pinball/game/game.dart';
import 'helpers.dart'; import 'helpers.dart';
FlameTester<PinballGame> flameBlocTester({ FlameTester<PinballGame> flameBlocTester({
required GameBloc gameBloc, required GameBloc Function() gameBloc,
}) { }) {
return FlameTester<PinballGame>( return FlameTester<PinballGame>(
PinballGameTest.create, PinballGameTest.create,
pumpWidget: (gameWidget, tester) async { pumpWidget: (gameWidget, tester) async {
await tester.pumpWidget( await tester.pumpWidget(
BlocProvider.value( BlocProvider.value(
value: gameBloc, value: gameBloc(),
child: gameWidget, child: gameWidget,
), ),
); );

@ -18,6 +18,8 @@ class MockContact extends Mock implements Contact {}
class MockGameBloc extends Mock implements GameBloc {} class MockGameBloc extends Mock implements GameBloc {}
class MockGameState extends Mock implements GameState {}
class MockThemeCubit extends Mock implements ThemeCubit {} class MockThemeCubit extends Mock implements ThemeCubit {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@ -37,3 +39,5 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
class MockTapUpInfo extends Mock implements TapUpInfo {} class MockTapUpInfo extends Mock implements TapUpInfo {}
class MockEventPosition extends Mock implements EventPosition {} class MockEventPosition extends Mock implements EventPosition {}
class MockBonusLetter extends Mock implements BonusLetter {}

@ -1,33 +1,71 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart'; import 'package:pinball/landing/landing.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
group('LandingPage', () { group('LandingPage', () {
testWidgets('renders TextButton', (tester) async { testWidgets('renders correctly', (tester) async {
await tester.pumpApp(const LandingPage()); final l10n = await AppLocalizations.delegate.load(Locale('en'));
expect(find.byType(TextButton), findsOneWidget); await tester.pumpApp(LandingPage());
expect(find.byType(TextButton), findsNWidgets(2));
expect(find.text(l10n.play), findsOneWidget);
expect(find.text(l10n.howToPlay), findsOneWidget);
}); });
testWidgets('tapping on TextButton navigates to CharacterSelectionPage', testWidgets('tapping on play button navigates to CharacterSelectionPage',
(tester) async { (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
final navigator = MockNavigator(); final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {}); when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp( await tester.pumpApp(
const LandingPage(), LandingPage(),
navigator: navigator, navigator: navigator,
); );
await tester.tap(
find.byType( await tester.tap(find.widgetWithText(TextButton, l10n.play));
TextButton,
),
);
verify(() => navigator.push<void>(any())).called(1); verify(() => navigator.push<void>(any())).called(1);
}); });
testWidgets('tapping on how to play button displays dialog with controls',
(tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(LandingPage());
await tester.tap(find.widgetWithText(TextButton, l10n.howToPlay));
await tester.pump();
expect(find.byType(Dialog), findsOneWidget);
});
});
group('KeyIndicator', () {
testWidgets('fromKeyName renders correctly', (tester) async {
const keyName = 'A';
await tester.pumpApp(
KeyIndicator.fromKeyName(keyName: keyName),
);
expect(find.text(keyName), findsOneWidget);
});
testWidgets('fromIcon renders correctly', (tester) async {
const keyIcon = Icons.keyboard_arrow_down;
await tester.pumpApp(
KeyIndicator.fromIcon(keyIcon: keyIcon),
);
expect(find.byIcon(keyIcon), findsOneWidget);
});
}); });
} }

Loading…
Cancel
Save