Merge branch 'main' into feat/connect-theming

pull/375/head
Allison Ryan 3 years ago
commit f09a825c7f

@ -11,6 +11,6 @@ jobs:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with: with:
flutter_channel: stable flutter_channel: stable
flutter_version: 2.10.0 flutter_version: 2.10.5
coverage_excludes: "lib/gen/*.dart" coverage_excludes: "lib/gen/*.dart"
test_optimization: false test_optimization: false

@ -39,6 +39,7 @@ class App extends StatelessWidget {
providers: [ providers: [
BlocProvider(create: (_) => CharacterThemeCubit()), BlocProvider(create: (_) => CharacterThemeCubit()),
BlocProvider(create: (_) => StartGameBloc()), BlocProvider(create: (_) => StartGameBloc()),
BlocProvider(create: (_) => GameBloc()),
], ],
child: MaterialApp( child: MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',

@ -1,76 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template footer}
/// Footer widget with links to the main tech stack.
/// {@endtemplate}
class Footer extends StatelessWidget {
/// {@macro footer}
const Footer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
_MadeWithFlutterAndFirebase(),
_GoogleIO(),
],
),
);
}
}
class _GoogleIO extends StatelessWidget {
const _GoogleIO({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return Text(
l10n.footerGoogleIOText,
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
);
}
}
class _MadeWithFlutterAndFirebase extends StatelessWidget {
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: l10n.footerMadeWithText,
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
children: <TextSpan>[
TextSpan(
text: l10n.footerFlutterLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink('https://flutter.dev'),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' & '),
TextSpan(
text: l10n.footerFirebaseLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink('https://firebase.google.com'),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
],
),
);
}
}

@ -23,13 +23,15 @@ class BallSpawningBehavior extends Component
void onNewState(GameState state) { void onNewState(GameState state) {
final plunger = gameRef.descendants().whereType<Plunger>().single; final plunger = gameRef.descendants().whereType<Plunger>().single;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single; final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
final characterThemeBloc = readProvider<CharacterThemeCubit>(); final characterTheme =
final ball = ControlledBall.launch( readProvider<CharacterThemeCubit>().state.characterTheme;
characterTheme: characterThemeBloc.state.characterTheme, final ball = Ball(assetPath: characterTheme.ball.keyName)
)..initialPosition = Vector2( ..initialPosition = Vector2(
plunger.body.position.x, plunger.body.position.x,
plunger.body.position.y - Ball.size.y, plunger.body.position.y - Ball.size.y,
); )
..layer = Layer.launcher
..zIndex = ZIndexes.ballOnLaunchRamp;
canvas.add(ball); canvas.add(ball);
} }

@ -1,5 +1,6 @@
export 'ball_spawning_behavior.dart'; export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart'; export 'ball_theming_behavior.dart';
export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart'; export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart'; export 'camera_focusing_behavior.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';

@ -0,0 +1,41 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Behavior that handles playing a bonus sound effect
class BonusNoiseBehavior extends Component {
@override
Future<void> onLoad() async {
await add(
FlameBlocListener<GameBloc, GameState>(
listenWhen: (previous, current) {
return previous.bonusHistory.length != current.bonusHistory.length;
},
onNewState: (state) {
final bonus = state.bonusHistory.last;
final audioPlayer = readProvider<PinballPlayer>();
switch (bonus) {
case GameBonus.googleWord:
audioPlayer.play(PinballAudio.google);
break;
case GameBonus.sparkyTurboCharge:
audioPlayer.play(PinballAudio.sparky);
break;
case GameBonus.dinoChomp:
// TODO(erickzanardo): Add sound
break;
case GameBonus.androidSpaceship:
// TODO(erickzanardo): Add sound
break;
case GameBonus.dashNest:
// TODO(erickzanardo): Add sound
break;
}
},
),
);
}
}

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
@ -12,4 +11,4 @@ export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart'; export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart'; export 'multipliers/multipliers.dart';
export 'sparky_scorch.dart'; export 'sparky_scorch/sparky_scorch.dart';

@ -1,66 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
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';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball}
/// A [Ball] with a [BallController] attached.
///
/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is
/// not over, a new [Ball] will be spawned.
/// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
ControlledBall.launch({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
layer = Layer.launcher;
zIndex = ZIndexes.ballOnLaunchRamp;
}
/// {@macro controlled_ball}
ControlledBall.bonus({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super() {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
class BallController extends ComponentController<Ball>
with FlameBlocReader<GameBloc, GameState> {
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge
/// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async {
bloc.add(const SparkyTurboChargeActivated());
component.stop();
// TODO(alestiago): Refactor this hard coded duration once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1564
await Future<void>.delayed(
const Duration(milliseconds: 2583),
);
component.resume();
await component.add(
BallTurboChargingBehavior(impulse: Vector2(40, 110)),
);
}
}

@ -41,11 +41,12 @@ class FlutterForestBonusBehavior extends Component
if (signpost.bloc.isFullyProgressed()) { if (signpost.bloc.isFullyProgressed()) {
bloc.add(const BonusActivated(GameBonus.dashNest)); bloc.add(const BonusActivated(GameBonus.dashNest));
final characterTheme =
readProvider<CharacterThemeCubit>().state.characterTheme;
canvas.add( canvas.add(
ControlledBall.bonus( Ball(assetPath: characterTheme.ball.keyName)
characterTheme: ..initialPosition = Vector2(29.2, -24.5)
readProvider<CharacterThemeCubit>().state.characterTheme, ..zIndex = ZIndexes.ballOnBoard,
)..initialPosition = Vector2(29.2, -24.5),
); );
animatronic.playing = true; animatronic.playing = true;
signpost.bloc.onProgressed(); signpost.bloc.onProgressed();

@ -1,7 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -22,7 +21,6 @@ class GoogleWordBonusBehavior extends Component
.every((letter) => letter.bloc.state == GoogleLetterState.lit); .every((letter) => letter.bloc.state == GoogleLetterState.lit);
if (achievedBonus) { if (achievedBonus) {
readProvider<PinballPlayer>().play(PinballAudio.google);
bloc.add(const BonusActivated(GameBonus.googleWord)); bloc.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) { for (final letter in googleLetters) {
letter.bloc.onReset(); letter.bloc.onReset();

@ -0,0 +1 @@
export 'sparky_computer_bonus_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';
/// Adds a [GameBonus.sparkyTurboCharge] when a [Ball] enters the
/// [SparkyComputer].
class SparkyComputerBonusBehavior extends Component
with ParentIsA<SparkyScorch>, FlameBlocReader<GameBloc, GameState> {
@override
void onMount() {
super.onMount();
final sparkyComputer = parent.firstChild<SparkyComputer>()!;
final animatronic = parent.firstChild<SparkyAnimatronic>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
sparkyComputer.bloc.stream.listen((state) async {
final listenWhen = state == SparkyComputerState.withBall;
if (!listenWhen) return;
bloc.add(const BonusActivated(GameBonus.sparkyTurboCharge));
animatronic.playing = true;
});
}
}

@ -1,9 +1,9 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/components.dart'; import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_scorch} /// {@template sparky_scorch}
@ -33,51 +33,20 @@ class SparkyScorch extends Component {
BumperNoiseBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9),
SparkyAnimatronic()..position = Vector2(-14, -58.2), SparkyAnimatronic()..position = Vector2(-14, -58.2),
SparkyComputer(), SparkyComputer(
],
);
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
class SparkyComputerSensor extends BodyComponent
with InitialPosition, ContactCallbacks {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor()
: super(
renderBody: false,
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['turbo_charge_sensor']),
],
),
SparkyComputerBonusBehavior(),
], ],
); );
@override /// Creates [SparkyScorch] without any children.
Body createBody() { ///
final shape = PolygonShape() /// This can be used for testing [SparkyScorch]'s behaviors in isolation.
..setAsBox( @visibleForTesting
1, SparkyScorch.test();
0.1,
Vector2.zero(),
-0.18,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.turboCharge();
gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
}
} }

@ -24,7 +24,8 @@ class PinballGame extends PinballForge2DGame
required GameBloc gameBloc, required GameBloc gameBloc,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballPlayer player,
}) : _gameBloc = gameBloc, }) : focusNode = FocusNode(),
_gameBloc = gameBloc,
_player = player, _player = player,
_characterThemeBloc = characterThemeBloc, _characterThemeBloc = characterThemeBloc,
_l10n = l10n, _l10n = l10n,
@ -40,6 +41,8 @@ class PinballGame extends PinballForge2DGame
@override @override
Color backgroundColor() => Colors.transparent; Color backgroundColor() => Colors.transparent;
final FocusNode focusNode;
final CharacterThemeCubit _characterThemeBloc; final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player; final PinballPlayer _player;
@ -71,6 +74,7 @@ class PinballGame extends PinballForge2DGame
FlameProvider<AppLocalizations>.value(_l10n), FlameProvider<AppLocalizations>.value(_l10n),
], ],
children: [ children: [
BonusNoiseBehavior(),
GameBlocStatusListener(), GameBlocStatusListener(),
BallSpawningBehavior(), BallSpawningBehavior(),
BallThemingBehavior(), BallThemingBehavior(),
@ -195,8 +199,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
if (info.raw.kind == PointerDeviceKind.mouse) { if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single; final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug() final ball = Ball()..initialPosition = info.eventPosition.game;
..initialPosition = info.eventPosition.game;
canvas.add(ball); canvas.add(ball);
} }
} }
@ -223,7 +226,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
void _turboChargeBall(Vector2 line) { void _turboChargeBall(Vector2 line) {
final canvas = descendants().whereType<ZCanvasComponent>().single; final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()..initialPosition = lineStart!; final ball = Ball()..initialPosition = lineStart!;
final impulse = line * -1 * 10; final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse)); ball.add(BallTurboChargingBehavior(impulse: impulse));
canvas.add(ball); canvas.add(ball);
@ -269,7 +272,7 @@ class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
void render(Canvas canvas) { void render(Canvas canvas) {
final debugText = [ final debugText = [
'FPS: ${gameRef.fps().toStringAsFixed(1)}', 'FPS: ${gameRef.fps().toStringAsFixed(1)}',
'BALLS: ${gameRef.descendants().whereType<ControlledBall>().length}', 'BALLS: ${gameRef.descendants().whereType<Ball>().length}',
].join(' | '); ].join(' | ');
final height = _debugTextPaint.measureTextHeight(debugText); final height = _debugTextPaint.measureTextHeight(debugText);

@ -7,7 +7,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -21,15 +23,9 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode; final bool isDebugMode;
static Route route({ static Route route({bool isDebugMode = kDebugMode}) {
bool isDebugMode = kDebugMode,
}) {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (context) { builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
return PinballGamePage(
isDebugMode: isDebugMode,
);
},
); );
} }
@ -38,11 +34,6 @@ class PinballGamePage extends StatelessWidget {
final characterThemeBloc = context.read<CharacterThemeCubit>(); final characterThemeBloc = context.read<CharacterThemeCubit>();
final player = context.read<PinballPlayer>(); final player = context.read<PinballPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>(); final leaderboardRepository = context.read<LeaderboardRepository>();
return BlocProvider(
create: (_) => GameBloc(),
child: Builder(
builder: (context) {
final gameBloc = context.read<GameBloc>(); final gameBloc = context.read<GameBloc>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame( ? DebugPinballGame(
@ -71,9 +62,6 @@ class PinballGamePage extends StatelessWidget {
create: (_) => AssetsManagerCubit(loadables)..load(), create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game), child: PinballGameView(game: game),
); );
},
),
);
} }
} }
@ -117,8 +105,15 @@ class PinballGameLoadedView extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: MouseRegion(
onHover: (_) {
if (!game.focusNode.hasFocus) {
game.focusNode.requestFocus();
}
},
child: GameWidget<PinballGame>( child: GameWidget<PinballGame>(
game: game, game: game,
focusNode: game.focusNode,
initialActiveOverlays: const [PinballGame.playButtonOverlay], initialActiveOverlays: const [PinballGame.playButtonOverlay],
overlayBuilderMap: { overlayBuilderMap: {
PinballGame.playButtonOverlay: (context, game) { PinballGame.playButtonOverlay: (context, game) {
@ -132,7 +127,9 @@ class PinballGameLoadedView extends StatelessWidget {
}, },
), ),
), ),
),
const _PositionedGameHud(), const _PositionedGameHud(),
const _PositionedInfoIcon(),
], ],
), ),
); );
@ -150,6 +147,7 @@ class _PositionedGameHud extends StatelessWidget {
final isGameOver = context.select( final isGameOver = context.select(
(GameBloc bloc) => bloc.state.status.isGameOver, (GameBloc bloc) => bloc.state.status.isGameOver,
); );
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
@ -165,3 +163,27 @@ class _PositionedGameHud extends StatelessWidget {
); );
} }
} }
class _PositionedInfoIcon extends StatelessWidget {
const _PositionedInfoIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
child: BlocBuilder<GameBloc, GameState>(
builder: (context, state) {
return Visibility(
visible: state.status.isGameOver,
child: IconButton(
iconSize: 50,
icon: Assets.images.linkBox.infoIcon.image(),
onPressed: () => showMoreInformationDialog(context),
),
);
},
),
);
}
}

@ -104,22 +104,6 @@
"@toSubmit": { "@toSubmit": {
"description": "Ending text displayed on initials input screen informational text span" "description": "Ending text displayed on initials input screen informational text span"
}, },
"footerMadeWithText": "Made with ",
"@footerMadeWithText": {
"description": "Text shown on the footer which mentions technologies used to build the app."
},
"footerFlutterLinkText": "Flutter",
"@footerFlutterLinkText": {
"description": "Text on the link shown on the footer which navigates to the Flutter page"
},
"footerFirebaseLinkText": "Firebase",
"@footerFirebaseLinkText": {
"description": "Text on the link shown on the footer which navigates to the Firebase page"
},
"footerGoogleIOText": "Google I/O",
"@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O"
},
"linkBoxTitle": "Resources", "linkBoxTitle": "Resources",
"@linkBoxTitle": { "@linkBoxTitle": {
"description": "Text shown on the link box title section." "description": "Text shown on the link box title section."

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

@ -0,0 +1,218 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Inflates [MoreInformationDialog] using [showDialog].
Future<void> showMoreInformationDialog(BuildContext context) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
return showDialog<void>(
context: context,
barrierColor: PinballColors.transparent,
barrierDismissible: true,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth * 0.87,
width: gameWidgetWidth,
child: const MoreInformationDialog(),
),
);
},
);
}
/// {@template more_information_dialog}
/// Dialog used to show informational links
/// {@endtemplate}
class MoreInformationDialog extends StatelessWidget {
/// {@macro more_information_dialog}
const MoreInformationDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: PinballColors.transparent,
child: _LinkBoxDecoration(
child: Column(
children: const [
SizedBox(height: 16),
_LinkBoxHeader(),
Expanded(
child: _LinkBoxBody(),
),
],
),
),
);
}
}
class _LinkBoxHeader extends StatelessWidget {
const _LinkBoxHeader({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final indent = MediaQuery.of(context).size.width / 5;
return Column(
children: [
Text(
l10n.linkBoxTitle,
style: Theme.of(context).textTheme.headline3!.copyWith(
color: PinballColors.blue,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Divider(
color: PinballColors.white,
endIndent: indent,
indent: indent,
thickness: 2,
),
],
);
}
}
class _LinkBoxDecoration extends StatelessWidget {
const _LinkBoxDecoration({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const CrtBackground().copyWith(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.all(
color: PinballColors.white,
width: 5,
),
),
child: child,
);
}
}
class _LinkBoxBody extends StatelessWidget {
const _LinkBoxBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const _MadeWithFlutterAndFirebase(),
_TextLink(
text: l10n.linkBoxOpenSourceCode,
link: _MoreInformationUrl.openSourceCode,
),
_TextLink(
text: l10n.linkBoxGoogleIOText,
link: _MoreInformationUrl.googleIOEvent,
),
_TextLink(
text: l10n.linkBoxFlutterGames,
link: _MoreInformationUrl.flutterGamesWebsite,
),
_TextLink(
text: l10n.linkBoxHowItsMade,
link: _MoreInformationUrl.howItsMadeArticle,
),
_TextLink(
text: l10n.linkBoxTermsOfService,
link: _MoreInformationUrl.termsOfService,
),
_TextLink(
text: l10n.linkBoxPrivacyPolicy,
link: _MoreInformationUrl.privacyPolicy,
),
],
);
}
}
class _TextLink extends StatelessWidget {
const _TextLink({
Key? key,
required this.text,
required this.link,
}) : super(key: key);
final String text;
final String link;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: () => openLink(link),
child: Text(
text,
style: theme.textTheme.headline5!.copyWith(
color: PinballColors.white,
),
overflow: TextOverflow.ellipsis,
),
);
}
}
class _MadeWithFlutterAndFirebase extends StatelessWidget {
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: l10n.linkBoxMadeWithText,
style: theme.textTheme.headline5!.copyWith(color: PinballColors.white),
children: <TextSpan>[
TextSpan(
text: l10n.linkBoxFlutterLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink(_MoreInformationUrl.flutterWebsite),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' & '),
TextSpan(
text: l10n.linkBoxFirebaseLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink(_MoreInformationUrl.firebaseWebsite),
style: theme.textTheme.headline5!.copyWith(
decoration: TextDecoration.underline,
),
),
],
),
);
}
}
abstract class _MoreInformationUrl {
static const flutterWebsite = 'https://flutter.dev';
static const firebaseWebsite = 'https://firebase.google.com';
static const openSourceCode = 'https://github.com/VGVentures/pinball';
static const googleIOEvent = 'https://events.google.com/io/';
static const flutterGamesWebsite = 'http://flutter.dev/games';
static const howItsMadeArticle =
'https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d';
static const termsOfService = 'https://policies.google.com/terms';
static const privacyPolicy = 'https://policies.google.com/privacy';
}

@ -14,13 +14,13 @@ class $AssetsMusicGen {
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get afterLaunch => 'assets/sfx/after_launch.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3'; String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get launcher => 'assets/sfx/launcher.mp3'; String get launcher => 'assets/sfx/launcher.mp3';
String get sparky => 'assets/sfx/sparky.mp3';
} }
class Assets { class Assets {

@ -25,6 +25,9 @@ enum PinballAudio {
/// Launcher /// Launcher
launcher, launcher,
/// Sparky
sparky,
} }
/// Defines the contract of the creation of an [AudioPool]. /// Defines the contract of the creation of an [AudioPool].
@ -161,6 +164,11 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.google, path: Assets.sfx.google,
), ),
PinballAudio.sparky: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.sparky,
),
PinballAudio.launcher: _SimplePlayAudio( PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,

@ -141,6 +141,10 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'), .onCall('packages/pinball_audio/assets/sfx/google.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/sparky.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio.onCall( () => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
@ -211,7 +215,7 @@ void main() {
}); });
}); });
group('googleBonus', () { group('google', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());
player.play(PinballAudio.google); player.play(PinballAudio.google);
@ -223,6 +227,18 @@ void main() {
}); });
}); });
group('sparky', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.sparky);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.sparky}'),
).called(1);
});
});
group('launcher', () { group('launcher', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());

@ -61,13 +61,13 @@ class ChromeDino extends BodyComponent
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const mouthAngle = -(halfSweepingAngle + 0.28); const mouthAngle = -(halfSweepingAngle + 0.28);
final size = Vector2(5.5, 6); final size = Vector2(6, 6);
final topEdge = PolygonShape() final topEdge = PolygonShape()
..setAsBox( ..setAsBox(
size.x / 2, size.x / 2,
0.1, 0.1,
initialPosition + Vector2(-4.2, -1.4), initialPosition + Vector2(-4, -1.4),
mouthAngle, mouthAngle,
); );
final topEdgeFixtureDef = FixtureDef(topEdge, density: 100); final topEdgeFixtureDef = FixtureDef(topEdge, density: 100);
@ -76,7 +76,7 @@ class ChromeDino extends BodyComponent
..setAsBox( ..setAsBox(
0.1, 0.1,
size.y / 2, size.y / 2,
initialPosition + Vector2(-1.3, 0.5), initialPosition + Vector2(-1, 0.5),
-halfSweepingAngle, -halfSweepingAngle,
); );
final backEdgeFixtureDef = FixtureDef(backEdge, density: 100); final backEdgeFixtureDef = FixtureDef(backEdge, density: 100);
@ -85,7 +85,7 @@ class ChromeDino extends BodyComponent
..setAsBox( ..setAsBox(
size.x / 2, size.x / 2,
0.1, 0.1,
initialPosition + Vector2(-3.5, 4.7), initialPosition + Vector2(-3.3, 4.7),
mouthAngle, mouthAngle,
); );
final bottomEdgeFixtureDef = FixtureDef( final bottomEdgeFixtureDef = FixtureDef(
@ -110,7 +110,7 @@ class ChromeDino extends BodyComponent
..setAsBox( ..setAsBox(
0.2, 0.2,
0.2, 0.2,
initialPosition + Vector2(-3.5, 1.5), initialPosition + Vector2(-3, 1.5),
0, 0,
); );
final insideSensorFixtureDef = FixtureDef( final insideSensorFixtureDef = FixtureDef(

@ -36,5 +36,5 @@ export 'spaceship_rail.dart';
export 'spaceship_ramp/spaceship_ramp.dart'; export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart'; export 'sparky_computer/sparky_computer.dart';
export 'z_indexes.dart'; export 'z_indexes.dart';

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

@ -0,0 +1,35 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_computer_sensor_ball_contact_behavior}
/// When a [Ball] enters the [SparkyComputer] it is stopped for a period of time
/// before a [BallTurboChargingBehavior] is applied to it.
/// {@endtemplate}
class SparkyComputerSensorBallContactBehavior
extends ContactBehavior<SparkyComputer> {
@override
Future<void> beginContact(Object other, Contact contact) async {
super.beginContact(other, contact);
if (other is! Ball) return;
other.stop();
parent.bloc.onBallEntered();
await parent.add(
TimerComponent(
period: 1.5,
removeOnFinish: true,
onTick: () async {
other.resume();
await other.add(
BallTurboChargingBehavior(
impulse: Vector2(40, 110),
),
);
parent.bloc.onBallTurboCharged();
},
),
);
}
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'sparky_computer_state.dart';
class SparkyComputerCubit extends Cubit<SparkyComputerState> {
SparkyComputerCubit() : super(SparkyComputerState.withoutBall);
void onBallEntered() {
emit(SparkyComputerState.withBall);
}
void onBallTurboCharged() {
emit(SparkyComputerState.withoutBall);
}
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'sparky_computer_cubit.dart';
enum SparkyComputerState {
withoutBall,
withBall,
}

@ -2,31 +2,52 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_computer_cubit.dart';
/// {@template sparky_computer} /// {@template sparky_computer}
/// A computer owned by Sparky. /// A computer owned by Sparky.
/// {@endtemplate} /// {@endtemplate}
class SparkyComputer extends Component { class SparkyComputer extends BodyComponent {
/// {@macro sparky_computer} /// {@macro sparky_computer}
SparkyComputer() SparkyComputer({Iterable<Component>? children})
: super( : bloc = SparkyComputerCubit(),
super(
renderBody: false,
children: [ children: [
_ComputerBase(), SparkyComputerSensorBallContactBehavior()
..applyTo(['turbo_charge_sensor']),
_ComputerBaseSpriteComponent(),
_ComputerTopSpriteComponent(), _ComputerTopSpriteComponent(),
_ComputerGlowSpriteComponent(), _ComputerGlowSpriteComponent(),
...?children,
], ],
); );
}
class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { /// Creates a [SparkyComputer] without any children.
_ComputerBase() ///
: super( /// This can be used for testing [SparkyComputer]'s behaviors in isolation.
renderBody: false, // TODO(alestiago): Refactor injecting bloc once the following is merged:
children: [_ComputerBaseSpriteComponent()], // https://github.com/flame-engine/flame/pull/1538
) { @visibleForTesting
zIndex = ZIndexes.computerBase; SparkyComputer.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SparkyComputerCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
@ -45,30 +66,44 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
topEdge.vertex2, topEdge.vertex2,
Vector2(-9.4, -47.1), Vector2(-9.4, -47.1),
); );
final turboChargeSensor = PolygonShape()
..setAsBox(
1,
0.1,
Vector2(-13.2, -49.9),
-0.18,
);
return [ return [
FixtureDef(leftEdge), FixtureDef(leftEdge),
FixtureDef(topEdge), FixtureDef(topEdge),
FixtureDef(rightEdge), FixtureDef(rightEdge),
FixtureDef(
turboChargeSensor,
isSensor: true,
userData: 'turbo_charge_sensor',
),
]; ];
} }
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef(position: initialPosition); final body = world.createBody(BodyDef());
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
} }
class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { class _ComputerBaseSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_ComputerBaseSpriteComponent() _ComputerBaseSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.44, -48.15), position: Vector2(-12.44, -48.15),
); ) {
zIndex = ZIndexes.computerBase;
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {

@ -11,7 +11,7 @@ dependencies:
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame
path: packages/flame_forge2d/ path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:

@ -112,7 +112,7 @@ packages:
path: "packages/flame_forge2d" path: "packages/flame_forge2d"
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
url: "https://github.com/flame-engine/flame/" url: "https://github.com/flame-engine/flame"
source: git source: git
version: "0.11.0" version: "0.11.0"
flutter: flutter:

@ -11,7 +11,7 @@ dependencies:
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame
path: packages/flame_forge2d/ path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

@ -0,0 +1,141 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SparkyComputerSensorBallContactBehavior',
() {
test('can be instantiated', () {
expect(
SparkyComputerSensorBallContactBehavior(),
isA<SparkyComputerSensorBallContactBehavior>(),
);
});
group('beginContact', () {
flameTester.test(
'stops a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
final ball = _MockBall();
await behavior.beginContact(ball, _MockContact());
verify(ball.stop).called(1);
},
);
flameTester.test(
'emits onBallEntered when contacts with a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
await behavior.beginContact(_MockBall(), _MockContact());
verify(sparkyComputer.bloc.onBallEntered).called(1);
},
);
flameTester.test(
'adds TimerComponent when contacts with a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
await behavior.beginContact(_MockBall(), _MockContact());
await game.ready();
expect(
sparkyComputer.firstChild<TimerComponent>(),
isA<TimerComponent>(),
);
},
);
flameTester.test(
'TimerComponent resumes ball and calls onBallTurboCharged onTick',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
final ball = _MockBall();
await behavior.beginContact(ball, _MockContact());
await game.ready();
game.update(
sparkyComputer.firstChild<TimerComponent>()!.timer.limit,
);
await game.ready();
verify(ball.resume).called(1);
verify(sparkyComputer.bloc.onBallTurboCharged).called(1);
},
);
});
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'SparkyComputerCubit',
() {
blocTest<SparkyComputerCubit, SparkyComputerState>(
'onBallEntered emits withBall',
build: SparkyComputerCubit.new,
act: (bloc) => bloc.onBallEntered(),
expect: () => [SparkyComputerState.withBall],
);
blocTest<SparkyComputerCubit, SparkyComputerState>(
'onBallTurboCharged emits withoutBall',
build: SparkyComputerCubit.new,
act: (bloc) => bloc.onBallTurboCharged(),
expect: () => [SparkyComputerState.withoutBall],
);
},
);
}

@ -0,0 +1,93 @@
// 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/sparky_computer/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {}
void main() {
group('SparkyComputer', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.glow.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
final component = SparkyComputer();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(SparkyComputer());
await tester.pump();
game.camera
..followVector2(Vector2(0, -20))
..zoom = 7;
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/sparky-computer.png'),
);
},
);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
when(bloc.close).thenAnswer((_) async {});
final sparkyComputer = SparkyComputer.test(bloc: bloc);
await game.ensureAdd(sparkyComputer);
game.remove(sparkyComputer);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final sparkyComputer = SparkyComputer(
children: [component],
);
await game.ensureAdd(sparkyComputer);
expect(sparkyComputer.children, contains(component));
});
flameTester.test('a SparkyComputerSensorBallContactBehavior',
(game) async {
final sparkyComputer = SparkyComputer();
await game.ensureAdd(sparkyComputer);
expect(
sparkyComputer.children
.whereType<SparkyComputerSensorBallContactBehavior>()
.single,
isNotNull,
);
});
});
});
}

@ -1,45 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('SparkyComputer', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.glow.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
final component = SparkyComputer();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(SparkyComputer());
await tester.pump();
game.camera
..followVector2(Vector2(0, -20))
..zoom = 7;
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/sparky-computer.png'),
);
},
);
});
}

@ -1,4 +1,5 @@
import 'dart:ui'; import 'dart:ui';
import 'package:collection/collection.dart' as collection;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball_flame/src/canvas/canvas_wrapper.dart'; import 'package:pinball_flame/src/canvas/canvas_wrapper.dart';
@ -56,7 +57,14 @@ class _ZCanvas extends CanvasWrapper {
final List<ZIndex> _zBuffer = []; final List<ZIndex> _zBuffer = [];
/// Postpones the rendering of [ZIndex] component and its children. /// Postpones the rendering of [ZIndex] component and its children.
void buffer(ZIndex component) => _zBuffer.add(component); void buffer(ZIndex component) {
final lowerBound = collection.lowerBound<ZIndex>(
_zBuffer,
component,
compare: (a, b) => a.zIndex.compareTo(b.zIndex),
);
_zBuffer.insert(lowerBound, component);
}
/// Renders all [ZIndex] components and their children. /// Renders all [ZIndex] components and their children.
/// ///
@ -69,8 +77,7 @@ class _ZCanvas extends CanvasWrapper {
/// before the second one. /// before the second one.
/// {@endtemplate} /// {@endtemplate}
void render() => _zBuffer void render() => _zBuffer
..sort((a, b) => a.zIndex.compareTo(b.zIndex)) ..forEach(_render)
..whereType<Component>().forEach(_render)
..clear(); ..clear();
void _render(Component component) => component.renderTree(canvas); void _render(Component component) => component.renderTree(canvas);

@ -10,7 +10,7 @@ dependencies:
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame
path: packages/flame_forge2d/ path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:

@ -245,7 +245,7 @@ packages:
path: "packages/flame_forge2d" path: "packages/flame_forge2d"
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
url: "https://github.com/flame-engine/flame/" url: "https://github.com/flame-engine/flame"
source: git source: git
version: "0.11.0" version: "0.11.0"
flame_test: flame_test:
@ -821,4 +821,4 @@ packages:
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.16.0 <3.0.0" dart: ">=2.16.0 <3.0.0"
flutter: ">=2.10.0" flutter: ">=2.10.5"

@ -5,6 +5,7 @@ publish_to: none
environment: environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.16.0 <3.0.0"
flutter: 2.10.5
dependencies: dependencies:
authentication_repository: authentication_repository:
@ -18,7 +19,7 @@ dependencies:
flame_bloc: ^1.4.0 flame_bloc: ^1.4.0
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame
path: packages/flame_forge2d/ path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:

@ -0,0 +1,186 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
BonusNoiseBehavior child, {
required PinballPlayer player,
required GameBloc bloc,
}) {
return ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: bloc,
children: [
FlameProvider<PinballPlayer>.value(
player,
children: [
child,
],
),
],
),
);
}
}
class _MockPinballPlayer extends Mock implements PinballPlayer {}
class _MockGameBloc extends Mock implements GameBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BonusNoiseBehavior', () {
late PinballPlayer player;
late GameBloc bloc;
final flameTester = FlameTester(_TestGame.new);
setUpAll(() {
registerFallbackValue(PinballAudio.google);
});
setUp(() {
player = _MockPinballPlayer();
when(() => player.play(any())).thenAnswer((_) {});
bloc = _MockGameBloc();
});
flameTester.testGameWidget(
'plays google sound',
setUp: (game, _) async {
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [GameBonus.googleWord],
status: GameStatus.playing,
);
const initialState = GameState.initial();
whenListen(
bloc,
Stream.fromIterable([initialState, state]),
initialState: initialState,
);
final behavior = BonusNoiseBehavior();
await game.pump(behavior, player: player, bloc: bloc);
},
verify: (_, __) async {
verify(() => player.play(PinballAudio.google)).called(1);
},
);
flameTester.testGameWidget(
'plays sparky sound',
setUp: (game, _) async {
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [GameBonus.sparkyTurboCharge],
status: GameStatus.playing,
);
const initialState = GameState.initial();
whenListen(
bloc,
Stream.fromIterable([initialState, state]),
initialState: initialState,
);
final behavior = BonusNoiseBehavior();
await game.pump(behavior, player: player, bloc: bloc);
},
verify: (_, __) async {
verify(() => player.play(PinballAudio.sparky)).called(1);
},
);
flameTester.testGameWidget(
'plays dino chomp sound',
setUp: (game, _) async {
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [GameBonus.dinoChomp],
status: GameStatus.playing,
);
const initialState = GameState.initial();
whenListen(
bloc,
Stream.fromIterable([initialState, state]),
initialState: initialState,
);
final behavior = BonusNoiseBehavior();
await game.pump(behavior, player: player, bloc: bloc);
},
verify: (_, __) async {
// TODO(erickzanardo): Change when the sound is implemented
verifyNever(() => player.play(any()));
},
);
flameTester.testGameWidget(
'plays android spaceship sound',
setUp: (game, _) async {
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [GameBonus.androidSpaceship],
status: GameStatus.playing,
);
const initialState = GameState.initial();
whenListen(
bloc,
Stream.fromIterable([initialState, state]),
initialState: initialState,
);
final behavior = BonusNoiseBehavior();
await game.pump(behavior, player: player, bloc: bloc);
},
verify: (_, __) async {
// TODO(erickzanardo): Change when the sound is implemented
verifyNever(() => player.play(any()));
},
);
flameTester.testGameWidget(
'plays dash nest sound',
setUp: (game, _) async {
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [GameBonus.dashNest],
status: GameStatus.playing,
);
const initialState = GameState.initial();
whenListen(
bloc,
Stream.fromIterable([initialState, state]),
initialState: initialState,
);
final behavior = BonusNoiseBehavior();
await game.pump(behavior, player: player, bloc: bloc);
},
verify: (_, __) async {
// TODO(erickzanardo): Change when the sound is implemented
verifyNever(() => player.play(any()));
},
);
});
}

@ -1,71 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.load(theme.Assets.images.dash.ball.keyName);
}
Future<void> pump(Ball child, {required GameBloc gameBloc}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockBall extends Mock implements Ball {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BallController', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() {
ball = Ball();
gameBloc = _MockGameBloc();
});
final flameBlocTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
BallController(_MockBall()),
isA<BallController>(),
);
});
flameBlocTester.testGameWidget(
'turboCharge adds TurboChargeActivated',
setUp: (game, tester) async {
await game.onLoad();
final controller = BallController(ball);
await ball.add(controller);
await game.pump(ball, gameBloc: gameBloc);
await controller.turboCharge();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const SparkyTurboChargeActivated()))
.called(1);
},
);
});
}

@ -0,0 +1,86 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.computer.glow.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.sparky.bumper.a.lit.keyName,
Assets.images.sparky.bumper.a.dimmed.keyName,
Assets.images.sparky.bumper.b.lit.keyName,
Assets.images.sparky.bumper.b.dimmed.keyName,
Assets.images.sparky.bumper.c.lit.keyName,
Assets.images.sparky.bumper.c.dimmed.keyName,
]);
}
Future<void> pump(
SparkyScorch child, {
required GameBloc gameBloc,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('SparkyComputerBonusBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.testGameWidget(
'adds GameBonus.sparkyTurboCharge to the game and plays animatronic '
'when SparkyComputerState.withBall is emitted',
setUp: (game, tester) async {
final behavior = SparkyComputerBonusBehavior();
final parent = SparkyScorch.test();
final sparkyComputer = SparkyComputer();
final animatronic = SparkyAnimatronic();
await parent.addAll([
sparkyComputer,
animatronic,
]);
await game.pump(parent, gameBloc: gameBloc);
await parent.ensureAdd(behavior);
sparkyComputer.bloc.onBallEntered();
await tester.pump();
verify(
() => gameBloc.add(const BonusActivated(GameBonus.sparkyTurboCharge)),
).called(1);
expect(animatronic.playing, isTrue);
},
);
});
}

@ -1,10 +1,11 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.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:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/sparky_scorch/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';
@ -25,13 +26,16 @@ class _TestGame extends Forge2DGame {
Assets.images.sparky.bumper.c.dimmed.keyName, Assets.images.sparky.bumper.c.dimmed.keyName,
]); ]);
} }
}
class _MockControlledBall extends Mock implements ControlledBall {}
class _MockBallController extends Mock implements BallController {} Future<void> pump(SparkyScorch child) async {
await ensureAdd(
class _MockContact extends Mock implements Contact {} FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [child],
),
);
}
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -41,15 +45,18 @@ void main() {
group('SparkyScorch', () { group('SparkyScorch', () {
flameTester.test('loads correctly', (game) async { flameTester.test('loads correctly', (game) async {
final component = SparkyScorch(); final component = SparkyScorch();
await game.ensureAdd(component); await game.pump(component);
expect(game.contains(component), isTrue); expect(
game.descendants().whereType<SparkyScorch>().length,
equals(1),
);
}); });
group('loads', () { group('loads', () {
flameTester.test( flameTester.test(
'a SparkyComputer', 'a SparkyComputer',
(game) async { (game) async {
await game.ensureAdd(SparkyScorch()); await game.pump(SparkyScorch());
expect( expect(
game.descendants().whereType<SparkyComputer>().length, game.descendants().whereType<SparkyComputer>().length,
equals(1), equals(1),
@ -60,7 +67,7 @@ void main() {
flameTester.test( flameTester.test(
'a SparkyAnimatronic', 'a SparkyAnimatronic',
(game) async { (game) async {
await game.ensureAdd(SparkyScorch()); await game.pump(SparkyScorch());
expect( expect(
game.descendants().whereType<SparkyAnimatronic>().length, game.descendants().whereType<SparkyAnimatronic>().length,
equals(1), equals(1),
@ -71,7 +78,7 @@ void main() {
flameTester.test( flameTester.test(
'three SparkyBumper', 'three SparkyBumper',
(game) async { (game) async {
await game.ensureAdd(SparkyScorch()); await game.pump(SparkyScorch());
expect( expect(
game.descendants().whereType<SparkyBumper>().length, game.descendants().whereType<SparkyBumper>().length,
equals(3), equals(3),
@ -82,7 +89,7 @@ void main() {
flameTester.test( flameTester.test(
'three SparkyBumpers with BumperNoiseBehavior', 'three SparkyBumpers with BumperNoiseBehavior',
(game) async { (game) async {
await game.ensureAdd(SparkyScorch()); await game.pump(SparkyScorch());
final bumpers = game.descendants().whereType<SparkyBumper>(); final bumpers = game.descendants().whereType<SparkyBumper>();
for (final bumper in bumpers) { for (final bumper in bumpers) {
expect( expect(
@ -93,41 +100,30 @@ void main() {
}, },
); );
}); });
});
group('SparkyComputerSensor', () { group('adds', () {
flameTester.test('calls turboCharge', (game) async { flameTester.test(
final sensor = SparkyComputerSensor(); 'ScoringContactBehavior to SparkyComputer',
final ball = _MockControlledBall(); (game) async {
final controller = _MockBallController(); await game.pump(SparkyScorch());
when(() => ball.controller).thenReturn(controller);
when(controller.turboCharge).thenAnswer((_) async {});
await game.ensureAddAll([
sensor,
SparkyAnimatronic(),
]);
sensor.beginContact(ball, _MockContact()); final sparkyComputer =
game.descendants().whereType<SparkyComputer>().single;
expect(
sparkyComputer.firstChild<ScoringContactBehavior>(),
isNotNull,
);
},
);
verify(() => ball.controller.turboCharge()).called(1); flameTester.test('a SparkyComputerBonusBehavior', (game) async {
final sparkyScorch = SparkyScorch();
await game.pump(sparkyScorch);
expect(
sparkyScorch.children.whereType<SparkyComputerBonusBehavior>().single,
isNotNull,
);
}); });
flameTester.test('plays SparkyAnimatronic', (game) async {
final sensor = SparkyComputerSensor();
final sparkyAnimatronic = SparkyAnimatronic();
final ball = _MockControlledBall();
final controller = _MockBallController();
when(() => ball.controller).thenReturn(controller);
when(controller.turboCharge).thenAnswer((_) async {});
await game.ensureAddAll([
sensor,
sparkyAnimatronic,
]);
expect(sparkyAnimatronic.playing, isFalse);
sensor.beginContact(ball, _MockContact());
expect(sparkyAnimatronic.playing, isTrue);
}); });
}); });
} }

@ -426,14 +426,12 @@ void main() {
when(() => tapUpEvent.raw).thenReturn(raw); when(() => tapUpEvent.raw).thenReturn(raw);
await game.ready(); await game.ready();
final previousBalls = final previousBalls = game.descendants().whereType<Ball>().toList();
game.descendants().whereType<ControlledBall>().toList();
game.onTapUp(0, tapUpEvent); game.onTapUp(0, tapUpEvent);
await game.ready(); await game.ready();
final currentBalls = final currentBalls = game.descendants().whereType<Ball>().toList();
game.descendants().whereType<ControlledBall>().toList();
expect( expect(
currentBalls.length, currentBalls.length,
@ -492,14 +490,13 @@ void main() {
game.lineEnd = endPosition; game.lineEnd = endPosition;
await game.ready(); await game.ready();
final previousBalls = final previousBalls = game.descendants().whereType<Ball>().toList();
game.descendants().whereType<ControlledBall>().toList();
game.onPanEnd(_MockDragEndInfo()); game.onPanEnd(_MockDragEndInfo());
await game.ready(); await game.ready();
expect( expect(
game.descendants().whereType<ControlledBall>().length, game.descendants().whereType<Ball>().length,
equals(previousBalls.length + 1), equals(previousBalls.length + 1),
); );
}, },

@ -1,5 +1,7 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'dart:ui';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -8,7 +10,9 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -79,6 +83,7 @@ void main() {
await tester.pumpApp( await tester.pumpApp(
PinballGamePage(), PinballGamePage(),
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
); );
expect(find.byType(PinballGameView), findsOneWidget); expect(find.byType(PinballGameView), findsOneWidget);
@ -167,6 +172,7 @@ void main() {
), ),
), ),
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
); );
await tester.tap(find.text('Tap me')); await tester.tap(find.text('Tap me'));
@ -290,5 +296,90 @@ void main() {
findsNothing, findsNothing,
); );
}); });
testWidgets('keep focus on game when mouse hovers over it', (tester) async {
final startGameState = StartGameState.initial().copyWith(
status: StartGameStatus.play,
);
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
startGameBloc,
Stream.value(startGameState),
initialState: startGameState,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
game.focusNode.unfocus();
await tester.pump();
expect(game.focusNode.hasFocus, isFalse);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo((game.size / 2).toOffset());
await tester.pump();
expect(game.focusNode.hasFocus, isTrue);
});
group('info icon', () {
testWidgets('renders on game over', (tester) async {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(
find.image(Assets.images.linkBox.infoIcon),
findsOneWidget,
);
});
testWidgets('opens MoreInformationDialog when tapped', (tester) async {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
await tester.tap(find.byType(IconButton));
await tester.pump();
expect(
find.byType(MoreInformationDialog),
findsOneWidget,
);
});
});
}); });
} }

@ -2,7 +2,7 @@ import 'package:flutter/gestures.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';
import 'package:pinball/footer/footer.dart'; import 'package:pinball/more_information/more_information.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart';
@ -28,15 +28,34 @@ class _MockUrlLauncher extends Mock
implements UrlLauncherPlatform {} implements UrlLauncherPlatform {}
void main() { void main() {
group('Footer', () { group('MoreInformationDialog', () {
late UrlLauncherPlatform urlLauncher; late UrlLauncherPlatform urlLauncher;
setUp(() async { setUp(() async {
urlLauncher = _MockUrlLauncher(); urlLauncher = _MockUrlLauncher();
UrlLauncherPlatform.instance = urlLauncher; UrlLauncherPlatform.instance = urlLauncher;
}); });
group('showMoreInformationDialog', () {
testWidgets('inflates the dialog', (tester) async {
await tester.pumpApp(
Builder(
builder: (context) {
return TextButton(
onPressed: () => showMoreInformationDialog(context),
child: const Text('test'),
);
},
),
);
await tester.tap(find.text('test'));
await tester.pump();
expect(find.byType(MoreInformationDialog), findsOneWidget);
});
});
testWidgets('renders "Made with..." and "Google I/O"', (tester) async { testWidgets('renders "Made with..." and "Google I/O"', (tester) async {
await tester.pumpApp(const Footer()); await tester.pumpApp(const MoreInformationDialog());
expect(find.text('Google I/O'), findsOneWidget); expect(find.text('Google I/O'), findsOneWidget);
expect( expect(
find.byWidgetPredicate( find.byWidgetPredicate(
@ -63,7 +82,7 @@ void main() {
headers: any(named: 'headers'), headers: any(named: 'headers'),
), ),
).thenAnswer((_) async => true); ).thenAnswer((_) async => true);
await tester.pumpApp(const Footer()); await tester.pumpApp(const MoreInformationDialog());
final flutterTextFinder = find.byWidgetPredicate( final flutterTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && _tapTextSpan(widget, 'Flutter'), (widget) => widget is RichText && _tapTextSpan(widget, 'Flutter'),
); );
@ -98,7 +117,7 @@ void main() {
headers: any(named: 'headers'), headers: any(named: 'headers'),
), ),
).thenAnswer((_) async => true); ).thenAnswer((_) async => true);
await tester.pumpApp(const Footer()); await tester.pumpApp(const MoreInformationDialog());
final firebaseTextFinder = find.byWidgetPredicate( final firebaseTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && _tapTextSpan(widget, 'Firebase'), (widget) => widget is RichText && _tapTextSpan(widget, 'Firebase'),
); );
@ -117,5 +136,50 @@ void main() {
); );
}, },
); );
<String, String>{
'Open Source Code': 'https://github.com/VGVentures/pinball',
'Google I/O': 'https://events.google.com/io/',
'Flutter Games': 'http://flutter.dev/games',
'How its made':
'https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d',
'Terms of Service': 'https://policies.google.com/terms',
'Privacy Policy': 'https://policies.google.com/privacy',
}.forEach((text, link) {
testWidgets(
'tapping on "$text" opens the link - $link',
(tester) async {
when(() => urlLauncher.canLaunch(any()))
.thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
link,
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
await tester.pumpApp(const MoreInformationDialog());
await tester.tap(find.text(text));
await tester.pumpAndSettle();
verify(
() => urlLauncher.launch(
link,
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
},
);
});
}); });
} }
Loading…
Cancel
Save