Merge branch 'main' into refactor/spaceship-entrance

pull/298/head
Allison Ryan 3 years ago
commit b7d1f022ae

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 200 KiB

@ -18,17 +18,17 @@ class AndroidAcres extends Component {
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-25, 1.3), )..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-32.8, -9.2), )..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.5, -13.8),
], ],

@ -51,7 +51,7 @@ class _BottomGroupSide extends Component {
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
children: [ children: [
ScoringBehavior(points: 5000)..applyTo(['bouncy_edge']), ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment, (22.64 * direction) + centerXAdjustment,

@ -10,6 +10,7 @@ export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word/google_word.dart'; export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart'; export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';
export 'sparky_scorch.dart'; export 'sparky_scorch.dart';

@ -14,7 +14,8 @@ class DinoDesert extends Component {
children: [ children: [
ChromeDino( ChromeDino(
children: [ children: [
ScoringBehavior(points: 200000)..applyTo(['inside_mouth']), ScoringBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']),
], ],
)..initialPosition = Vector2(12.6, -6.9), )..initialPosition = Vector2(12.6, -6.9),
_BarrierBehindDino(), _BarrierBehindDino(),

@ -18,22 +18,22 @@ class FlutterForest extends Component with ZIndex {
children: [ children: [
Signpost( Signpost(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: Points.fiveThousand),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringBehavior(points: 200000), ScoringBehavior(points: Points.twoHundredThousand),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),

@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex {
children: [ children: [
GoogleLetter( GoogleLetter(
0, 0,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-13.1, 1.72), )..initialPosition = position + Vector2(-13.1, 1.72),
GoogleLetter( GoogleLetter(
1, 1,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-8.33, -0.75), )..initialPosition = position + Vector2(-8.33, -0.75),
GoogleLetter( GoogleLetter(
2, 2,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-2.88, -1.85), )..initialPosition = position + Vector2(-2.88, -1.85),
GoogleLetter( GoogleLetter(
3, 3,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(2.88, -1.85), )..initialPosition = position + Vector2(2.88, -1.85),
GoogleLetter( GoogleLetter(
4, 4,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(8.33, -0.75), )..initialPosition = position + Vector2(8.33, -0.75),
GoogleLetter( GoogleLetter(
5, 5,
children: [ScoringBehavior(points: 5000)], children: [ScoringBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(13.1, 1.72), )..initialPosition = position + Vector2(13.1, 1.72),
GoogleWordBonusBehavior(), GoogleWordBonusBehavior(),
], ],

@ -0,0 +1 @@
export 'multiballs_behavior.dart';

@ -0,0 +1,28 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiball] when there is a bonus ball.
class MultiballsBehavior extends Component
with
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
newState.bonusHistory.last == GameBonus.dashNest;
return hasChanged && lastBonusIsMultiball;
}
@override
void onNewState(GameState state) {
parent.children.whereType<Multiball>().forEach((multiball) {
multiball.bloc.onAnimate();
});
}
}

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiballs_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
class Multiballs extends Component with ZIndex {
/// {@macro multiballs_component}
Multiballs()
: super(
children: [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
MultiballsBehavior(),
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [Multiballs] without any children.
///
/// This can be used for testing [Multiballs]'s behaviors in isolation.
@visibleForTesting
Multiballs.test();
}

@ -12,21 +12,21 @@ import 'package:pinball_flame/pinball_flame.dart';
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> { class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
/// {@macro scoring_behavior} /// {@macro scoring_behavior}
ScoringBehavior({ ScoringBehavior({
required int points, required Points points,
}) : _points = points; }) : _points = points;
final int _points; final Points _points;
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
if (other is! Ball) return; if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points)); gameRef.read<GameBloc>().add(Scored(points: _points.value));
gameRef.audio.score(); gameRef.audio.score();
gameRef.firstChild<ZCanvasComponent>()!.add( gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreText( ScoreComponent(
text: _points.toString(), points: _points,
position: other.body.position, position: other.body.position,
), ),
); );

@ -16,17 +16,17 @@ class SparkyScorch extends Component {
children: [ children: [
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9),
@ -47,7 +47,7 @@ class SparkyComputerSensor extends BodyComponent
: super( : super(
renderBody: false, renderBody: false,
children: [ children: [
ScoringBehavior(points: 200000), ScoringBehavior(points: Points.twentyThousand),
], ],
); );

@ -114,6 +114,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName), images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.lit.keyName), images.load(components.Assets.images.multiplier.x3.lit.keyName),
@ -124,6 +126,10 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.multiplier.x5.dimmed.keyName), images.load(components.Assets.images.multiplier.x5.dimmed.keyName),
images.load(components.Assets.images.multiplier.x6.lit.keyName), images.load(components.Assets.images.multiplier.x6.lit.keyName),
images.load(components.Assets.images.multiplier.x6.dimmed.keyName), images.load(components.Assets.images.multiplier.x6.dimmed.keyName),
images.load(components.Assets.images.score.fiveThousand.keyName),
images.load(components.Assets.images.score.twentyThousand.keyName),
images.load(components.Assets.images.score.twoHundredThousand.keyName),
images.load(components.Assets.images.score.oneMillion.keyName),
images.load(dashTheme.leaderboardIcon.keyName), images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),

@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame
final decals = [ final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)), GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(), Multipliers(),
Multiballs(),
]; ];
final characterAreas = [ final characterAreas = [
AndroidAcres(), AndroidAcres(),
@ -87,7 +88,7 @@ class PinballGame extends Forge2DGame
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) { if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.pull(); descendants().whereType<Plunger>().single.pullFor(2);
} else { } else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right;
@ -103,21 +104,12 @@ class PinballGame extends Forge2DGame
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.release();
} else {
_moveFlippersDown(); _moveFlippersDown();
}
super.onTapUp(info); super.onTapUp(info);
} }
@override @override
void onTapCancel() { void onTapCancel() {
descendants().whereType<Plunger>().single.release();
_moveFlippersDown(); _moveFlippersDown();
super.onTapCancel(); super.onTapCancel();
} }

@ -44,15 +44,14 @@ class PinballGamePage extends StatelessWidget {
...game.preLoadAssets(), ...game.preLoadAssets(),
pinballAudio.load(), pinballAudio.load(),
...BonusAnimation.loadAssets(), ...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
]; ];
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => GameBloc()),
BlocProvider( BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
create: (_) => AssetsManagerCubit(loadables)..load(),
),
], ],
child: PinballGameView(game: game), child: PinballGameView(game: game),
); );

@ -126,7 +126,7 @@ class _BonusAnimationState extends State<BonusAnimation>
); );
animation = spriteSheet.createAnimation( animation = spriteSheet.createAnimation(
row: 0, row: 0,
stepTime: 1 / 24, stepTime: 1 / 12,
to: spriteSheet.rows * spriteSheet.columns, to: spriteSheet.rows * spriteSheet.columns,
loop: false, loop: false,
); );

@ -52,7 +52,6 @@ extension on Control {
Future<void> showHowToPlayDialog(BuildContext context) { Future<void> showHowToPlayDialog(BuildContext context) {
return showDialog<void>( return showDialog<void>(
context: context, context: context,
barrierDismissible: false,
builder: (_) => HowToPlayDialog(), builder: (_) => HowToPlayDialog(),
); );
} }

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/cubit/character_theme_cubit.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -118,19 +117,7 @@ class _CharacterPreview extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CharacterThemeCubit, CharacterThemeState>( return BlocBuilder<CharacterThemeCubit, CharacterThemeState>(
builder: (context, state) { builder: (context, state) {
return Column( return SelectedCharacter(currentCharacter: state.characterTheme);
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
state.characterTheme.name,
style: Theme.of(context).textTheme.headline2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Expanded(child: state.characterTheme.icon.image()),
],
);
}, },
); );
} }

@ -0,0 +1,102 @@
import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template selected_character}
/// Shows an animated version of the character currently selected.
/// {@endtemplate}
class SelectedCharacter extends StatefulWidget {
/// {@macro selected_character}
const SelectedCharacter({
Key? key,
required this.currentCharacter,
}) : super(key: key);
/// The character that is selected at the moment.
final CharacterTheme currentCharacter;
@override
State<SelectedCharacter> createState() => _SelectedCharacterState();
/// Returns a list of assets to be loaded.
static List<Future> loadAssets() {
return [
Flame.images.load(const DashTheme().animation.keyName),
Flame.images.load(const AndroidTheme().animation.keyName),
Flame.images.load(const DinoTheme().animation.keyName),
Flame.images.load(const SparkyTheme().animation.keyName),
];
}
}
class _SelectedCharacterState extends State<SelectedCharacter>
with TickerProviderStateMixin {
SpriteAnimationController? _controller;
@override
void initState() {
super.initState();
_setupCharacterAnimation();
}
@override
void didUpdateWidget(covariant SelectedCharacter oldWidget) {
super.didUpdateWidget(oldWidget);
_setupCharacterAnimation();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
widget.currentCharacter.name,
style: Theme.of(context).textTheme.headline2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: SpriteAnimationWidget(
controller: _controller!,
anchor: Anchor.center,
),
);
},
),
),
],
);
}
void _setupCharacterAnimation() {
final spriteSheet = SpriteSheet.fromColumnsAndRows(
image: Flame.images.fromCache(widget.currentCharacter.animation.keyName),
columns: 12,
rows: 6,
);
final animation = spriteSheet.createAnimation(
row: 0,
stepTime: 1 / 24,
to: spriteSheet.rows * spriteSheet.columns,
);
if (_controller != null) _controller?.dispose();
_controller = SpriteAnimationController(vsync: this, animation: animation)
..forward()
..repeat();
}
}

@ -1 +1,2 @@
export 'character_selection_page.dart'; export 'character_selection_page.dart';
export 'selected_character.dart';

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@ -28,9 +28,11 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesMultiballGen get multiball => const $AssetsImagesMultiballGen();
$AssetsImagesMultiplierGen get multiplier => $AssetsImagesMultiplierGen get multiplier =>
const $AssetsImagesMultiplierGen(); const $AssetsImagesMultiplierGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
@ -179,6 +181,18 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png'); const AssetGenImage('assets/images/launch_ramp/ramp.png');
} }
class $AssetsImagesMultiballGen {
const $AssetsImagesMultiballGen();
/// File path: assets/images/multiball/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiball/dimmed.png');
/// File path: assets/images/multiball/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiball/lit.png');
}
class $AssetsImagesMultiplierGen { class $AssetsImagesMultiplierGen {
const $AssetsImagesMultiplierGen(); const $AssetsImagesMultiplierGen();
@ -201,6 +215,26 @@ class $AssetsImagesPlungerGen {
const AssetGenImage('assets/images/plunger/rocket.png'); const AssetGenImage('assets/images/plunger/rocket.png');
} }
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();
/// File path: assets/images/score/five-thousand.png
AssetGenImage get fiveThousand =>
const AssetGenImage('assets/images/score/five-thousand.png');
/// File path: assets/images/score/one-million.png
AssetGenImage get oneMillion =>
const AssetGenImage('assets/images/score/one-million.png');
/// File path: assets/images/score/twenty-thousand.png
AssetGenImage get twentyThousand =>
const AssetGenImage('assets/images/score/twenty-thousand.png');
/// File path: assets/images/score/two-hundred-thousand.png
AssetGenImage get twoHundredThousand =>
const AssetGenImage('assets/images/score/two-hundred-thousand.png');
}
class $AssetsImagesSignpostGen { class $AssetsImagesSignpostGen {
const $AssetsImagesSignpostGen(); const $AssetsImagesSignpostGen();

@ -21,10 +21,11 @@ export 'kicker/kicker.dart';
export 'launch_ramp.dart'; export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'layer_sensor.dart'; export 'layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart'; export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'rocket.dart'; export 'rocket.dart';
export 'score_text.dart'; export 'score_component.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart'; export 'signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';

@ -0,0 +1,78 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiball_blinking_behavior}
/// Makes a [Multiball] blink back to [MultiballLightState.lit] when
/// [MultiballLightState.dimmed].
/// {@endtemplate}
class MultiballBlinkingBehavior extends TimerComponent
with ParentIsA<Multiball> {
/// {@macro multiball_blinking_behavior}
MultiballBlinkingBehavior() : super(period: 0.1);
final _maxBlinks = 10;
int _blinksCounter = 0;
bool _isAnimating = false;
void _onNewState(MultiballState state) {
final animationEnabled =
state.animationState == MultiballAnimationState.blinking;
final canBlink = _blinksCounter < _maxBlinks;
if (animationEnabled && canBlink) {
_start();
} else {
_stop();
}
}
void _start() {
if (!_isAnimating) {
_isAnimating = true;
timer
..reset()
..start();
_animate();
}
}
void _animate() {
parent.bloc.onBlink();
_blinksCounter++;
}
void _stop() {
if (_isAnimating) {
_isAnimating = false;
timer.stop();
_blinksCounter = 0;
parent.bloc.onStop();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (!_isAnimating) {
timer.stop();
} else {
if (_blinksCounter < _maxBlinks) {
_animate();
timer
..reset()
..start();
} else {
timer.stop();
}
}
}
}

@ -0,0 +1,37 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'multiball_state.dart';
class MultiballCubit extends Cubit<MultiballState> {
MultiballCubit() : super(const MultiballState.initial());
void onAnimate() {
emit(
state.copyWith(animationState: MultiballAnimationState.blinking),
);
}
void onStop() {
emit(
state.copyWith(animationState: MultiballAnimationState.idle),
);
}
void onBlink() {
switch (state.lightState) {
case MultiballLightState.lit:
emit(
state.copyWith(lightState: MultiballLightState.dimmed),
);
break;
case MultiballLightState.dimmed:
emit(
state.copyWith(lightState: MultiballLightState.lit),
);
break;
}
}
}

@ -0,0 +1,44 @@
// ignore_for_file: comment_references, public_member_api_docs
part of 'multiball_cubit.dart';
/// Indicates the different sprite states for [MultiballSpriteGroupComponent].
enum MultiballLightState {
lit,
dimmed,
}
// Indicates if the blinking animation is running.
enum MultiballAnimationState {
idle,
blinking,
}
class MultiballState extends Equatable {
const MultiballState({
required this.lightState,
required this.animationState,
});
const MultiballState.initial()
: this(
lightState: MultiballLightState.dimmed,
animationState: MultiballAnimationState.idle,
);
final MultiballLightState lightState;
final MultiballAnimationState animationState;
MultiballState copyWith({
MultiballLightState? lightState,
MultiballAnimationState? animationState,
}) {
return MultiballState(
lightState: lightState ?? this.lightState,
animationState: animationState ?? this.animationState,
);
}
@override
List<Object> get props => [lightState, animationState];
}

@ -0,0 +1,138 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import 'package:pinball_components/src/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/multiball_cubit.dart';
/// {@template multiball}
/// A [Component] for the multiball lighting decals on the board.
/// {@endtemplate}
class Multiball extends Component {
/// {@macro multiball}
Multiball._({
required Vector2 position,
double rotation = 0,
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [
MultiballBlinkingBehavior(),
MultiballSpriteGroupComponent(
position: position,
litAssetPath: Assets.images.multiball.lit.keyName,
dimmedAssetPath: Assets.images.multiball.dimmed.keyName,
rotation: rotation,
state: bloc.state.lightState,
),
...?children,
],
);
/// {@macro multiball}
Multiball.a({
Iterable<Component>? children,
}) : this._(
position: Vector2(-23, 7.5),
rotation: -24 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.b({
Iterable<Component>? children,
}) : this._(
position: Vector2(-7.2, -6.2),
rotation: -5 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.c({
Iterable<Component>? children,
}) : this._(
position: Vector2(-0.7, -9.3),
rotation: 2.7 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.d({
Iterable<Component>? children,
}) : this._(
position: Vector2(15, 7),
rotation: 24 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// Creates an [Multiball] without any children.
///
/// This can be used for testing [Multiball]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
Multiball.test({
required this.bloc,
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final MultiballCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
/// {@template multiball_sprite_group_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
@visibleForTesting
class MultiballSpriteGroupComponent
extends SpriteGroupComponent<MultiballLightState>
with HasGameRef, ParentIsA<Multiball> {
/// {@macro multiball_sprite_group_component}
MultiballSpriteGroupComponent({
required Vector2 position,
required String litAssetPath,
required String dimmedAssetPath,
required double rotation,
required MultiballLightState state,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
anchor: Anchor.center,
position: position,
angle: rotation,
current: state,
);
final String _litAssetPath;
final String _dimmedAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.lightState);
final sprites = {
MultiballLightState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
MultiballLightState.dimmed:
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -68,6 +68,14 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
return body; return body;
} }
var _pullingDownTime = 0.0;
/// Pulls the plunger down for the given amount of [seconds].
// ignore: use_setters_to_change_properties
void pullFor(double seconds) {
_pullingDownTime = seconds;
}
/// Set a constant downward velocity on the [Plunger]. /// Set a constant downward velocity on the [Plunger].
void pull() { void pull() {
body.linearVelocity = Vector2(0, 7); body.linearVelocity = Vector2(0, 7);
@ -79,11 +87,26 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void release() { void release() {
_pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11; final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release(); _spriteComponent.release();
} }
@override
void update(double dt) {
// Ensure that we only pull or release when the time is greater than zero.
if (_pullingDownTime > 0) {
_pullingDownTime -= dt;
if (_pullingDownTime <= 0) {
release();
} else {
pull();
}
}
super.update(dt);
}
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical
/// motion. /// motion.
Future<void> _anchorToJoint() async { Future<void> _anchorToJoint() async {

@ -0,0 +1,92 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
enum Points {
fiveThousand,
twentyThousand,
twoHundredThousand,
oneMillion,
}
/// {@template score_component}
/// A [ScoreComponent] that spawns at a given [position] with a moving
/// animation.
/// {@endtemplate}
class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
/// {@macro score_component}
ScoreComponent({
required this.points,
required Vector2 position,
}) : super(
position: position,
anchor: Anchor.center,
) {
zIndex = ZIndexes.score;
}
late final Effect _effect;
late Points points;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(points.asset),
);
this.sprite = sprite;
size = sprite.originalSize / 55;
await add(
_effect = MoveEffect.by(
Vector2(0, -5),
EffectController(duration: 1),
),
);
}
@override
void update(double dt) {
super.update(dt);
if (_effect.controller.completed) {
removeFromParent();
}
}
}
extension PointsX on Points {
int get value {
switch (this) {
case Points.fiveThousand:
return 5000;
case Points.twentyThousand:
return 20000;
case Points.twoHundredThousand:
return 200000;
case Points.oneMillion:
return 1000000;
}
}
}
extension on Points {
String get asset {
switch (this) {
case Points.fiveThousand:
return Assets.images.score.fiveThousand.keyName;
case Points.twentyThousand:
return Assets.images.score.twentyThousand.keyName;
case Points.twoHundredThousand:
return Assets.images.score.twoHundredThousand.keyName;
case Points.oneMillion:
return Assets.images.score.oneMillion.keyName;
}
}
}

@ -1,57 +0,0 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template score_text}
/// A [TextComponent] that spawns at a given [position] with a moving animation.
/// {@endtemplate}
class ScoreText extends TextComponent with ZIndex {
/// {@macro score_text}
ScoreText({
required String text,
required Vector2 position,
this.color = Colors.black,
}) : super(
text: text,
position: position,
anchor: Anchor.center,
) {
zIndex = ZIndexes.scoreText;
}
late final Effect _effect;
/// The [text]'s [Color].
final Color color;
@override
Future<void> onLoad() async {
textRenderer = TextPaint(
style: TextStyle(
fontFamily: PinballFonts.pixeloidMono,
color: color,
fontSize: 4,
),
);
await add(
_effect = MoveEffect.by(
Vector2(0, -5),
EffectController(duration: 1),
),
);
}
@override
void update(double dt) {
super.update(dt);
if (_effect.controller.completed) {
removeFromParent();
}
}
}

@ -101,10 +101,10 @@ abstract class ZIndexes {
static const androidBumper = _above + ballOnBoard; static const androidBumper = _above + ballOnBoard;
// Score Text // Score
static const scoreText = _above + spaceshipRampForegroundRailing; static const score = _above + spaceshipRampForegroundRailing;
// Debug information // Debug information
static const debugInfo = _above + scoreText; static const debugInfo = _above + score;
} }

@ -81,11 +81,13 @@ flutter:
- assets/images/google_word/letter5/ - assets/images/google_word/letter5/
- assets/images/google_word/letter6/ - assets/images/google_word/letter6/
- assets/images/signpost/ - assets/images/signpost/
- assets/images/multiball/
- assets/images/multiplier/x2/ - assets/images/multiplier/x2/
- assets/images/multiplier/x3/ - assets/images/multiplier/x3/
- assets/images/multiplier/x4/ - assets/images/multiplier/x4/
- assets/images/multiplier/x5/ - assets/images/multiplier/x5/
- assets/images/multiplier/x6/ - assets/images/multiplier/x6/
- assets/images/score/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -24,9 +24,10 @@ void main() {
addBoundariesStories(dashbook); addBoundariesStories(dashbook);
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreTextStories(dashbook); addScoreStories(dashbook);
addBackboardStories(dashbook); addBackboardStories(dashbook);
addDinoWallStories(dashbook); addDinoWallStories(dashbook);
addMultiballStories(dashbook);
addMultipliersStories(dashbook); addMultipliersStories(dashbook);
runApp(dashbook); runApp(dashbook);

@ -1,6 +1,5 @@
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
@ -8,7 +7,13 @@ class BackboardGameOverGame extends AssetsGame
with HasKeyboardHandlerComponents { with HasKeyboardHandlerComponents {
BackboardGameOverGame(this.score, this.character) BackboardGameOverGame(this.score, this.character)
: super( : super(
imagesFileNames: characterIconPaths.values.toList(), imagesFileNames: [
components.Assets.images.score.fiveThousand.keyName,
components.Assets.images.score.twentyThousand.keyName,
components.Assets.images.score.twoHundredThousand.keyName,
components.Assets.images.score.oneMillion.keyName,
...characterIconPaths.values.toList(),
],
); );
static const description = ''' static const description = '''
@ -30,21 +35,23 @@ class BackboardGameOverGame extends AssetsGame
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad();
camera camera
..followVector2(Vector2.zero()) ..followVector2(Vector2.zero())
..zoom = 5; ..zoom = 5;
await add( await add(
Backboard.gameOver( components.Backboard.gameOver(
position: Vector2(0, 20), position: Vector2(0, 20),
score: score, score: score,
characterIconPath: characterIconPaths[character]!, characterIconPath: characterIconPaths[character]!,
onSubmit: (initials) { onSubmit: (initials) {
add( add(
ScoreText( components.ScoreComponent(
text: 'User $initials made $score', points: components.Points.values
.firstWhere((element) => element.value == score),
position: Vector2(0, 50), position: Vector2(0, 50),
color: Colors.pink,
), ),
); );
}, },

@ -1,4 +1,5 @@
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/backboard/backboard_game_over_game.dart'; import 'package:sandbox/stories/backboard/backboard_game_over_game.dart';
import 'package:sandbox/stories/backboard/backboard_waiting_game.dart'; import 'package:sandbox/stories/backboard/backboard_waiting_game.dart';
@ -14,7 +15,11 @@ void addBackboardStories(Dashbook dashbook) {
title: 'Game over', title: 'Game over',
description: BackboardGameOverGame.description, description: BackboardGameOverGame.description,
gameBuilder: (context) => BackboardGameOverGame( gameBuilder: (context) => BackboardGameOverGame(
context.numberProperty('Score', 9000000000).toInt(), context.listProperty(
'Score',
Points.values.first.value,
Points.values.map((score) => score.value).toList(),
),
context.listProperty( context.listProperty(
'Character', 'Character',
BackboardGameOverGame.characterIconPaths.keys.first, BackboardGameOverGame.characterIconPaths.keys.first,

@ -0,0 +1,56 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class MultiballGame extends BallGame with KeyboardEvents {
MultiballGame()
: super(
imagesFileNames: [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
],
);
static const description = '''
Shows how the Multiball are rendered.
- Tap anywhere on the screen to spawn a ball into the game.
- Press space bar to animate multiballs.
''';
final List<Multiball> multiballs = [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
];
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addAll(multiballs);
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
for (final multiball in multiballs) {
multiball.bloc.onBlink();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}

@ -0,0 +1,11 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/multiball/multiball_game.dart';
void addMultiballStories(Dashbook dashbook) {
dashbook.storiesOf('Multiball').addGame(
title: 'Assets',
description: MultiballGame.description,
gameBuilder: (_) => MultiballGame(),
);
}

@ -0,0 +1,44 @@
import 'dart:math';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class ScoreGame extends AssetsGame with TapDetector {
ScoreGame()
: super(
imagesFileNames: [
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
],
);
static const description = '''
Simple game to show how score component works,
- Tap anywhere on the screen to spawn an image on the given location.
''';
final random = Random();
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
}
@override
void onTapUp(TapUpInfo info) {
final index = random.nextInt(Points.values.length);
final score = Points.values[index];
add(
ScoreComponent(
points: score,
position: info.eventPosition.game..multiply(Vector2(1, -1)),
),
);
}
}

@ -0,0 +1,11 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/score/score_game.dart';
void addScoreStories(Dashbook dashbook) {
dashbook.storiesOf('Score').addGame(
title: 'Basic',
description: ScoreGame.description,
gameBuilder: (_) => ScoreGame(),
);
}

@ -1,32 +0,0 @@
import 'dart:math';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class ScoreTextGame extends AssetsGame with TapDetector {
static const description = '''
Simple game to show how score text works,
- Tap anywhere on the screen to spawn an text on the given location.
''';
final random = Random();
@override
Future<void> onLoad() async {
camera.followVector2(Vector2.zero());
}
@override
void onTapUp(TapUpInfo info) {
add(
ScoreText(
text: random.nextInt(100000).toString(),
color: Colors.white,
position: info.eventPosition.game..multiply(Vector2(1, -1)),
),
);
}
}

@ -1,11 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/score_text/score_text_game.dart';
void addScoreTextStories(Dashbook dashbook) {
dashbook.storiesOf('ScoreText').addGame(
title: 'Basic',
description: ScoreTextGame.description,
gameBuilder: (_) => ScoreTextGame(),
);
}

@ -10,8 +10,9 @@ export 'flutter_forest/stories.dart';
export 'google_word/stories.dart'; export 'google_word/stories.dart';
export 'launch_ramp/stories.dart'; export 'launch_ramp/stories.dart';
export 'layer/stories.dart'; export 'layer/stories.dart';
export 'multiball/stories.dart';
export 'multipliers/stories.dart'; export 'multipliers/stories.dart';
export 'plunger/stories.dart'; export 'plunger/stories.dart';
export 'score_text/stories.dart'; export 'score/stories.dart';
export 'slingshot/stories.dart'; export 'slingshot/stories.dart';
export 'sparky_scorch/stories.dart'; export 'sparky_scorch/stories.dart';

@ -25,6 +25,8 @@ class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {} class MockMultiplierCubit extends Mock implements MultiplierCubit {}
class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@ -0,0 +1,158 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'MultiballBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlink every 0.1 seconds when animation state is animated',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
game.update(0);
verify(bloc.onBlink).called(1);
await tester.pump();
game.update(0.1);
await streamController.close();
verify(bloc.onBlink).called(1);
},
);
flameTester.testGameWidget(
'calls onStop when animation state is stopped',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await streamController.close();
verify(bloc.onStop).called(1);
},
);
flameTester.testGameWidget(
'onTick stops when there is no animation',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
behavior.onTick();
expect(behavior.timer.isRunning(), false);
},
);
flameTester.testGameWidget(
'onTick stops after 10 blinks repetitions',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.dimmed,
),
);
await tester.pump();
for (var i = 0; i < 10; i++) {
behavior.onTick();
}
expect(behavior.timer.isRunning(), false);
},
);
},
);
}

@ -0,0 +1,67 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'MultiballCubit',
() {
blocTest<MultiballCubit, MultiballState>(
'onAnimate emits animationState [animate]',
build: MultiballCubit.new,
act: (bloc) => bloc.onAnimate(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.blinking,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onStop emits animationState [stopped]',
build: MultiballCubit.new,
act: (bloc) => bloc.onStop(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.idle,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onBlink emits lightState [lit, dimmed, lit]',
build: MultiballCubit.new,
act: (bloc) => bloc
..onBlink()
..onBlink()
..onBlink(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.lit,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.dimmed,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.lit,
)
],
);
},
);
}

@ -0,0 +1,76 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/pinball_components.dart';
void main() {
group('MultiballState', () {
test('supports value equality', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
equals(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
isNotNull,
);
});
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
);
expect(
multiballState.copyWith(),
equals(multiballState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
);
final otherMultiballState = MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
);
expect(multiballState, isNot(equals(otherMultiballState)));
expect(
multiballState.copyWith(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
equals(otherMultiballState),
);
},
);
});
});
}

@ -0,0 +1,90 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
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 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('Multiball', () {
group('loads correctly', () {
flameTester.test('"a"', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"b"', (game) async {
final multiball = Multiball.b();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"c"', (game) async {
final multiball = Multiball.c();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"d"', (game) async {
final multiball = Multiball.d();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
});
flameTester.test(
'closes bloc when removed',
(game) async {
final bloc = MockMultiballCubit();
whenListen(
bloc,
const Stream<MultiballLightState>.empty(),
initialState: MultiballLightState.dimmed,
);
when(bloc.close).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await game.ensureAdd(multiball);
game.remove(multiball);
await game.ready();
verify(bloc.close).called(1);
},
);
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final multiball = Multiball.a(
children: [component],
);
await game.ensureAdd(multiball);
expect(multiball.children, contains(component));
});
flameTester.test('a MultiballBlinkingBehavior', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(
multiball.children.whereType<MultiballBlinkingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -121,6 +121,33 @@ void main() {
); );
}); });
group('pullFor', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
flameTester.testGameWidget(
'moves downwards for given period when pullFor is called',
setUp: (game, tester) async {
await game.ensureAdd(plunger);
},
verify: (game, tester) async {
plunger.pullFor(2);
game.update(0);
expect(plunger.body.linearVelocity.y, isPositive);
await tester.pump(const Duration(seconds: 2));
expect(plunger.body.linearVelocity.y, isZero);
},
);
});
group('pull', () { group('pull', () {
late Plunger plunger; late Plunger plunger;

@ -0,0 +1,202 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('ScoreComponent', () {
flameTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
points: Points.oneMillion,
position: Vector2.zero(),
),
);
},
verify: (game, tester) async {
final texts = game.descendants().whereType<SpriteComponent>().length;
expect(texts, equals(1));
},
);
flameTester.testGameWidget(
'has a movement effect',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
points: Points.oneMillion,
position: Vector2.zero(),
),
);
game.update(0.5);
await tester.pump();
},
verify: (game, tester) async {
final text = game.descendants().whereType<SpriteComponent>().first;
expect(text.firstChild<MoveEffect>(), isNotNull);
},
);
flameTester.testGameWidget(
'is removed once finished',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
points: Points.oneMillion,
position: Vector2.zero(),
),
);
game.update(1);
game.update(0); // Ensure all component removals
await tester.pump();
},
verify: (game, tester) async {
expect(game.children.length, equals(0));
},
);
group('renders correctly', () {
flameTester.testGameWidget(
'5000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
ScoreComponent(
points: Points.fiveThousand,
position: Vector2.zero(),
),
);
game.camera
..followVector2(Vector2.zero())
..zoom = 8;
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/5k.png'),
);
},
);
flameTester.testGameWidget(
'20000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
ScoreComponent(
points: Points.twentyThousand,
position: Vector2.zero(),
),
);
game.camera
..followVector2(Vector2.zero())
..zoom = 8;
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/20k.png'),
);
},
);
flameTester.testGameWidget(
'200000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
ScoreComponent(
points: Points.twoHundredThousand,
position: Vector2.zero(),
),
);
game.camera
..followVector2(Vector2.zero())
..zoom = 8;
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/200k.png'),
);
},
);
flameTester.testGameWidget(
'1000000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
ScoreComponent(
points: Points.oneMillion,
position: Vector2.zero(),
),
);
game.camera
..followVector2(Vector2.zero())
..zoom = 8;
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/1m.png'),
);
},
);
});
});
group('PointsX', () {
test('5k value return 5000', () {
expect(Points.fiveThousand.value, 5000);
});
test('20k value return 20000', () {
expect(Points.twentyThousand.value, 20000);
});
test('200k value return 200000', () {
expect(Points.twoHundredThousand.value, 200000);
});
test('1m value return 1000000', () {
expect(Points.oneMillion.value, 1000000);
});
});
}

@ -1,75 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('ScoreText', () {
final flameTester = FlameTester(TestGame.new);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreText(
text: '123',
position: Vector2.zero(),
color: Colors.white,
),
);
},
verify: (game, tester) async {
final texts = game.descendants().whereType<TextComponent>().length;
expect(texts, equals(1));
},
);
flameTester.testGameWidget(
'has a movement effect',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreText(
text: '123',
position: Vector2.zero(),
color: Colors.white,
),
);
game.update(0.5);
await tester.pump();
},
verify: (game, tester) async {
final text = game.descendants().whereType<TextComponent>().first;
expect(text.firstChild<MoveEffect>(), isNotNull);
},
);
flameTester.testGameWidget(
'is removed once finished',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreText(
text: '123',
position: Vector2.zero(),
color: Colors.white,
),
);
game.update(1);
game.update(0); // Ensure all component removals
},
verify: (game, tester) async {
expect(game.children.length, equals(0));
},
);
});
}

@ -401,13 +401,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
mockingjay:
dependency: "direct dev"
description:
name: mockingjay
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
mocktail: mocktail:
dependency: "direct dev" dependency: "direct dev"
description: description:

@ -49,7 +49,6 @@ dev_dependencies:
flame_test: ^1.3.0 flame_test: ^1.3.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mockingjay: ^0.3.0
mocktail: ^0.3.0 mocktail: ^0.3.0
very_good_analysis: ^2.4.0 very_good_analysis: ^2.4.0

@ -0,0 +1,9 @@
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{folder}/{imageId} {
allow read: if imageId.matches(".*\\.png") || imageId.matches(".*\\.jpg");
allow write: if false;
}
}
}

@ -3,7 +3,7 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';

@ -0,0 +1,136 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
group('MultiballsBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('listenWhen', () {
test(
'is true when the bonusHistory has changed '
'with a new GameBonus.dashNest', () {
final previous = GameState.initial();
final state = previous.copyWith(
bonusHistory: [GameBonus.dashNest],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isTrue,
);
});
test(
'is false when the bonusHistory has changed '
'with a bonus different than GameBonus.dashNest', () {
final previous =
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]);
final state = previous.copyWith(
bonusHistory: [...previous.bonusHistory, GameBonus.androidSpaceship],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isFalse,
);
});
test('is false when the bonusHistory state is the same', () {
final previous = GameState.initial();
final state = GameState(
score: 10,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isFalse,
);
});
});
group('onNewState', () {
flameBlocTester.testGameWidget(
"calls 'onAnimate' once for every multiball",
setUp: (game, tester) async {
final behavior = MultiballsBehavior();
final parent = Multiballs.test();
final multiballCubit = MockMultiballCubit();
final otherMultiballCubit = MockMultiballCubit();
final multiballs = [
Multiball.test(
bloc: multiballCubit,
),
Multiball.test(
bloc: otherMultiballCubit,
),
];
whenListen(
multiballCubit,
const Stream<MultiballState>.empty(),
initialState: MultiballState.initial(),
);
when(multiballCubit.onAnimate).thenAnswer((_) async {});
whenListen(
otherMultiballCubit,
const Stream<MultiballState>.empty(),
initialState: MultiballState.initial(),
);
when(otherMultiballCubit.onAnimate).thenAnswer((_) async {});
await parent.addAll(multiballs);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
await tester.pump();
behavior.onNewState(
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]),
);
for (final multiball in multiballs) {
verify(
multiball.bloc.onAnimate,
).called(1);
}
},
);
});
});
}

@ -0,0 +1,54 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('Multiballs', () {
flameBlocTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.ensureAdd(multiballs);
expect(game.contains(multiballs), isTrue);
},
);
group('loads', () {
flameBlocTester.testGameWidget(
'four Multiball',
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.ensureAdd(multiballs);
expect(
multiballs.descendants().whereType<Multiball>().length,
equals(4),
);
},
);
});
});
}

@ -5,7 +5,7 @@ import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';

@ -18,6 +18,14 @@ class _TestBodyComponent extends BodyComponent {
} }
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
];
group('ScoringBehavior', () { group('ScoringBehavior', () {
group('beginContact', () { group('beginContact', () {
late GameBloc bloc; late GameBloc bloc;
@ -51,12 +59,13 @@ void main() {
whenListen(bloc, Stream.value(state), initialState: state); whenListen(bloc, Stream.value(state), initialState: state);
return bloc; return bloc;
}, },
assets: assets,
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'emits Scored event with points', 'emits Scored event with points',
setUp: (game, tester) async { setUp: (game, tester) async {
const points = 20; const points = Points.oneMillion;
final scoringBehavior = ScoringBehavior(points: points); final scoringBehavior = ScoringBehavior(points: points);
await parent.add(scoringBehavior); await parent.add(scoringBehavior);
final canvas = ZCanvasComponent(children: [parent]); final canvas = ZCanvasComponent(children: [parent]);
@ -66,7 +75,7 @@ void main() {
verify( verify(
() => bloc.add( () => bloc.add(
const Scored(points: points), Scored(points: points.value),
), ),
).called(1); ).called(1);
}, },
@ -75,8 +84,7 @@ void main() {
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'plays score sound', 'plays score sound',
setUp: (game, tester) async { setUp: (game, tester) async {
const points = 20; final scoringBehavior = ScoringBehavior(points: Points.oneMillion);
final scoringBehavior = ScoringBehavior(points: points);
await parent.add(scoringBehavior); await parent.add(scoringBehavior);
final canvas = ZCanvasComponent(children: [parent]); final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
@ -88,9 +96,9 @@ void main() {
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
"adds a ScoreText component at Ball's position with points", "adds a ScoreComponent at Ball's position with points",
setUp: (game, tester) async { setUp: (game, tester) async {
const points = 20; const points = Points.oneMillion;
final scoringBehavior = ScoringBehavior(points: points); final scoringBehavior = ScoringBehavior(points: points);
await parent.add(scoringBehavior); await parent.add(scoringBehavior);
final canvas = ZCanvasComponent(children: [parent]); final canvas = ZCanvasComponent(children: [parent]);
@ -99,11 +107,11 @@ void main() {
scoringBehavior.beginContact(ball, MockContact()); scoringBehavior.beginContact(ball, MockContact());
await game.ready(); await game.ready();
final scoreText = game.descendants().whereType<ScoreText>(); final scoreText = game.descendants().whereType<ScoreComponent>();
expect(scoreText.length, equals(1)); expect(scoreText.length, equals(1));
expect( expect(
scoreText.first.text, scoreText.first.points,
equals(points.toString()), equals(points),
); );
expect( expect(
scoreText.first.position, scoreText.first.position,

@ -64,6 +64,8 @@ void main() {
Assets.images.launchRamp.ramp.keyName, Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.foregroundRailing.keyName, Assets.images.launchRamp.foregroundRailing.keyName,
Assets.images.launchRamp.backgroundRailing.keyName, Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
Assets.images.multiplier.x2.lit.keyName, Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName, Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName, Assets.images.multiplier.x3.lit.keyName,
@ -178,6 +180,18 @@ void main() {
); );
}); });
flameBlocTester.test(
'has only one Multiballs',
(game) async {
await game.ready();
expect(
game.descendants().whereType<Multiballs>().length,
equals(1),
);
},
);
flameBlocTester.test( flameBlocTester.test(
'one GoogleWord', 'one GoogleWord',
(game) async { (game) async {
@ -400,54 +414,9 @@ void main() {
game.onTapDown(tapDownEvent); game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7)); game.update(1);
});
flameTester.test('tap up releases plunger', (game) async {
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
game.onTapUp(tapUpEvent);
expect(plunger.body.linearVelocity.y, equals(0));
});
flameTester.test('tap cancel releases plunger', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
game.onTapCancel();
expect(plunger.body.linearVelocity.y, equals(0)); expect(plunger.body.linearVelocity.y, isPositive);
}); });
}); });
}); });

@ -24,6 +24,7 @@ class MockCallback extends Mock {
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
const animationDuration = 6;
setUp(() async { setUp(() async {
// TODO(arturplaczek): need to find for a better solution for loading image // TODO(arturplaczek): need to find for a better solution for loading image
@ -102,7 +103,7 @@ void main() {
await tester.pump(); await tester.pump();
await Future<void>.delayed(const Duration(seconds: 4)); await Future<void>.delayed(const Duration(seconds: animationDuration));
await tester.pump(); await tester.pump();
@ -133,7 +134,7 @@ void main() {
.state(find.byType(BonusAnimation)) .state(find.byType(BonusAnimation))
.didUpdateWidget(secondAnimation); .didUpdateWidget(secondAnimation);
await Future<void>.delayed(const Duration(seconds: 4)); await Future<void>.delayed(const Duration(seconds: animationDuration));
await tester.pump(); await tester.pump();

@ -147,7 +147,7 @@ void main() {
await tester.pump(); await tester.pump();
// TODO(arturplaczek): remove magic number once this is merged: // TODO(arturplaczek): remove magic number once this is merged:
// https://github.com/flame-engine/flame/pull/1564 // https://github.com/flame-engine/flame/pull/1564
await Future<void>.delayed(const Duration(seconds: 4)); await Future<void>.delayed(const Duration(seconds: 6));
await expectLater(find.byType(ScoreView), findsOneWidget); await expectLater(find.byType(ScoreView), findsOneWidget);
}); });

@ -1,8 +1,10 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/flame.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
@ -12,7 +14,12 @@ void main() {
late GameFlowController gameFlowController; late GameFlowController gameFlowController;
late CharacterThemeCubit characterThemeCubit; late CharacterThemeCubit characterThemeCubit;
setUp(() { setUp(() async {
Flame.images.prefix = '';
await Flame.images.load(const DashTheme().animation.keyName);
await Flame.images.load(const AndroidTheme().animation.keyName);
await Flame.images.load(const DinoTheme().animation.keyName);
await Flame.images.load(const SparkyTheme().animation.keyName);
game = MockPinballGame(); game = MockPinballGame();
gameFlowController = MockGameFlowController(); gameFlowController = MockGameFlowController();
characterThemeCubit = MockCharacterThemeCubit(); characterThemeCubit = MockCharacterThemeCubit();
@ -49,7 +56,7 @@ void main() {
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
); );
await tester.tap(find.text('Play')); await tester.tap(find.text('Play'));
await tester.pumpAndSettle(); await tester.pump();
expect(find.byType(CharacterSelectionDialog), findsOneWidget); expect(find.byType(CharacterSelectionDialog), findsOneWidget);
}); });
}); });

@ -9,7 +9,6 @@ export 'fakes.dart';
export 'forge2d.dart'; export 'forge2d.dart';
export 'key_testers.dart'; export 'key_testers.dart';
export 'mocks.dart'; export 'mocks.dart';
export 'navigator.dart';
export 'pump_app.dart'; export 'pump_app.dart';
export 'test_games.dart'; export 'test_games.dart';
export 'text_span.dart'; export 'text_span.dart';

@ -95,9 +95,7 @@ class MockAndroidBumper extends Mock implements AndroidBumper {}
class MockSparkyBumper extends Mock implements SparkyBumper {} class MockSparkyBumper extends Mock implements SparkyBumper {}
class MockMultiplier extends Mock implements Multiplier {} class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultipliersGroup extends Mock implements Multipliers {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {} class MockMultiplierCubit extends Mock implements MultiplierCubit {}

@ -1,37 +0,0 @@
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);
}

@ -11,7 +11,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
@ -51,7 +51,6 @@ MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() {
extension PumpApp on WidgetTester { extension PumpApp on WidgetTester {
Future<void> pumpApp( Future<void> pumpApp(
Widget widget, { Widget widget, {
MockNavigator? navigator,
GameBloc? gameBloc, GameBloc? gameBloc,
StartGameBloc? startGameBloc, StartGameBloc? startGameBloc,
AssetsManagerCubit? assetsManagerCubit, AssetsManagerCubit? assetsManagerCubit,
@ -92,9 +91,7 @@ extension PumpApp on WidgetTester {
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
], ],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null home: widget,
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
), ),
), ),
), ),

@ -73,5 +73,25 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(HowToPlayDialog), findsNothing); expect(find.byType(HowToPlayDialog), findsNothing);
}); });
testWidgets('can be dismissed', (tester) async {
await tester.pumpApp(
Builder(
builder: (context) {
return TextButton(
onPressed: () => showHowToPlayDialog(context),
child: const Text('test'),
);
},
),
);
expect(find.byType(HowToPlayDialog), findsNothing);
await tester.tap(find.text('test'));
await tester.pumpAndSettle();
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
expect(find.byType(HowToPlayDialog), findsNothing);
});
}); });
} }

@ -1,4 +1,5 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/flame.dart';
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:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -10,9 +11,15 @@ import 'package:pinball_ui/pinball_ui.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late CharacterThemeCubit characterThemeCubit; late CharacterThemeCubit characterThemeCubit;
setUp(() { setUp(() async {
Flame.images.prefix = '';
await Flame.images.load(const DashTheme().animation.keyName);
await Flame.images.load(const AndroidTheme().animation.keyName);
await Flame.images.load(const DinoTheme().animation.keyName);
await Flame.images.load(const SparkyTheme().animation.keyName);
characterThemeCubit = MockCharacterThemeCubit(); characterThemeCubit = MockCharacterThemeCubit();
whenListen( whenListen(
characterThemeCubit, characterThemeCubit,
@ -38,7 +45,7 @@ void main() {
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
); );
await tester.tap(find.text('test')); await tester.tap(find.text('test'));
await tester.pumpAndSettle(); await tester.pump();
expect(find.byType(CharacterSelectionDialog), findsOneWidget); expect(find.byType(CharacterSelectionDialog), findsOneWidget);
}); });
}); });
@ -50,7 +57,7 @@ void main() {
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
); );
await tester.tap(find.byKey(const Key('sparky_character_selection'))); await tester.tap(find.byKey(const Key('sparky_character_selection')));
await tester.pumpAndSettle(); await tester.pump();
verify( verify(
() => characterThemeCubit.characterSelected(const SparkyTheme()), () => characterThemeCubit.characterSelected(const SparkyTheme()),
).called(1); ).called(1);
@ -68,5 +75,47 @@ void main() {
expect(find.byType(CharacterSelectionDialog), findsNothing); expect(find.byType(CharacterSelectionDialog), findsNothing);
expect(find.byType(HowToPlayDialog), findsOneWidget); expect(find.byType(HowToPlayDialog), findsOneWidget);
}); });
testWidgets('updating the selected character updates the preview',
(tester) async {
await tester.pumpApp(_TestCharacterPreview());
expect(find.text('Dash'), findsOneWidget);
await tester.tap(find.text('test'));
await tester.pump();
expect(find.text('Android'), findsOneWidget);
});
});
}
class _TestCharacterPreview extends StatefulWidget {
@override
State<StatefulWidget> createState() => _TestCharacterPreviewState();
}
class _TestCharacterPreviewState extends State<_TestCharacterPreview> {
late CharacterTheme currentCharacter;
@override
void initState() {
super.initState();
currentCharacter = const DashTheme();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: SelectedCharacter(currentCharacter: currentCharacter)),
TextButton(
onPressed: () {
setState(() {
currentCharacter = const AndroidTheme();
}); });
},
child: const Text('test'),
)
],
);
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 917 B

@ -24,27 +24,22 @@
<meta property="og:description" <meta property="og:description"
content="Come play Pinball with your favorite Google Developer Mascots! Built with Flutter & Firebase for Google I/O 2022."> content="Come play Pinball with your favorite Google Developer Mascots! Built with Flutter & Firebase for Google I/O 2022.">
<!-- Open Graph Data -->
<meta property="og:title" content="Google I/O Pinball"> <meta property="og:title" content="Google I/O Pinball">
<!-- TODO(jonathandaniels-vgv): revisit once Google sets up deployments --> <!-- TODO(jonathandaniels-vgv): revisit once Google sets up deployments -->
<meta property="og:url" content="https://flutter.dev"> <meta property="og:url" content="https://flutter.dev">
<!-- TODO(jonathandaniels-vgv): swap this image with updated pinball image -->
<meta name="twitter:image"
content="https://firebasestorage.googleapis.com/v0/b/io-photobooth-dev.appspot.com/o/public%2Fphotobooth-metadata-image.jpeg?alt=media">
<!-- TODO(jonathandaniels-vgv): swap this image with updated pinball image -->
<meta property="og:image" <meta property="og:image"
content="https://firebasestorage.googleapis.com/v0/b/io-photobooth-dev.appspot.com/o/public%2Fphotobooth-metadata-image.jpeg?alt=media"> content="https://firebasestorage.googleapis.com/v0/b/pinball-dev.appspot.com/o/images%2Fpinball_share_image.png?alt=media">
<!-- Twitter Share Data -->
<meta name="twitter:image"
content="https://firebasestorage.googleapis.com/v0/b/pinball-dev.appspot.com/o/images%2Fpinball_share_image.png?alt=media">
<meta name="twitter:text:title" content="I/O Pinball Machine - Flutter"> <meta name="twitter:text:title" content="I/O Pinball Machine - Flutter">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="I/O Pinball Machine - Flutter"> <meta name="twitter:title" content="I/O Pinball Machine - Flutter">
<meta name="twitter:description" <meta name="twitter:description"
content="Come play Pinball with your favorite Google Developer Mascots! Built with Flutter & Firebase for Google I/O 2022."> content="Come play Pinball with your favorite Google Developer Mascots! Built with Flutter & Firebase for Google I/O 2022.">
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">

Loading…
Cancel
Save