mirror of https://github.com/flutter/pinball.git
commit
325f02751b
@ -0,0 +1,3 @@
|
||||
# Every request must be reviewed and accepted by:
|
||||
|
||||
* @erickzanardo @alestiago @RuiMiguel @allisonryan0002
|
@ -0,0 +1,18 @@
|
||||
name: geometry
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "packages/geometry/**"
|
||||
- ".github/workflows/geometry.yaml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/geometry/**"
|
||||
- ".github/workflows/geometry.yaml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
|
||||
with:
|
||||
working_directory: packages/geometry
|
After Width: | Height: | Size: 8.1 KiB |
@ -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,134 @@
|
||||
// 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 {
|
||||
/// {@macro bonus_word}
|
||||
BonusWord({required Vector2 position}) : _position = position;
|
||||
|
||||
final Vector2 _position;
|
||||
|
||||
@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,3 +1,2 @@
|
||||
export 'game_hud.dart';
|
||||
export 'pinball_game_page.dart';
|
||||
export 'widgets/widgets.dart';
|
||||
|
@ -1 +1,2 @@
|
||||
export 'game_hud.dart';
|
||||
export 'game_over_dialog.dart';
|
||||
|
@ -1,4 +1,15 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"play": "Play"
|
||||
"play": "Play",
|
||||
"@play": {
|
||||
"description": "Text displayed on the landing page play button"
|
||||
},
|
||||
"start": "Start",
|
||||
"@start": {
|
||||
"description": "Text displayed on the character selection page start button"
|
||||
},
|
||||
"characterSelectionTitle": "Choose your character!",
|
||||
"@characterSelectionTitle": {
|
||||
"description": "Title text displayed on the character selection page"
|
||||
}
|
||||
}
|
@ -1,4 +1,15 @@
|
||||
{
|
||||
"@@locale": "es",
|
||||
"play": "Jugar"
|
||||
"play": "Jugar",
|
||||
"@play": {
|
||||
"description": "Text displayed on the landing page play button"
|
||||
},
|
||||
"start": "Comienzo",
|
||||
"@start": {
|
||||
"description": "Text displayed on the character selection page start button"
|
||||
},
|
||||
"characterSelectionTitle": "¡Elige a tu personaje!",
|
||||
"@characterSelectionTitle": {
|
||||
"description": "Title text displayed on the character selection page"
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export 'cubit/theme_cubit.dart';
|
||||
export 'view/view.dart';
|
||||
|
@ -0,0 +1,130 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball/theme/theme.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
class CharacterSelectionPage extends StatelessWidget {
|
||||
const CharacterSelectionPage({Key? key}) : super(key: key);
|
||||
|
||||
static Route route() {
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => const CharacterSelectionPage(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => ThemeCubit(),
|
||||
child: const CharacterSelectionView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CharacterSelectionView extends StatelessWidget {
|
||||
const CharacterSelectionView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
Text(
|
||||
l10n.characterSelectionTitle,
|
||||
style: Theme.of(context).textTheme.headline3,
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
const _CharacterSelectionGridView(),
|
||||
const SizedBox(height: 20),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).push<void>(
|
||||
PinballGamePage.route(
|
||||
theme: context.read<ThemeCubit>().state.theme,
|
||||
),
|
||||
),
|
||||
child: Text(l10n.start),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CharacterSelectionGridView extends StatelessWidget {
|
||||
const _CharacterSelectionGridView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 20,
|
||||
crossAxisSpacing: 20,
|
||||
children: const [
|
||||
CharacterImageButton(
|
||||
DashTheme(),
|
||||
key: Key('characterSelectionPage_dashButton'),
|
||||
),
|
||||
CharacterImageButton(
|
||||
SparkyTheme(),
|
||||
key: Key('characterSelectionPage_sparkyButton'),
|
||||
),
|
||||
CharacterImageButton(
|
||||
AndroidTheme(),
|
||||
key: Key('characterSelectionPage_androidButton'),
|
||||
),
|
||||
CharacterImageButton(
|
||||
DinoTheme(),
|
||||
key: Key('characterSelectionPage_dinoButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(allisonryan0002): remove visibility when adding final UI.
|
||||
@visibleForTesting
|
||||
class CharacterImageButton extends StatelessWidget {
|
||||
const CharacterImageButton(
|
||||
this.characterTheme, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme characterTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentCharacterTheme = context.select<ThemeCubit, CharacterTheme>(
|
||||
(cubit) => cubit.state.theme.characterTheme,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.read<ThemeCubit>().characterSelected(characterTheme),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: (currentCharacterTheme == characterTheme)
|
||||
? Colors.blue.withOpacity(0.5)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: characterTheme.characterAsset.image(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'character_selection_page.dart';
|
@ -1 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/**/*.gen.dart
|
After Width: | Height: | Size: 274 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 166 KiB |
After Width: | Height: | Size: 223 KiB |
@ -1,4 +1,5 @@
|
||||
library pinball_theme;
|
||||
|
||||
export 'src/generated/generated.dart';
|
||||
export 'src/pinball_theme.dart';
|
||||
export 'src/themes/themes.dart';
|
||||
|
@ -0,0 +1,71 @@
|
||||
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
/// *****************************************************
|
||||
/// FlutterGen
|
||||
/// *****************************************************
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class $AssetsImagesGen {
|
||||
const $AssetsImagesGen();
|
||||
|
||||
AssetGenImage get android => const AssetGenImage('assets/images/android.png');
|
||||
AssetGenImage get dash => const AssetGenImage('assets/images/dash.png');
|
||||
AssetGenImage get dino => const AssetGenImage('assets/images/dino.png');
|
||||
AssetGenImage get sparky => const AssetGenImage('assets/images/sparky.png');
|
||||
}
|
||||
|
||||
class Assets {
|
||||
Assets._();
|
||||
|
||||
static const $AssetsImagesGen images = $AssetsImagesGen();
|
||||
}
|
||||
|
||||
class AssetGenImage extends AssetImage {
|
||||
const AssetGenImage(String assetName)
|
||||
: super(assetName, package: 'pinball_theme');
|
||||
|
||||
Image image({
|
||||
Key? key,
|
||||
ImageFrameBuilder? frameBuilder,
|
||||
ImageLoadingBuilder? loadingBuilder,
|
||||
ImageErrorWidgetBuilder? errorBuilder,
|
||||
String? semanticLabel,
|
||||
bool excludeFromSemantics = false,
|
||||
double? width,
|
||||
double? height,
|
||||
Color? color,
|
||||
BlendMode? colorBlendMode,
|
||||
BoxFit? fit,
|
||||
AlignmentGeometry alignment = Alignment.center,
|
||||
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||
Rect? centerSlice,
|
||||
bool matchTextDirection = false,
|
||||
bool gaplessPlayback = false,
|
||||
bool isAntiAlias = false,
|
||||
FilterQuality filterQuality = FilterQuality.low,
|
||||
}) {
|
||||
return Image(
|
||||
key: key,
|
||||
image: this,
|
||||
frameBuilder: frameBuilder,
|
||||
loadingBuilder: loadingBuilder,
|
||||
errorBuilder: errorBuilder,
|
||||
semanticLabel: semanticLabel,
|
||||
excludeFromSemantics: excludeFromSemantics,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
colorBlendMode: colorBlendMode,
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
repeat: repeat,
|
||||
centerSlice: centerSlice,
|
||||
matchTextDirection: matchTextDirection,
|
||||
gaplessPlayback: gaplessPlayback,
|
||||
isAntiAlias: isAntiAlias,
|
||||
filterQuality: filterQuality,
|
||||
);
|
||||
}
|
||||
|
||||
String get path => assetName;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'assets.gen.dart';
|
@ -0,0 +1,76 @@
|
||||
// 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';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('Baseboard', () {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final flameTester = FlameTester(PinballGameTest.create);
|
||||
|
||||
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,244 @@
|
||||
// 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();
|
||||
|
||||
group('BonusWord', () {
|
||||
final flameTester = FlameTester(PinballGameTest.create);
|
||||
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
/// [PinballGame] extension to reduce boilerplate in tests.
|
||||
extension PinballGameTest on PinballGame {
|
||||
/// Create [PinballGame] with default [PinballTheme].
|
||||
static PinballGame create() => PinballGame(
|
||||
theme: const PinballTheme(
|
||||
characterTheme: DashTheme(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// [DebugPinballGame] extension to reduce boilerplate in tests.
|
||||
extension DebugPinballGameTest on DebugPinballGame {
|
||||
/// Create [PinballGame] with default [PinballTheme].
|
||||
static DebugPinballGame create() => DebugPinballGame(
|
||||
theme: const PinballTheme(
|
||||
characterTheme: DashTheme(),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
// Copyright (c) 2021, Very Good Ventures
|
||||
// https://verygood.ventures
|
||||
//
|
||||
// Copyright (c) 2021, Very Good Ventures
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
// https://verygood.ventures
|
||||
// license that can be found in the LICENSE file or at
|
||||
export 'builders.dart';
|
||||
export 'extensions.dart';
|
||||
export 'key_testers.dart';
|
||||
export 'mocks.dart';
|
||||
export 'pump_app.dart';
|
||||
|
@ -0,0 +1,110 @@
|
||||
// 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:mockingjay/mockingjay.dart';
|
||||
import 'package:pinball/theme/theme.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
late ThemeCubit themeCubit;
|
||||
|
||||
setUp(() {
|
||||
themeCubit = MockThemeCubit();
|
||||
whenListen(
|
||||
themeCubit,
|
||||
const Stream<ThemeState>.empty(),
|
||||
initialState: const ThemeState.initial(),
|
||||
);
|
||||
});
|
||||
|
||||
group('CharacterSelectionPage', () {
|
||||
testWidgets('renders CharacterSelectionView', (tester) async {
|
||||
await tester.pumpApp(
|
||||
CharacterSelectionPage(),
|
||||
themeCubit: themeCubit,
|
||||
);
|
||||
expect(find.byType(CharacterSelectionView), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('route returns a valid navigation route', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push<void>(CharacterSelectionPage.route());
|
||||
},
|
||||
child: Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
themeCubit: themeCubit,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CharacterSelectionPage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('CharacterSelectionView', () {
|
||||
testWidgets('renders correctly', (tester) async {
|
||||
const titleText = 'Choose your character!';
|
||||
await tester.pumpApp(
|
||||
CharacterSelectionView(),
|
||||
themeCubit: themeCubit,
|
||||
);
|
||||
|
||||
expect(find.text(titleText), findsOneWidget);
|
||||
expect(find.byType(CharacterImageButton), findsNWidgets(4));
|
||||
expect(find.byType(TextButton), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calls characterSelected when a character image is tapped',
|
||||
(tester) async {
|
||||
const sparkyButtonKey = Key('characterSelectionPage_sparkyButton');
|
||||
|
||||
await tester.pumpApp(
|
||||
CharacterSelectionView(),
|
||||
themeCubit: themeCubit,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(sparkyButtonKey));
|
||||
|
||||
verify(() => themeCubit.characterSelected(SparkyTheme())).called(1);
|
||||
});
|
||||
|
||||
testWidgets('navigates to PinballGamePage when start is tapped',
|
||||
(tester) async {
|
||||
final navigator = MockNavigator();
|
||||
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||
|
||||
await tester.pumpApp(
|
||||
CharacterSelectionView(),
|
||||
themeCubit: themeCubit,
|
||||
navigator: navigator,
|
||||
);
|
||||
await tester.ensureVisible(find.byType(TextButton));
|
||||
await tester.tap(find.byType(TextButton));
|
||||
|
||||
verify(() => navigator.push<void>(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('CharacterImageButton renders correctly', (tester) async {
|
||||
await tester.pumpApp(
|
||||
CharacterImageButton(DashTheme()),
|
||||
themeCubit: themeCubit,
|
||||
);
|
||||
|
||||
expect(find.byType(Image), findsOneWidget);
|
||||
});
|
||||
}
|
Loading…
Reference in new issue