Merge branch 'main' into feat/crossing_upper_ramps

pull/40/head
RuiAlonso 4 years ago
commit a527a1390a

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

@ -12,12 +12,59 @@ import 'package:pinball/game/game.dart';
/// {@template bonus_word}
/// Loads all [BonusLetter]s to compose a [BonusWord].
/// {@endtemplate}
class BonusWord extends Component {
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();

@ -74,6 +74,25 @@ class Plunger extends BodyComponent with KeyboardHandler {
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}

@ -150,7 +150,6 @@ class PinballGame extends Forge2DGame
}
Future<void> _addPlunger() async {
late PlungerAnchor plungerAnchor;
final compressionDistance = camera.viewport.effectiveSize.y / 12;
await add(
@ -165,14 +164,6 @@ class PinballGame extends Forge2DGame
compressionDistance: compressionDistance,
),
);
await add(plungerAnchor = PlungerAnchor(plunger: plunger));
world.createJoint(
PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: plungerAnchor,
),
);
}
Future<void> _addBaseboards() async {

@ -4,6 +4,18 @@
"@play": {
"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": {
"description": "Text displayed on the character selection page start button"

@ -13,12 +13,182 @@ class LandingPage extends StatelessWidget {
return Scaffold(
body: Center(
child: TextButton(
onPressed: () =>
Navigator.of(context).push<void>(CharacterSelectionPage.route()),
child: Text(l10n.play),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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]
[![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_link]: https://opensource.org/licenses/MIT

@ -106,3 +106,12 @@ num factorial(num n) {
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
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
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),
);
});
});
}

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

@ -11,10 +11,9 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('Ball', () {
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
(game) async {

@ -5,12 +5,10 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('Baseboard', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
final flameTester = FlameTester(Forge2DGame.new);
flameTester.test(
'loads correctly',

@ -12,10 +12,9 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('BonusWord', () {
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads the letters correctly',
(game) async {
@ -26,6 +25,95 @@ void main() {
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', () {

@ -176,6 +176,7 @@ void main() {
final flipper = Flipper.left(position: Vector2.zero());
final ball = Ball(position: Vector2.zero());
await game.ready();
await game.ensureAddAll([flipper, ball]);
expect(

@ -5,14 +5,11 @@ import 'package:flame_test/flame_test.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(Forge2DGame.new);
group('JointAnchor', () {
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
(game) async {

@ -6,11 +6,9 @@ import 'package:flutter/material.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(PinballGameTest.create);
final flameTester = FlameTester(Forge2DGame.new);
group('Pathway', () {
const width = 50.0;

@ -13,7 +13,7 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
final flameTester = FlameTester(Forge2DGame.new);
group('Plunger', () {
const compressionDistance = 0.0;

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

@ -18,18 +18,13 @@ void main() {
// [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416
group('components', () {
bool Function(Component) componentSelector<T>() =>
(component) => component is T;
flameTester.test(
'has three Walls',
(game) async {
await game.ready();
final walls = game.children
.where(
(component) => component is Wall && component is! BottomWall,
)
.toList();
final walls = game.children.where(
(component) => component is Wall && component is! BottomWall,
);
// TODO(allisonryan0002): expect 3 when launch track is added and
// temporary wall is removed.
expect(walls.length, 4);
@ -42,10 +37,8 @@ void main() {
await game.ready();
expect(
() => game.children.singleWhere(
componentSelector<BottomWall>(),
),
returnsNormally,
game.children.whereType<BottomWall>().length,
equals(1),
);
},
);
@ -54,24 +47,18 @@ void main() {
'has only one Plunger',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is Plunger,
),
returnsNormally,
game.children.whereType<Plunger>().length,
equals(1),
);
},
);
flameTester.test('has only one FlipperGroup', (game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is FlipperGroup,
),
returnsNormally,
game.children.whereType<FlipperGroup>().length,
equals(1),
);
});
@ -79,7 +66,7 @@ void main() {
'has two Baseboards',
(game) async {
await game.ready();
final baseboards = game.children.whereType<Baseboard>().toList();
final baseboards = game.children.whereType<Baseboard>();
expect(baseboards.length, 2);
},
);

@ -24,6 +24,8 @@ class MockRampAreaCallback extends Mock implements RampAreaCallback {}
class MockGameBloc extends Mock implements GameBloc {}
class MockGameState extends Mock implements GameState {}
class MockThemeCubit extends Mock implements ThemeCubit {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {

@ -1,33 +1,71 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart';
import '../../helpers/helpers.dart';
void main() {
group('LandingPage', () {
testWidgets('renders TextButton', (tester) async {
await tester.pumpApp(const LandingPage());
expect(find.byType(TextButton), findsOneWidget);
testWidgets('renders correctly', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
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 {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp(
const LandingPage(),
LandingPage(),
navigator: navigator,
);
await tester.tap(
find.byType(
TextButton,
),
);
await tester.tap(find.widgetWithText(TextButton, l10n.play));
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