Merge branch 'main' into refactor/priority-layer

pull/83/head
RuiAlonso 4 years ago
commit ea993bac07

@ -29,6 +29,12 @@
"type": "dart",
"program": "lib/main_production.dart",
"args": ["--flavor", "production", "--target", "lib/main_production.dart"]
},
{
"name": "Launch component sandbox",
"request": "launch",
"type": "dart",
"program": "packages/pinball_components/sandbox/lib/main.dart"
}
]
}

@ -73,18 +73,32 @@ class BonusWord extends Component with BlocComponent<GameBloc, GameState> {
@override
Future<void> onLoad() async {
await super.onLoad();
final letters = GameBloc.bonusWord.split('');
for (var i = 0; i < letters.length; i++) {
unawaited(
add(
BonusLetter(
letter: letters[i],
index: i,
)..initialPosition = _position - Vector2(16 - (i * 6), -30),
),
final offsets = [
Vector2(-12.92, -1.82),
Vector2(-8.33, 0.65),
Vector2(-2.88, 1.75),
];
offsets.addAll(
offsets.reversed
.map(
(offset) => Vector2(-offset.x, offset.y),
)
.toList(),
);
assert(offsets.length == GameBloc.bonusWord.length, 'Invalid positions');
final letters = <BonusLetter>[];
for (var i = 0; i < GameBloc.bonusWord.length; i++) {
letters.add(
BonusLetter(
letter: GameBloc.bonusWord[i],
index: i,
)..initialPosition = _position + offsets[i],
);
}
await addAll(letters);
}
}
@ -103,8 +117,8 @@ class BonusLetter extends BodyComponent<PinballGame>
paint = Paint()..color = _disableColor;
}
/// The area size of this [BonusLetter].
static final areaSize = Vector2.all(4);
/// The size of the [BonusLetter].
static final size = Vector2.all(3.7);
static const _activeColor = Colors.green;
static const _disableColor = Colors.red;
@ -136,7 +150,7 @@ class BonusLetter extends BodyComponent<PinballGame>
@override
Body createBody() {
final shape = CircleShape()..radius = areaSize.x / 2;
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..isSensor = true;

@ -27,33 +27,30 @@ class LauncherRamp extends Component with HasGameRef<PinballGame> {
RampOpeningBallContactCallback<_LauncherRampOpening>(),
);
final launcherRampRotation =
-math.atan(18.6 / PinballGame.boardBounds.height);
final straightPath = Pathway.straight(
color: const Color.fromARGB(255, 34, 255, 0),
start: position + Vector2(-1.2, 10),
end: position + Vector2(-1.2, 117),
width: 5,
rotation: launcherRampRotation,
start: position + Vector2(-4.5, -10),
end: position + Vector2(-4.5, 117),
width: 4,
rotation: PinballGame.boardPerspectiveAngle,
)
..initialPosition = position
..layer = layer;
final curvedPath = Pathway.arc(
color: const Color.fromARGB(255, 251, 255, 0),
center: position + Vector2(-2.8, 87.2),
center: position + Vector2(-7, 87.2),
radius: 16.3,
angle: math.pi / 2,
width: 5,
width: 4,
rotation: 3 * math.pi / 2,
)..layer = layer;
final leftOpening = _LauncherRampOpening(rotation: math.pi / 2)
..initialPosition = position + Vector2(-11.8, 66.3)
..initialPosition = position + Vector2(-13.8, 66.7)
..layer = Layer.opening;
final rightOpening = _LauncherRampOpening(rotation: 0)
..initialPosition = position + Vector2(-4.9, 59.4)
..initialPosition = position + Vector2(-6.8, 59.4)
..layer = Layer.opening;
await addAll([
@ -81,9 +78,9 @@ class _LauncherRampOpening extends RampOpening {
final double _rotation;
// TODO(ruialonso): Avoid magic number 3, should be propotional to
// TODO(ruialonso): Avoid magic number 2.5, should be propotional to
// [JetpackRamp].
static final Vector2 _size = Vector2(3, .1);
static final Vector2 _size = Vector2(2.5, .1);
@override
Shape get shape => PolygonShape()

@ -21,9 +21,15 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
@override
Body createBody() {
final shape = PolygonShape()..setAsBoxXY(2, 0.75);
final shape = PolygonShape()
..setAsBox(
1.35,
0.5,
Vector2.zero(),
PinballGame.boardPerspectiveAngle,
);
final fixtureDef = FixtureDef(shape)..density = 5;
final fixtureDef = FixtureDef(shape)..density = 20;
final bodyDef = BodyDef()
..position = initialPosition
@ -36,7 +42,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
/// Set a constant downward velocity on the [Plunger].
void _pull() {
body.linearVelocity = Vector2(0, -3);
body.linearVelocity = Vector2(0, -7);
}
/// Set an upward velocity on the [Plunger].
@ -44,7 +50,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition].
void _release() {
final velocity = (initialPosition.y - body.position.y) * 9;
final velocity = (initialPosition.y - body.position.y) * 4;
body.linearVelocity = Vector2(0, velocity);
}
@ -121,12 +127,12 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
plunger.body,
anchor.body,
anchor.body.position,
Vector2(0, -1),
Vector2(18.6, PinballGame.boardBounds.height),
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 50;
motorSpeed = 80;
maxMotorForce = motorSpeed;
collideConnected = true;
}

@ -1,5 +1,7 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
@ -24,6 +26,8 @@ class PinballGame extends Forge2DGame
width: boardSize.x,
height: -boardSize.y,
);
static final boardPerspectiveAngle =
-math.atan(18.6 / PinballGame.boardBounds.height);
@override
void onAttach() {
@ -60,13 +64,8 @@ class PinballGame extends Forge2DGame
}
Future<void> _addPlunger() async {
plunger = Plunger(compressionDistance: 2);
plunger.initialPosition = boardBounds.bottomRight.toVector2() +
Vector2(
-5,
10,
);
plunger = Plunger(compressionDistance: 29)
..initialPosition = boardBounds.center.toVector2() + Vector2(41.5, -49);
await add(plunger);
}
@ -74,8 +73,8 @@ class PinballGame extends Forge2DGame
await add(
BonusWord(
position: Vector2(
boardBounds.center.dx,
boardBounds.bottom + 10,
boardBounds.center.dx - 3.07,
boardBounds.center.dy - 2.4,
),
),
);

@ -70,7 +70,7 @@ class _PinballGameViewState extends State<PinballGameView> {
showDialog<void>(
context: context,
builder: (_) {
return const GameOverDialog();
return GameOverDialog(theme: widget.theme.characterTheme);
},
);
}

@ -1,21 +1,40 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template game_over_dialog}
/// [Dialog] displayed when the [PinballGame] is over.
/// {@endtemplate}
class GameOverDialog extends StatelessWidget {
/// {@macro game_over_dialog}
const GameOverDialog({Key? key}) : super(key: key);
const GameOverDialog({Key? key, required this.theme}) : super(key: key);
/// Current [CharacterTheme] to customize dialog
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
return const Dialog(
final l10n = context.l10n;
return Dialog(
child: SizedBox(
width: 200,
height: 200,
child: Center(
child: Text('Game Over'),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(l10n.gameOver),
TextButton(
onPressed: () => Navigator.of(context).push<void>(
LeaderboardPage.route(theme: theme),
),
child: Text(l10n.leaderboard),
),
],
),
),
),
);

@ -23,5 +23,33 @@
"characterSelectionTitle": "Choose your character!",
"@characterSelectionTitle": {
"description": "Title text displayed on the character selection page"
},
"gameOver": "Game Over",
"@gameOver": {
"description": "Text displayed on the ending dialog when game finishes"
},
"leaderboard": "Leaderboard",
"@leaderboard": {
"description": "Text displayed on the ending dialog leaderboard button"
},
"rank": "Rank",
"@rank": {
"description": "Text displayed on the leaderboard page header rank column"
},
"character": "Character",
"@character": {
"description": "Text displayed on the leaderboard page header character column"
},
"username": "Username",
"@username": {
"description": "Text displayed on the leaderboard page header userName column"
},
"score": "Score",
"@score": {
"description": "Text displayed on the leaderboard page header score column"
},
"retry": "Retry",
"@retry": {
"description": "Text displayed on the retry button leaders board page"
}
}

@ -1,2 +1,3 @@
export 'bloc/leaderboard_bloc.dart';
export 'models/leader_board_entry.dart';
export 'view/leaderboard_page.dart';

@ -0,0 +1,306 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
class LeaderboardPage extends StatelessWidget {
const LeaderboardPage({Key? key, required this.theme}) : super(key: key);
final CharacterTheme theme;
static Route route({required CharacterTheme theme}) {
return MaterialPageRoute<void>(
builder: (_) => LeaderboardPage(theme: theme),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LeaderboardBloc(
context.read<LeaderboardRepository>(),
)..add(const Top10Fetched()),
child: LeaderboardView(theme: theme),
);
}
}
class LeaderboardView extends StatelessWidget {
const LeaderboardView({Key? key, required this.theme}) : super(key: key);
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
Text(
l10n.leaderboard,
style: Theme.of(context).textTheme.headline3,
),
const SizedBox(height: 80),
BlocBuilder<LeaderboardBloc, LeaderboardState>(
builder: (context, state) {
switch (state.status) {
case LeaderboardStatus.loading:
return _LeaderboardLoading(theme: theme);
case LeaderboardStatus.success:
return _LeaderboardRanking(
ranking: state.leaderboard,
theme: theme,
);
case LeaderboardStatus.error:
return _LeaderboardError(theme: theme);
}
},
),
const SizedBox(height: 20),
TextButton(
onPressed: () => Navigator.of(context).push<void>(
CharacterSelectionPage.route(),
),
child: Text(l10n.retry),
),
],
),
),
),
);
}
}
class _LeaderboardLoading extends StatelessWidget {
const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key);
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}
class _LeaderboardError extends StatelessWidget {
const _LeaderboardError({Key? key, required this.theme}) : super(key: key);
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20),
child: Text(
'There was en error loading data!',
style:
Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red),
),
);
}
}
class _LeaderboardRanking extends StatelessWidget {
const _LeaderboardRanking({
Key? key,
required this.ranking,
required this.theme,
}) : super(key: key);
final List<LeaderboardEntry> ranking;
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_LeaderboardHeaders(theme: theme),
_LeaderboardList(
ranking: ranking,
theme: theme,
),
],
),
);
}
}
class _LeaderboardHeaders extends StatelessWidget {
const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key);
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_LeaderboardHeaderItem(title: l10n.rank, theme: theme),
_LeaderboardHeaderItem(title: l10n.character, theme: theme),
_LeaderboardHeaderItem(title: l10n.username, theme: theme),
_LeaderboardHeaderItem(title: l10n.score, theme: theme),
],
);
}
}
class _LeaderboardHeaderItem extends StatelessWidget {
const _LeaderboardHeaderItem({
Key? key,
required this.title,
required this.theme,
}) : super(key: key);
final CharacterTheme theme;
final String title;
@override
Widget build(BuildContext context) {
return Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: theme.ballColor,
),
child: Text(
title,
style: Theme.of(context).textTheme.headline5,
),
),
);
}
}
class _LeaderboardList extends StatelessWidget {
const _LeaderboardList({
Key? key,
required this.ranking,
required this.theme,
}) : super(key: key);
final List<LeaderboardEntry> ranking;
final CharacterTheme theme;
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
itemBuilder: (_, index) => _LeaderBoardCompetitor(
entry: ranking[index],
theme: theme,
),
itemCount: ranking.length,
);
}
}
class _LeaderBoardCompetitor extends StatelessWidget {
const _LeaderBoardCompetitor({
Key? key,
required this.entry,
required this.theme,
}) : super(key: key);
final CharacterTheme theme;
final LeaderboardEntry entry;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_LeaderboardCompetitorField(
text: entry.rank,
theme: theme,
),
_LeaderboardCompetitorCharacter(
characterAsset: entry.character,
theme: theme,
),
_LeaderboardCompetitorField(
text: entry.playerInitials,
theme: theme,
),
_LeaderboardCompetitorField(
text: entry.score.toString(),
theme: theme,
),
],
);
}
}
class _LeaderboardCompetitorField extends StatelessWidget {
const _LeaderboardCompetitorField({
Key? key,
required this.text,
required this.theme,
}) : super(key: key);
final CharacterTheme theme;
final String text;
@override
Widget build(BuildContext context) {
return Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: theme.ballColor,
width: 2,
),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(text),
),
),
);
}
}
class _LeaderboardCompetitorCharacter extends StatelessWidget {
const _LeaderboardCompetitorCharacter({
Key? key,
required this.characterAsset,
required this.theme,
}) : super(key: key);
final CharacterTheme theme;
final AssetGenImage characterAsset;
@override
Widget build(BuildContext context) {
return Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: theme.ballColor,
width: 2,
),
),
child: SizedBox(
height: 30,
child: characterAsset.image(),
),
),
);
}
}

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:ui';
import 'package:flame/components.dart';
@ -22,11 +23,14 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
}
/// The size of the [Ball]
final Vector2 size = Vector2.all(2);
final Vector2 size = Vector2.all(3);
/// The base [Color] used to tint this [Ball]
final Color baseColor;
double _boostTimer = 0;
static const _boostDuration = 2.0;
@override
Future<void> onLoad() async {
await super.onLoad();
@ -69,4 +73,26 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
void resume() {
body.setType(BodyType.dynamic);
}
@override
void update(double dt) {
super.update(dt);
if (_boostTimer > 0) {
_boostTimer -= dt;
final direction = body.linearVelocity.normalized();
final effect = FireEffect(
burstPower: _boostTimer,
direction: direction,
position: body.position,
);
unawaited(gameRef.add(effect));
}
}
/// Applies a boost on this [Ball]
void boost(Vector2 impulse) {
body.applyLinearImpulse(impulse);
_boostTimer = _boostDuration;
}
}

@ -1,3 +1,4 @@
export 'ball.dart';
export 'fire_effect.dart';
export 'initial_position.dart';
export 'layer.dart';

@ -0,0 +1,113 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/particles.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle;
import 'package:flutter/material.dart';
const _particleRadius = 0.25;
// TODO(erickzanardo): This component could just be a ParticleComponet,
/// unfortunately there is a Particle Component is not a PositionComponent,
/// which makes it hard to be used since we have camera transformations and on
// top of that, PositionComponent has a bug inside forge 2d games
///
/// https://github.com/flame-engine/flame/issues/1484
/// https://github.com/flame-engine/flame/issues/1484
/// {@template fire_effect}
/// A [BodyComponent] which creates a fire trail effect using the given
/// parameters
/// {@endtemplate}
class FireEffect extends BodyComponent {
/// {@macro fire_effect}
FireEffect({
required this.burstPower,
required this.position,
required this.direction,
});
/// A [double] value that will define how "strong" the burst of particles
/// will be
final double burstPower;
/// The position of the burst
final Vector2 position;
/// Which direction the burst will aim
final Vector2 direction;
late Particle _particle;
@override
Body createBody() {
final bodyDef = BodyDef()..position = position;
final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
Future<void> onLoad() async {
await super.onLoad();
final children = [
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.yellow.darken((index + 1) / 4),
);
}),
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.red.darken((index + 1) / 4),
);
}),
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.orange.darken((index + 1) / 4),
);
}),
];
final rng = math.Random();
final spreadTween = Tween<double>(begin: -0.2, end: 0.2);
_particle = Particle.generate(
count: (rng.nextDouble() * (burstPower * 10)).toInt(),
generator: (_) {
final spread = Vector2(
spreadTween.transform(rng.nextDouble()),
spreadTween.transform(rng.nextDouble()),
);
final finalDirection = Vector2(direction.x, -direction.y) + spread;
final speed = finalDirection * (burstPower * 20);
return AcceleratedParticle(
lifespan: 5 / burstPower,
position: Vector2.zero(),
speed: speed,
child: children[rng.nextInt(children.length)],
);
},
);
}
@override
void update(double dt) {
super.update(dt);
_particle.update(dt);
if (_particle.shouldRemove) {
removeFromParent();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
_particle.render(canvas);
}
}

@ -1,11 +1,2 @@
import 'package:flame_forge2d/flame_forge2d.dart';
String buildSourceLink(String path) {
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
}
class BasicGame extends Forge2DGame {
BasicGame() {
images.prefix = '';
}
}
export 'games.dart';
export 'methods.dart';

@ -0,0 +1,74 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
class BasicGame extends Forge2DGame {
BasicGame() {
images.prefix = '';
}
}
abstract class LineGame extends BasicGame with PanDetector {
Vector2? _lineEnd;
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
unawaited(add(_PreviewLine()));
}
@override
void onPanStart(DragStartInfo info) {
_lineEnd = info.eventPosition.game;
}
@override
void onPanUpdate(DragUpdateInfo info) {
_lineEnd = info.eventPosition.game;
}
@override
void onPanEnd(DragEndInfo info) {
if (_lineEnd != null) {
final line = _lineEnd! - Vector2.zero();
onLine(line);
_lineEnd = null;
}
}
void onLine(Vector2 line);
}
class _PreviewLine extends PositionComponent with HasGameRef<LineGame> {
static final _previewLinePaint = Paint()
..color = Colors.pink
..strokeWidth = 0.2
..style = PaintingStyle.stroke;
Vector2? lineEnd;
@override
void update(double dt) {
super.update(dt);
lineEnd = gameRef._lineEnd?.clone()?..multiply(Vector2(1, -1));
}
@override
void render(Canvas canvas) {
super.render(canvas);
if (lineEnd != null) {
canvas.drawLine(
Vector2.zero().toOffset(),
lineEnd!.toOffset(),
_previewLinePaint,
);
}
}
}

@ -0,0 +1,3 @@
String buildSourceLink(String path) {
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
}

@ -6,11 +6,13 @@
// https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/stories/effects/effects.dart';
import 'package:sandbox/stories/stories.dart';
void main() {
final dashbook = Dashbook(theme: ThemeData.dark());
addBallStories(dashbook);
addEffectsStories(dashbook);
runApp(dashbook);
}

@ -2,17 +2,27 @@ import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/ball_booster.dart';
import 'package:sandbox/stories/ball/basic.dart';
void addBallStories(Dashbook dashbook) {
dashbook.storiesOf('Ball').add(
'Basic',
(context) => GameWidget(
game: BasicBallGame(
color: context.colorProperty('color', Colors.blue),
),
dashbook.storiesOf('Ball')
..add(
'Basic',
(context) => GameWidget(
game: BasicBallGame(
color: context.colorProperty('color', Colors.blue),
),
codeLink: buildSourceLink('ball/basic.dart'),
info: BasicBallGame.info,
);
),
codeLink: buildSourceLink('ball/basic.dart'),
info: BasicBallGame.info,
)
..add(
'Booster',
(context) => GameWidget(
game: BallBoosterExample(),
),
codeLink: buildSourceLink('ball/ball_booster.dart'),
info: BallBoosterExample.info,
);
}

@ -0,0 +1,16 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BallBoosterExample extends LineGame {
static const info = '';
@override
void onLine(Vector2 line) {
final ball = Ball(baseColor: Colors.transparent);
add(ball);
ball.mounted.then((value) => ball.boost(line * -1 * 20));
}
}

@ -4,7 +4,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BasicBallGame extends BasicGame with TapDetector {
BasicBallGame({ required this.color });
BasicBallGame({required this.color});
static const info = '''
Basic example of how a Ball works, tap anywhere on the
@ -15,8 +15,8 @@ class BasicBallGame extends BasicGame with TapDetector {
@override
void onTapUp(TapUpInfo info) {
add(Ball(baseColor: color)
..initialPosition = info.eventPosition.game,
add(
Ball(baseColor: color)..initialPosition = info.eventPosition.game,
);
}
}

@ -0,0 +1,13 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/effects/fire_effect.dart';
void addEffectsStories(Dashbook dashbook) {
dashbook.storiesOf('Effects').add(
'Fire Effect',
(context) => GameWidget(game: FireEffectExample()),
codeLink: buildSourceLink('effects/fire_effect.dart'),
info: FireEffectExample.info,
);
}

@ -0,0 +1,46 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class FireEffectExample extends LineGame {
static const info = 'Demonstrate the fire trail effect '
'drag a line to define the trail direction';
@override
void onLine(Vector2 line) {
add(_EffectEmitter(line));
}
}
class _EffectEmitter extends Component {
_EffectEmitter(this.line) {
_direction = line.normalized();
_force = line.length;
}
static const _timerLimit = 2.0;
var _timer = _timerLimit;
final Vector2 line;
late Vector2 _direction;
late double _force;
@override
void update(double dt) {
super.update(dt);
if (_timer > 0) {
add(
FireEffect(
burstPower: (_timer / _timerLimit) * _force,
position: Vector2.zero(),
direction: _direction,
),
);
_timer -= dt;
} else {
removeFromParent();
}
}
}

@ -1 +1,2 @@
export 'mocks.dart';
export 'test_game.dart';

@ -0,0 +1,5 @@
import 'dart:ui';
import 'package:mocktail/mocktail.dart';
class MockCanvas extends Mock implements Canvas {}

@ -86,7 +86,7 @@ void main() {
final fixture = ball.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1));
expect(fixture.shape.radius, equals(1.5));
},
);
@ -158,5 +158,29 @@ void main() {
);
});
});
group('boost', () {
flameTester.test('applies an impulse to the ball', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
expect(ball.body.linearVelocity, equals(Vector2.zero()));
ball.boost(Vector2.all(10));
expect(ball.body.linearVelocity.x, greaterThan(0));
expect(ball.body.linearVelocity.y, greaterThan(0));
});
flameTester.test('adds fire effect components to the game', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.boost(Vector2.all(10));
game.update(0);
await game.ready();
expect(game.children.whereType<FireEffect>().length, greaterThan(0));
});
});
});
}

@ -0,0 +1,55 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
setUpAll(() {
registerFallbackValue(Offset.zero);
registerFallbackValue(Paint());
});
group('FireEffect', () {
flameTester.test('is removed once its particles are done', (game) async {
await game.ensureAdd(
FireEffect(
burstPower: 1,
position: Vector2.zero(),
direction: Vector2.all(2),
),
);
await game.ready();
expect(game.children.whereType<FireEffect>().length, equals(1));
game.update(5);
await game.ready();
expect(game.children.whereType<FireEffect>().length, equals(0));
});
flameTester.test('render circles on the canvas', (game) async {
final effect = FireEffect(
burstPower: 1,
position: Vector2.zero(),
direction: Vector2.all(2),
);
await game.ensureAdd(effect);
await game.ready();
final canvas = MockCanvas();
effect.render(canvas);
verify(() => canvas.drawCircle(any(), any(), any()))
.called(greaterThan(0));
});
});
}

@ -7,11 +7,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/landing/landing.dart';
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
import '../../helpers/mocks.dart';
void main() {
group('App', () {

@ -4,6 +4,7 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';

@ -187,7 +187,7 @@ void main() {
final fixture = bonusLetter.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(2));
expect(fixture.shape.radius, equals(1.85));
},
);
});

@ -104,10 +104,7 @@ void main() {
);
await tester.pump();
expect(
find.text('Game Over'),
findsOneWidget,
);
expect(find.byType(GameOverDialog), findsOneWidget);
},
);

@ -0,0 +1,44 @@
// 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/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../../helpers/helpers.dart';
void main() {
group('GameOverDialog', () {
testWidgets('renders correctly', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(
const GameOverDialog(
theme: DashTheme(),
),
);
expect(find.text(l10n.gameOver), findsOneWidget);
expect(find.text(l10n.leaderboard), findsOneWidget);
});
testWidgets('tapping on leaderboard button navigates to LeaderBoardPage',
(tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp(
const GameOverDialog(
theme: DashTheme(),
),
navigator: navigator,
);
await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard));
verify(() => navigator.push<void>(any())).called(1);
});
});
}

@ -8,4 +8,5 @@ export 'builders.dart';
export 'extensions.dart';
export 'key_testers.dart';
export 'mocks.dart';
export 'navigator.dart';
export 'pump_app.dart';

@ -1,3 +1,4 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
@ -6,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart';
@ -37,6 +39,9 @@ class MockGameState extends Mock implements GameState {}
class MockThemeCubit extends Mock implements ThemeCubit {}
class MockLeaderboardBloc extends MockBloc<LeaderboardEvent, LeaderboardState>
implements LeaderboardBloc {}
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'helpers.dart';
Future<void> expectNavigatesToRoute<Type>(
WidgetTester tester,
Route route, {
bool hasFlameGameInside = false,
}) async {
// ignore: avoid_dynamic_calls
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(route);
},
child: const Text('Tap me'),
);
},
),
),
);
await tester.tap(find.text('Tap me'));
if (hasFlameGameInside) {
// We can't use pumpAndSettle here because the page renders a Flame game
// which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation
} else {
await tester.pumpAndSettle();
}
expect(find.byType(Type), findsOneWidget);
}

@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
@ -22,26 +23,30 @@ extension PumpApp on WidgetTester {
MockNavigator? navigator,
GameBloc? gameBloc,
ThemeCubit? themeCubit,
LeaderboardRepository? leaderboardRepository,
}) {
return pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider.value(
value: themeCubit ?? MockThemeCubit(),
),
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
],
child: MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
RepositoryProvider.value(
value: leaderboardRepository ?? MockLeaderboardRepository(),
child: MultiBlocProvider(
providers: [
BlocProvider.value(
value: themeCubit ?? MockThemeCubit(),
),
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
],
supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
child: MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
),
),
),
);

@ -0,0 +1,150 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
group('LeaderboardPage', () {
testWidgets('renders LeaderboardView', (tester) async {
await tester.pumpApp(
LeaderboardPage(
theme: DashTheme(),
),
);
expect(find.byType(LeaderboardView), findsOneWidget);
});
testWidgets('route returns a valid navigation route', (tester) async {
await expectNavigatesToRoute<LeaderboardPage>(
tester,
LeaderboardPage.route(
theme: DashTheme(),
),
);
});
});
group('LeaderboardView', () {
late LeaderboardBloc leaderboardBloc;
setUp(() {
leaderboardBloc = MockLeaderboardBloc();
});
testWidgets('renders correctly', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial());
await tester.pumpApp(
BlocProvider.value(
value: leaderboardBloc,
child: LeaderboardView(
theme: DashTheme(),
),
),
);
expect(find.text(l10n.leaderboard), findsOneWidget);
expect(find.text(l10n.retry), findsOneWidget);
});
testWidgets('renders loading view when bloc emits [loading]',
(tester) async {
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial());
await tester.pumpApp(
BlocProvider.value(
value: leaderboardBloc,
child: LeaderboardView(
theme: DashTheme(),
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
expect(find.text('There was en error loading data!'), findsNothing);
expect(find.byType(ListView), findsNothing);
});
testWidgets('renders error view when bloc emits [error]', (tester) async {
when(() => leaderboardBloc.state).thenReturn(
LeaderboardState.initial().copyWith(status: LeaderboardStatus.error),
);
await tester.pumpApp(
BlocProvider.value(
value: leaderboardBloc,
child: LeaderboardView(
theme: DashTheme(),
),
),
);
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('There was en error loading data!'), findsOneWidget);
expect(find.byType(ListView), findsNothing);
});
testWidgets('renders success view when bloc emits [success]',
(tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
when(() => leaderboardBloc.state).thenReturn(
LeaderboardState(
status: LeaderboardStatus.success,
ranking: LeaderboardRanking(ranking: 0, outOf: 0),
leaderboard: [
LeaderboardEntry(
rank: '1',
playerInitials: 'ABC',
score: 10000,
character: DashTheme().characterAsset,
),
],
),
);
await tester.pumpApp(
BlocProvider.value(
value: leaderboardBloc,
child: LeaderboardView(
theme: DashTheme(),
),
),
);
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('There was en error loading data!'), findsNothing);
expect(find.text(l10n.rank), findsOneWidget);
expect(find.text(l10n.character), findsOneWidget);
expect(find.text(l10n.username), findsOneWidget);
expect(find.text(l10n.score), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
});
testWidgets('navigates to CharacterSelectionPage when retry is tapped',
(tester) async {
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp(
LeaderboardPage(
theme: DashTheme(),
),
navigator: navigator,
);
await tester.ensureVisible(find.byType(TextButton));
await tester.tap(find.byType(TextButton));
verify(() => navigator.push<void>(any())).called(1);
});
});
}
Loading…
Cancel
Save