diff --git a/firebase.json b/firebase.json index 80e2ae69..1338aeba 100644 --- a/firebase.json +++ b/firebase.json @@ -1,11 +1,33 @@ { + "firestore": { + "rules": "firestore.rules" + }, "hosting": { "public": "build/web", "site": "ashehwkdkdjruejdnensjsjdne", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "headers": [ + { + "source": "**/*.@(jpg|jpeg|gif|png)", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=3600" + } + ] + }, + { + "source": "**", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache, no-store, must-revalidate" + } + ] + } ] + }, + "storage": { + "rules": "storage.rules" } } diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 00000000..db8d29c1 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,29 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /leaderboard/{userId} { + + function prohibited(initials) { + let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials; + return initials in prohibitedInitials; + } + + function inCharLimit(initials) { + return initials.size() < 4; + } + + function isAuthedUser(auth) { + return request.auth.uid != null; && auth.token.firebase.sign_in_provider == "anonymous" + } + + // Leaderboard can be read if it doesn't contain any prohibited initials + allow read: if !prohibited(resource.data.playerInitials); + + // A leaderboard entry can be created if the user is authenticated, + // it's 3 characters long, and not a prohibited combination. + allow create: if isAuthedUser(request.auth) && + inCharLimit(request.resource.data.playerInitials) && + !prohibited(request.resource.data.playerInitials); + } + } +} \ No newline at end of file diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index d778b55b..de46512b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -8,6 +8,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -34,8 +35,11 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], - child: BlocProvider( - create: (context) => CharacterThemeCubit(), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => CharacterThemeCubit()), + BlocProvider(create: (_) => StartGameBloc()), + ], child: MaterialApp( title: 'I/O Pinball', theme: PinballTheme.standard, diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 82b71741..649ef196 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -15,7 +15,16 @@ class AndroidAcres extends Component { AndroidAcres() : super( children: [ - SpaceshipRamp(), + SpaceshipRamp( + children: [ + RampShotBehavior( + points: Points.fiveThousand, + ), + RampBonusBehavior( + points: Points.oneMillion, + ), + ], + ), SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart index e4ac5981..91b1e132 100644 --- a/lib/game/components/android_acres/behaviors/behaviors.dart +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -1 +1,3 @@ export 'android_spaceship_bonus_behavior.dart'; +export 'ramp_bonus_behavior.dart'; +export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart new file mode 100644 index 00000000..218ad8b4 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_bonus_behavior} +/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. +/// {@endtemplate} +class RampBonusBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_bonus_behavior} + RampBonusBehavior({ + required Points points, + }) : _points = points, + super(); + + /// Creates a [RampBonusBehavior]. + /// + /// This can be used for testing [RampBonusBehavior] in isolation. + @visibleForTesting + RampBonusBehavior.test({ + required Points points, + required this.subscription, + }) : _points = points, + super(); + + final Points _points; + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final achievedOneMillionPoints = state.hits % 10 == 0; + + if (achievedOneMillionPoints) { + parent.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -60), + duration: 2, + ), + ); + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart new file mode 100644 index 00000000..8a9c1a9c --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_shot_behavior} +/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. +/// {@endtemplate} +class RampShotBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_shot_behavior} + RampShotBehavior({ + required Points points, + }) : _points = points, + super(); + + /// Creates a [RampShotBehavior]. + /// + /// This can be used for testing [RampShotBehavior] in isolation. + @visibleForTesting + RampShotBehavior.test({ + required Points points, + required this.subscription, + }) : _points = points, + super(); + + final Points _points; + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final achievedOneMillionPoints = state.hits % 10 == 0; + + if (!achievedOneMillionPoints) { + gameRef.read().add(const MultiplierIncreased()); + + parent.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -45), + ), + ); + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 9dc81135..132639d4 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -17,7 +16,7 @@ class ControlledBall extends Ball with Controls { /// A [Ball] that launches from the [Plunger]. ControlledBall.launch({ required CharacterTheme characterTheme, - }) : super(baseColor: characterTheme.ballColor) { + }) : super(assetPath: characterTheme.ball.keyName) { controller = BallController(this); layer = Layer.launcher; zIndex = ZIndexes.ballOnLaunchRamp; @@ -28,13 +27,13 @@ class ControlledBall extends Ball with Controls { /// {@endtemplate} ControlledBall.bonus({ required CharacterTheme characterTheme, - }) : super(baseColor: characterTheme.ballColor) { + }) : super(assetPath: characterTheme.ball.keyName) { controller = BallController(this); zIndex = ZIndexes.ballOnBoard; } /// [Ball] used in [DebugPinballGame]. - ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { + ControlledBall.debug() : super() { controller = BallController(this); zIndex = ZIndexes.ballOnBoard; } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 4e7d65ae..ac324417 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -13,7 +13,6 @@ extension PinballGameAssetsX on PinballGame { return [ images.load(components.Assets.images.boardBackground.keyName), - images.load(components.Assets.images.ball.ball.keyName), images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.signpost.inactive.keyName), images.load(components.Assets.images.signpost.active1.keyName), @@ -140,6 +139,10 @@ extension PinballGameAssetsX on PinballGame { images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName), + images.load(androidTheme.ball.keyName), + images.load(dashTheme.ball.keyName), + images.load(dinoTheme.ball.keyName), + images.load(sparkyTheme.ball.keyName), ]; } } diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 3a626ba4..21b9ac5c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -60,7 +60,6 @@ class PinballGamePage extends StatelessWidget { return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), ], @@ -105,36 +104,43 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { + final isPlaying = context.select( + (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, + ); final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final screenWidth = MediaQuery.of(context).size.width; final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); - return Stack( - children: [ - Positioned.fill( - child: GameWidget( - game: game, - initialActiveOverlays: const [PinballGame.playButtonOverlay], - overlayBuilderMap: { - PinballGame.playButtonOverlay: (context, game) { - return Positioned( - bottom: 20, - right: 0, - left: 0, - child: PlayButtonOverlay(game: game), - ); + return StartGameListener( + game: game, + child: Stack( + children: [ + Positioned.fill( + child: GameWidget( + game: game, + initialActiveOverlays: const [PinballGame.playButtonOverlay], + overlayBuilderMap: { + PinballGame.playButtonOverlay: (context, game) { + return const Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(), + ); + }, }, - }, + ), ), - ), - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - Positioned( - top: 16, - left: leftMargin, - child: const GameHud(), - ), - ], + Positioned( + top: 16, + left: leftMargin, + child: Visibility( + visible: isPlaying, + child: const GameHud(), + ), + ), + ], + ), ); } } diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 1d4a10fb..7a954c77 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:pinball/game/pinball_game.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_ui/pinball_ui.dart'; /// {@template play_button_overlay} @@ -9,13 +9,7 @@ import 'package:pinball_ui/pinball_ui.dart'; /// {@endtemplate} class PlayButtonOverlay extends StatelessWidget { /// {@macro play_button_overlay} - const PlayButtonOverlay({ - Key? key, - required PinballGame game, - }) : _game = game, - super(key: key); - - final PinballGame _game; + const PlayButtonOverlay({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -23,9 +17,8 @@ class PlayButtonOverlay extends StatelessWidget { return PinballButton( text: l10n.play, - onTap: () async { - _game.gameFlowController.start(); - await showCharacterSelectionDialog(context); + onTap: () { + context.read().add(const PlayTapped()); }, ); } diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index e91698f5..426fcbe5 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -3,10 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; import 'package:platform_helper/platform_helper.dart'; @@ -51,24 +49,16 @@ extension on Control { } } -Future showHowToPlayDialog(BuildContext context) { - final audio = context.read(); - return showDialog( - context: context, - builder: (_) => HowToPlayDialog(), - ).then((_) { - audio.ioPinballVoiceOver(); - }); -} - class HowToPlayDialog extends StatefulWidget { HowToPlayDialog({ Key? key, + required this.onDismissCallback, @visibleForTesting PlatformHelper? platformHelper, }) : platformHelper = platformHelper ?? PlatformHelper(), super(key: key); final PlatformHelper platformHelper; + final VoidCallback onDismissCallback; @override State createState() => _HowToPlayDialogState(); @@ -82,6 +72,7 @@ class _HowToPlayDialogState extends State { closeTimer = Timer(const Duration(seconds: 3), () { if (mounted) { Navigator.of(context).pop(); + widget.onDismissCallback.call(); } }); } @@ -96,10 +87,17 @@ class _HowToPlayDialogState extends State { Widget build(BuildContext context) { final isMobile = widget.platformHelper.isMobile; final l10n = context.l10n; - return PinballDialog( - title: l10n.howToPlay, - subtitle: l10n.tipsForFlips, - child: isMobile ? const _MobileBody() : const _DesktopBody(), + + return WillPopScope( + onWillPop: () { + widget.onDismissCallback.call(); + return Future.value(true); + }, + child: PinballDialog( + title: l10n.howToPlay, + subtitle: l10n.tipsForFlips, + child: isMobile ? const _MobileBody() : const _DesktopBody(), + ), ); } } diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 3b00829b..1f7b0374 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -1,20 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; -/// Inflates [CharacterSelectionDialog] using [showDialog]. -Future showCharacterSelectionDialog(BuildContext context) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const CharacterSelectionDialog(), - ); -} - /// {@template character_selection_dialog} /// Dialog used to select the playing character of the game. /// {@endtemplate character_selection_dialog} @@ -59,7 +50,7 @@ class _SelectCharacterButton extends StatelessWidget { return PinballButton( onTap: () async { Navigator.of(context).pop(); - await showHowToPlayDialog(context); + context.read().add(const CharacterSelected()); }, text: l10n.select, ); @@ -74,36 +65,40 @@ class _CharacterGrid extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( - children: [ - _Character( - key: const Key('sparky_character_selection'), - character: const SparkyTheme(), - isSelected: state.isSparkySelected, - ), - const SizedBox(height: 6), - _Character( - key: const Key('android_character_selection'), - character: const AndroidTheme(), - isSelected: state.isAndroidSelected, - ), - ], + Expanded( + child: Column( + children: [ + _Character( + key: const Key('sparky_character_selection'), + character: const SparkyTheme(), + isSelected: state.isSparkySelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('android_character_selection'), + character: const AndroidTheme(), + isSelected: state.isAndroidSelected, + ), + ], + ), ), const SizedBox(width: 6), - Column( - children: [ - _Character( - key: const Key('dash_character_selection'), - character: const DashTheme(), - isSelected: state.isDashSelected, - ), - const SizedBox(height: 6), - _Character( - key: const Key('dino_character_selection'), - character: const DinoTheme(), - isSelected: state.isDinoSelected, - ), - ], + Expanded( + child: Column( + children: [ + _Character( + key: const Key('dash_character_selection'), + character: const DashTheme(), + isSelected: state.isDashSelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('dino_character_selection'), + character: const DinoTheme(), + isSelected: state.isDinoSelected, + ), + ], + ), ), ], ); diff --git a/lib/start_game/bloc/start_game_bloc.dart b/lib/start_game/bloc/start_game_bloc.dart index ba44d88c..3a96b57b 100644 --- a/lib/start_game/bloc/start_game_bloc.dart +++ b/lib/start_game/bloc/start_game_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:pinball/game/game.dart'; part 'start_game_event.dart'; part 'start_game_state.dart'; @@ -10,23 +9,16 @@ part 'start_game_state.dart'; /// {@endtemplate} class StartGameBloc extends Bloc { /// {@macro start_game_bloc} - StartGameBloc({ - required PinballGame game, - }) : _game = game, - super(const StartGameState.initial()) { + StartGameBloc() : super(const StartGameState.initial()) { on(_onPlayTapped); on(_onCharacterSelected); on(_onHowToPlayFinished); } - final PinballGame _game; - void _onPlayTapped( PlayTapped event, Emitter emit, ) { - _game.gameFlowController.start(); - emit( state.copyWith( status: StartGameStatus.selectCharacter, diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart index 7171c66d..9e63b170 100644 --- a/lib/start_game/start_game.dart +++ b/lib/start_game/start_game.dart @@ -1 +1,2 @@ export 'bloc/start_game_bloc.dart'; +export 'widgets/start_game_listener.dart'; diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart new file mode 100644 index 00000000..df34b324 --- /dev/null +++ b/lib/start_game/widgets/start_game_listener.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template start_game_listener} +/// Listener that manages the display of dialogs for [StartGameStatus]. +/// +/// It's responsible for starting the game after pressing play button +/// and playing a sound after the 'how to play' dialog. +/// {@endtemplate} +class StartGameListener extends StatelessWidget { + /// {@macro start_game_listener} + const StartGameListener({ + Key? key, + required Widget child, + required PinballGame game, + }) : _child = child, + _game = game, + super(key: key); + + final Widget _child; + final PinballGame _game; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + switch (state.status) { + case StartGameStatus.initial: + break; + case StartGameStatus.selectCharacter: + _onSelectCharacter(context); + _game.gameFlowController.start(); + break; + case StartGameStatus.howToPlay: + _onHowToPlay(context); + break; + case StartGameStatus.play: + break; + } + }, + child: _child, + ); + } + + void _onSelectCharacter(BuildContext context) { + _showPinballDialog( + context: context, + child: const CharacterSelectionDialog(), + barrierDismissible: false, + ); + } + + void _onHowToPlay(BuildContext context) { + final audio = context.read(); + + _showPinballDialog( + context: context, + child: HowToPlayDialog( + onDismissCallback: () { + context.read().add(const HowToPlayFinished()); + audio.ioPinballVoiceOver(); + }, + ), + ); + } + + void _showPinballDialog({ + required BuildContext context, + required Widget child, + bool barrierDismissible = true, + }) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + + showDialog( + context: context, + barrierColor: PinballColors.transparent, + barrierDismissible: barrierDismissible, + builder: (_) { + return Center( + child: SizedBox( + height: gameWidgetWidth * 0.87, + width: gameWidgetWidth, + child: child, + ), + ); + }, + ); + } +} diff --git a/packages/pinball_components/assets/images/ball/ball.png b/packages/pinball_components/assets/images/ball/ball.png deleted file mode 100644 index 43332c9a..00000000 Binary files a/packages/pinball_components/assets/images/ball/ball.png and /dev/null differ diff --git a/packages/pinball_components/lib/src/components/ball/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart index 9234e69c..e8cea997 100644 --- a/packages/pinball_components/lib/src/components/ball/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; export 'behaviors/behaviors.dart'; @@ -14,11 +15,11 @@ export 'behaviors/behaviors.dart'; class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// {@macro ball} Ball({ - required this.baseColor, + String? assetPath, }) : super( renderBody: false, children: [ - _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + _BallSpriteComponent(assetPath: assetPath), BallScalingBehavior(), BallGravitatingBehavior(), ], @@ -35,7 +36,7 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// /// This can be used for testing [Ball]'s behaviors in isolation. @visibleForTesting - Ball.test({required this.baseColor}) + Ball.test() : super( children: [_BallSpriteComponent()], ); @@ -43,9 +44,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); - /// The base [Color] used to tint this [Ball]. - final Color baseColor; - @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; @@ -79,14 +77,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { } class _BallSpriteComponent extends SpriteComponent with HasGameRef { + _BallSpriteComponent({ + this.assetPath, + }) : super( + anchor: Anchor.center, + ); + + final String? assetPath; + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.ball.ball.keyName, + final sprite = Sprite( + gameRef.images + .fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName), ); this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.center; + size = sprite.originalSize / 12.5; } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 798983a7..fdb80ad0 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -32,7 +32,7 @@ export 'signpost/signpost.dart'; export 'skill_shot/skill_shot.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; -export 'spaceship_ramp.dart'; +export 'spaceship_ramp/spaceship_ramp.dart'; export 'sparky_animatronic.dart'; export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart new file mode 100644 index 00000000..1f9b6284 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart @@ -0,0 +1 @@ +export 'ramp_ball_ascending_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart new file mode 100644 index 00000000..2d0aad7c --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart @@ -0,0 +1,24 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_ball_ascending_contact_behavior} +/// Detects an ascending [Ball] that enters into the [SpaceshipRamp]. +/// +/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of +/// the [SpaceshipRamp]. +/// {@endtemplate} +class RampBallAscendingContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.body.linearVelocity.y < 0) { + parent.parent.bloc.onAscendingBallEntered(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart new file mode 100644 index 00000000..d27a7a2c --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'spaceship_ramp_state.dart'; + +class SpaceshipRampCubit extends Cubit { + SpaceshipRampCubit() : super(const SpaceshipRampState.initial()); + + void onAscendingBallEntered() { + emit( + state.copyWith(hits: state.hits + 1), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart new file mode 100644 index 00000000..7fae894f --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -0,0 +1,24 @@ +// ignore_for_file: public_member_api_docs + +part of 'spaceship_ramp_cubit.dart'; + +class SpaceshipRampState extends Equatable { + const SpaceshipRampState({ + required this.hits, + }) : assert(hits >= 0, "Hits can't be negative"); + + const SpaceshipRampState.initial() : this(hits: 0); + + final int hits; + + SpaceshipRampState copyWith({ + int? hits, + }) { + return SpaceshipRampState( + hits: hits ?? this.hits, + ); + } + + @override + List get props => [hits]; +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart similarity index 77% rename from packages/pinball_components/lib/src/components/spaceship_ramp.dart rename to packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index c1be0943..0b407517 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/spaceship_ramp_cubit.dart'; + /// {@template spaceship_ramp} /// Ramp leading into the [AndroidSpaceship]. /// {@endtemplate} class SpaceshipRamp extends Component { /// {@macro spaceship_ramp} - SpaceshipRamp() - : super( + SpaceshipRamp({ + Iterable? children, + }) : this._( + children: children, + bloc: SpaceshipRampCubit(), + ); + + SpaceshipRamp._({ + Iterable? children, + required this.bloc, + }) : super( children: [ + // TODO(ruimiguel): refactor RampScoringSensor and + // _SpaceshipRampOpening to be in only one sensor if possible. + RampScoringSensor( + children: [ + RampBallAscendingContactBehavior(), + ], + )..initialPosition = Vector2(1.7, -20.4), _SpaceshipRampOpening( outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, @@ -34,60 +53,30 @@ class SpaceshipRamp extends Component { _SpaceshipRampForegroundRailing(), _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), _SpaceshipRampBackgroundRailingSpriteComponent(), - _SpaceshipRampArrowSpriteComponent(), + SpaceshipRampArrowSpriteComponent( + current: bloc.state.hits, + ), + ...?children, ], ); - /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. + /// Creates a [SpaceshipRamp] without any children. /// - /// If the current state is the last one it cycles back to the initial state. - void progress() => - firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); -} - -/// Indicates the state of the arrow on the [SpaceshipRamp]. -@visibleForTesting -enum SpaceshipRampArrowSpriteState { - /// Arrow with no dashes lit up. - inactive, - - /// Arrow with 1 light lit up. - active1, + /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. + @visibleForTesting + SpaceshipRamp.test({ + required this.bloc, + }) : super(); - /// Arrow with 2 lights lit up. - active2, - - /// Arrow with 3 lights lit up. - active3, - - /// Arrow with 4 lights lit up. - active4, - - /// Arrow with all 5 lights lit up. - active5, -} - -extension on SpaceshipRampArrowSpriteState { - String get path { - switch (this) { - case SpaceshipRampArrowSpriteState.inactive: - return Assets.images.android.ramp.arrow.inactive.keyName; - case SpaceshipRampArrowSpriteState.active1: - return Assets.images.android.ramp.arrow.active1.keyName; - case SpaceshipRampArrowSpriteState.active2: - return Assets.images.android.ramp.arrow.active2.keyName; - case SpaceshipRampArrowSpriteState.active3: - return Assets.images.android.ramp.arrow.active3.keyName; - case SpaceshipRampArrowSpriteState.active4: - return Assets.images.android.ramp.arrow.active4.keyName; - case SpaceshipRampArrowSpriteState.active5: - return Assets.images.android.ramp.arrow.active5.keyName; - } - } + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SpaceshipRampCubit bloc; - SpaceshipRampArrowSpriteState get next { - return SpaceshipRampArrowSpriteState - .values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; + @override + void onRemove() { + bloc.close(); + super.onRemove(); } } @@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// {@endtemplate} -class _SpaceshipRampArrowSpriteComponent - extends SpriteGroupComponent - with HasGameRef, ZIndex { +@visibleForTesting +class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA, ZIndex { /// {@macro spaceship_ramp_arrow_sprite_component} - _SpaceshipRampArrowSpriteComponent() - : super( + SpaceshipRampArrowSpriteComponent({ + required int current, + }) : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), + current: current, ) { zIndex = ZIndexes.spaceshipRampArrow; } - /// Changes arrow image to the next [Sprite]. - void progress() => current = current?.next; - @override Future onLoad() async { await super.onLoad(); - final sprites = {}; + parent.bloc.stream.listen((state) { + current = state.hits % SpaceshipRampArrowSpriteState.values.length; + }); + + final sprites = {}; this.sprites = sprites; for (final spriteState in SpaceshipRampArrowSpriteState.values) { - sprites[spriteState] = Sprite( + sprites[spriteState.index] = Sprite( gameRef.images.fromCache(spriteState.path), ); } - current = SpaceshipRampArrowSpriteState.inactive; + current = 0; size = sprites[current]!.originalSize / 10; } } +/// Indicates the state of the arrow on the [SpaceshipRamp]. +@visibleForTesting +enum SpaceshipRampArrowSpriteState { + /// Arrow with no dashes lit up. + inactive, + + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, +} + +extension on SpaceshipRampArrowSpriteState { + String get path { + switch (this) { + case SpaceshipRampArrowSpriteState.inactive: + return Assets.images.android.ramp.arrow.inactive.keyName; + case SpaceshipRampArrowSpriteState.active1: + return Assets.images.android.ramp.arrow.active1.keyName; + case SpaceshipRampArrowSpriteState.active2: + return Assets.images.android.ramp.arrow.active2.keyName; + case SpaceshipRampArrowSpriteState.active3: + return Assets.images.android.ramp.arrow.active3.keyName; + case SpaceshipRampArrowSpriteState.active4: + return Assets.images.android.ramp.arrow.active4.keyName; + case SpaceshipRampArrowSpriteState.active5: + return Assets.images.android.ramp.arrow.active5.keyName; + } + } +} + class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { @@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor { ); } } + +/// {@template ramp_scoring_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SpaceshipRamp]. +/// {@endtemplate} +class RampScoringSensor extends BodyComponent + with ParentIsA, InitialPosition, Layered { + /// {@macro ramp_scoring_sensor} + RampScoringSensor({ + Iterable? children, + }) : super( + children: children, + renderBody: false, + ) { + layer = Layer.spaceshipEntranceRamp; + } + + /// Creates a [RampScoringSensor] without any children. + /// + @visibleForTesting + RampScoringSensor.test(); + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox( + 2.6, + .5, + initialPosition, + -5 * math.pi / 180, + ); + + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 89d16450..bee6a280 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -24,6 +24,14 @@ abstract class AssetsGame extends Forge2DGame { } abstract class LineGame extends AssetsGame with PanDetector { + LineGame({ + List? imagesFileNames, + }) : super( + imagesFileNames: [ + if (imagesFileNames != null) ...imagesFileNames, + ], + ); + Vector2? _lineEnd; @override diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart index 32638c2d..78cebd95 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart @@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidBumperAGame extends BallGame { AndroidBumperAGame() : super( - color: const Color(0xFF0000FF), imagesFileNames: [ Assets.images.android.bumper.a.lit.keyName, Assets.images.android.bumper.a.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart index bfd4206c..9bd2caff 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart @@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidBumperBGame extends BallGame { AndroidBumperBGame() : super( - color: const Color(0xFF0000FF), imagesFileNames: [ Assets.images.android.bumper.b.lit.keyName, Assets.images.android.bumper.b.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index dee83e26..4093ad33 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, imagesFileNames: [ diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index cabe4d54..fe4e6dae 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -9,7 +9,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRampGame extends BallGame with KeyboardEvents { SpaceshipRampGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, imagesFileNames: [ @@ -54,7 +53,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { ) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { - _spaceshipRamp.progress(); + _spaceshipRamp.bloc.onAscendingBallEntered(); return KeyEventResult.handled; } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart index a66459a6..ac0989e2 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart @@ -1,9 +1,20 @@ import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:sandbox/common/common.dart'; class BallBoosterGame extends LineGame { + BallBoosterGame() + : super( + imagesFileNames: [ + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, + Assets.images.ball.flameEffect.keyName, + ], + ); + static const description = ''' Shows how a Ball with a boost works. @@ -12,7 +23,7 @@ class BallBoosterGame extends LineGame { @override void onLine(Vector2 line) { - final ball = Ball(baseColor: Colors.transparent); + final ball = Ball(); final impulse = line * -1 * 20; ball.add(BallTurboChargingBehavior(impulse: impulse)); diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart index e57a0322..f3ba50f3 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart @@ -1,17 +1,20 @@ import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:sandbox/common/common.dart'; class BallGame extends AssetsGame with TapDetector, Traceable { BallGame({ - this.color = Colors.blue, this.ballPriority = 0, this.ballLayer = Layer.all, + this.character, List? imagesFileNames, }) : super( imagesFileNames: [ - Assets.images.ball.ball.keyName, + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, if (imagesFileNames != null) ...imagesFileNames, ], ); @@ -22,14 +25,23 @@ class BallGame extends AssetsGame with TapDetector, Traceable { - Tap anywhere on the screen to spawn a ball into the game. '''; - final Color color; + static final characterBallPaths = { + 'Dash': theme.Assets.images.dash.ball.keyName, + 'Sparky': theme.Assets.images.sparky.ball.keyName, + 'Android': theme.Assets.images.android.ball.keyName, + 'Dino': theme.Assets.images.dino.ball.keyName, + }; + final int ballPriority; final Layer ballLayer; + final String? character; @override void onTapUp(TapUpInfo info) { add( - Ball(baseColor: color) + Ball( + assetPath: characterBallPaths[character], + ) ..initialPosition = info.eventPosition.game ..layer = ballLayer ..priority = ballPriority, diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index eb472282..146ebcda 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart @@ -1,5 +1,4 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/ball_booster_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; @@ -7,10 +6,14 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; void addBallStories(Dashbook dashbook) { dashbook.storiesOf('Ball') ..addGame( - title: 'Colored', + title: 'Themed', description: BallGame.description, gameBuilder: (context) => BallGame( - color: context.colorProperty('color', Colors.blue), + character: context.listProperty( + 'Character', + BallGame.characterBallPaths.keys.first, + BallGame.characterBallPaths.keys.toList(), + ), ), ) ..addGame( diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart index bc537de2..94389f60 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart @@ -1,14 +1,10 @@ -import 'dart:ui'; - import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class GoogleLetterGame extends BallGame { GoogleLetterGame() : super( - color: const Color(0xFF009900), imagesFileNames: [ Assets.images.googleWord.letter1.lit.keyName, Assets.images.googleWord.letter1.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart index ea3bd4db..b6955a26 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class LaunchRampGame extends BallGame { LaunchRampGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnLaunchRamp, ballLayer: Layer.launcher, ); diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart index 0f1ec2e4..0ee58cc9 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -6,8 +6,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class PlungerGame extends BallGame with KeyboardEvents, Traceable { - PlungerGame() : super(color: const Color(0xFFFF0000)); - static const description = ''' Shows how Plunger is rendered. diff --git a/packages/pinball_components/test/src/components/ball/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart index 655836a0..9195e0b2 100644 --- a/packages/pinball_components/test/src/components/ball/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -2,31 +2,36 @@ import 'package:flame_forge2d/flame_forge2d.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 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, + ]; - group('Ball', () { - const baseColor = Color(0xFFFFFFFF); + final flameTester = FlameTester(() => TestGame(assets)); + group('Ball', () { test( 'can be instantiated', () { - expect(Ball(baseColor: baseColor), isA()); - expect(Ball.test(baseColor: baseColor), isA()); + expect(Ball(), isA()); + expect(Ball.test(), isA()); }, ); flameTester.test( 'loads correctly', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ready(); await game.ensureAdd(ball); @@ -36,7 +41,7 @@ void main() { group('adds', () { flameTester.test('a BallScalingBehavior', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect( ball.descendants().whereType().length, @@ -45,7 +50,7 @@ void main() { }); flameTester.test('a BallGravitatingBehavior', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect( ball.descendants().whereType().length, @@ -58,7 +63,7 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect(ball.body.bodyType, equals(BodyType.dynamic)); @@ -67,7 +72,7 @@ void main() { group('can be moved', () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); game.update(1); @@ -75,7 +80,7 @@ void main() { }); flameTester.test('by applying velocity', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.body.gravityScale = Vector2.zero(); @@ -90,7 +95,7 @@ void main() { flameTester.test( 'exists', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect(ball.body.fixtures[0], isA()); @@ -100,7 +105,7 @@ void main() { flameTester.test( 'is dense', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -111,7 +116,7 @@ void main() { flameTester.test( 'shape is circular', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -123,7 +128,7 @@ void main() { flameTester.test( 'has Layer.all as default filter maskBits', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ready(); await game.ensureAdd(ball); await game.ready(); @@ -137,7 +142,7 @@ void main() { group('stop', () { group("can't be moved", () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); @@ -152,7 +157,7 @@ void main() { flameTester.test( 'by its weight when previously stopped', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -165,7 +170,7 @@ void main() { flameTester.test( 'by applying velocity when previously stopped', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); ball.resume(); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart index d78df37a..ce193dc8 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart @@ -1,21 +1,19 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = Assets.images.ball.ball.keyName; + final asset = theme.Assets.images.dash.ball.keyName; final flameTester = FlameTester(() => TestGame([asset])); group('BallGravitatingBehavior', () { - const baseColor = Color(0xFFFFFFFF); test('can be instantiated', () { expect( BallGravitatingBehavior(), @@ -24,7 +22,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallGravitatingBehavior(); await ball.add(behavior); await game.ensureAdd(ball); @@ -37,12 +35,10 @@ void main() { flameTester.test( "overrides the body's horizontal gravity symmetrically", (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(10, 0); + final ball1 = Ball.test()..initialPosition = Vector2(10, 0); await ball1.add(BallGravitatingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(-10, 0); + final ball2 = Ball.test()..initialPosition = Vector2(-10, 0); await ball2.add(BallGravitatingBehavior()); await game.ensureAddAll([ball1, ball2]); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index 0aeeda98..bd0cca49 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -1,21 +1,19 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = Assets.images.ball.ball.keyName; + final asset = theme.Assets.images.dash.ball.keyName; final flameTester = FlameTester(() => TestGame([asset])); group('BallScalingBehavior', () { - const baseColor = Color(0xFFFFFFFF); test('can be instantiated', () { expect( BallScalingBehavior(), @@ -24,7 +22,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallScalingBehavior(); await ball.add(behavior); await game.ensureAdd(ball); @@ -35,12 +33,10 @@ void main() { }); flameTester.test('scales the shape radius', (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, 10); + final ball1 = Ball.test()..initialPosition = Vector2(0, 10); await ball1.add(BallScalingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, -10); + final ball2 = Ball.test()..initialPosition = Vector2(0, -10); await ball2.add(BallScalingBehavior()); await game.ensureAddAll([ball1, ball2]); @@ -57,12 +53,10 @@ void main() { flameTester.test( 'scales the sprite', (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, 10); + final ball1 = Ball.test()..initialPosition = Vector2(0, 10); await ball1.add(BallScalingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, -10); + final ball2 = Ball.test()..initialPosition = Vector2(0, -10); await ball2.add(BallScalingBehavior()); await game.ensureAddAll([ball1, ball2]); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart index 00f34832..79eb030e 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart @@ -1,12 +1,10 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.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 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -16,9 +14,8 @@ void main() { group( 'BallTurboChargingBehavior', () { - final assets = [Assets.images.ball.ball.keyName]; - final flameTester = FlameTester(() => TestGame(assets)); - const baseColor = Color(0xFFFFFFFF); + final asset = theme.Assets.images.dash.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); test('can be instantiated', () { expect( @@ -28,7 +25,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); await ball.add(behavior); await game.ensureAdd(ball); @@ -41,7 +38,7 @@ void main() { flameTester.test( 'impulses the ball velocity when loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); await game.ensureAdd(ball); final impulse = Vector2.all(1); final behavior = BallTurboChargingBehavior(impulse: impulse); @@ -59,7 +56,7 @@ void main() { ); flameTester.test('adds sprite', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); await ball.ensureAdd( @@ -73,7 +70,7 @@ void main() { }); flameTester.test('removes sprite after it finishes', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart index 8d052fab..dfc33967 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -4,11 +4,11 @@ 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/material.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/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -20,7 +20,10 @@ class _MockFixture extends Mock implements Fixture {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group( 'ChromeDinoChompingBehavior', @@ -35,7 +38,7 @@ void main() { flameTester.test( 'beginContact sets ball sprite to be invisible and calls onChomp', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoChompingBehavior(); final bloc = _MockChromeDinoCubit(); whenListen( diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart index 1d0a55b4..8c2cbe57 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -5,11 +5,11 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.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/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -17,7 +17,10 @@ class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group( 'ChromeDinoSpittingBehavior', @@ -33,7 +36,7 @@ void main() { flameTester.test( 'sets ball sprite to visible and sets a linear velocity', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); @@ -71,7 +74,7 @@ void main() { flameTester.test( 'calls onSpit', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart index 873b7c44..80c01983 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -7,7 +6,7 @@ void main() { group( 'ChromeDinoCubit', () { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); blocTest( 'onOpenMouth emits true', diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart index 2e050575..0d7f9c83 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -61,7 +60,7 @@ void main() { 'copies correctly ' 'when all arguments specified', () { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); const chromeDinoState = ChromeDinoState( status: ChromeDinoStatus.chomping, isMouthOpen: true, diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper_test.dart index c34d0d1c..314b1f77 100644 --- a/packages/pinball_components/test/src/components/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper_test.dart @@ -2,9 +2,9 @@ import 'package:flame_forge2d/flame_forge2d.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 'package:pinball_theme/pinball_theme.dart' as theme; import '../../helpers/helpers.dart'; @@ -13,6 +13,7 @@ void main() { final assets = [ Assets.images.flipper.left.keyName, Assets.images.flipper.right.keyName, + theme.Assets.images.dash.ball.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); @@ -89,7 +90,7 @@ void main() { 'has greater mass than Ball', (game) async { final flipper = Flipper(side: BoardSide.left); - final ball = Ball(baseColor: Colors.white); + final ball = Ball(); await game.ready(); await game.ensureAddAll([flipper, ball]); diff --git a/packages/pinball_components/test/src/components/golden/ball/android.png b/packages/pinball_components/test/src/components/golden/ball/android.png new file mode 100644 index 00000000..2a659092 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/android.png differ diff --git a/packages/pinball_components/test/src/components/golden/ball/dash.png b/packages/pinball_components/test/src/components/golden/ball/dash.png new file mode 100644 index 00000000..c95afc88 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/dash.png differ diff --git a/packages/pinball_components/test/src/components/golden/ball/dino.png b/packages/pinball_components/test/src/components/golden/ball/dino.png new file mode 100644 index 00000000..8ea10758 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/dino.png differ diff --git a/packages/pinball_components/test/src/components/golden/ball/sparky.png b/packages/pinball_components/test/src/components/golden/ball/sparky.png new file mode 100644 index 00000000..afdeb263 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/sparky.png differ diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart new file mode 100644 index 00000000..ea37550a --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart @@ -0,0 +1,117 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.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/spaceship_ramp/behavior/behavior.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]; + + final flameTester = FlameTester(() => TestGame(assets)); + + group( + 'RampBallAscendingContactBehavior', + () { + test('can be instantiated', () { + expect( + RampBallAscendingContactBehavior(), + isA(), + ); + }); + + group('beginContact', () { + late Ball ball; + late Body body; + + setUp(() { + ball = _MockBall(); + body = _MockBody(); + + when(() => ball.body).thenReturn(body); + }); + + flameTester.test( + "calls 'onAscendingBallEntered' when a ball enters into the ramp", + (game) async { + final behavior = RampBallAscendingContactBehavior(); + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + + final rampSensor = RampScoringSensor.test(); + final spaceshipRamp = SpaceshipRamp.test( + bloc: bloc, + ); + + when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, _MockContact()); + + verify(bloc.onAscendingBallEntered).called(1); + }, + ); + + flameTester.test( + "doesn't call 'onAscendingBallEntered' when a ball goes out the ramp", + (game) async { + final behavior = RampBallAscendingContactBehavior(); + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + + final rampSensor = RampScoringSensor.test(); + final spaceshipRamp = SpaceshipRamp.test( + bloc: bloc, + ); + + when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, _MockContact()); + + verifyNever(bloc.onAscendingBallEntered); + }, + ); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart new file mode 100644 index 00000000..b7e899fe --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('SpaceshipRampCubit', () { + group('onAscendingBallEntered', () { + blocTest( + 'emits hits incremented and arrow goes to the next value', + build: SpaceshipRampCubit.new, + act: (bloc) => bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(), + expect: () => [ + SpaceshipRampState(hits: 1), + SpaceshipRampState(hits: 2), + SpaceshipRampState(hits: 3), + ], + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart new file mode 100644 index 00000000..536f4e8e --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart @@ -0,0 +1,78 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/components/components.dart'; + +void main() { + group('SpaceshipRampState', () { + test('supports value equality', () { + expect( + SpaceshipRampState(hits: 0), + equals( + SpaceshipRampState(hits: 0), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + SpaceshipRampState(hits: 0), + isNotNull, + ); + }); + }); + + test( + 'throws AssertionError ' + 'when hits is negative', + () { + expect( + () => SpaceshipRampState(hits: -1), + throwsAssertionError, + ); + }, + ); + + group('copyWith', () { + test( + 'throws AssertionError ' + 'when hits is decreased', + () { + const rampState = SpaceshipRampState(hits: 0); + expect( + () => rampState.copyWith(hits: rampState.hits - 1), + throwsAssertionError, + ); + }, + ); + + test( + 'copies correctly ' + 'when no argument specified', + () { + const rampState = SpaceshipRampState(hits: 0); + expect( + rampState.copyWith(), + equals(rampState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const rampState = SpaceshipRampState(hits: 0); + final otherRampState = SpaceshipRampState(hits: rampState.hits + 1); + expect(rampState, isNot(equals(otherRampState))); + + expect( + rampState.copyWith(hits: rampState.hits + 1), + equals(otherRampState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart similarity index 53% rename from packages/pinball_components/test/src/components/spaceship_ramp_test.dart rename to packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index 0f2ce13a..b74cfb88 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -1,12 +1,16 @@ // 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_flame/pinball_flame.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,28 +29,35 @@ void main() { final flameTester = FlameTester(() => TestGame(assets)); group('SpaceshipRamp', () { - flameTester.test('loads correctly', (game) async { - final component = SpaceshipRamp(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); + flameTester.test( + 'loads correctly', + (game) async { + final spaceshipRamp = SpaceshipRamp(); + await game.ensureAdd(spaceshipRamp); + expect(game.children, contains(spaceshipRamp)); + }, + ); group('renders correctly', () { - const goldenFilePath = 'golden/spaceship_ramp/'; + const goldenFilePath = '../golden/spaceship_ramp/'; final centerForSpaceshipRamp = Vector2(-13, -55); flameTester.testGameWidget( 'inactive sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.inactive, ); @@ -64,15 +75,21 @@ void main() { 'active1 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component.progress(); + ramp.bloc.onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active1, ); @@ -90,17 +107,23 @@ void main() { 'active2 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active2, ); @@ -118,18 +141,24 @@ void main() { 'active3 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active3, ); @@ -147,19 +176,25 @@ void main() { 'active4 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active4, ); @@ -177,20 +212,26 @@ void main() { 'active5 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active5, ); @@ -204,5 +245,34 @@ void main() { }, ); }); + + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final ramp = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ramp); + game.remove(ramp); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final ramp = SpaceshipRamp(children: [component]); + await game.ensureAdd(ramp); + expect(ramp.children, contains(component)); + }); + }); }); } diff --git a/packages/pinball_theme/assets/images/android/ball.png b/packages/pinball_theme/assets/images/android/ball.png new file mode 100644 index 00000000..b5cfbc3f Binary files /dev/null and b/packages/pinball_theme/assets/images/android/ball.png differ diff --git a/packages/pinball_theme/assets/images/dash/ball.png b/packages/pinball_theme/assets/images/dash/ball.png new file mode 100644 index 00000000..fa754cbc Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/ball.png differ diff --git a/packages/pinball_theme/assets/images/dino/ball.png b/packages/pinball_theme/assets/images/dino/ball.png new file mode 100644 index 00000000..02b99c43 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/ball.png differ diff --git a/packages/pinball_theme/assets/images/sparky/ball.png b/packages/pinball_theme/assets/images/sparky/ball.png new file mode 100644 index 00000000..95e5a10b Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/ball.png differ diff --git a/packages/pinball_theme/lib/src/generated/assets.gen.dart b/packages/pinball_theme/lib/src/generated/assets.gen.dart index 3feeecce..545f514b 100644 --- a/packages/pinball_theme/lib/src/generated/assets.gen.dart +++ b/packages/pinball_theme/lib/src/generated/assets.gen.dart @@ -36,9 +36,9 @@ class $AssetsImagesAndroidGen { AssetGenImage get background => const AssetGenImage('assets/images/android/background.png'); - /// File path: assets/images/android/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/android/character.png'); + /// File path: assets/images/android/ball.png + AssetGenImage get ball => + const AssetGenImage('assets/images/android/ball.png'); /// File path: assets/images/android/icon.png AssetGenImage get icon => @@ -60,9 +60,8 @@ class $AssetsImagesDashGen { AssetGenImage get background => const AssetGenImage('assets/images/dash/background.png'); - /// File path: assets/images/dash/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/dash/character.png'); + /// File path: assets/images/dash/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png'); /// File path: assets/images/dash/icon.png AssetGenImage get icon => const AssetGenImage('assets/images/dash/icon.png'); @@ -83,9 +82,8 @@ class $AssetsImagesDinoGen { AssetGenImage get background => const AssetGenImage('assets/images/dino/background.png'); - /// File path: assets/images/dino/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/dino/character.png'); + /// File path: assets/images/dino/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png'); /// File path: assets/images/dino/icon.png AssetGenImage get icon => const AssetGenImage('assets/images/dino/icon.png'); @@ -106,9 +104,9 @@ class $AssetsImagesSparkyGen { AssetGenImage get background => const AssetGenImage('assets/images/sparky/background.png'); - /// File path: assets/images/sparky/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/sparky/character.png'); + /// File path: assets/images/sparky/ball.png + AssetGenImage get ball => + const AssetGenImage('assets/images/sparky/ball.png'); /// File path: assets/images/sparky/icon.png AssetGenImage get icon => diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index 8989c717..6e7d76b2 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template android_theme} @@ -12,7 +11,7 @@ class AndroidTheme extends CharacterTheme { String get name => 'Android'; @override - Color get ballColor => Colors.green; + AssetGenImage get ball => Assets.images.android.ball; @override AssetGenImage get background => Assets.images.android.background; diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 072c917f..596f41a0 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template character_theme} @@ -15,8 +14,8 @@ abstract class CharacterTheme extends Equatable { /// Name of character. String get name; - /// Ball color for this theme. - Color get ballColor; + /// Asset for the ball. + AssetGenImage get ball; /// Asset for the background. AssetGenImage get background; @@ -33,7 +32,7 @@ abstract class CharacterTheme extends Equatable { @override List get props => [ name, - ballColor, + ball, background, icon, leaderboardIcon, diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index 7584c8ed..be3a8873 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dash_theme} @@ -12,7 +11,7 @@ class DashTheme extends CharacterTheme { String get name => 'Dash'; @override - Color get ballColor => Colors.blue; + AssetGenImage get ball => Assets.images.dash.ball; @override AssetGenImage get background => Assets.images.dash.background; diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 3baf466c..1de42d41 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dino_theme} @@ -12,7 +11,7 @@ class DinoTheme extends CharacterTheme { String get name => 'Dino'; @override - Color get ballColor => Colors.grey; + AssetGenImage get ball => Assets.images.dino.ball; @override AssetGenImage get background => Assets.images.dino.background; diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index 7884a22f..1699f3ae 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template sparky_theme} @@ -9,7 +8,7 @@ class SparkyTheme extends CharacterTheme { const SparkyTheme(); @override - Color get ballColor => Colors.orange; + AssetGenImage get ball => Assets.images.sparky.ball; @override String get name => 'Sparky'; diff --git a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart new file mode 100644 index 00000000..acd17717 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -0,0 +1,152 @@ +// 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/behaviors/behaviors.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockStreamSubscription extends Mock + implements StreamSubscription {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.oneMillion.keyName, + ]; + + group('RampBonusBehavior', () { + const shotPoints = Points.oneMillion; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + 'when hits are multiples of 10 times adds a ScoringBehavior', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState(hits: 9), + ); + final behavior = RampBonusBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 10)); + + final scores = game.descendants().whereType(); + await game.ready(); + + expect(scores.length, 1); + }, + ); + + flameBlocTester.testGameWidget( + "when hits are not multiple of 10 times doesn't add any ScoringBehavior", + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final behavior = RampBonusBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 1)); + + final scores = game.descendants().whereType(); + await game.ready(); + + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'closes subscription when removed', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final subscription = _MockStreamSubscription(); + when(subscription.cancel).thenAnswer((_) async {}); + + final behavior = RampBonusBehavior.test( + points: shotPoints, + subscription: subscription, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + parent.remove(behavior); + await game.ready(); + + verify(subscription.cancel).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart new file mode 100644 index 00000000..23f02220 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -0,0 +1,156 @@ +// 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/behaviors/behaviors.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockStreamSubscription extends Mock + implements StreamSubscription {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.fiveThousand.keyName, + ]; + + group('RampShotBehavior', () { + const shotPoints = Points.fiveThousand; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + 'when hits are not multiple of 10 times ' + 'increases multiplier and adds a ScoringBehavior', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final behavior = RampShotBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 1)); + + final scores = game.descendants().whereType(); + await game.ready(); + + verify(() => gameBloc.add(MultiplierIncreased())).called(1); + expect(scores.length, 1); + }, + ); + + flameBlocTester.testGameWidget( + 'when hits multiple of 10 times ' + "doesn't increase multiplier, neither ScoringBehavior", + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState(hits: 9), + ); + final behavior = RampShotBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 10)); + + final scores = game.children.whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(MultiplierIncreased())); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'closes subscription when removed', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final subscription = _MockStreamSubscription(); + when(subscription.cancel).thenAnswer((_) async {}); + + final behavior = RampShotBehavior.test( + points: shotPoints, + subscription: subscription, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + parent.remove(behavior); + await game.ready(); + + verify(subscription.cancel).called(1); + }, + ); + }); +} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index d8d31b4e..dc142ffd 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -2,13 +2,12 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; -import 'package:flame/extensions.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; import '../../helpers/helpers.dart'; @@ -33,13 +32,16 @@ class _MockBall extends Mock implements Ball {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; group('BallController', () { late Ball ball; late GameBloc gameBloc; setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); + ball = Ball(); gameBloc = _MockGameBloc(); whenListen( gameBloc, @@ -51,6 +53,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); test('can be instantiated', () { @@ -68,7 +71,7 @@ void main() { await ball.add(controller); await game.ensureAdd(ball); - final otherBall = Ball(baseColor: const Color(0xFF00FFFF)); + final otherBall = Ball(); final otherController = BallController(otherBall); await otherBall.add(otherController); await game.ensureAdd(otherBall); @@ -106,6 +109,7 @@ void main() { flameBlocTester.testGameWidget( 'adds TurboChargeActivated', setUp: (game, tester) async { + await game.images.loadAll(assets); final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index f9e2988d..71b41029 100644 --- a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart +++ b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart @@ -10,15 +10,21 @@ import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.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' as theme; import '../../../../helpers/helpers.dart'; class _MockGameBloc extends Mock implements GameBloc {} void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; - final assets = [Assets.images.dash.animatronic.keyName]; + final assets = [ + Assets.images.dash.animatronic.keyName, + theme.Assets.images.dash.ball.keyName, + ]; setUp(() { gameBloc = _MockGameBloc(); @@ -32,6 +38,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); void _contactedBumper(DashNestBumper bumper) => diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 1d7a44f5..e1ed3084 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -8,6 +8,7 @@ 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; import '../helpers/helpers.dart'; @@ -43,7 +44,10 @@ void main() { Assets.images.backbox.marquee.keyName, Assets.images.backbox.displayDivider.keyName, Assets.images.boardBackground.keyName, - Assets.images.ball.ball.keyName, + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, Assets.images.ball.flameEffect.keyName, Assets.images.baseboard.left.keyName, Assets.images.baseboard.right.keyName, @@ -531,8 +535,11 @@ void main() { game.onTapUp(0, tapUpEvent); await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + expect( - game.descendants().whereType().length, + currentBalls.length, equals(previousBalls.length + 1), ); }, diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 0ed6e744..90d1b194 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -200,12 +200,11 @@ void main() { find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - // expect( - // find.byType(GameHud), - // findsNothing, - // ); + + expect( + find.byType(GameHud), + findsNothing, + ); }); testWidgets('renders a hud on play state', (tester) async { diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 843592c3..f10c5f5b 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -2,64 +2,44 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/bloc/start_game_bloc.dart'; import '../../../helpers/helpers.dart'; -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockGameFlowController extends Mock implements GameFlowController {} - -class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} +class _MockStartGameBloc extends Mock implements StartGameBloc {} void main() { group('PlayButtonOverlay', () { - late PinballGame game; - late GameFlowController gameFlowController; - late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; setUp(() async { await mockFlameImages(); - - game = _MockPinballGame(); - gameFlowController = _MockGameFlowController(); - characterThemeCubit = _MockCharacterThemeCubit(); + startGameBloc = _MockStartGameBloc(); whenListen( - characterThemeCubit, - const Stream.empty(), - initialState: const CharacterThemeState.initial(), + startGameBloc, + Stream.value(const StartGameState.initial()), + initialState: const StartGameState.initial(), ); - when(() => characterThemeCubit.state) - .thenReturn(const CharacterThemeState.initial()); - when(() => game.gameFlowController).thenReturn(gameFlowController); - when(gameFlowController.start).thenAnswer((_) {}); }); testWidgets('renders correctly', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - expect(find.text('Play'), findsOneWidget); - }); + await tester.pumpApp(const PlayButtonOverlay()); - testWidgets('calls gameFlowController.start when tapped', (tester) async { - await tester.pumpApp( - PlayButtonOverlay(game: game), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Play')); - await tester.pump(); - verify(gameFlowController.start).called(1); + expect(find.text('Play'), findsOneWidget); }); - testWidgets('displays CharacterSelectionDialog when tapped', + testWidgets('adds PlayTapped event to StartGameBloc when taped', (tester) async { await tester.pumpApp( - PlayButtonOverlay(game: game), - characterThemeCubit: characterThemeCubit, + const PlayButtonOverlay(), + startGameBloc: startGameBloc, ); + await tester.tap(find.text('Play')); await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); + + verify(() => startGameBloc.add(const PlayTapped())).called(1); }); }); } diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart index 232aa1d5..c6e60d73 100644 --- a/test/how_to_play/how_to_play_dialog_test.dart +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -3,13 +3,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:platform_helper/platform_helper.dart'; import '../helpers/helpers.dart'; -class _MockPinballAudio extends Mock implements PinballAudio {} - class _MockPlatformHelper extends Mock implements PlatformHelper {} void main() { @@ -25,7 +22,11 @@ void main() { testWidgets( 'can be instantiated without passing in a platform helper', (tester) async { - await tester.pumpApp(HowToPlayDialog()); + await tester.pumpApp( + HowToPlayDialog( + onDismissCallback: () {}, + ), + ); expect(find.byType(HowToPlayDialog), findsOneWidget); }, ); @@ -35,6 +36,7 @@ void main() { await tester.pumpApp( HowToPlayDialog( platformHelper: platformHelper, + onDismissCallback: () {}, ), ); expect(find.text(l10n.howToPlay), findsOneWidget); @@ -49,6 +51,7 @@ void main() { await tester.pumpApp( HowToPlayDialog( platformHelper: platformHelper, + onDismissCallback: () {}, ), ); expect(find.text(l10n.howToPlay), findsOneWidget); @@ -62,7 +65,12 @@ void main() { Builder( builder: (context) { return TextButton( - onPressed: () => showHowToPlayDialog(context), + onPressed: () => showDialog( + context: context, + builder: (_) => HowToPlayDialog( + onDismissCallback: () {}, + ), + ), child: const Text('test'), ); }, @@ -82,7 +90,12 @@ void main() { Builder( builder: (context) { return TextButton( - onPressed: () => showHowToPlayDialog(context), + onPressed: () => showDialog( + context: context, + builder: (_) => HowToPlayDialog( + onDismissCallback: () {}, + ), + ), child: const Text('test'), ); }, @@ -96,30 +109,5 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(HowToPlayDialog), findsNothing); }); - - testWidgets( - 'plays the I/O Pinball voice over audio on dismiss', - (tester) async { - final audio = _MockPinballAudio(); - await tester.pumpApp( - Builder( - builder: (context) { - return TextButton( - onPressed: () => showHowToPlayDialog(context), - child: const Text('test'), - ); - }, - ), - pinballAudio: audio, - ); - expect(find.byType(HowToPlayDialog), findsNothing); - await tester.tap(find.text('test')); - await tester.pumpAndSettle(); - - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - verify(audio.ioPinballVoiceOver).called(1); - }, - ); }); } diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 7d64dd39..a9c0c7ef 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -2,8 +2,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -11,14 +11,19 @@ import '../../helpers/helpers.dart'; class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} +class _MockStartGameBloc extends Mock implements StartGameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; setUp(() async { await mockFlameImages(); characterThemeCubit = _MockCharacterThemeCubit(); + startGameBloc = _MockStartGameBloc(); + whenListen( characterThemeCubit, const Stream.empty(), @@ -29,25 +34,6 @@ void main() { }); group('CharacterSelectionDialog', () { - group('showCharacterSelectionDialog', () { - testWidgets('inflates the dialog', (tester) async { - await tester.pumpApp( - Builder( - builder: (context) { - return TextButton( - onPressed: () => showCharacterSelectionDialog(context), - child: const Text('test'), - ); - }, - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('test')); - await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); - }); - }); - testWidgets('selecting a new character calls characterSelected on cubit', (tester) async { await tester.pumpApp( @@ -63,15 +49,22 @@ void main() { testWidgets( 'tapping the select button dismisses the character ' - 'dialog and shows the how to play dialog', (tester) async { + 'dialog and calls CharacterSelected event to the bloc', (tester) async { + whenListen( + startGameBloc, + const Stream.empty(), + initialState: const StartGameState.initial(), + ); + await tester.pumpApp( const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, + startGameBloc: startGameBloc, ); await tester.tap(find.byType(PinballButton)); await tester.pumpAndSettle(); expect(find.byType(CharacterSelectionDialog), findsNothing); - expect(find.byType(HowToPlayDialog), findsOneWidget); + verify(() => startGameBloc.add(const CharacterSelected())).called(1); }); testWidgets('updating the selected character updates the preview', diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index 6663ff4e..ac548a93 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -24,9 +24,7 @@ void main() { group('StartGameBloc', () { blocTest( 'on PlayTapped changes status to selectCharacter', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const PlayTapped()), expect: () => [ const StartGameState( @@ -37,9 +35,7 @@ void main() { blocTest( 'on CharacterSelected changes status to howToPlay', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const CharacterSelected()), expect: () => [ const StartGameState( @@ -50,9 +46,7 @@ void main() { blocTest( 'on HowToPlayFinished changes status to play', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const HowToPlayFinished()), expect: () => [ const StartGameState( diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart new file mode 100644 index 00000000..ca646bc9 --- /dev/null +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -0,0 +1,269 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +import '../../helpers/helpers.dart'; + +class _MockStartGameBloc extends Mock implements StartGameBloc {} + +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockGameFlowController extends Mock implements GameFlowController {} + +class _MockPinballAudio extends Mock implements PinballAudio {} + +void main() { + late StartGameBloc startGameBloc; + late PinballGame pinballGame; + late PinballAudio pinballAudio; + late CharacterThemeCubit characterThemeCubit; + + group('StartGameListener', () { + setUp(() async { + await mockFlameImages(); + + startGameBloc = _MockStartGameBloc(); + pinballGame = _MockPinballGame(); + pinballAudio = _MockPinballAudio(); + characterThemeCubit = _MockCharacterThemeCubit(); + }); + + group('on selectCharacter status', () { + testWidgets( + 'calls start on the game controller', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + final gameController = _MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + verify(gameController.start).called(1); + }, + ); + + testWidgets( + 'shows SelectCharacter dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + whenListen( + characterThemeCubit, + Stream.value(const CharacterThemeState.initial()), + initialState: const CharacterThemeState.initial(), + ); + final gameController = _MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + characterThemeCubit: characterThemeCubit, + ); + + await tester.pump(kThemeAnimationDuration); + + expect( + find.byType(CharacterSelectionDialog), + findsOneWidget, + ); + }, + ); + }); + + testWidgets( + 'on howToPlay status shows HowToPlay dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'do nothing on play status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.play), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + testWidgets( + 'do nothing on initial status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.initial), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + group('on dismiss HowToPlayDialog', () { + setUp(() { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + }); + + testWidgets( + 'adds HowToPlayFinished event', + (tester) async { + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify( + () => startGameBloc.add(const HowToPlayFinished()), + ).called(1); + }, + ); + + testWidgets( + 'plays the I/O Pinball voice over audio', + (tester) async { + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + pinballAudio: pinballAudio, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify(pinballAudio.ioPinballVoiceOver).called(1); + }, + ); + }); + }); +}