diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart index 8995c16b..8fc56905 100644 --- a/lib/game/behaviors/ball_spawning_behavior.dart +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -13,7 +13,8 @@ class BallSpawningBehavior extends Component bool listenWhen(GameState? previousState, GameState newState) { if (!newState.status.isPlaying) return false; - final startedGame = previousState?.status.isWaiting ?? true; + final startedGame = (previousState?.status.isWaiting ?? true) || + (previousState?.status.isGameOver ?? true); final lostRound = (previousState?.rounds ?? newState.rounds + 1) > newState.rounds; return startedGame || lostRound; diff --git a/lib/game/behaviors/character_selection_behavior.dart b/lib/game/behaviors/character_selection_behavior.dart index e62438f6..27003d75 100644 --- a/lib/game/behaviors/character_selection_behavior.dart +++ b/lib/game/behaviors/character_selection_behavior.dart @@ -2,8 +2,6 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; -import 'package:platform_helper/platform_helper.dart'; /// Updates the [ArcadeBackground] and launch [Ball] to reflect character /// selections. @@ -13,14 +11,12 @@ class CharacterSelectionBehavior extends Component HasGameRef { @override void onNewState(CharacterThemeState state) { - if (!readProvider().isMobile) { - gameRef - .descendants() - .whereType() - .single - .bloc - .onCharacterSelected(state.characterTheme); - } + gameRef + .descendants() + .whereType() + .single + .bloc + .onCharacterSelected(state.characterTheme); gameRef .descendants() .whereType() diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index c63bf514..feea7304 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -19,7 +19,7 @@ class GameBloc extends Bloc { static const _maxScore = 9999999999; void _onGameStarted(GameStarted _, Emitter emit) { - emit(state.copyWith(status: GameStatus.playing)); + emit(const GameState.initial().copyWith(status: GameStatus.playing)); } void _onGameOver(GameOver _, Emitter emit) { diff --git a/lib/game/components/backbox/displays/game_over_info_display.dart b/lib/game/components/backbox/displays/game_over_info_display.dart index 52939345..2db7e20b 100644 --- a/lib/game/components/backbox/displays/game_over_info_display.dart +++ b/lib/game/components/backbox/displays/game_over_info_display.dart @@ -66,7 +66,7 @@ class GameOverInfoDisplay extends Component with HasGameRef { @override Future onLoad() async { await super.onLoad(); - gameRef.overlays.add(PinballGame.playButtonOverlay); + gameRef.overlays.add(PinballGame.replayButtonOverlay); } } @@ -290,7 +290,7 @@ class OpenSourceTextComponent extends TextComponent with HasGameRef, Tappable { ); @override - bool onTapDown(TapDownInfo info) { + bool onTapUp(TapUpInfo info) { openLink(ShareRepository.openSourceCode); return true; } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 969ea1ac..103f029c 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,7 +1,6 @@ export 'android_acres/android_acres.dart'; export 'backbox/backbox.dart'; export 'bottom_group.dart'; -export 'controlled_plunger.dart'; export 'dino_desert/dino_desert.dart'; export 'drain/drain.dart'; export 'flutter_forest/flutter_forest.dart'; diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart deleted file mode 100644 index f709de66..00000000 --- a/lib/game/components/controlled_plunger.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flutter/services.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template controlled_plunger} -/// A [Plunger] with a [PlungerController] attached. -/// {@endtemplate} -class ControlledPlunger extends Plunger with Controls { - /// {@macro controlled_plunger} - ControlledPlunger({required double compressionDistance}) - : super(compressionDistance: compressionDistance) { - controller = PlungerController(this); - } - - @override - void release() { - super.release(); - - add(PlungerNoiseBehavior()); - } -} - -/// A behavior attached to the plunger when it launches the ball which plays the -/// related sound effects. -class PlungerNoiseBehavior extends Component { - @override - Future onLoad() async { - await super.onLoad(); - readProvider().play(PinballAudio.launcher); - } - - @override - void update(double dt) { - super.update(dt); - removeFromParent(); - } -} - -/// {@template plunger_controller} -/// A [ComponentController] that controls a [Plunger]s movement. -/// {@endtemplate} -class PlungerController extends ComponentController - with KeyboardHandler, FlameBlocReader { - /// {@macro plunger_controller} - PlungerController(Plunger plunger) : super(plunger); - - /// The [LogicalKeyboardKey]s that will control the [Flipper]. - /// - /// [onKeyEvent] method listens to when one of these keys is pressed. - static const List _keys = [ - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.space, - LogicalKeyboardKey.keyS, - ]; - - @override - bool onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - if (bloc.state.status.isGameOver) return true; - if (!_keys.contains(event.logicalKey)) return true; - - if (event is RawKeyDownEvent) { - component.pull(); - } else if (event is RawKeyUpEvent) { - component.release(); - } - - return false; - } -} diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 1a5a06df..c463cd94 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -5,6 +5,7 @@ import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:platform_helper/platform_helper.dart'; /// Listens to the [GameBloc] and updates the game accordingly. class GameBlocStatusListener extends Component @@ -21,12 +22,18 @@ class GameBlocStatusListener extends Component break; case GameStatus.playing: readProvider().play(PinballAudio.backgroundMusic); + _resetBonuses(); gameRef .descendants() .whereType() - .forEach(_addFlipperKeyControls); + .forEach(_addFlipperBehaviors); + gameRef + .descendants() + .whereType() + .forEach(_addPlungerBehaviors); gameRef.overlays.remove(PinballGame.playButtonOverlay); + gameRef.overlays.remove(PinballGame.replayButtonOverlay); break; case GameStatus.gameOver: readProvider().play(PinballAudio.gameOverVoiceOver); @@ -40,18 +47,60 @@ class GameBlocStatusListener extends Component gameRef .descendants() .whereType() - .forEach(_removeFlipperKeyControls); + .forEach(_removeFlipperBehaviors); + gameRef + .descendants() + .whereType() + .forEach(_removePlungerBehaviors); break; } } - void _addFlipperKeyControls(Flipper flipper) { - flipper - ..add(FlipperKeyControllingBehavior()) - ..moveDown(); + void _resetBonuses() { + gameRef + .descendants() + .whereType>() + .single + .bloc + .onReset(); } - void _removeFlipperKeyControls(Flipper flipper) => flipper + void _addPlungerBehaviors(Plunger plunger) { + final platformHelper = readProvider(); + const pullingStrength = 7.0; + final provider = + plunger.firstChild>()!; + + if (platformHelper.isMobile) { + provider.add( + PlungerAutoPullingBehavior(strength: pullingStrength), + ); + } else { + provider.addAll( + [ + PlungerKeyControllingBehavior(), + PlungerPullingBehavior(strength: pullingStrength), + ], + ); + } + } + + void _removePlungerBehaviors(Plunger plunger) { + plunger + .descendants() + .whereType() + .forEach(plunger.remove); + plunger + .descendants() + .whereType() + .forEach(plunger.remove); + } + + void _addFlipperBehaviors(Flipper flipper) => flipper + ..add(FlipperKeyControllingBehavior()) + ..moveDown(); + + void _removeFlipperBehaviors(Flipper flipper) => flipper .descendants() .whereType() .forEach(flipper.remove); diff --git a/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart index ed19f495..2313e921 100644 --- a/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart +++ b/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart @@ -17,7 +17,7 @@ class GoogleWordBonusBehavior extends Component { onNewState: (state) { readBloc() .add(const BonusActivated(GameBonus.googleWord)); - readBloc().onBonusAwarded(); + readBloc().onReset(); add(BonusBallSpawningBehavior()); add(GoogleWordAnimatingBehavior()); }, diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index 4729515a..99b44a80 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -1,5 +1,4 @@ import 'package:flame/components.dart'; -import 'package:pinball/game/components/components.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template launcher} @@ -13,8 +12,7 @@ class Launcher extends Component { children: [ LaunchRamp(), Flapper(), - ControlledPlunger(compressionDistance: 9.2) - ..initialPosition = Vector2(41, 43.7), + Plunger()..initialPosition = Vector2(41, 43.7), RocketSpriteComponent()..position = Vector2(42.8, 62.3), ], ); diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index fccd494e..0d0ef26a 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -11,8 +11,7 @@ extension PinballGameAssetsX on PinballGame { const sparkyTheme = SparkyTheme(); const androidTheme = AndroidTheme(); const dinoTheme = DinoTheme(); - - final gameAssets = [ + return [ images.load(components.Assets.images.boardBackground.keyName), images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.signpost.inactive.keyName), @@ -155,14 +154,10 @@ extension PinballGameAssetsX on PinballGame { images.load(dinoTheme.ball.keyName), images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.ball.keyName), + images.load(androidTheme.background.keyName), + images.load(dashTheme.background.keyName), + images.load(dinoTheme.background.keyName), + images.load(sparkyTheme.background.keyName), ]; - - return (platformHelper.isMobile) ? gameAssets : gameAssets - ..addAll([ - images.load(androidTheme.background.keyName), - images.load(dashTheme.background.keyName), - images.load(dinoTheme.background.keyName), - images.load(sparkyTheme.background.keyName), - ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index b1f3c98a..c102eb0b 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -38,10 +38,13 @@ class PinballGame extends PinballForge2DGame images.prefix = ''; } - /// Identifier of the play button overlay + /// Identifier of the play button overlay. static const playButtonOverlay = 'play_button'; - /// Identifier of the mobile controls overlay + /// Identifier of the replay button overlay. + static const replayButtonOverlay = 'replay_button'; + + /// Identifier of the mobile controls overlay. static const mobileControlsOverlay = 'mobile_controls'; @override @@ -156,9 +159,15 @@ class PinballGame extends PinballForge2DGame final rocket = descendants().whereType().first; final bounds = rocket.topLeftPosition & rocket.size; - // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. - if (bounds.contains(info.eventPosition.game.toOffset())) { - descendants().whereType().single.pullFor(2); + // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 + // we need to check it at the highest level manually. + final tappedRocket = bounds.contains(info.eventPosition.game.toOffset()); + if (tappedRocket) { + descendants() + .whereType>() + .first + .bloc + .pulled(); } else { final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide[pointerId] = diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index efc11996..06fde72d 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -100,22 +100,25 @@ class PinballGameLoadedView extends StatelessWidget { focusNode: game.focusNode, initialActiveOverlays: const [PinballGame.playButtonOverlay], overlayBuilderMap: { - PinballGame.playButtonOverlay: (context, game) { - return const Positioned( - bottom: 20, - right: 0, - left: 0, - child: PlayButtonOverlay(), - ); - }, - PinballGame.mobileControlsOverlay: (context, game) { - return Positioned( - bottom: 0, - left: 0, - right: 0, - child: MobileControls(game: game), - ); - }, + PinballGame.playButtonOverlay: (_, game) => const Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(), + ), + PinballGame.mobileControlsOverlay: (_, game) => Positioned( + bottom: 0, + left: 0, + right: 0, + child: MobileControls(game: game), + ), + PinballGame.replayButtonOverlay: (context, game) => + const Positioned( + bottom: 20, + right: 0, + left: 0, + child: ReplayButtonOverlay(), + ) }, ), ), diff --git a/lib/game/view/widgets/replay_button_overlay.dart b/lib/game/view/widgets/replay_button_overlay.dart index c0b2a67d..806f6ed7 100644 --- a/lib/game/view/widgets/replay_button_overlay.dart +++ b/lib/game/view/widgets/replay_button_overlay.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -18,6 +19,7 @@ class ReplayButtonOverlay extends StatelessWidget { return PinballButton( text: l10n.replay, onTap: () { + context.read().add(const GameStarted()); context.read().add(const ReplayTapped()); }, ); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 64093ac6..6b6e55aa 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -213,7 +213,7 @@ "@socialMediaAccount": { "description": "Text displayed on share screen for description" }, - "iGotScoreAtPinball": "I got {score} at the #IOPinball machine, can you beat my score? See you at #GoogleIO!", + "iGotScoreAtPinball": "I got {score} points in #IOPinball, can you beat my score? \nSee you at #GoogleIO!", "@iGotScoreAtPinball": { "description": "Text to share score on Social Network", "placeholders": { diff --git a/lib/main.dart b/lib/main.dart index 877843ee..11bf35aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:authentication_repository/authentication_repository.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; @@ -17,11 +15,8 @@ void main() { final authenticationRepository = AuthenticationRepository(firebaseAuth); final pinballAudioPlayer = PinballAudioPlayer(); final platformHelper = PlatformHelper(); - unawaited( - Firebase.initializeApp().then( - (_) => authenticationRepository.authenticateAnonymously(), - ), - ); + await Firebase.initializeApp(); + await authenticationRepository.authenticateAnonymously(); return App( authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, diff --git a/packages/pinball_components/lib/src/components/android_animatronic.dart b/packages/pinball_components/lib/src/components/android_animatronic/android_animatronic.dart similarity index 83% rename from packages/pinball_components/lib/src/components/android_animatronic.dart rename to packages/pinball_components/lib/src/components/android_animatronic/android_animatronic.dart index 772d88c4..c78b387c 100644 --- a/packages/pinball_components/lib/src/components/android_animatronic.dart +++ b/packages/pinball_components/lib/src/components/android_animatronic/android_animatronic.dart @@ -1,6 +1,8 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/android_animatronic/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template android_animatronic} @@ -13,6 +15,7 @@ class AndroidAnimatronic extends BodyComponent : super( children: [ _AndroidAnimatronicSpriteAnimationComponent(), + AndroidAnimatronicBallContactBehavior(), ...?children, ], renderBody: false, @@ -21,6 +24,13 @@ class AndroidAnimatronic extends BodyComponent zIndex = ZIndexes.androidHead; } + /// Creates an [AndroidAnimatronic] without any children. + /// + /// This can be used for testing [AndroidAnimatronic]'s behaviors in + /// isolation. + @visibleForTesting + AndroidAnimatronic.test(); + @override Body createBody() { final shape = EllipseShape( diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart b/packages/pinball_components/lib/src/components/android_animatronic/behaviors/android_animatronic_ball_contact_behavior.dart.dart similarity index 58% rename from packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart rename to packages/pinball_components/lib/src/components/android_animatronic/behaviors/android_animatronic_ball_contact_behavior.dart.dart index b577b7b3..6c74e21a 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart +++ b/packages/pinball_components/lib/src/components/android_animatronic/behaviors/android_animatronic_ball_contact_behavior.dart.dart @@ -1,18 +1,15 @@ // ignore_for_file: public_member_api_docs -import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -class AndroidSpaceshipEntranceBallContactBehavior - extends ContactBehavior - with FlameBlocReader { +class AndroidAnimatronicBallContactBehavior + extends ContactBehavior { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); if (other is! Ball) return; - - bloc.onBallEntered(); + readBloc().onBallContacted(); } } diff --git a/packages/pinball_components/lib/src/components/android_animatronic/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/android_animatronic/behaviors/behaviors.dart new file mode 100644 index 00000000..e85e749f --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_animatronic/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'android_animatronic_ball_contact_behavior.dart.dart'; diff --git a/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart index 0fd4628d..d09ff1e4 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart @@ -5,7 +5,6 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; export 'cubit/android_spaceship_cubit.dart'; @@ -17,9 +16,6 @@ class AndroidSpaceship extends Component { _SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position, _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), - AndroidSpaceshipEntrance( - children: [AndroidSpaceshipEntranceBallContactBehavior()], - ), _SpaceshipHole( outsideLayer: Layer.spaceshipExitRail, outsidePriority: ZIndexes.ballOnSpaceshipRail, @@ -134,35 +130,6 @@ class _LightBeamSpriteComponent extends SpriteComponent } } -class AndroidSpaceshipEntrance extends BodyComponent - with ParentIsA, Layered { - AndroidSpaceshipEntrance({Iterable? children}) - : super( - children: children, - renderBody: false, - ) { - layer = Layer.spaceship; - } - - @override - Body createBody() { - final shape = PolygonShape() - ..setAsBox( - 2, - 0.1, - Vector2(-27.4, -37.2), - -0.12, - ); - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef(); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - class _SpaceshipHole extends LayerSensor { _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) : super( diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart deleted file mode 100644 index cbf54e5d..00000000 --- a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart +++ /dev/null @@ -1 +0,0 @@ -export 'android_spaceship_entrance_ball_contact_behavior.dart.dart'; diff --git a/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart index 334c9cc3..5057d742 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart @@ -5,7 +5,7 @@ part 'android_spaceship_state.dart'; class AndroidSpaceshipCubit extends Cubit { AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus); - void onBallEntered() => emit(AndroidSpaceshipState.withBonus); + void onBallContacted() => emit(AndroidSpaceshipState.withBonus); void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus); } diff --git a/packages/pinball_components/lib/src/components/bumping_behavior.dart b/packages/pinball_components/lib/src/components/bumping_behavior.dart index 17931838..0d259860 100644 --- a/packages/pinball_components/lib/src/components/bumping_behavior.dart +++ b/packages/pinball_components/lib/src/components/bumping_behavior.dart @@ -7,7 +7,9 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@endtemplate} class BumpingBehavior extends ContactBehavior { /// {@macro bumping_behavior} - BumpingBehavior({required double strength}) : _strength = strength; + BumpingBehavior({required double strength}) + : assert(strength >= 0, "Strength can't be negative."), + _strength = strength; /// Determines how strong the bump is. final double _strength; diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 1116ee88..63684921 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,4 +1,4 @@ -export 'android_animatronic.dart'; +export 'android_animatronic/android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; export 'android_spaceship/android_spaceship.dart'; export 'arcade_background/arcade_background.dart'; @@ -27,7 +27,7 @@ export 'launch_ramp.dart'; export 'layer_sensor/layer_sensor.dart'; export 'multiball/multiball.dart'; export 'multiplier/multiplier.dart'; -export 'plunger.dart'; +export 'plunger/plunger.dart'; export 'rocket.dart'; export 'score_component/score_component.dart'; export 'signpost/signpost.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart index 95566e75..ca4fcece 100644 --- a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart @@ -14,7 +14,21 @@ class FlipperKeyControllingBehavior extends Component @override Future onLoad() async { await super.onLoad(); - _keys = parent.side.flipperKeys; + + switch (parent.side) { + case BoardSide.left: + _keys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]; + break; + case BoardSide.right: + _keys = [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]; + break; + } } @override @@ -33,20 +47,3 @@ class FlipperKeyControllingBehavior extends Component return false; } } - -extension on BoardSide { - List get flipperKeys { - switch (this) { - case BoardSide.left: - return [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ]; - case BoardSide.right: - return [ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ]; - } - } -} diff --git a/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart b/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart index c16d4a2e..2119c2f8 100644 --- a/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart +++ b/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart @@ -17,7 +17,7 @@ class GoogleWordAnimatingBehavior extends TimerComponent _blinks++; } else { timer.stop(); - bloc.onAnimationFinished(); + bloc.onReset(); shouldRemove = true; } } diff --git a/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart index 8a8b976d..cd69fc9d 100644 --- a/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart +++ b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart @@ -68,7 +68,7 @@ class GoogleWordCubit extends Cubit { ); } - void onAnimationFinished() { + void onReset() { emit(GoogleWordState.initial()); _lastLitLetter = 0; } diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart deleted file mode 100644 index 6f38eb37..00000000 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template plunger} -/// [Plunger] serves as a spring, that shoots the ball on the right side of the -/// play field. -/// -/// [Plunger] ignores gravity so the player controls its downward [pull]. -/// {@endtemplate} -class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { - /// {@macro plunger} - Plunger({ - required this.compressionDistance, - }) : super( - renderBody: false, - children: [_PlungerSpriteAnimationGroupComponent()], - ) { - zIndex = ZIndexes.plunger; - layer = Layer.launcher; - } - - /// Creates a [Plunger] without any children. - /// - /// This can be used for testing [Plunger]'s behaviors in isolation. - @visibleForTesting - Plunger.test({required this.compressionDistance}); - - /// Distance the plunger can lower. - final double compressionDistance; - - List _createFixtureDefs() { - final fixturesDef = []; - - final leftShapeVertices = [ - Vector2(0, 0), - Vector2(-1.8, 0), - Vector2(-1.8, -2.2), - Vector2(0, -0.3), - ]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle)) - .toList(); - final leftTriangleShape = PolygonShape()..set(leftShapeVertices); - - final leftTriangleFixtureDef = FixtureDef(leftTriangleShape)..density = 80; - fixturesDef.add(leftTriangleFixtureDef); - - final rightShapeVertices = [ - Vector2(0, 0), - Vector2(1.8, 0), - Vector2(1.8, -2.2), - Vector2(0, -0.3), - ]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle)) - .toList(); - final rightTriangleShape = PolygonShape()..set(rightShapeVertices); - - final rightTriangleFixtureDef = FixtureDef(rightTriangleShape) - ..density = 80; - fixturesDef.add(rightTriangleFixtureDef); - - return fixturesDef; - } - - @override - Body createBody() { - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - type: BodyType.dynamic, - gravityScale: Vector2.zero(), - ); - - final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); - return body; - } - - var _pullingDownTime = 0.0; - - /// Pulls the plunger down for the given amount of [seconds]. - // ignore: use_setters_to_change_properties - void pullFor(double seconds) { - _pullingDownTime = seconds; - } - - /// Set a constant downward velocity on the [Plunger]. - void pull() { - final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; - - body.linearVelocity = Vector2(0, 7); - sprite.pull(); - } - - /// Set an upward velocity on the [Plunger]. - /// - /// The velocity's magnitude depends on how far the [Plunger] has been pulled - /// from its original [initialPosition]. - void release() { - final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; - - _pullingDownTime = 0; - final velocity = (initialPosition.y - body.position.y) * 11; - body.linearVelocity = Vector2(0, velocity); - sprite.release(); - } - - @override - void update(double dt) { - // Ensure that we only pull or release when the time is greater than zero. - if (_pullingDownTime > 0) { - _pullingDownTime -= PinballForge2DGame.clampDt(dt); - if (_pullingDownTime <= 0) { - release(); - } else { - pull(); - } - } - super.update(dt); - } - - /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical - /// motion. - Future _anchorToJoint() async { - final anchor = PlungerAnchor(plunger: this); - await add(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: this, - anchor: anchor, - ); - - world.createJoint( - PrismaticJoint(jointDef)..setLimits(-compressionDistance, 0), - ); - } - - @override - Future onLoad() async { - await super.onLoad(); - await _anchorToJoint(); - } -} - -/// Animation states associated with a [Plunger]. -enum _PlungerAnimationState { - /// Pull state. - pull, - - /// Release state. - release, -} - -/// Animations for pulling and releasing [Plunger]. -class _PlungerSpriteAnimationGroupComponent - extends SpriteAnimationGroupComponent<_PlungerAnimationState> - with HasGameRef { - _PlungerSpriteAnimationGroupComponent() - : super( - anchor: Anchor.center, - position: Vector2(1.87, 14.9), - ); - - void pull() { - if (current != _PlungerAnimationState.pull) { - animation?.reset(); - } - current = _PlungerAnimationState.pull; - } - - void release() { - if (current != _PlungerAnimationState.release) { - animation?.reset(); - } - current = _PlungerAnimationState.release; - } - - @override - Future onLoad() async { - await super.onLoad(); - final spriteSheet = await gameRef.images.load( - Assets.images.plunger.plunger.keyName, - ); - const amountPerRow = 20; - const amountPerColumn = 1; - final textureSize = Vector2( - spriteSheet.width / amountPerRow, - spriteSheet.height / amountPerColumn, - ); - size = textureSize / 10; - final pullAnimation = SpriteAnimation.fromFrameData( - spriteSheet, - SpriteAnimationData.sequenced( - amount: amountPerRow * amountPerColumn ~/ 2, - amountPerRow: amountPerRow ~/ 2, - stepTime: 1 / 24, - textureSize: textureSize, - texturePosition: Vector2.zero(), - loop: false, - ), - ); - animations = { - _PlungerAnimationState.release: pullAnimation.reversed(), - _PlungerAnimationState.pull: pullAnimation, - }; - current = _PlungerAnimationState.release; - } -} - -/// {@template plunger_anchor} -/// [JointAnchor] positioned below a [Plunger]. -/// {@endtemplate} -class PlungerAnchor extends JointAnchor { - /// {@macro plunger_anchor} - PlungerAnchor({ - required Plunger plunger, - }) { - initialPosition = Vector2( - 0, - plunger.compressionDistance, - ); - } -} - -/// {@template plunger_anchor_prismatic_joint_def} -/// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on -/// the vertical axis. -/// -/// The [Plunger] is constrained vertically between its starting position and -/// the [JointAnchor]. The [JointAnchor] must be below the [Plunger]. -/// {@endtemplate} -class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { - /// {@macro plunger_anchor_prismatic_joint_def} - PlungerAnchorPrismaticJointDef({ - required Plunger plunger, - required PlungerAnchor anchor, - }) { - initialize( - plunger.body, - anchor.body, - plunger.body.position + anchor.body.position, - Vector2(16, BoardDimensions.bounds.height), - ); - enableLimit = true; - lowerTranslation = double.negativeInfinity; - enableMotor = true; - motorSpeed = 1000; - maxMotorForce = motorSpeed; - collideConnected = true; - } -} diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart new file mode 100644 index 00000000..0c772a0e --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart @@ -0,0 +1,5 @@ +export 'plunger_jointing_behavior.dart'; +export 'plunger_key_controlling_behavior.dart'; +export 'plunger_noise_behavior.dart'; +export 'plunger_pulling_behavior.dart'; +export 'plunger_releasing_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_jointing_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_jointing_behavior.dart new file mode 100644 index 00000000..06332bef --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_jointing_behavior.dart @@ -0,0 +1,54 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class PlungerJointingBehavior extends Component with ParentIsA { + PlungerJointingBehavior({required double compressionDistance}) + : _compressionDistance = compressionDistance; + + final double _compressionDistance; + + @override + Future onLoad() async { + await super.onLoad(); + final anchor = JointAnchor() + ..initialPosition = Vector2(0, _compressionDistance); + await add(anchor); + + final jointDef = _PlungerAnchorPrismaticJointDef( + plunger: parent, + anchor: anchor, + ); + + parent.world.createJoint( + PrismaticJoint(jointDef)..setLimits(-_compressionDistance, 0), + ); + } +} + +/// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on +/// the vertical axis. +/// +/// The [Plunger] is constrained vertically between its starting position and +/// the [JointAnchor]. The [JointAnchor] must be below the [Plunger]. +class _PlungerAnchorPrismaticJointDef extends PrismaticJointDef { + /// {@macro plunger_anchor_prismatic_joint_def} + _PlungerAnchorPrismaticJointDef({ + required Plunger plunger, + required BodyComponent anchor, + }) { + initialize( + plunger.body, + anchor.body, + plunger.body.position + anchor.body.position, + Vector2(16, BoardDimensions.bounds.height), + ); + enableLimit = true; + lowerTranslation = double.negativeInfinity; + enableMotor = true; + motorSpeed = 1000; + maxMotorForce = motorSpeed; + collideConnected = true; + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart new file mode 100644 index 00000000..fcff816a --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart @@ -0,0 +1,33 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// Allows controlling the [Plunger]'s movement with keyboard input. +class PlungerKeyControllingBehavior extends Component + with KeyboardHandler, FlameBlocReader { + /// The [LogicalKeyboardKey]s that will control the [Plunger]. + /// + /// [onKeyEvent] method listens to when one of these keys is pressed. + static const List _keys = [ + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.space, + LogicalKeyboardKey.keyS, + ]; + + @override + bool onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (!_keys.contains(event.logicalKey)) return true; + + if (event is RawKeyDownEvent) { + bloc.pulled(); + } else if (event is RawKeyUpEvent) { + bloc.released(); + } + + return false; + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart new file mode 100644 index 00000000..96cb9bd2 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart @@ -0,0 +1,19 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Plays the [PinballAudio.launcher] sound. +/// +/// It is attached when the plunger is released. +class PlungerNoiseBehavior extends Component + with FlameBlocListenable { + @override + void onNewState(PlungerState state) { + super.onNewState(state); + if (state.isReleasing) { + readProvider().play(PinballAudio.launcher); + } + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart new file mode 100644 index 00000000..db6bcaa3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class PlungerPullingBehavior extends Component + with FlameBlocReader { + PlungerPullingBehavior({ + required double strength, + }) : assert(strength >= 0, "Strength can't be negative."), + _strength = strength; + + final double _strength; + + late final Plunger _plunger; + + @override + Future onLoad() async { + await super.onLoad(); + _plunger = parent!.parent! as Plunger; + } + + @override + void update(double dt) { + if (bloc.state.isPulling) { + _plunger.body.linearVelocity = Vector2(0, _strength); + } + } +} + +class PlungerAutoPullingBehavior extends PlungerPullingBehavior { + PlungerAutoPullingBehavior({ + required double strength, + }) : super(strength: strength); + + @override + void update(double dt) { + super.update(dt); + + final joint = _plunger.body.joints.whereType().single; + final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit(); + if (reachedBottom) { + bloc.released(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart new file mode 100644 index 00000000..d2935818 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart @@ -0,0 +1,31 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class PlungerReleasingBehavior extends Component + with FlameBlocListenable { + PlungerReleasingBehavior({ + required double strength, + }) : assert(strength >= 0, "Strength can't be negative."), + _strength = strength; + + final double _strength; + + late final Plunger _plunger; + + @override + Future onLoad() async { + await super.onLoad(); + _plunger = parent!.parent! as Plunger; + } + + @override + void onNewState(PlungerState state) { + super.onNewState(state); + if (state.isReleasing) { + final velocity = + (_plunger.initialPosition.y - _plunger.body.position.y) * _strength; + _plunger.body.linearVelocity = Vector2(0, velocity); + } + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart new file mode 100644 index 00000000..601257d2 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart @@ -0,0 +1,15 @@ +import 'package:bloc/bloc.dart'; + +part 'plunger_state.dart'; + +class PlungerCubit extends Cubit { + PlungerCubit() : super(PlungerState.releasing); + + void pulled() { + emit(PlungerState.pulling); + } + + void released() { + emit(PlungerState.releasing); + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart new file mode 100644 index 00000000..8b82ef96 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart @@ -0,0 +1,12 @@ +part of 'plunger_cubit.dart'; + +enum PlungerState { + pulling, + + releasing, +} + +extension PlungerStateX on PlungerState { + bool get isPulling => this == PlungerState.pulling; + bool get isReleasing => this == PlungerState.releasing; +} diff --git a/packages/pinball_components/lib/src/components/plunger/plunger.dart b/packages/pinball_components/lib/src/components/plunger/plunger.dart new file mode 100644 index 00000000..9f3b6873 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/plunger.dart @@ -0,0 +1,139 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'behaviors/behaviors.dart'; +export 'cubit/plunger_cubit.dart'; + +/// {@template plunger} +/// [Plunger] serves as a spring, that shoots the ball on the right side of the +/// play field. +/// +/// [Plunger] ignores gravity so the player controls its downward movement. +/// {@endtemplate} +class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { + /// {@macro plunger} + Plunger() + : super( + renderBody: false, + children: [ + FlameBlocProvider( + create: PlungerCubit.new, + children: [ + _PlungerSpriteAnimationGroupComponent(), + PlungerReleasingBehavior(strength: 11), + PlungerNoiseBehavior(), + ], + ), + PlungerJointingBehavior(compressionDistance: 9.2), + ], + ) { + zIndex = ZIndexes.plunger; + layer = Layer.launcher; + } + + /// Creates a [Plunger] without any children. + /// + /// This can be used for testing [Plunger]'s behaviors in isolation. + @visibleForTesting + Plunger.test(); + + List _createFixtureDefs() { + final leftShapeVertices = [ + Vector2(0, 0), + Vector2(-1.8, 0), + Vector2(-1.8, -2.2), + Vector2(0, -0.3), + ]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle)); + final leftTriangleShape = PolygonShape()..set(leftShapeVertices); + + final rightShapeVertices = [ + Vector2(0, 0), + Vector2(1.8, 0), + Vector2(1.8, -2.2), + Vector2(0, -0.3), + ]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle)); + final rightTriangleShape = PolygonShape()..set(rightShapeVertices); + + return [ + FixtureDef( + leftTriangleShape, + density: 80, + ), + FixtureDef( + rightTriangleShape, + density: 80, + ), + ]; + } + + @override + Body createBody() { + final bodyDef = BodyDef( + position: initialPosition, + type: BodyType.dynamic, + gravityScale: Vector2.zero(), + ); + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +class _PlungerSpriteAnimationGroupComponent + extends SpriteAnimationGroupComponent + with HasGameRef, FlameBlocListenable { + _PlungerSpriteAnimationGroupComponent() + : super( + anchor: Anchor.center, + position: Vector2(1.87, 14.9), + ); + + @override + void onNewState(PlungerState state) { + super.onNewState(state); + final startedReleasing = state.isReleasing && !current!.isReleasing; + final startedPulling = state.isPulling && !current!.isPulling; + if (startedReleasing || startedPulling) { + animation?.reset(); + } + + current = state; + } + + @override + Future onLoad() async { + await super.onLoad(); + final spriteSheet = await gameRef.images.load( + Assets.images.plunger.plunger.keyName, + ); + const amountPerRow = 20; + const amountPerColumn = 1; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + final pullAnimation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn ~/ 2, + amountPerRow: amountPerRow ~/ 2, + stepTime: 1 / 24, + textureSize: textureSize, + texturePosition: Vector2.zero(), + loop: false, + ), + ); + animations = { + PlungerState.releasing: pullAnimation.reversed(), + PlungerState.pulling: pullAnimation, + }; + + current = readBloc().state; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index fe52f8b8..7fe3ac5e 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -18,6 +18,8 @@ dependencies: flutter: sdk: flutter intl: ^0.17.0 + pinball_audio: + path: ../pinball_audio pinball_flame: path: ../pinball_flame pinball_theme: 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 0ee58cc9..50af919f 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -1,11 +1,10 @@ import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class PlungerGame extends BallGame with KeyboardEvents, Traceable { +class PlungerGame extends BallGame + with HasKeyboardHandlerComponents, Traceable { static const description = ''' Shows how Plunger is rendered. @@ -13,39 +12,16 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable { - Tap anywhere on the screen to spawn a ball into the game. '''; - static const _downKeys = [ - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.space, - ]; - - late Plunger plunger; - @override Future onLoad() async { await super.onLoad(); final center = screenToWorld(camera.viewport.canvasSize! / 2); - await add( - plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(center.x - 8.8, center.y), - ); - await traceAllBodies(); - } + final plunger = Plunger() + ..initialPosition = Vector2(center.x - 8.8, center.y); + await add(plunger); + await plunger.add(PlungerKeyControllingBehavior()); - @override - KeyEventResult onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - final movedPlungerDown = _downKeys.contains(event.logicalKey); - if (movedPlungerDown) { - if (event is RawKeyDownEvent) { - plunger.pull(); - } else if (event is RawKeyUpEvent) { - plunger.release(); - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; + await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index b5ac88b7..9fcb0f89 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + audioplayers: + dependency: transitive + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.20.1" bloc: dependency: transitive description: @@ -57,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" dashbook: dependency: "direct main" description: @@ -106,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + flame_audio: + dependency: transitive + description: + name: flame_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" flame_bloc: dependency: transitive description: @@ -179,6 +200,20 @@ packages: relative: true source: path version: "1.0.0+1" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" intl: dependency: transitive description: @@ -249,6 +284,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" path_provider_linux: dependency: transitive description: @@ -256,6 +312,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: @@ -270,6 +333,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + pinball_audio: + dependency: transitive + description: + path: "../../pinball_audio" + relative: true + source: path + version: "1.0.0+1" pinball_components: dependency: "direct main" description: @@ -415,6 +485,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -492,6 +569,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: diff --git a/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/android_animatronic/behaviors/android_animatronic_ball_contact_behavior_test.dart similarity index 69% rename from packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart rename to packages/pinball_components/test/src/components/android_animatronic/behaviors/android_animatronic_ball_contact_behavior_test.dart index 4b0f16ea..4d8bb675 100644 --- a/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/android_animatronic/behaviors/android_animatronic_ball_contact_behavior_test.dart @@ -7,7 +7,7 @@ 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/android_spaceship/behaviors/behaviors.dart'; +import 'package:pinball_components/src/components/android_animatronic/behaviors/behaviors.dart'; import '../../../../helpers/helpers.dart'; @@ -23,19 +23,19 @@ void main() { final flameTester = FlameTester(TestGame.new); group( - 'AndroidSpaceshipEntranceBallContactBehavior', + 'AndroidAnimatronicBallContactBehavior', () { test('can be instantiated', () { expect( - AndroidSpaceshipEntranceBallContactBehavior(), - isA(), + AndroidAnimatronicBallContactBehavior(), + isA(), ); }); flameTester.test( - 'beginContact calls onBallEntered when entrance contacts with a ball', + 'beginContact calls onBallContacted when in contact with a ball', (game) async { - final behavior = AndroidSpaceshipEntranceBallContactBehavior(); + final behavior = AndroidAnimatronicBallContactBehavior(); final bloc = _MockAndroidSpaceshipCubit(); whenListen( bloc, @@ -43,20 +43,20 @@ void main() { initialState: AndroidSpaceshipState.withoutBonus, ); - final entrance = AndroidSpaceshipEntrance(); + final animatronic = AndroidAnimatronic.test(); final androidSpaceship = FlameBlocProvider.value( value: bloc, children: [ - AndroidSpaceship.test(children: [entrance]) + AndroidSpaceship.test(children: [animatronic]) ], ); - await entrance.add(behavior); + await animatronic.add(behavior); await game.ensureAdd(androidSpaceship); behavior.beginContact(_MockBall(), _MockContact()); - verify(bloc.onBallEntered).called(1); + verify(bloc.onBallContacted).called(1); }, ); }, diff --git a/packages/pinball_components/test/src/components/android_animatronic_test.dart b/packages/pinball_components/test/src/components/android_animatronic_test.dart index 65114778..55b564fe 100644 --- a/packages/pinball_components/test/src/components/android_animatronic_test.dart +++ b/packages/pinball_components/test/src/components/android_animatronic_test.dart @@ -4,6 +4,7 @@ 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_components/src/components/android_animatronic/behaviors/behaviors.dart'; import '../../helpers/helpers.dart'; @@ -58,13 +59,26 @@ void main() { }, ); - flameTester.test('adds new children', (game) async { - final component = Component(); - final androidAnimatronic = AndroidAnimatronic( - children: [component], - ); - await game.ensureAdd(androidAnimatronic); - expect(androidAnimatronic.children, contains(component)); + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final androidAnimatronic = AndroidAnimatronic( + children: [component], + ); + await game.ensureAdd(androidAnimatronic); + expect(androidAnimatronic.children, contains(component)); + }); + + flameTester.test('a AndroidAnimatronicBallContactBehavior', (game) async { + final androidAnimatronic = AndroidAnimatronic(); + await game.ensureAdd(androidAnimatronic); + expect( + androidAnimatronic.children + .whereType() + .single, + isNotNull, + ); + }); }); }); } diff --git a/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart index 70edd32e..a282865c 100644 --- a/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart +++ b/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart @@ -6,7 +6,6 @@ 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/android_spaceship/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; @@ -84,26 +83,5 @@ void main() { ); }, ); - - flameTester.test( - 'AndroidSpaceshipEntrance has an ' - 'AndroidSpaceshipEntranceBallContactBehavior', (game) async { - final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); - final provider = - FlameBlocProvider.value( - value: bloc, - children: [androidSpaceship], - ); - await game.ensureAdd(provider); - - final androidSpaceshipEntrance = - androidSpaceship.firstChild(); - expect( - androidSpaceshipEntrance!.children - .whereType() - .single, - isNotNull, - ); - }); }); } diff --git a/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart b/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart index 47b763af..f7de3674 100644 --- a/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart +++ b/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart @@ -9,7 +9,7 @@ void main() { blocTest( 'onBallEntered emits withBonus', build: AndroidSpaceshipCubit.new, - act: (bloc) => bloc.onBallEntered(), + act: (bloc) => bloc.onBallContacted(), expect: () => [AndroidSpaceshipState.withBonus], ); diff --git a/packages/pinball_components/test/src/components/bumping_behavior_test.dart b/packages/pinball_components/test/src/components/bumping_behavior_test.dart index 07e35cca..7a87a46c 100644 --- a/packages/pinball_components/test/src/components/bumping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/bumping_behavior_test.dart @@ -24,6 +24,20 @@ void main() { final flameTester = FlameTester(TestGame.new); group('BumpingBehavior', () { + test('can be instantiated', () { + expect( + BumpingBehavior(strength: 0), + isA(), + ); + }); + + test('throws assertion error when strength is negative ', () { + expect( + () => BumpingBehavior(strength: -1), + throwsAssertionError, + ); + }); + flameTester.test('can be added', (game) async { final behavior = BumpingBehavior(strength: 0); final component = _TestBodyComponent(); diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart index 3d6c3b83..70c93439 100644 --- a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart @@ -19,19 +19,18 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final behavior = FlipperJointingBehavior(); final parent = Flipper.test(side: BoardSide.left); + final behavior = FlipperJointingBehavior(); await game.ensureAdd(parent); await parent.ensureAdd(behavior); expect(parent.contains(behavior), isTrue); }); flameTester.test('creates a joint', (game) async { - final behavior = FlipperJointingBehavior(); final parent = Flipper.test(side: BoardSide.left); + final behavior = FlipperJointingBehavior(); await game.ensureAdd(parent); await parent.ensureAdd(behavior); - expect(parent.body.joints, isNotEmpty); }); }); diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart index 11af6187..6a6ac91c 100644 --- a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -8,8 +9,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; - class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { @@ -27,7 +26,7 @@ class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FlipperKeyControllingBehavior', () { - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(Forge2DGame.new); group( 'onKeyEvent', diff --git a/packages/pinball_components/test/src/components/golden/plunger/pull.png b/packages/pinball_components/test/src/components/golden/plunger/pull.png index cdbb3e31..3a3e204f 100644 Binary files a/packages/pinball_components/test/src/components/golden/plunger/pull.png and b/packages/pinball_components/test/src/components/golden/plunger/pull.png differ diff --git a/packages/pinball_components/test/src/components/golden/plunger/release.png b/packages/pinball_components/test/src/components/golden/plunger/release.png index cda853c3..2aae1a50 100644 Binary files a/packages/pinball_components/test/src/components/golden/plunger/release.png and b/packages/pinball_components/test/src/components/golden/plunger/release.png differ diff --git a/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart b/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart index 7224aeed..6275678c 100644 --- a/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart @@ -41,7 +41,7 @@ void main() { ); flameTester.testGameWidget( - 'calls onAnimationFinished and removes itself ' + 'calls onReset and removes itself ' 'after all blinks complete', setUp: (game, tester) async { final behavior = GoogleWordAnimatingBehavior(); @@ -53,7 +53,7 @@ void main() { } await game.ready(); - verify(bloc.onAnimationFinished).called(1); + verify(bloc.onReset).called(1); expect( game.descendants().whereType().isEmpty, isTrue, diff --git a/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart b/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart index b5000387..152b5f96 100644 --- a/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart +++ b/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart @@ -62,9 +62,9 @@ void main() { ); blocTest( - 'onAnimationFinished emits initial state', + 'onReset emits initial state', build: GoogleWordCubit.new, - act: (bloc) => bloc.onAnimationFinished(), + act: (bloc) => bloc.onReset(), expect: () => [GoogleWordState.initial()], ); }, diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart new file mode 100644 index 00000000..940ea625 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart @@ -0,0 +1,36 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(Forge2DGame.new); + + group('PlungerJointingBehavior', () { + test('can be instantiated', () { + expect( + PlungerJointingBehavior(compressionDistance: 0), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final parent = Plunger.test(); + final behavior = PlungerJointingBehavior(compressionDistance: 0); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.children, contains(behavior)); + }); + + flameTester.test('creates a joint', (game) async { + final behavior = PlungerJointingBehavior(compressionDistance: 0); + final parent = Plunger.test(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.body.joints, isNotEmpty); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart new file mode 100644 index 00000000..1147d7f3 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart @@ -0,0 +1,194 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + PlungerKeyControllingBehavior child, { + PlungerCubit? plungerBloc, + }) async { + final plunger = Plunger.test(); + await ensureAdd(plunger); + return plunger.ensureAdd( + FlameBlocProvider.value( + value: plungerBloc ?? _MockPlungerCubit(), + children: [child], + ), + ); + } +} + +class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('PlungerKeyControllingBehavior', () { + test('can be instantiated', () { + expect( + PlungerKeyControllingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + group('onKeyEvent', () { + late PlungerCubit plungerBloc; + + setUp(() { + plungerBloc = _MockPlungerCubit(); + }); + + group('pulls when', () { + flameTester.test( + 'down arrow is pressed', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowDown, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.pulled()).called(1); + }, + ); + + flameTester.test( + '"s" is pressed', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyS, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.pulled()).called(1); + }, + ); + + flameTester.test( + 'space is pressed', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.space, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.pulled()).called(1); + }, + ); + }); + + group('releases when', () { + flameTester.test( + 'down arrow is released', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowDown, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.released()).called(1); + }, + ); + + flameTester.test( + '"s" is released', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyS, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.released()).called(1); + }, + ); + + flameTester.test( + 'space is released', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.space, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.released()).called(1); + }, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart new file mode 100644 index 00000000..a5e11ad0 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart @@ -0,0 +1,91 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + Component child, { + PinballAudioPlayer? pinballAudioPlayer, + PlungerCubit? plungerBloc, + }) async { + final parent = Component(); + await ensureAdd(parent); + return parent.ensureAdd( + FlameProvider.value( + pinballAudioPlayer ?? _MockPinballAudioPlayer(), + children: [ + FlameBlocProvider.value( + value: plungerBloc ?? PlungerCubit(), + children: [child], + ), + ], + ), + ); + } +} + +class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('PlungerNoiseBehavior', () { + late PinballAudioPlayer audioPlayer; + + setUp(() { + audioPlayer = _MockPinballAudioPlayer(); + }); + + test('can be instantiated', () { + expect( + PlungerNoiseBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerNoiseBehavior(); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + 'plays the correct sound when released', + (game) async { + final plungerBloc = _MockPlungerCubit(); + final streamController = StreamController(); + whenListen( + plungerBloc, + streamController.stream, + initialState: PlungerState.pulling, + ); + + final behavior = PlungerNoiseBehavior(); + await game.pump( + behavior, + pinballAudioPlayer: audioPlayer, + plungerBloc: plungerBloc, + ); + + streamController.add(PlungerState.releasing); + await Future.delayed(Duration.zero); + + verify(() => audioPlayer.play(PinballAudio.launcher)).called(1); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart new file mode 100644 index 00000000..4eec7029 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart @@ -0,0 +1,160 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + PlungerPullingBehavior behavior, { + PlungerCubit? plungerBloc, + }) async { + final plunger = Plunger.test(); + await ensureAdd(plunger); + return plunger.ensureAdd( + FlameBlocProvider.value( + value: plungerBloc ?? _MockPlungerCubit(), + children: [behavior], + ), + ); + } +} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} + +class _MockPrismaticJoint extends Mock implements PrismaticJoint {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('PlungerPullingBehavior', () { + test('can be instantiated', () { + expect( + PlungerPullingBehavior(strength: 0), + isA(), + ); + }); + + test('throws assertion error when strength is negative ', () { + expect( + () => PlungerPullingBehavior(strength: -1), + throwsAssertionError, + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerPullingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + 'applies vertical linear velocity when pulled', + (game) async { + final plungerBloc = _MockPlungerCubit(); + whenListen( + plungerBloc, + Stream.value(PlungerState.pulling), + initialState: PlungerState.pulling, + ); + + const strength = 2.0; + final behavior = PlungerPullingBehavior( + strength: strength, + ); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + game.update(0); + + final plunger = behavior.ancestors().whereType().single; + expect(plunger.body.linearVelocity.x, equals(0)); + expect(plunger.body.linearVelocity.y, equals(strength)); + }, + ); + }); + + group('PlungerAutoPullingBehavior', () { + test('can be instantiated', () { + expect( + PlungerAutoPullingBehavior(strength: 0), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerAutoPullingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + "pulls while joint hasn't reached limit", + (game) async { + final plungerBloc = _MockPlungerCubit(); + whenListen( + plungerBloc, + Stream.value(PlungerState.pulling), + initialState: PlungerState.pulling, + ); + + const strength = 2.0; + final behavior = PlungerAutoPullingBehavior( + strength: strength, + ); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + final plunger = behavior.ancestors().whereType().single; + final joint = _MockPrismaticJoint(); + when(joint.getJointTranslation).thenReturn(2); + when(joint.getLowerLimit).thenReturn(0); + plunger.body.joints.add(joint); + + game.update(0); + + expect(plunger.body.linearVelocity.x, equals(0)); + expect(plunger.body.linearVelocity.y, equals(strength)); + }, + ); + + flameTester.test( + 'releases when joint reaches limit', + (game) async { + final plungerBloc = _MockPlungerCubit(); + whenListen( + plungerBloc, + Stream.value(PlungerState.pulling), + initialState: PlungerState.pulling, + ); + + const strength = 2.0; + final behavior = PlungerAutoPullingBehavior( + strength: strength, + ); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + final plunger = behavior.ancestors().whereType().single; + final joint = _MockPrismaticJoint(); + when(joint.getJointTranslation).thenReturn(0); + when(joint.getLowerLimit).thenReturn(0); + plunger.body.joints.add(joint); + + game.update(0); + + verify(plungerBloc.released).called(1); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart new file mode 100644 index 00000000..501753c4 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart @@ -0,0 +1,79 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.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'; + +class _TestGame extends Forge2DGame { + Future pump( + PlungerReleasingBehavior behavior, { + PlungerCubit? plungerBloc, + }) async { + final plunger = Plunger.test(); + await ensureAdd(plunger); + return plunger.ensureAdd( + FlameBlocProvider.value( + value: plungerBloc ?? PlungerCubit(), + children: [behavior], + ), + ); + } +} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} + +void main() { + group('PlungerReleasingBehavior', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + test('can be instantiated', () { + expect( + PlungerReleasingBehavior(strength: 0), + isA(), + ); + }); + + test('throws assertion error when strength is negative ', () { + expect( + () => PlungerReleasingBehavior(strength: -1), + throwsAssertionError, + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerReleasingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test('applies vertical linear velocity', (game) async { + final plungerBloc = _MockPlungerCubit(); + final streamController = StreamController(); + whenListen( + plungerBloc, + streamController.stream, + initialState: PlungerState.pulling, + ); + + final behavior = PlungerReleasingBehavior(strength: 2); + await game.pump( + behavior, + plungerBloc: plungerBloc, + ); + + streamController.add(PlungerState.releasing); + await Future.delayed(Duration.zero); + + final plunger = behavior.ancestors().whereType().single; + expect(plunger.body.linearVelocity.x, equals(0)); + expect(plunger.body.linearVelocity.y, isNot(greaterThan(0))); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/plunger_test.dart b/packages/pinball_components/test/src/components/plunger/plunger_test.dart new file mode 100644 index 00000000..32a6a45b --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/plunger_test.dart @@ -0,0 +1,116 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Plunger', () { + test('can be instantiated', () { + expect(Plunger(), isA()); + }); + + flameTester.test( + 'loads correctly', + (game) async { + final plunger = Plunger(); + await game.ensureAdd(plunger); + expect(game.children, contains(plunger)); + }, + ); + + group('adds', () { + flameTester.test( + 'a PlungerReleasingBehavior', + (game) async { + final plunger = Plunger(); + await game.ensureAdd(plunger); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a PlungerJointingBehavior', + (game) async { + final plunger = Plunger(); + await game.ensureAdd(plunger); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a PlungerNoiseBehavior', + (game) async { + final plunger = Plunger(); + await game.ensureAdd(plunger); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + }); + + group('renders correctly', () { + const goldenPath = '../golden/plunger/'; + flameTester.testGameWidget( + 'pulling', + setUp: (game, tester) async { + await game.ensureAdd(Plunger()); + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 4.1; + }, + verify: (game, tester) async { + final plunger = game.descendants().whereType().first; + final bloc = plunger + .descendants() + .whereType>() + .single + .bloc; + bloc.pulled(); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPath}pull.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'releasing', + setUp: (game, tester) async { + await game.ensureAdd(Plunger()); + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 4.1; + }, + verify: (game, tester) async { + final plunger = game.descendants().whereType().first; + final bloc = plunger + .descendants() + .whereType>() + .single + .bloc; + bloc.released(); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPath}release.png'), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart deleted file mode 100644 index e28bdaed..00000000 --- a/packages/pinball_components/test/src/components/plunger_test.dart +++ /dev/null @@ -1,391 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); - - group('Plunger', () { - const compressionDistance = 0.0; - - test('can be instantiated', () { - expect( - Plunger(compressionDistance: compressionDistance), - isA(), - ); - expect( - Plunger.test(compressionDistance: compressionDistance), - isA(), - ); - }); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.ensureAdd(Plunger(compressionDistance: compressionDistance)); - - game.camera.followVector2(Vector2.zero()); - game.camera.zoom = 4.1; - }, - verify: (game, tester) async { - final plunger = game.descendants().whereType().first; - plunger.pull(); - game.update(1); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/plunger/pull.png'), - ); - - plunger.release(); - game.update(1); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/plunger/release.png'), - ); - }, - ); - - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - expect(game.contains(plunger), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'is dynamic', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - expect(plunger.body.bodyType, equals(BodyType.dynamic)); - }, - ); - - flameTester.test( - 'ignores gravity', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - expect(plunger.body.gravityScale, equals(Vector2.zero())); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - expect(plunger.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'shape is a polygon', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - final fixture = plunger.body.fixtures[0]; - expect(fixture.shape.shapeType, equals(ShapeType.polygon)); - }, - ); - - flameTester.test( - 'has density', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - final fixture = plunger.body.fixtures[0]; - expect(fixture.density, greaterThan(0)); - }, - ); - }); - - group('pullFor', () { - late Plunger plunger; - - setUp(() { - plunger = Plunger( - compressionDistance: compressionDistance, - ); - }); - - flameTester.testGameWidget( - 'moves downwards for given period when pullFor is called', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - }, - verify: (game, tester) async { - plunger.pullFor(2); - game.update(0); - - expect(plunger.body.linearVelocity.y, isPositive); - - // Call game update at 120 FPS, so that the plunger will act as if it - // was pulled for 2 seconds. - for (var i = 0.0; i < 2; i += 1 / 120) { - game.update(1 / 20); - } - - expect(plunger.body.linearVelocity.y, isZero); - }, - ); - }); - - group('pull', () { - late Plunger plunger; - - setUp(() { - plunger = Plunger( - compressionDistance: compressionDistance, - ); - }); - - flameTester.test( - 'moves downwards when pull is called', - (game) async { - await game.ensureAdd(plunger); - plunger.pull(); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - - flameTester.test( - 'moves downwards when pull is called ' - 'and plunger is below its starting position', (game) async { - await game.ensureAdd(plunger); - plunger.pull(); - plunger.release(); - plunger.pull(); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }); - }); - - group('release', () { - late Plunger plunger; - - setUp(() { - plunger = Plunger( - compressionDistance: compressionDistance, - ); - }); - - flameTester.test( - 'moves upwards when release is called ' - 'and plunger is below its starting position', (game) async { - await game.ensureAdd(plunger); - plunger.body.setTransform(Vector2(0, 1), 0); - plunger.release(); - - expect(plunger.body.linearVelocity.y, isNegative); - expect(plunger.body.linearVelocity.x, isZero); - }); - - flameTester.test( - 'does not move when release is called ' - 'and plunger is in its starting position', - (game) async { - await game.ensureAdd(plunger); - plunger.release(); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - }); - - group('PlungerAnchor', () { - const compressionDistance = 10.0; - - flameTester.test( - 'position is a compression distance below the Plunger', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - final plungerAnchor = PlungerAnchor(plunger: plunger); - await game.ensureAdd(plungerAnchor); - - expect( - plungerAnchor.body.position.y, - equals(plunger.body.position.y + compressionDistance), - ); - }, - ); - }); - - group('PlungerAnchorPrismaticJointDef', () { - const compressionDistance = 10.0; - late Plunger plunger; - late PlungerAnchor anchor; - - setUp(() { - plunger = Plunger( - compressionDistance: compressionDistance, - ); - anchor = PlungerAnchor(plunger: plunger); - }); - - group('initializes with', () { - flameTester.test( - 'plunger body as bodyA', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - - expect(jointDef.bodyA, equals(plunger.body)); - }, - ); - - flameTester.test( - 'anchor body as bodyB', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.bodyB, equals(anchor.body)); - }, - ); - - flameTester.test( - 'limits enabled', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.enableLimit, isTrue); - }, - ); - - flameTester.test( - 'lower translation limit as negative infinity', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); - }, - ); - - flameTester.test( - 'connected body collision enabled', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.collideConnected, isTrue); - }, - ); - }); - - flameTester.testGameWidget( - 'plunger cannot go below anchor', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - // Giving anchor a shape for the plunger to collide with. - anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - await tester.pump(const Duration(seconds: 1)); - }, - verify: (game, tester) async { - expect(plunger.body.position.y < anchor.body.position.y, isTrue); - }, - ); - - flameTester.testGameWidget( - 'plunger cannot excessively exceed starting position', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - plunger.body.setTransform(Vector2(0, -1), 0); - - await tester.pump(const Duration(seconds: 1)); - }, - verify: (game, tester) async { - expect(plunger.body.position.y < 1, isTrue); - }, - ); - }); -} diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index c40405cb..410d3151 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -2,7 +2,6 @@ library pinball_flame; export 'src/behaviors/behaviors.dart'; export 'src/canvas/canvas.dart'; -export 'src/component_controller.dart'; export 'src/flame_provider.dart'; export 'src/keyboard_input_controller.dart'; export 'src/layer.dart'; diff --git a/packages/pinball_flame/lib/src/component_controller.dart b/packages/pinball_flame/lib/src/component_controller.dart deleted file mode 100644 index 6afc1c40..00000000 --- a/packages/pinball_flame/lib/src/component_controller.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flutter/foundation.dart'; - -/// {@template component_controller} -/// A [ComponentController] is a [Component] in charge of handling the logic -/// associated with another [Component]. -/// {@endtemplate} -abstract class ComponentController extends Component { - /// {@macro component_controller} - ComponentController(this.component); - - /// The [Component] controlled by this [ComponentController]. - final T component; - - @override - Future addToParent(Component parent) async { - assert( - parent == component, - 'ComponentController should be child of $component.', - ); - await super.addToParent(parent); - } - - @override - Future add(Component component) { - throw Exception('ComponentController cannot add other components.'); - } -} - -/// Mixin that attaches a single [ComponentController] to a [Component]. -mixin Controls on Component { - /// The [ComponentController] attached to this [Component]. - late T controller; - - @override - @mustCallSuper - Future onLoad() async { - await super.onLoad(); - await add(controller); - } -} diff --git a/packages/pinball_flame/test/src/component_controller_test.dart b/packages/pinball_flame/test/src/component_controller_test.dart deleted file mode 100644 index addcf2b0..00000000 --- a/packages/pinball_flame/test/src/component_controller_test.dart +++ /dev/null @@ -1,96 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/game.dart'; -import 'package:flame/src/components/component.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -class TestComponentController extends ComponentController { - TestComponentController(Component component) : super(component); -} - -class ControlledComponent extends Component - with Controls { - ControlledComponent() : super() { - controller = TestComponentController(this); - } -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(FlameGame.new); - - group('ComponentController', () { - flameTester.test( - 'can be instantiated', - (game) async { - expect( - TestComponentController(Component()), - isA(), - ); - }, - ); - - flameTester.test( - 'throws AssertionError when not attached to controlled component', - (game) async { - final component = Component(); - final controller = TestComponentController(component); - - final anotherComponent = Component(); - await expectLater( - () async => await anotherComponent.add(controller), - throwsAssertionError, - ); - }, - ); - - flameTester.test( - 'throws Exception when adding a component', - (game) async { - final component = ControlledComponent(); - final controller = TestComponentController(component); - - await expectLater( - () async => controller.add(Component()), - throwsException, - ); - }, - ); - - flameTester.test( - 'throws Exception when adding multiple components', - (game) async { - final component = ControlledComponent(); - final controller = TestComponentController(component); - - await expectLater( - () async => controller.addAll([ - Component(), - Component(), - ]), - throwsException, - ); - }, - ); - }); - - group('Controls', () { - flameTester.test( - 'can be instantiated', - (game) async { - expect(ControlledComponent(), isA()); - }, - ); - - flameTester.test('adds controller', (game) async { - final component = ControlledComponent(); - - await game.add(component); - await game.ready(); - - expect(component.contains(component.controller), isTrue); - }); - }); -} diff --git a/test/game/behaviors/ball_spawning_behavior_test.dart b/test/game/behaviors/ball_spawning_behavior_test.dart index d723c65e..dc272571 100644 --- a/test/game/behaviors/ball_spawning_behavior_test.dart +++ b/test/game/behaviors/ball_spawning_behavior_test.dart @@ -130,7 +130,7 @@ void main() { await game.pump([ behavior, ZCanvasComponent(), - Plunger.test(compressionDistance: 10), + Plunger.test(), ]); expect(game.descendants().whereType(), isEmpty); diff --git a/test/game/behaviors/character_selection_behavior_test.dart b/test/game/behaviors/character_selection_behavior_test.dart index edf17999..acf140a2 100644 --- a/test/game/behaviors/character_selection_behavior_test.dart +++ b/test/game/behaviors/character_selection_behavior_test.dart @@ -77,45 +77,8 @@ void main() { ); flameTester.test( - 'onNewState does not call onCharacterSelected on the arcade background ' - 'bloc when platform is mobile', + 'onNewState calls onCharacterSelected on the arcade background bloc', (game) async { - final platformHelper = _MockPlatformHelper(); - when(() => platformHelper.isMobile).thenAnswer((_) => true); - final arcadeBackgroundBloc = _MockArcadeBackgroundCubit(); - whenListen( - arcadeBackgroundBloc, - const Stream.empty(), - initialState: const ArcadeBackgroundState.initial(), - ); - final behavior = CharacterSelectionBehavior(); - await game.pump( - [ - behavior, - ZCanvasComponent(), - Plunger.test(compressionDistance: 10), - Ball.test(), - ], - platformHelper: platformHelper, - ); - - const dinoThemeState = CharacterThemeState(theme.DinoTheme()); - behavior.onNewState(dinoThemeState); - await game.ready(); - - verifyNever( - () => arcadeBackgroundBloc - .onCharacterSelected(dinoThemeState.characterTheme), - ); - }, - ); - - flameTester.test( - 'onNewState calls onCharacterSelected on the arcade background ' - 'bloc when platform is not mobile', - (game) async { - final platformHelper = _MockPlatformHelper(); - when(() => platformHelper.isMobile).thenAnswer((_) => false); final arcadeBackgroundBloc = _MockArcadeBackgroundCubit(); whenListen( arcadeBackgroundBloc, @@ -130,10 +93,9 @@ void main() { arcadeBackground, behavior, ZCanvasComponent(), - Plunger.test(compressionDistance: 10), + Plunger.test(), Ball.test(), ], - platformHelper: platformHelper, ); const dinoThemeState = CharacterThemeState(theme.DinoTheme()); @@ -165,7 +127,7 @@ void main() { ball, behavior, ZCanvasComponent(), - Plunger.test(compressionDistance: 10), + Plunger.test(), ArcadeBackground.test(), ], platformHelper: platformHelper, diff --git a/test/game/components/backbox/displays/game_over_info_display_test.dart b/test/game/components/backbox/displays/game_over_info_display_test.dart index 2bee4005..bb092347 100644 --- a/test/game/components/backbox/displays/game_over_info_display_test.dart +++ b/test/game/components/backbox/displays/game_over_info_display_test.dart @@ -176,7 +176,7 @@ void main() { final openSourceLink = component.descendants().whereType().first; - openSourceLink.onTapDown(_MockTapDownInfo()); + openSourceLink.onTapUp(_MockTapUpInfo()); await game.ready(); diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart deleted file mode 100644 index 68bde767..00000000 --- a/test/game/components/controlled_plunger_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'dart:collection'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/components.dart'; -import 'package:flame/input.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { - @override - Future onLoad() async { - images.prefix = ''; - await images.load(Assets.images.plunger.plunger.keyName); - } - - Future pump( - Plunger child, { - GameBloc? gameBloc, - PinballAudioPlayer? pinballAudioPlayer, - }) { - return ensureAdd( - FlameBlocProvider.value( - value: gameBloc ?? GameBloc() - ..add(const GameStarted()), - children: [ - FlameProvider.value( - pinballAudioPlayer ?? _MockPinballAudioPlayer(), - children: [child], - ) - ], - ), - ); - } -} - -class _MockGameBloc extends Mock implements GameBloc {} - -class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(_TestGame.new); - - group('PlungerController', () { - late GameBloc gameBloc; - - final flameBlocTester = FlameTester(_TestGame.new); - - late Plunger plunger; - late PlungerController controller; - - setUp(() { - gameBloc = _MockGameBloc(); - plunger = ControlledPlunger(compressionDistance: 10); - controller = PlungerController(plunger); - plunger.add(controller); - }); - - group('onKeyEvent', () { - final downKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.space, - LogicalKeyboardKey.keyS, - ]); - - testRawKeyDownEvents(downKeys, (event) { - flameTester.test( - 'moves down ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - await game.pump(plunger); - controller.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(downKeys, (event) { - flameTester.test( - 'moves up ' - 'when ${event.logicalKey.keyLabel} is released ' - 'and plunger is below its starting position', - (game) async { - await game.pump(plunger); - plunger.body.setTransform(Vector2(0, 1), 0); - controller.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isNegative); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(downKeys, (event) { - flameTester.test( - 'does not move when ${event.logicalKey.keyLabel} is released ' - 'and plunger is in its starting position', - (game) async { - await game.pump(plunger); - controller.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(downKeys, (event) { - flameBlocTester.testGameWidget( - 'does nothing when is game over', - setUp: (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.gameOver, - ), - ); - - await game.pump(plunger, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - }, - verify: (game, tester) async { - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - }); - - flameTester.test( - 'adds the PlungerNoiseBehavior plunger is released', - (game) async { - await game.pump(plunger); - plunger.body.setTransform(Vector2(0, 1), 0); - plunger.release(); - - await game.ready(); - final count = - game.descendants().whereType().length; - expect(count, equals(1)); - }, - ); - }); - - group('PlungerNoiseBehavior', () { - late PinballAudioPlayer audioPlayer; - - setUp(() { - audioPlayer = _MockPinballAudioPlayer(); - }); - - flameTester.test('plays the correct sound on load', (game) async { - final parent = ControlledPlunger(compressionDistance: 10); - await game.pump(parent, pinballAudioPlayer: audioPlayer); - await parent.ensureAdd(PlungerNoiseBehavior()); - verify(() => audioPlayer.play(PinballAudio.launcher)).called(1); - }); - - test('is removed on the first update', () { - final parent = Component(); - final behavior = PlungerNoiseBehavior(); - parent.add(behavior); - parent.update(0); // Run a tick to ensure it is added - - behavior.update(0); // Run its own update where the removal happens - - expect(behavior.shouldRemove, isTrue); - }); - }); -} diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index d468ce2f..874f901c 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -36,6 +36,8 @@ class _TestGame extends Forge2DGame with HasTappables { Future pump( Iterable children, { PinballAudioPlayer? pinballAudioPlayer, + PlatformHelper? platformHelper, + GoogleWordCubit? googleWordBloc, }) async { return ensureAdd( FlameMultiBlocProvider( @@ -46,6 +48,9 @@ class _TestGame extends Forge2DGame with HasTappables { FlameBlocProvider.value( value: CharacterThemeCubit(), ), + FlameBlocProvider.value( + value: googleWordBloc ?? GoogleWordCubit(), + ), ], children: [ MultiFlameProvider( @@ -57,7 +62,7 @@ class _TestGame extends Forge2DGame with HasTappables { _MockAppLocalizations(), ), FlameProvider.value( - _MockPlatformHelper(), + platformHelper ?? PlatformHelper(), ), ], children: children, @@ -75,10 +80,11 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockShareRepository extends Mock implements ShareRepository {} -class _MockPlatformHelper extends Mock implements PlatformHelper { - @override - bool get isMobile => false; -} +class _MockPlatformHelper extends Mock implements PlatformHelper {} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} + +class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {} class _MockAppLocalizations extends Mock implements AppLocalizations { @override @@ -196,7 +202,6 @@ void main() { await flipper.ensureAdd(behavior); expect(state.status, GameStatus.gameOver); - component.onNewState(state); await game.ready(); @@ -207,6 +212,77 @@ void main() { }, ); + flameTester.test( + 'removes PlungerKeyControllingBehavior from Plunger', + (game) async { + final component = GameBlocStatusListener(); + final leaderboardRepository = _MockLeaderboardRepository(); + final shareRepository = _MockShareRepository(); + final backbox = Backbox( + leaderboardRepository: leaderboardRepository, + shareRepository: shareRepository, + entries: const [], + ); + final plunger = Plunger.test(); + await game.pump( + [component, backbox, plunger], + ); + + await plunger.ensureAdd( + FlameBlocProvider( + create: PlungerCubit.new, + children: [PlungerKeyControllingBehavior()], + ), + ); + + expect(state.status, GameStatus.gameOver); + component.onNewState(state); + await game.ready(); + + expect( + plunger.children.whereType(), + isEmpty, + ); + }, + ); + + flameTester.test( + 'removes PlungerPullingBehavior from Plunger', + (game) async { + final component = GameBlocStatusListener(); + final leaderboardRepository = _MockLeaderboardRepository(); + final shareRepository = _MockShareRepository(); + final backbox = Backbox( + leaderboardRepository: leaderboardRepository, + shareRepository: shareRepository, + entries: const [], + ); + final plunger = Plunger.test(); + await game.pump( + [component, backbox, plunger], + ); + + await plunger.ensureAdd( + FlameBlocProvider( + create: PlungerCubit.new, + children: [ + PlungerPullingBehavior(strength: 0), + PlungerAutoPullingBehavior(strength: 0) + ], + ), + ); + + expect(state.status, GameStatus.gameOver); + component.onNewState(state); + await game.ready(); + + expect( + plunger.children.whereType(), + isEmpty, + ); + }, + ); + flameTester.test( 'plays the game over voice over', (game) async { @@ -263,7 +339,21 @@ void main() { ); flameTester.test( - 'adds key controlling behavior to Flippers when the game is started', + 'resets the GoogleWordCubit', + (game) async { + final googleWordBloc = _MockGoogleWordCubit(); + final component = GameBlocStatusListener(); + await game.pump([component], googleWordBloc: googleWordBloc); + + expect(state.status, equals(GameStatus.playing)); + component.onNewState(state); + + verify(googleWordBloc.onReset).called(1); + }, + ); + + flameTester.test( + 'adds FlipperKeyControllingBehavior to Flippers', (game) async { final component = GameBlocStatusListener(); final leaderboardRepository = _MockLeaderboardRepository(); @@ -288,6 +378,120 @@ void main() { ); }, ); + + flameTester.test( + 'adds PlungerKeyControllingBehavior to Plunger when on desktop', + (game) async { + final platformHelper = _MockPlatformHelper(); + when(() => platformHelper.isMobile).thenReturn(false); + final component = GameBlocStatusListener(); + final leaderboardRepository = _MockLeaderboardRepository(); + final shareRepository = _MockShareRepository(); + final backbox = Backbox( + leaderboardRepository: leaderboardRepository, + shareRepository: shareRepository, + entries: const [], + ); + final plunger = Plunger.test(); + await game.pump( + [component, backbox, plunger], + platformHelper: platformHelper, + ); + await plunger.ensureAdd( + FlameBlocProvider( + create: _MockPlungerCubit.new, + ), + ); + + expect(state.status, GameStatus.playing); + + component.onNewState(state); + await game.ready(); + + expect( + plunger + .descendants() + .whereType() + .length, + equals(1), + ); + }, + ); + + flameTester.test( + 'adds PlungerPullingBehavior to Plunger when on desktop', + (game) async { + final platformHelper = _MockPlatformHelper(); + when(() => platformHelper.isMobile).thenReturn(false); + final component = GameBlocStatusListener(); + final leaderboardRepository = _MockLeaderboardRepository(); + final shareRepository = _MockShareRepository(); + final backbox = Backbox( + leaderboardRepository: leaderboardRepository, + shareRepository: shareRepository, + entries: const [], + ); + final plunger = Plunger.test(); + await game.pump( + [component, backbox, plunger], + platformHelper: platformHelper, + ); + await plunger.ensureAdd( + FlameBlocProvider( + create: _MockPlungerCubit.new, + ), + ); + + expect(state.status, GameStatus.playing); + + component.onNewState(state); + await game.ready(); + + expect( + plunger.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'adds PlungerAutoPullingBehavior to Plunger when on mobile', + (game) async { + final platformHelper = _MockPlatformHelper(); + when(() => platformHelper.isMobile).thenReturn(true); + final component = GameBlocStatusListener(); + final leaderboardRepository = _MockLeaderboardRepository(); + final shareRepository = _MockShareRepository(); + final backbox = Backbox( + leaderboardRepository: leaderboardRepository, + shareRepository: shareRepository, + entries: const [], + ); + final plunger = Plunger.test(); + await game.pump( + [component, backbox, plunger], + platformHelper: platformHelper, + ); + await plunger.ensureAdd( + FlameBlocProvider( + create: _MockPlungerCubit.new, + ), + ); + + expect(state.status, GameStatus.playing); + + component.onNewState(state); + await game.ready(); + + expect( + plunger + .descendants() + .whereType() + .length, + equals(1), + ); + }, + ); }); }); }); diff --git a/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart index acabe4c7..17726156 100644 --- a/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart +++ b/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart @@ -75,7 +75,7 @@ void main() { flameTester.testGameWidget( 'adds GameBonus.googleWord to the game when all letters ' - 'in google word are activated and calls onBonusAwarded', + 'in google word are activated and calls onReset', setUp: (game, tester) async { final behavior = GoogleWordBonusBehavior(); final parent = GoogleGallery.test(); @@ -114,7 +114,7 @@ void main() { verify( () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), ).called(1); - verify(googleWordBloc.onBonusAwarded).called(1); + verify(googleWordBloc.onReset).called(1); }, ); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 289fb4fa..a95f329b 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame/input.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -395,7 +396,7 @@ void main() { }); group('plunger control', () { - flameTester.test('tap down moves plunger down', (game) async { + flameTester.test('tap down emits plunging', (game) async { await game.ready(); final eventPosition = _MockEventPosition(); @@ -408,13 +409,15 @@ void main() { when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); - final plunger = game.descendants().whereType().first; - game.onTapDown(0, tapDownEvent); - game.update(1); + final plungerBloc = game + .descendants() + .whereType>() + .single + .bloc; - expect(plunger.body.linearVelocity.y, isPositive); + expect(plungerBloc.state, PlungerState.pulling); }); }); }); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index a0a0f22c..669117ed 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -307,6 +307,23 @@ void main() { expect(find.byType(MobileControls), findsOneWidget); }); + testWidgets( + 'ReplayButtonOverlay when the overlay is added', + (tester) async { + await tester.pumpApp( + PinballGameView(game), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + + game.overlays.add(PinballGame.replayButtonOverlay); + + await tester.pump(); + + expect(find.byType(ReplayButtonOverlay), findsOneWidget); + }, + ); + group('info icon', () { testWidgets('renders on game over', (tester) async { final gameState = GameState.initial().copyWith( diff --git a/test/game/view/widgets/replay_button_overlay_test.dart b/test/game/view/widgets/replay_button_overlay_test.dart index 1497031a..5c3e4884 100644 --- a/test/game/view/widgets/replay_button_overlay_test.dart +++ b/test/game/view/widgets/replay_button_overlay_test.dart @@ -8,24 +8,32 @@ import '../../../helpers/helpers.dart'; class _MockStartGameBloc extends Mock implements StartGameBloc {} +class _MockGameBloc extends Mock implements GameBloc {} + void main() { group('ReplayButtonOverlay', () { late StartGameBloc startGameBloc; + late _MockGameBloc gameBloc; setUp(() async { await mockFlameImages(); startGameBloc = _MockStartGameBloc(); + gameBloc = _MockGameBloc(); whenListen( startGameBloc, Stream.value(const StartGameState.initial()), initialState: const StartGameState.initial(), ); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); }); testWidgets('renders correctly', (tester) async { await tester.pumpApp(const ReplayButtonOverlay()); - expect(find.text('Replay'), findsOneWidget); }); @@ -33,6 +41,7 @@ void main() { (tester) async { await tester.pumpApp( const ReplayButtonOverlay(), + gameBloc: gameBloc, startGameBloc: startGameBloc, ); @@ -41,5 +50,19 @@ void main() { verify(() => startGameBloc.add(const ReplayTapped())).called(1); }); + + testWidgets('adds GameStarted event to GameBloc when tapped', + (tester) async { + await tester.pumpApp( + const ReplayButtonOverlay(), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + + await tester.tap(find.text('Replay')); + await tester.pump(); + + verify(() => gameBloc.add(const GameStarted())).called(1); + }); }); } diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 613fd5b8..2f16567a 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1,3 +1,2 @@ -export 'key_testers.dart'; export 'mock_flame_images.dart'; export 'pump_app.dart'; diff --git a/test/helpers/key_testers.dart b/test/helpers/key_testers.dart deleted file mode 100644 index ff870d6c..00000000 --- a/test/helpers/key_testers.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; -import 'package:mocktail/mocktail.dart'; - -class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { - @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return super.toString(); - } -} - -class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { - @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return super.toString(); - } -} - -@isTest -void testRawKeyUpEvents( - List keys, - Function(RawKeyUpEvent) test, -) { - for (final key in keys) { - test(_mockKeyUpEvent(key)); - } -} - -RawKeyUpEvent _mockKeyUpEvent(LogicalKeyboardKey key) { - final event = _MockRawKeyUpEvent(); - when(() => event.logicalKey).thenReturn(key); - return event; -} - -@isTest -void testRawKeyDownEvents( - List keys, - Function(RawKeyDownEvent) test, -) { - for (final key in keys) { - test(_mockKeyDownEvent(key)); - } -} - -RawKeyDownEvent _mockKeyDownEvent(LogicalKeyboardKey key) { - final event = _MockRawKeyDownEvent(); - when(() => event.logicalKey).thenReturn(key); - return event; -}