diff --git a/lib/assets_manager/cubit/assets_manager_cubit.dart b/lib/assets_manager/cubit/assets_manager_cubit.dart index eb0f7e31..7932f194 100644 --- a/lib/assets_manager/cubit/assets_manager_cubit.dart +++ b/lib/assets_manager/cubit/assets_manager_cubit.dart @@ -19,21 +19,31 @@ class AssetsManagerCubit extends Cubit { /// do its job without adding too much delay for the user, we are letting /// the UI paint first, and then we start loading the assets. await Future.delayed(const Duration(seconds: 1)); + final loadables = Function()>[ + _game.preFetchLeaderboard, + ..._game.preLoadAssets(), + ..._audioPlayer.load(), + ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(), + ]; emit( state.copyWith( - loadables: [ - _game.preFetchLeaderboard(), - ..._game.preLoadAssets(), - ..._audioPlayer.load(), - ...BonusAnimation.loadAssets(), - ...SelectedCharacter.loadAssets(), - ], + assetsCount: loadables.length, ), ); - final all = state.loadables.map((loadable) async { - await loadable; - emit(state.copyWith(loaded: [...state.loaded, loadable])); - }).toList(); - await Future.wait(all); + + late void Function() _triggerLoad; + _triggerLoad = () async { + if (loadables.isEmpty) return; + final loadable = loadables.removeAt(0); + await loadable(); + _triggerLoad(); + emit(state.copyWith(loaded: state.loaded + 1)); + }; + + const _throttleSize = 3; + for (var i = 0; i < _throttleSize; i++) { + _triggerLoad(); + } } } diff --git a/lib/assets_manager/cubit/assets_manager_state.dart b/lib/assets_manager/cubit/assets_manager_state.dart index 4847adc6..9c1c5984 100644 --- a/lib/assets_manager/cubit/assets_manager_state.dart +++ b/lib/assets_manager/cubit/assets_manager_state.dart @@ -1,44 +1,42 @@ part of 'assets_manager_cubit.dart'; /// {@template assets_manager_state} -/// State used to load the game assets +/// State used to load the game assets. /// {@endtemplate} class AssetsManagerState extends Equatable { /// {@macro assets_manager_state} const AssetsManagerState({ - required this.loadables, + required this.assetsCount, required this.loaded, }); /// {@macro assets_manager_state} - const AssetsManagerState.initial() - : this(loadables: const [], loaded: const []); + const AssetsManagerState.initial() : this(assetsCount: 0, loaded: 0); - /// List of futures to load - final List loadables; + /// Number of assets to load. + final int assetsCount; - /// List of loaded futures - final List loaded; + /// Number of already loaded assets. + final int loaded; - /// Returns a value between 0 and 1 to indicate the loading progress - double get progress => - loadables.isEmpty ? 0 : loaded.length / loadables.length; + /// Returns a value between 0 and 1 to indicate the loading progress. + double get progress => loaded == 0 ? 0 : loaded / assetsCount; - /// Only returns false if all the assets have been loaded + /// Only returns false if all the assets have been loaded. bool get isLoading => progress != 1; /// Returns a copy of this instance with the given parameters - /// updated + /// updated. AssetsManagerState copyWith({ - List? loadables, - List? loaded, + int? assetsCount, + int? loaded, }) { return AssetsManagerState( - loadables: loadables ?? this.loadables, + assetsCount: assetsCount ?? this.assetsCount, loaded: loaded ?? this.loaded, ); } @override - List get props => [loaded, loadables]; + List get props => [loaded, assetsCount]; } 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/behaviors.dart b/lib/game/behaviors/behaviors.dart index af5bc2a7..03a99b57 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -7,4 +7,5 @@ export 'camera_focusing_behavior.dart'; export 'character_selection_behavior.dart'; export 'cow_bumper_noise_behavior.dart'; export 'kicker_noise_behavior.dart'; +export 'rollover_noise_behavior.dart'; export 'scoring_behavior.dart'; diff --git a/lib/game/behaviors/camera_focusing_behavior.dart b/lib/game/behaviors/camera_focusing_behavior.dart index 8a13821d..1d454f02 100644 --- a/lib/game/behaviors/camera_focusing_behavior.dart +++ b/lib/game/behaviors/camera_focusing_behavior.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_bloc/flame_bloc.dart'; @@ -7,9 +9,9 @@ import 'package:pinball_components/pinball_components.dart'; /// {@template focus_data} /// Defines a [Camera] focus point. /// {@endtemplate} -class FocusData { - /// {@template focus_data} - FocusData({ +class _FocusData { + /// {@macro focus_data} + const _FocusData({ required this.zoom, required this.position, }); @@ -24,7 +26,11 @@ class FocusData { /// Changes the game focus when the [GameBloc] status changes. class CameraFocusingBehavior extends Component with FlameBlocListenable, HasGameRef { - late final Map _foci; + final Map _foci = {}; + + GameStatus? _activeFocus; + + final _previousSize = Vector2.zero(); @override bool listenWhen(GameState? previousState, GameState newState) { @@ -32,51 +38,62 @@ class CameraFocusingBehavior extends Component } @override - void onNewState(GameState state) { - switch (state.status) { - case GameStatus.waiting: - break; - case GameStatus.playing: - _zoom(_foci['game']!); - break; - case GameStatus.gameOver: - _zoom(_foci['backbox']!); - break; - } - } + void onNewState(GameState state) => _zoomTo(state.status); @override - Future onLoad() async { - await super.onLoad(); - _foci = { - 'game': FocusData( - zoom: gameRef.size.y / 16, - position: Vector2(0, -7.8), - ), - 'waiting': FocusData( - zoom: gameRef.size.y / 18, + void onGameResize(Vector2 size) { + super.onGameResize(size); + if (size == _previousSize) { + return; + } + _previousSize.setFrom(size); + + final maxWidth = size.x / 90; + final maxHeight = size.y / 160; + + final scale = min(maxHeight, maxWidth); + + _foci.addAll({ + GameStatus.waiting: _FocusData( + zoom: scale + (maxWidth > maxHeight ? 0.3 : -0.5), position: Vector2(0, -112), ), - 'backbox': FocusData( - zoom: gameRef.size.y / 10, + GameStatus.playing: _FocusData( + zoom: scale + (maxWidth > maxHeight ? 0.5 : -0.2), + position: Vector2(0, -7.8), + ), + GameStatus.gameOver: _FocusData( + zoom: scale + (maxWidth > maxHeight ? 2.8 : 3.3), position: Vector2(0, -111), ), - }; + }); + + if (_activeFocus != null) { + _snap(_activeFocus!); + } + } - _snap(_foci['waiting']!); + @override + Future onLoad() async { + await super.onLoad(); + _snap(GameStatus.waiting); } - void _snap(FocusData data) { + void _snap(GameStatus focusKey) { + final focusData = _foci[_activeFocus = focusKey]!; + gameRef.camera ..speed = 100 - ..followVector2(data.position) - ..zoom = data.zoom; + ..followVector2(focusData.position) + ..zoom = focusData.zoom; } - void _zoom(FocusData data) { - final zoom = CameraZoom(value: data.zoom); + void _zoomTo(GameStatus focusKey) { + final focusData = _foci[_activeFocus = focusKey]!; + + final zoom = CameraZoom(value: focusData.zoom); zoom.completed.then((_) { - gameRef.camera.moveTo(data.position); + gameRef.camera.moveTo(focusData.position); }); add(zoom); } 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/behaviors/rollover_noise_behavior.dart b/lib/game/behaviors/rollover_noise_behavior.dart new file mode 100644 index 00000000..06b2f77a --- /dev/null +++ b/lib/game/behaviors/rollover_noise_behavior.dart @@ -0,0 +1,13 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class RolloverNoiseBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + readProvider().play(PinballAudio.rollover); + } +} 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/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 8fcab789..1ba8457e 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -82,6 +82,9 @@ class GameState extends Equatable { /// The score displayed at the game. int get displayScore => roundScore + totalScore; + /// The max multiplier in game. + bool get isMaxMultiplier => multiplier == 6; + GameState copyWith({ int? totalScore, int? roundScore, diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index fd59ace3..77f30069 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -16,41 +16,44 @@ class AndroidAcres extends Component { AndroidAcres() : super( children: [ + SpaceshipRamp( + children: [ + RampShotBehavior(points: Points.fiveThousand), + RampBonusBehavior(points: Points.oneMillion), + RampProgressBehavior(), + RampMultiplierBehavior(), + RampResetBehavior(), + ], + ), + SpaceshipRail(), + AndroidBumper.a( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-25.2, 1.5), + AndroidBumper.b( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-32.9, -9.3), + AndroidBumper.cow( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + CowBumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-20.7, -13), FlameBlocProvider( create: AndroidSpaceshipCubit.new, children: [ - SpaceshipRamp( - children: [ - RampShotBehavior(points: Points.fiveThousand), - RampBonusBehavior(points: Points.oneMillion), - ], - ), - SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( children: [ ScoringContactBehavior(points: Points.twoHundredThousand), ], )..initialPosition = Vector2(-26, -28.25), - AndroidBumper.a( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-25.2, 1.5), - AndroidBumper.b( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-32.9, -9.3), - AndroidBumper.cow( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - CowBumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-20.7, -13), AndroidSpaceshipBonusBehavior(), ], ), diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart index 91b1e132..c4e44a6e 100644 --- a/lib/game/components/android_acres/behaviors/behaviors.dart +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -1,3 +1,6 @@ export 'android_spaceship_bonus_behavior.dart'; export 'ramp_bonus_behavior.dart'; +export 'ramp_multiplier_behavior.dart'; +export 'ramp_progress_behavior.dart'; +export 'ramp_reset_behavior.dart'; export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart index bc28650f..6984ca68 100644 --- a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -1,60 +1,40 @@ -import 'dart:async'; - import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template ramp_bonus_behavior} /// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. /// {@endtemplate} -class RampBonusBehavior extends Component with ParentIsA { +class RampBonusBehavior extends Component + with FlameBlocListenable { /// {@macro ramp_bonus_behavior} RampBonusBehavior({ required Points points, }) : _points = points, super(); - /// Creates a [RampBonusBehavior]. - /// - /// This can be used for testing [RampBonusBehavior] in isolation. - @visibleForTesting - RampBonusBehavior.test({ - required Points points, - required this.subscription, - }) : _points = points, - super(); - final Points _points; - /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. - @visibleForTesting - StreamSubscription? subscription; - @override - void onMount() { - super.onMount(); - - subscription = subscription ?? - parent.bloc.stream.listen((state) { - final achievedOneMillionPoints = state.hits % 10 == 0; - - if (achievedOneMillionPoints) { - parent.add( - ScoringBehavior( - points: _points, - position: Vector2(0, -60), - duration: 2, - ), - ); - } - }); + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + final hitsIncreased = previousState.hits < newState.hits; + final achievedOneMillionPoints = newState.hits % 10 == 0; + + return hitsIncreased && achievedOneMillionPoints; } @override - void onRemove() { - subscription?.cancel(); - super.onRemove(); + void onNewState(SpaceshipRampState state) { + parent!.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -60), + duration: 2, + ), + ); } } diff --git a/lib/game/components/android_acres/behaviors/ramp_multiplier_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_multiplier_behavior.dart new file mode 100644 index 00000000..90211376 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_multiplier_behavior.dart @@ -0,0 +1,27 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Increases the multiplier when a [Ball] is shot 5 times into the +/// [SpaceshipRamp]. +class RampMultiplierBehavior extends Component + with FlameBlocListenable { + @override + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + final hitsIncreased = previousState.hits < newState.hits; + final achievedFiveShots = newState.hits % 5 == 0; + final notMaxMultiplier = + !readBloc().state.isMaxMultiplier; + return hitsIncreased & achievedFiveShots && notMaxMultiplier; + } + + @override + void onNewState(SpaceshipRampState state) { + readBloc().add(const MultiplierIncreased()); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart new file mode 100644 index 00000000..fab67b4e --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Changes arrow lit when a [Ball] is shot into the [SpaceshipRamp]. +class RampProgressBehavior extends Component + with FlameBlocListenable { + @override + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + return previousState.hits < newState.hits; + } + + @override + void onNewState(SpaceshipRampState state) { + final gameBloc = readBloc(); + final spaceshipCubit = readBloc(); + + final canProgress = !gameBloc.state.isMaxMultiplier || + (gameBloc.state.isMaxMultiplier && !state.arrowFullyLit); + + if (canProgress) { + spaceshipCubit.onProgressed(); + } + + if (spaceshipCubit.state.arrowFullyLit && !gameBloc.state.isMaxMultiplier) { + spaceshipCubit.onProgressed(); + } + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_reset_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_reset_behavior.dart new file mode 100644 index 00000000..314a4be7 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_reset_behavior.dart @@ -0,0 +1,19 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Reset [SpaceshipRamp] state when GameState.rounds changes. +class RampResetBehavior extends Component + with FlameBlocListenable { + @override + bool listenWhen(GameState previousState, GameState newState) { + return previousState.rounds != newState.rounds; + } + + @override + void onNewState(GameState state) { + readBloc().onReset(); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart index b15f5e30..b71e9a46 100644 --- a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -1,64 +1,36 @@ -import 'dart:async'; - import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flutter/cupertino.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template ramp_shot_behavior} /// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. /// {@endtemplate} class RampShotBehavior extends Component - with ParentIsA, FlameBlocReader { + with FlameBlocListenable { /// {@macro ramp_shot_behavior} RampShotBehavior({ required Points points, }) : _points = points, super(); - /// Creates a [RampShotBehavior]. - /// - /// This can be used for testing [RampShotBehavior] in isolation. - @visibleForTesting - RampShotBehavior.test({ - required Points points, - required this.subscription, - }) : _points = points, - super(); - final Points _points; - /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. - @visibleForTesting - StreamSubscription? subscription; - @override - void onMount() { - super.onMount(); - - subscription = subscription ?? - parent.bloc.stream.listen((state) { - final achievedOneMillionPoints = state.hits % 10 == 0; - - if (!achievedOneMillionPoints) { - bloc.add(const MultiplierIncreased()); - - parent.add( - ScoringBehavior( - points: _points, - position: Vector2(0, -45), - ), - ); - } - }); + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + return previousState.hits < newState.hits; } @override - void onRemove() { - subscription?.cancel(); - super.onRemove(); + void onNewState(SpaceshipRampState state) { + parent!.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -45), + ), + ); } } 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..efd085a5 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,17 @@ 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); @@ -36,22 +42,63 @@ class GameBlocStatusListener extends Component .state .characterTheme, ); - 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 + .firstChild>()! + .add(FlipperKeyControllingBehavior()); + + 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 787fcefc..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,8 +17,9 @@ 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/google_gallery/google_gallery.dart b/lib/game/components/google_gallery/google_gallery.dart index 0b3d4b10..ec3f9e36 100644 --- a/lib/game/components/google_gallery/google_gallery.dart +++ b/lib/game/components/google_gallery/google_gallery.dart @@ -22,12 +22,14 @@ class GoogleGallery extends Component with ZIndex { side: BoardSide.right, children: [ ScoringContactBehavior(points: Points.fiveThousand), + RolloverNoiseBehavior(), ], ), GoogleRollover( side: BoardSide.left, children: [ ScoringContactBehavior(points: Points.fiveThousand), + RolloverNoiseBehavior(), ], ), GoogleWord(position: Vector2(-4.45, 1.8)), 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..4e786f12 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -6,163 +6,196 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets; /// Add methods to help loading and caching game assets. extension PinballGameAssetsX on PinballGame { /// Returns a list of assets to be loaded - List> preLoadAssets() { + List Function()> preLoadAssets() { const dashTheme = DashTheme(); const sparkyTheme = SparkyTheme(); const androidTheme = AndroidTheme(); const dinoTheme = DinoTheme(); - - final gameAssets = [ - images.load(components.Assets.images.boardBackground.keyName), - images.load(components.Assets.images.ball.flameEffect.keyName), - images.load(components.Assets.images.signpost.inactive.keyName), - images.load(components.Assets.images.signpost.active1.keyName), - images.load(components.Assets.images.signpost.active2.keyName), - images.load(components.Assets.images.signpost.active3.keyName), - images.load(components.Assets.images.flipper.left.keyName), - images.load(components.Assets.images.flipper.right.keyName), - images.load(components.Assets.images.baseboard.left.keyName), - images.load(components.Assets.images.baseboard.right.keyName), - images.load(components.Assets.images.kicker.left.lit.keyName), - images.load(components.Assets.images.kicker.left.dimmed.keyName), - images.load(components.Assets.images.kicker.right.lit.keyName), - images.load(components.Assets.images.kicker.right.dimmed.keyName), - images.load(components.Assets.images.slingshot.upper.keyName), - images.load(components.Assets.images.slingshot.lower.keyName), - images.load(components.Assets.images.launchRamp.ramp.keyName), - images.load( - components.Assets.images.launchRamp.foregroundRailing.keyName, - ), - images.load( - components.Assets.images.launchRamp.backgroundRailing.keyName, - ), - images.load(components.Assets.images.dino.bottomWall.keyName), - images.load(components.Assets.images.dino.topWall.keyName), - images.load(components.Assets.images.dino.topWallTunnel.keyName), - images.load(components.Assets.images.dino.animatronic.head.keyName), - images.load(components.Assets.images.dino.animatronic.mouth.keyName), - images.load(components.Assets.images.dash.animatronic.keyName), - images.load(components.Assets.images.dash.bumper.a.active.keyName), - images.load(components.Assets.images.dash.bumper.a.inactive.keyName), - images.load(components.Assets.images.dash.bumper.b.active.keyName), - images.load(components.Assets.images.dash.bumper.b.inactive.keyName), - images.load(components.Assets.images.dash.bumper.main.active.keyName), - images.load(components.Assets.images.dash.bumper.main.inactive.keyName), - images.load(components.Assets.images.plunger.plunger.keyName), - images.load(components.Assets.images.plunger.rocket.keyName), - images.load(components.Assets.images.boundary.bottom.keyName), - images.load(components.Assets.images.boundary.outer.keyName), - images.load(components.Assets.images.boundary.outerBottom.keyName), - images.load(components.Assets.images.android.spaceship.saucer.keyName), - images + return [ + () => images.load(components.Assets.images.boardBackground.keyName), + () => images.load(components.Assets.images.ball.flameEffect.keyName), + () => images.load(components.Assets.images.signpost.inactive.keyName), + () => images.load(components.Assets.images.signpost.active1.keyName), + () => images.load(components.Assets.images.signpost.active2.keyName), + () => images.load(components.Assets.images.signpost.active3.keyName), + () => images.load(components.Assets.images.flipper.left.keyName), + () => images.load(components.Assets.images.flipper.right.keyName), + () => images.load(components.Assets.images.baseboard.left.keyName), + () => images.load(components.Assets.images.baseboard.right.keyName), + () => images.load(components.Assets.images.kicker.left.lit.keyName), + () => images.load(components.Assets.images.kicker.left.dimmed.keyName), + () => images.load(components.Assets.images.kicker.right.lit.keyName), + () => images.load(components.Assets.images.kicker.right.dimmed.keyName), + () => images.load(components.Assets.images.slingshot.upper.keyName), + () => images.load(components.Assets.images.slingshot.lower.keyName), + () => images.load(components.Assets.images.launchRamp.ramp.keyName), + () => images.load( + components.Assets.images.launchRamp.foregroundRailing.keyName, + ), + () => images.load( + components.Assets.images.launchRamp.backgroundRailing.keyName, + ), + () => images.load(components.Assets.images.dino.bottomWall.keyName), + () => images.load(components.Assets.images.dino.topWall.keyName), + () => images.load(components.Assets.images.dino.topWallTunnel.keyName), + () => images.load(components.Assets.images.dino.animatronic.head.keyName), + () => + images.load(components.Assets.images.dino.animatronic.mouth.keyName), + () => images.load(components.Assets.images.dash.animatronic.keyName), + () => images.load(components.Assets.images.dash.bumper.a.active.keyName), + () => + images.load(components.Assets.images.dash.bumper.a.inactive.keyName), + () => images.load(components.Assets.images.dash.bumper.b.active.keyName), + () => + images.load(components.Assets.images.dash.bumper.b.inactive.keyName), + () => + images.load(components.Assets.images.dash.bumper.main.active.keyName), + () => images + .load(components.Assets.images.dash.bumper.main.inactive.keyName), + () => images.load(components.Assets.images.plunger.plunger.keyName), + () => images.load(components.Assets.images.plunger.rocket.keyName), + () => images.load(components.Assets.images.boundary.bottom.keyName), + () => images.load(components.Assets.images.boundary.outer.keyName), + () => images.load(components.Assets.images.boundary.outerBottom.keyName), + () => images + .load(components.Assets.images.android.spaceship.saucer.keyName), + () => images .load(components.Assets.images.android.spaceship.animatronic.keyName), - images.load(components.Assets.images.android.spaceship.lightBeam.keyName), - images.load(components.Assets.images.android.ramp.boardOpening.keyName), - images.load( - components.Assets.images.android.ramp.railingForeground.keyName, - ), - images.load( - components.Assets.images.android.ramp.railingBackground.keyName, - ), - images.load(components.Assets.images.android.ramp.main.keyName), - images.load(components.Assets.images.android.ramp.arrow.inactive.keyName), - images.load( - components.Assets.images.android.ramp.arrow.active1.keyName, - ), - images.load( - components.Assets.images.android.ramp.arrow.active2.keyName, - ), - images.load( - components.Assets.images.android.ramp.arrow.active3.keyName, - ), - images.load( - components.Assets.images.android.ramp.arrow.active4.keyName, - ), - images.load( - components.Assets.images.android.ramp.arrow.active5.keyName, - ), - images.load(components.Assets.images.android.rail.main.keyName), - images.load(components.Assets.images.android.rail.exit.keyName), - images.load(components.Assets.images.android.bumper.a.lit.keyName), - images.load(components.Assets.images.android.bumper.a.dimmed.keyName), - images.load(components.Assets.images.android.bumper.b.lit.keyName), - images.load(components.Assets.images.android.bumper.b.dimmed.keyName), - images.load(components.Assets.images.android.bumper.cow.lit.keyName), - images.load(components.Assets.images.android.bumper.cow.dimmed.keyName), - images.load(components.Assets.images.sparky.computer.top.keyName), - images.load(components.Assets.images.sparky.computer.base.keyName), - images.load(components.Assets.images.sparky.computer.glow.keyName), - images.load(components.Assets.images.sparky.animatronic.keyName), - images.load(components.Assets.images.sparky.bumper.a.lit.keyName), - images.load(components.Assets.images.sparky.bumper.a.dimmed.keyName), - images.load(components.Assets.images.sparky.bumper.b.lit.keyName), - images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), - images.load(components.Assets.images.sparky.bumper.c.lit.keyName), - images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), - images.load(components.Assets.images.backbox.marquee.keyName), - images.load(components.Assets.images.backbox.displayDivider.keyName), - images.load(components.Assets.images.backbox.button.facebook.keyName), - images.load(components.Assets.images.backbox.button.twitter.keyName), - images.load( - components.Assets.images.backbox.displayTitleDecoration.keyName, - ), - images.load(components.Assets.images.googleWord.letter1.lit.keyName), - images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), - images.load(components.Assets.images.googleWord.letter2.lit.keyName), - images.load(components.Assets.images.googleWord.letter2.dimmed.keyName), - images.load(components.Assets.images.googleWord.letter3.lit.keyName), - images.load(components.Assets.images.googleWord.letter3.dimmed.keyName), - images.load(components.Assets.images.googleWord.letter4.lit.keyName), - images.load(components.Assets.images.googleWord.letter4.dimmed.keyName), - images.load(components.Assets.images.googleWord.letter5.lit.keyName), - images.load(components.Assets.images.googleWord.letter5.dimmed.keyName), - images.load(components.Assets.images.googleWord.letter6.lit.keyName), - images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), - images.load(components.Assets.images.googleRollover.left.decal.keyName), - images.load(components.Assets.images.googleRollover.left.pin.keyName), - images.load(components.Assets.images.googleRollover.right.decal.keyName), - images.load(components.Assets.images.googleRollover.right.pin.keyName), - images.load(components.Assets.images.multiball.lit.keyName), - images.load(components.Assets.images.multiball.dimmed.keyName), - images.load(components.Assets.images.multiplier.x2.lit.keyName), - images.load(components.Assets.images.multiplier.x2.dimmed.keyName), - images.load(components.Assets.images.multiplier.x3.lit.keyName), - images.load(components.Assets.images.multiplier.x3.dimmed.keyName), - images.load(components.Assets.images.multiplier.x4.lit.keyName), - images.load(components.Assets.images.multiplier.x4.dimmed.keyName), - images.load(components.Assets.images.multiplier.x5.lit.keyName), - images.load(components.Assets.images.multiplier.x5.dimmed.keyName), - images.load(components.Assets.images.multiplier.x6.lit.keyName), - images.load(components.Assets.images.multiplier.x6.dimmed.keyName), - images.load(components.Assets.images.score.fiveThousand.keyName), - images.load(components.Assets.images.score.twentyThousand.keyName), - images.load(components.Assets.images.score.twoHundredThousand.keyName), - images.load(components.Assets.images.score.oneMillion.keyName), - images.load(components.Assets.images.flapper.backSupport.keyName), - images.load(components.Assets.images.flapper.frontSupport.keyName), - images.load(components.Assets.images.flapper.flap.keyName), - images.load(components.Assets.images.skillShot.decal.keyName), - images.load(components.Assets.images.skillShot.pin.keyName), - images.load(components.Assets.images.skillShot.lit.keyName), - images.load(components.Assets.images.skillShot.dimmed.keyName), - images.load(components.Assets.images.displayArrows.arrowLeft.keyName), - images.load(components.Assets.images.displayArrows.arrowRight.keyName), - images.load(androidTheme.leaderboardIcon.keyName), - images.load(androidTheme.ball.keyName), - images.load(dashTheme.leaderboardIcon.keyName), - images.load(dashTheme.ball.keyName), - images.load(dinoTheme.leaderboardIcon.keyName), - images.load(dinoTheme.ball.keyName), - images.load(sparkyTheme.leaderboardIcon.keyName), - images.load(sparkyTheme.ball.keyName), + () => images + .load(components.Assets.images.android.spaceship.lightBeam.keyName), + () => images + .load(components.Assets.images.android.ramp.boardOpening.keyName), + () => images.load( + components.Assets.images.android.ramp.railingForeground.keyName, + ), + () => images.load( + components.Assets.images.android.ramp.railingBackground.keyName, + ), + () => images.load(components.Assets.images.android.ramp.main.keyName), + () => images + .load(components.Assets.images.android.ramp.arrow.inactive.keyName), + () => images.load( + components.Assets.images.android.ramp.arrow.active1.keyName, + ), + () => images.load( + components.Assets.images.android.ramp.arrow.active2.keyName, + ), + () => images.load( + components.Assets.images.android.ramp.arrow.active3.keyName, + ), + () => images.load( + components.Assets.images.android.ramp.arrow.active4.keyName, + ), + () => images.load( + components.Assets.images.android.ramp.arrow.active5.keyName, + ), + () => images.load(components.Assets.images.android.rail.main.keyName), + () => images.load(components.Assets.images.android.rail.exit.keyName), + () => images.load(components.Assets.images.android.bumper.a.lit.keyName), + () => + images.load(components.Assets.images.android.bumper.a.dimmed.keyName), + () => images.load(components.Assets.images.android.bumper.b.lit.keyName), + () => + images.load(components.Assets.images.android.bumper.b.dimmed.keyName), + () => + images.load(components.Assets.images.android.bumper.cow.lit.keyName), + () => images + .load(components.Assets.images.android.bumper.cow.dimmed.keyName), + () => images.load(components.Assets.images.sparky.computer.top.keyName), + () => images.load(components.Assets.images.sparky.computer.base.keyName), + () => images.load(components.Assets.images.sparky.computer.glow.keyName), + () => images.load(components.Assets.images.sparky.animatronic.keyName), + () => images.load(components.Assets.images.sparky.bumper.a.lit.keyName), + () => + images.load(components.Assets.images.sparky.bumper.a.dimmed.keyName), + () => images.load(components.Assets.images.sparky.bumper.b.lit.keyName), + () => + images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), + () => images.load(components.Assets.images.sparky.bumper.c.lit.keyName), + () => + images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), + () => images.load(components.Assets.images.backbox.marquee.keyName), + () => + images.load(components.Assets.images.backbox.displayDivider.keyName), + () => + images.load(components.Assets.images.backbox.button.facebook.keyName), + () => + images.load(components.Assets.images.backbox.button.twitter.keyName), + () => images.load( + components.Assets.images.backbox.displayTitleDecoration.keyName, + ), + () => + images.load(components.Assets.images.googleWord.letter1.lit.keyName), + () => images + .load(components.Assets.images.googleWord.letter1.dimmed.keyName), + () => + images.load(components.Assets.images.googleWord.letter2.lit.keyName), + () => images + .load(components.Assets.images.googleWord.letter2.dimmed.keyName), + () => + images.load(components.Assets.images.googleWord.letter3.lit.keyName), + () => images + .load(components.Assets.images.googleWord.letter3.dimmed.keyName), + () => + images.load(components.Assets.images.googleWord.letter4.lit.keyName), + () => images + .load(components.Assets.images.googleWord.letter4.dimmed.keyName), + () => + images.load(components.Assets.images.googleWord.letter5.lit.keyName), + () => images + .load(components.Assets.images.googleWord.letter5.dimmed.keyName), + () => + images.load(components.Assets.images.googleWord.letter6.lit.keyName), + () => images + .load(components.Assets.images.googleWord.letter6.dimmed.keyName), + () => images + .load(components.Assets.images.googleRollover.left.decal.keyName), + () => + images.load(components.Assets.images.googleRollover.left.pin.keyName), + () => images + .load(components.Assets.images.googleRollover.right.decal.keyName), + () => images + .load(components.Assets.images.googleRollover.right.pin.keyName), + () => images.load(components.Assets.images.multiball.lit.keyName), + () => images.load(components.Assets.images.multiball.dimmed.keyName), + () => images.load(components.Assets.images.multiplier.x2.lit.keyName), + () => images.load(components.Assets.images.multiplier.x2.dimmed.keyName), + () => images.load(components.Assets.images.multiplier.x3.lit.keyName), + () => images.load(components.Assets.images.multiplier.x3.dimmed.keyName), + () => images.load(components.Assets.images.multiplier.x4.lit.keyName), + () => images.load(components.Assets.images.multiplier.x4.dimmed.keyName), + () => images.load(components.Assets.images.multiplier.x5.lit.keyName), + () => images.load(components.Assets.images.multiplier.x5.dimmed.keyName), + () => images.load(components.Assets.images.multiplier.x6.lit.keyName), + () => images.load(components.Assets.images.multiplier.x6.dimmed.keyName), + () => images.load(components.Assets.images.score.fiveThousand.keyName), + () => images.load(components.Assets.images.score.twentyThousand.keyName), + () => images + .load(components.Assets.images.score.twoHundredThousand.keyName), + () => images.load(components.Assets.images.score.oneMillion.keyName), + () => images.load(components.Assets.images.flapper.backSupport.keyName), + () => images.load(components.Assets.images.flapper.frontSupport.keyName), + () => images.load(components.Assets.images.flapper.flap.keyName), + () => images.load(components.Assets.images.skillShot.decal.keyName), + () => images.load(components.Assets.images.skillShot.pin.keyName), + () => images.load(components.Assets.images.skillShot.lit.keyName), + () => images.load(components.Assets.images.skillShot.dimmed.keyName), + () => + images.load(components.Assets.images.displayArrows.arrowLeft.keyName), + () => images + .load(components.Assets.images.displayArrows.arrowRight.keyName), + () => images.load(androidTheme.leaderboardIcon.keyName), + () => images.load(androidTheme.ball.keyName), + () => images.load(dashTheme.leaderboardIcon.keyName), + () => images.load(dashTheme.ball.keyName), + () => images.load(dinoTheme.leaderboardIcon.keyName), + () => 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 dca26b84..ad81425f 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 @@ -125,6 +128,7 @@ class PinballGame extends PinballForge2DGame SkillShot( children: [ ScoringContactBehavior(points: Points.oneMillion), + RolloverNoiseBehavior(), ], ), AndroidAcres(), @@ -155,17 +159,28 @@ 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; + final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide[pointerId] = - leftSide ? BoardSide.left : BoardSide.right; - final flippers = descendants().whereType().where((flipper) { - return flipper.side == focusedBoardSide[pointerId]; - }); - flippers.first.moveUp(); + tappedLeftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants() + .whereType() + .where((flipper) => flipper.side == focusedBoardSide[pointerId]); + for (final flipper in flippers) { + flipper + .descendants() + .whereType>() + .forEach((provider) => provider.bloc.moveUp()); + } } } @@ -186,11 +201,15 @@ class PinballGame extends PinballForge2DGame void _moveFlippersDown(int pointerId) { if (focusedBoardSide[pointerId] != null) { - final flippers = descendants().whereType().where((flipper) { - return flipper.side == focusedBoardSide[pointerId]; - }); - flippers.first.moveDown(); - focusedBoardSide.remove(pointerId); + final flippers = descendants() + .whereType() + .where((flipper) => flipper.side == focusedBoardSide[pointerId]); + for (final flipper in flippers) { + flipper + .descendants() + .whereType>() + .forEach((provider) => provider.bloc.moveDown()); + } } } } @@ -220,9 +239,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { @override Future onLoad() async { await super.onLoad(); - await add(PreviewLine()); - - await add(_DebugInformation()); + await addAll([PreviewLine(), _DebugInformation()]); } @override @@ -237,14 +254,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { } @override - void onPanStart(DragStartInfo info) { - lineStart = info.eventPosition.game; - } + void onPanStart(DragStartInfo info) => lineStart = info.eventPosition.game; @override - void onPanUpdate(DragUpdateInfo info) { - lineEnd = info.eventPosition.game; - } + void onPanUpdate(DragUpdateInfo info) => lineEnd = info.eventPosition.game; @override void onPanEnd(DragEndInfo info) { 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/bonus_animation.dart b/lib/game/view/widgets/bonus_animation.dart index 35e600f2..a52aafc0 100644 --- a/lib/game/view/widgets/bonus_animation.dart +++ b/lib/game/view/widgets/bonus_animation.dart @@ -72,14 +72,16 @@ class BonusAnimation extends StatefulWidget { final VoidCallback? _onCompleted; /// Returns a list of assets to be loaded for animations. - static List loadAssets() { + static List loadAssets() { Flame.images.prefix = ''; return [ - Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName), - Flame.images.load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName), - Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName), - Flame.images.load(Assets.images.bonusAnimation.androidSpaceship.keyName), - Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName), + () => Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName), + () => Flame.images + .load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName), + () => Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName), + () => Flame.images + .load(Assets.images.bonusAnimation.androidSpaceship.keyName), + () => Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName), ]; } 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/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 2355d6cc..5504459f 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -132,10 +132,15 @@ class _Character extends StatelessWidget { Widget build(BuildContext context) { return Expanded( child: Opacity( - opacity: isSelected ? 1 : 0.3, + opacity: isSelected ? 1 : 0.4, child: TextButton( onPressed: () => context.read().characterSelected(character), + style: ButtonStyle( + overlayColor: MaterialStateProperty.all( + PinballColors.transparent, + ), + ), child: character.icon.image(fit: BoxFit.contain), ), ), diff --git a/lib/select_character/view/selected_character.dart b/lib/select_character/view/selected_character.dart index a061b05b..ed95297f 100644 --- a/lib/select_character/view/selected_character.dart +++ b/lib/select_character/view/selected_character.dart @@ -22,12 +22,12 @@ class SelectedCharacter extends StatefulWidget { State createState() => _SelectedCharacterState(); /// Returns a list of assets to be loaded. - static List loadAssets() { + static List loadAssets() { return [ - Flame.images.load(const DashTheme().animation.keyName), - Flame.images.load(const AndroidTheme().animation.keyName), - Flame.images.load(const DinoTheme().animation.keyName), - Flame.images.load(const SparkyTheme().animation.keyName), + () => Flame.images.load(const DashTheme().animation.keyName), + () => Flame.images.load(const AndroidTheme().animation.keyName), + () => Flame.images.load(const DinoTheme().animation.keyName), + () => Flame.images.load(const SparkyTheme().animation.keyName), ]; } } diff --git a/packages/pinball_audio/assets/sfx/rollover.mp3 b/packages/pinball_audio/assets/sfx/rollover.mp3 new file mode 100644 index 00000000..543a3560 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/rollover.mp3 differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index c8b66234..0b8fb20b 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -26,6 +26,7 @@ class $AssetsSfxGen { String get kickerA => 'assets/sfx/kicker_a.mp3'; String get kickerB => 'assets/sfx/kicker_b.mp3'; String get launcher => 'assets/sfx/launcher.mp3'; + String get rollover => 'assets/sfx/rollover.mp3'; String get sparky => 'assets/sfx/sparky.mp3'; } diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 9682b520..1e1a7688 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -33,6 +33,9 @@ enum PinballAudio { /// Kicker. kicker, + /// Rollover. + rollover, + /// Sparky. sparky, @@ -56,7 +59,7 @@ typedef CreateAudioPool = Future Function( }); /// Defines the contract for playing a single audio. -typedef PlaySingleAudio = Future Function(String); +typedef PlaySingleAudio = Future Function(String, {double volume}); /// Defines the contract for looping a single audio. typedef LoopSingleAudio = Future Function(String, {double volume}); @@ -81,18 +84,20 @@ class _SimplePlayAudio extends _Audio { required this.preCacheSingleAudio, required this.playSingleAudio, required this.path, + this.volume, }); final PreCacheSingleAudio preCacheSingleAudio; final PlaySingleAudio playSingleAudio; final String path; + final double? volume; @override Future load() => preCacheSingleAudio(prefixFile(path)); @override void play() { - playSingleAudio(prefixFile(path)); + playSingleAudio(prefixFile(path), volume: volume ?? 1); } } @@ -266,6 +271,12 @@ class PinballAudioPlayer { playSingleAudio: _playSingleAudio, path: Assets.sfx.launcher, ), + PinballAudio.rollover: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.rollover, + volume: 0.3, + ), PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, playSingleAudio: _playSingleAudio, @@ -323,10 +334,10 @@ class PinballAudioPlayer { late final Map audios; /// Loads the sounds effects into the memory. - List> load() { + List Function()> load() { _configureAudioCache(FlameAudio.audioCache); - return audios.values.map((a) => a.load()).toList(); + return audios.values.map((a) => a.load).toList(); } /// Plays the received audio. diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 3e147329..47d788bf 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -29,15 +29,15 @@ class _MockConfigureAudioCache extends Mock { } class _MockPlaySingleAudio extends Mock { - Future onCall(String url); + Future onCall(String path, {double volume}); } class _MockLoopSingleAudio extends Mock { - Future onCall(String url, {double volume}); + Future onCall(String path, {double volume}); } abstract class _PreCacheSingleAudio { - Future onCall(String url); + Future onCall(String path); } class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} @@ -74,7 +74,8 @@ void main() { when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); playSingleAudio = _MockPlaySingleAudio(); - when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); + when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async {}); loopSingleAudio = _MockLoopSingleAudio(); when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume'))) @@ -101,7 +102,9 @@ void main() { group('load', () { test('creates the bumpers pools', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); verify( () => createAudioPool.onCall( @@ -121,7 +124,9 @@ void main() { }); test('creates the kicker pools', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); verify( () => createAudioPool.onCall( @@ -141,7 +146,9 @@ void main() { }); test('configures the audio cache instance', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) .called(1); @@ -153,13 +160,17 @@ void main() { playSingleAudio: playSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall, ); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); expect(FlameAudio.audioCache.prefix, equals('')); }); test('pre cache the assets', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); verify( () => preCacheSingleAudio @@ -195,6 +206,10 @@ void main() { () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/rollover.mp3'), + ).called(1); verify( () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'), @@ -237,7 +252,9 @@ void main() { group('when seed is true', () { test('plays the bumper A sound pool', () async { when(seed.nextBool).thenReturn(true); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.bumper); verify(() => bumperAPool.start(volume: 0.6)).called(1); @@ -247,7 +264,9 @@ void main() { group('when seed is false', () { test('plays the bumper B sound pool', () async { when(seed.nextBool).thenReturn(false); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.bumper); verify(() => bumperBPool.start(volume: 0.6)).called(1); @@ -286,7 +305,9 @@ void main() { group('when seed is true', () { test('plays the kicker A sound pool', () async { when(seed.nextBool).thenReturn(true); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.kicker); verify(() => kickerAPool.start(volume: 0.6)).called(1); @@ -296,7 +317,9 @@ void main() { group('when seed is false', () { test('plays the kicker B sound pool', () async { when(seed.nextBool).thenReturn(false); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.kicker); verify(() => kickerBPool.start(volume: 0.6)).called(1); @@ -306,7 +329,9 @@ void main() { group('cow moo', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.cowMoo); verify( @@ -319,7 +344,9 @@ void main() { final clock = _MockClock(); await withClock(clock, () async { when(clock.now).thenReturn(DateTime(2022)); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer ..play(PinballAudio.cowMoo) ..play(PinballAudio.cowMoo); @@ -342,84 +369,127 @@ void main() { group('google', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.google); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.google}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.google}', + volume: any(named: 'volume'), + ), ).called(1); }); }); group('sparky', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.sparky); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.sparky}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.sparky}', + volume: any(named: 'volume'), + ), ).called(1); }); }); group('dino', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.dino); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.dino}', + volume: any(named: 'volume'), + ), ).called(1); }); }); group('android', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.android); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.android}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.android}', + volume: any(named: 'volume'), + ), ).called(1); }); }); group('dash', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.dash); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.dash}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.dash}', + volume: any(named: 'volume'), + ), ).called(1); }); }); group('launcher', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.launcher); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.launcher}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.launcher}', + volume: any(named: 'volume'), + ), + ).called(1); + }); + }); + + group('rollover', () { + test('plays the correct file', () async { + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); + audioPlayer.play(PinballAudio.rollover); + + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.rollover}', + volume: .3, + ), ).called(1); }); }); group('ioPinballVoiceOver', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.ioPinballVoiceOver); verify( () => playSingleAudio.onCall( 'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}', + volume: any(named: 'volume'), ), ).called(1); }); @@ -427,12 +497,15 @@ void main() { group('gameOverVoiceOver', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.gameOverVoiceOver); verify( () => playSingleAudio.onCall( 'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}', + volume: any(named: 'volume'), ), ).called(1); }); @@ -440,7 +513,9 @@ void main() { group('backgroundMusic', () { test('plays the correct file', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer.play(PinballAudio.backgroundMusic); verify( @@ -452,7 +527,9 @@ void main() { }); test('plays only once', () async { - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); audioPlayer ..play(PinballAudio.backgroundMusic) ..play(PinballAudio.backgroundMusic); @@ -470,7 +547,9 @@ void main() { 'throws assertions error when playing an unregistered audio', () async { audioPlayer.audios.remove(PinballAudio.google); - await Future.wait(audioPlayer.load()); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); expect( () => audioPlayer.play(PinballAudio.google), 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/ball/behaviors/ball_turbo_charging_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart index f1e5a855..006282ce 100644 --- a/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_turbo_charging_behavior.dart @@ -57,7 +57,7 @@ class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent Future onLoad() async { await super.onLoad(); - final spriteSheet = await gameRef.images.load( + final spriteSheet = gameRef.images.fromCache( Assets.images.ball.flameEffect.keyName, ); diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 4be27cef..d1ad34e0 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -109,7 +109,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition, ZIndex { final topLeftCurve = BezierCurveShape( controlPoints: [ - topWall.vertex1, + topWall.vertex2, Vector2(-31.5, -69.9), Vector2(-32.3, -57.2), ], @@ -123,7 +123,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition, ZIndex { final upperLeftWallCurve = BezierCurveShape( controlPoints: [ - topLeftWall.vertex1, + topLeftWall.vertex2, Vector2(-33.9, -40.7), Vector2(-32.5, -39), ], 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/behaviors.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart index ef3630e7..d3743ae9 100644 --- a/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart @@ -1,2 +1,3 @@ export 'flipper_jointing_behavior.dart'; export 'flipper_key_controlling_behavior.dart'; +export 'flipper_moving_behavior.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..b002420a 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 @@ -1,11 +1,11 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; /// Allows controlling the [Flipper]'s movement with keyboard input. class FlipperKeyControllingBehavior extends Component - with KeyboardHandler, ParentIsA { + with KeyboardHandler, FlameBlocReader { /// The [LogicalKeyboardKey]s that will control the [Flipper]. /// /// [onKeyEvent] method listens to when one of these keys is pressed. @@ -14,7 +14,21 @@ class FlipperKeyControllingBehavior extends Component @override Future onLoad() async { await super.onLoad(); - _keys = parent.side.flipperKeys; + final flipper = parent!.parent! as Flipper; + switch (flipper.side) { + case BoardSide.left: + _keys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]; + break; + case BoardSide.right: + _keys = [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]; + break; + } } @override @@ -25,28 +39,11 @@ class FlipperKeyControllingBehavior extends Component if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { - parent.moveUp(); + bloc.moveUp(); } else if (event is RawKeyUpEvent) { - parent.moveDown(); + bloc.moveDown(); } 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/flipper/behaviors/flipper_moving_behavior.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_moving_behavior.dart new file mode 100644 index 00000000..13989192 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_moving_behavior.dart @@ -0,0 +1,40 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class FlipperMovingBehavior extends Component + with + FlameBlocListenable, + FlameBlocReader { + FlipperMovingBehavior({ + required double strength, + }) : assert(strength >= 0, "Strength can't be negative"), + _strength = strength; + + final double _strength; + + late final Flipper _flipper; + + void _moveUp() => _flipper.body.linearVelocity = Vector2(0, -_strength); + + void _moveDown() => _flipper.body.linearVelocity = Vector2(0, _strength); + + @override + void onNewState(FlipperState state) { + super.onNewState(state); + if (bloc.state.isMovingDown) _moveDown(); + } + + @override + void update(double dt) { + super.update(dt); + if (bloc.state.isMovingUp) _moveUp(); + } + + @override + Future onLoad() async { + await super.onLoad(); + _flipper = parent!.parent! as Flipper; + _moveDown(); + } +} diff --git a/packages/pinball_components/lib/src/components/flipper/cubit/flipper_cubit.dart b/packages/pinball_components/lib/src/components/flipper/cubit/flipper_cubit.dart new file mode 100644 index 00000000..21ddb2d4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/cubit/flipper_cubit.dart @@ -0,0 +1,11 @@ +import 'package:bloc/bloc.dart'; + +part 'flipper_state.dart'; + +class FlipperCubit extends Cubit { + FlipperCubit() : super(FlipperState.movingDown); + + void moveUp() => emit(FlipperState.movingUp); + + void moveDown() => emit(FlipperState.movingDown); +} diff --git a/packages/pinball_components/lib/src/components/flipper/cubit/flipper_state.dart b/packages/pinball_components/lib/src/components/flipper/cubit/flipper_state.dart new file mode 100644 index 00000000..e14a495e --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/cubit/flipper_state.dart @@ -0,0 +1,11 @@ +part of 'flipper_cubit.dart'; + +enum FlipperState { + movingDown, + movingUp, +} + +extension FlipperStateX on FlipperState { + bool get isMovingDown => this == FlipperState.movingDown; + bool get isMovingUp => this == FlipperState.movingUp; +} diff --git a/packages/pinball_components/lib/src/components/flipper/flipper.dart b/packages/pinball_components/lib/src/components/flipper/flipper.dart index 280c157f..265e7924 100644 --- a/packages/pinball_components/lib/src/components/flipper/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper/flipper.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; import 'package:pinball_components/pinball_components.dart'; export 'behaviors/behaviors.dart'; +export 'cubit/flipper_cubit.dart'; /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. @@ -21,6 +23,10 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { children: [ _FlipperSpriteComponent(side: side), FlipperJointingBehavior(), + FlameBlocProvider( + create: FlipperCubit.new, + children: [FlipperMovingBehavior(strength: 90)], + ), ], ); @@ -33,29 +39,12 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// The size of the [Flipper]. static final size = Vector2(13.5, 4.3); - /// The speed required to move the [Flipper] to its highest position. - /// - /// The higher the value, the faster the [Flipper] will move. - static const double _speed = 90; - /// Whether the [Flipper] is on the left or right side of the board. /// /// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion, /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion. final BoardSide side; - /// Applies downward linear velocity to the [Flipper], moving it to its - /// resting position. - void moveDown() { - body.linearVelocity = Vector2(0, _speed); - } - - /// Applies upward linear velocity to the [Flipper], moving it to its highest - /// position. - void moveUp() { - body.linearVelocity = Vector2(0, -_speed); - } - List _createFixtureDefs() { final direction = side.direction; diff --git a/packages/pinball_components/lib/src/components/google_word/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/google_word/behaviors/behaviors.dart new file mode 100644 index 00000000..02dab3a8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_word/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'google_word_animating_behavior.dart'; 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 new file mode 100644 index 00000000..2119c2f8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class GoogleWordAnimatingBehavior extends TimerComponent + with FlameBlocReader { + GoogleWordAnimatingBehavior() : super(period: 0.35, repeat: true); + + final _maxBlinks = 7; + int _blinks = 0; + + @override + void onTick() { + super.onTick(); + if (_blinks != _maxBlinks * 2) { + bloc.switched(); + _blinks++; + } else { + timer.stop(); + 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 197771d6..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 @@ -23,7 +23,52 @@ class GoogleWordCubit extends Cubit { } } + void switched() { + switch (state.letterSpriteStates[0]!) { + case GoogleLetterSpriteState.lit: + emit( + GoogleWordState( + letterSpriteStates: { + for (int i = 0; i < _lettersInGoogle; i++) + if (i.isEven) + i: GoogleLetterSpriteState.dimmed + else + i: GoogleLetterSpriteState.lit + }, + ), + ); + break; + case GoogleLetterSpriteState.dimmed: + emit( + GoogleWordState( + letterSpriteStates: { + for (int i = 0; i < _lettersInGoogle; i++) + if (i.isEven) + i: GoogleLetterSpriteState.lit + else + i: GoogleLetterSpriteState.dimmed + }, + ), + ); + break; + } + } + void onBonusAwarded() { + emit( + GoogleWordState( + letterSpriteStates: { + for (int i = 0; i < _lettersInGoogle; i++) + if (i.isEven) + i: GoogleLetterSpriteState.lit + else + i: GoogleLetterSpriteState.dimmed + }, + ), + ); + } + + void onReset() { emit(GoogleWordState.initial()); _lastLitLetter = 0; } diff --git a/packages/pinball_components/lib/src/components/google_word/google_word.dart b/packages/pinball_components/lib/src/components/google_word/google_word.dart index 72126d2c..f9c93e2c 100644 --- a/packages/pinball_components/lib/src/components/google_word/google_word.dart +++ b/packages/pinball_components/lib/src/components/google_word/google_word.dart @@ -1,6 +1,7 @@ import 'package:flame/components.dart'; import 'package:pinball_components/pinball_components.dart'; +export 'behaviors/behaviors.dart'; export 'cubit/google_word_cubit.dart'; /// {@template google_word} 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..ce845197 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart @@ -0,0 +1,11 @@ +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..fbb7a437 --- /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 = gameRef.images.fromCache( + 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/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart index db98a30a..bb641a82 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart @@ -16,7 +16,8 @@ class RampBallAscendingContactBehavior if (other is! Ball) return; if (other.body.linearVelocity.y < 0) { - parent.parent.bloc.onAscendingBallEntered(); + readBloc() + .onAscendingBallEntered(); } } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart index c3dc9e3e..8dc0f69f 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -6,9 +6,17 @@ part 'spaceship_ramp_state.dart'; class SpaceshipRampCubit extends Cubit { SpaceshipRampCubit() : super(const SpaceshipRampState.initial()); - void onAscendingBallEntered() { + void onAscendingBallEntered() => emit(state.copyWith(hits: state.hits + 1)); + + void onProgressed() { + final index = ArrowLightState.values.indexOf(state.lightState); emit( - state.copyWith(hits: state.hits + 1), + state.copyWith( + lightState: + ArrowLightState.values[(index + 1) % ArrowLightState.values.length], + ), ); } + + void onReset() => emit(const SpaceshipRampState.initial()); } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart index 2979f05f..f73110da 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -1,22 +1,55 @@ +// ignore_for_file: comment_references + part of 'spaceship_ramp_cubit.dart'; class SpaceshipRampState extends Equatable { const SpaceshipRampState({ required this.hits, + required this.lightState, }) : assert(hits >= 0, "Hits can't be negative"); - const SpaceshipRampState.initial() : this(hits: 0); + const SpaceshipRampState.initial() + : this( + hits: 0, + lightState: ArrowLightState.inactive, + ); final int hits; + final ArrowLightState lightState; + + bool get arrowFullyLit => lightState == ArrowLightState.active5; SpaceshipRampState copyWith({ int? hits, + ArrowLightState? lightState, }) { return SpaceshipRampState( hits: hits ?? this.hits, + lightState: lightState ?? this.lightState, ); } @override - List get props => [hits]; + List get props => [hits, lightState]; +} + +/// Indicates the state of the arrow on the [SpaceshipRamp]. +enum ArrowLightState { + /// Arrow with no lights lit up. + inactive, + + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index 07a5e79b..8044b79a 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; 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/gen/assets.gen.dart'; @@ -19,30 +20,32 @@ class SpaceshipRamp extends Component { Iterable? children, }) : this._( children: children, - bloc: SpaceshipRampCubit(), ); SpaceshipRamp._({ Iterable? children, - required this.bloc, }) : super( children: [ - _SpaceshipRampOpening( - outsideLayer: Layer.spaceship, - outsidePriority: ZIndexes.ballOnSpaceship, - rotation: math.pi, - ) - ..initialPosition = Vector2(-13.7, -18.6) - ..layer = Layer.spaceshipEntranceRamp, - _SpaceshipRampBackground(), - SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), - _SpaceshipRampForegroundRailing(), - SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), - _SpaceshipRampBackgroundRailingSpriteComponent(), - SpaceshipRampArrowSpriteComponent( - current: bloc.state.hits, + FlameBlocProvider( + create: SpaceshipRampCubit.new, + children: [ + _SpaceshipRampOpening( + outsideLayer: Layer.spaceship, + outsidePriority: ZIndexes.ballOnSpaceship, + rotation: math.pi, + ) + ..initialPosition = Vector2(-13.7, -18.6) + ..layer = Layer.spaceshipEntranceRamp, + _SpaceshipRampBackground(), + SpaceshipRampBoardOpening() + ..initialPosition = Vector2(3.4, -39.5), + _SpaceshipRampForegroundRailing(), + SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), + _SpaceshipRampBackgroundRailingSpriteComponent(), + SpaceshipRampArrowSpriteComponent(), + ...?children, + ], ), - ...?children, ], ); @@ -51,16 +54,8 @@ class SpaceshipRamp extends Component { /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. @visibleForTesting SpaceshipRamp.test({ - required this.bloc, - }) : super(); - - final SpaceshipRampCubit bloc; - - @override - void onRemove() { - bloc.close(); - super.onRemove(); - } + Iterable? children, + }) : super(children: children); } class _SpaceshipRampBackground extends BodyComponent @@ -167,82 +162,71 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// {@endtemplate} @visibleForTesting -class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent - with HasGameRef, ParentIsA, ZIndex { +class SpaceshipRampArrowSpriteComponent + extends SpriteGroupComponent + with + HasGameRef, + ZIndex, + FlameBlocListenable { /// {@macro spaceship_ramp_arrow_sprite_component} - SpaceshipRampArrowSpriteComponent({ - required int current, - }) : super( + SpaceshipRampArrowSpriteComponent() + : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), - current: current, ) { zIndex = ZIndexes.spaceshipRampArrow; } + @override + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + return previousState.lightState != newState.lightState; + } + + @override + void onNewState(SpaceshipRampState state) { + current = state.lightState; + } + @override Future onLoad() async { await super.onLoad(); - parent.bloc.stream.listen((state) { - current = state.hits % SpaceshipRampArrowSpriteState.values.length; - }); - - final sprites = {}; + final sprites = {}; this.sprites = sprites; - for (final spriteState in SpaceshipRampArrowSpriteState.values) { - sprites[spriteState.index] = Sprite( + for (final spriteState in ArrowLightState.values) { + sprites[spriteState] = Sprite( gameRef.images.fromCache(spriteState.path), ); } - current = 0; + current = ArrowLightState.inactive; size = sprites[current]!.originalSize / 10; } } -/// Indicates the state of the arrow on the [SpaceshipRamp]. -@visibleForTesting -enum SpaceshipRampArrowSpriteState { - /// Arrow with no dashes lit up. - inactive, - - /// Arrow with 1 light lit up. - active1, - - /// Arrow with 2 lights lit up. - active2, - - /// Arrow with 3 lights lit up. - active3, - - /// Arrow with 4 lights lit up. - active4, - - /// Arrow with all 5 lights lit up. - active5, -} - -extension on SpaceshipRampArrowSpriteState { +extension on ArrowLightState { String get path { switch (this) { - case SpaceshipRampArrowSpriteState.inactive: + case ArrowLightState.inactive: return Assets.images.android.ramp.arrow.inactive.keyName; - case SpaceshipRampArrowSpriteState.active1: + case ArrowLightState.active1: return Assets.images.android.ramp.arrow.active1.keyName; - case SpaceshipRampArrowSpriteState.active2: + case ArrowLightState.active2: return Assets.images.android.ramp.arrow.active2.keyName; - case SpaceshipRampArrowSpriteState.active3: + case ArrowLightState.active3: return Assets.images.android.ramp.arrow.active3.keyName; - case SpaceshipRampArrowSpriteState.active4: + case ArrowLightState.active4: return Assets.images.android.ramp.arrow.active4.keyName; - case SpaceshipRampArrowSpriteState.active5: + case ArrowLightState.active5: return Assets.images.android.ramp.arrow.active5.keyName; } } } class SpaceshipRampBoardOpening extends BodyComponent - with Layered, ZIndex, InitialPosition, ParentIsA { + with Layered, ZIndex, InitialPosition { SpaceshipRampBoardOpening() : super( renderBody: false, 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/android_acres/android_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart index 185f5351..c7c9b76c 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flame/input.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; @@ -29,11 +30,14 @@ class AndroidSpaceshipGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await addAll( - [ - AndroidSpaceship(position: Vector2.zero()), - AndroidAnimatronic(), - ], + await add( + FlameBlocProvider( + create: AndroidSpaceshipCubit.new, + children: [ + AndroidSpaceship(position: Vector2.zero()), + AndroidAnimatronic(), + ], + ), ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index 1027002a..b19aef56 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -44,9 +44,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { await super.onLoad(); camera.followVector2(Vector2(-12, -50)); - await add( - _spaceshipRamp = SpaceshipRamp(), - ); + + _spaceshipRamp = SpaceshipRamp(); + await add(_spaceshipRamp); await traceAllBodies(); } @@ -57,7 +57,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { ) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { - _spaceshipRamp.bloc.onAscendingBallEntered(); + _spaceshipRamp + .readBloc() + .onProgressed(); return KeyEventResult.handled; } diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart index c1d435d5..fef1a145 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart @@ -10,6 +10,11 @@ class LaunchRampGame extends BallGame { : super( ballPriority: ZIndexes.ballOnLaunchRamp, ballLayer: Layer.launcher, + imagesFileNames: [ + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + ], ); static const description = ''' 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..328afce4 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,18 @@ import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flame_bloc/flame_bloc.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 { + PlungerGame() + : super( + imagesFileNames: [ + Assets.images.plunger.plunger.keyName, + ], + ); + static const description = ''' Shows how Plunger is rendered. @@ -13,39 +20,21 @@ 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); + final plunger = Plunger() + ..initialPosition = Vector2(center.x - 8.8, center.y); await add( - plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(center.x - 8.8, center.y), + FlameBlocProvider( + create: PlungerCubit.new, + children: [plunger], + ), ); - await traceAllBodies(); - } + 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..5e4c6061 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,8 +120,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" - flame_bloc: + flame_audio: dependency: transitive + description: + name: flame_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flame_bloc: + dependency: "direct main" description: name: flame_bloc url: "https://pub.dartlang.org" @@ -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/sandbox/pubspec.yaml b/packages/pinball_components/sandbox/pubspec.yaml index 791020d0..cbe1c7be 100644 --- a/packages/pinball_components/sandbox/pubspec.yaml +++ b/packages/pinball_components/sandbox/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: dashbook: ^0.1.7 flame: ^1.1.1 + flame_bloc: ^1.4.0 flame_forge2d: git: url: https://github.com/flame-engine/flame 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/ball/behaviors/ball_turbo_charging_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart index 79eb030e..09eabe0e 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart @@ -14,8 +14,11 @@ void main() { group( 'BallTurboChargingBehavior', () { - final asset = theme.Assets.images.dash.ball.keyName; - final flameTester = FlameTester(() => TestGame([asset])); + final assets = [ + theme.Assets.images.dash.ball.keyName, + Assets.images.ball.flameEffect.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); test('can be instantiated', () { expect( 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..3128d286 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 @@ -1,15 +1,14 @@ // 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/src/components/components.dart'; -import '../../../../helpers/helpers.dart'; - void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FlipperJointingBehavior', () { - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(Forge2DGame.new); test('can be instantiated', () { expect( @@ -19,19 +18,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..307264f0 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,8 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -8,7 +11,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + Future pump( + FlipperKeyControllingBehavior behavior, { + required BoardSide side, + FlipperCubit? flipperBloc, + }) async { + final flipper = Flipper.test(side: side); + await ensureAdd(flipper); + await flipper.ensureAdd( + FlameBlocProvider.value( + value: flipperBloc ?? FlipperCubit(), + children: [behavior], + ), + ); + } +} + +class _MockFlipperCubit extends Mock implements FlipperCubit {} class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override @@ -27,26 +47,32 @@ class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FlipperKeyControllingBehavior', () { - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(_TestGame.new); group( 'onKeyEvent', () { - late Flipper rightFlipper; - late Flipper leftFlipper; + late FlipperCubit flipperBloc; setUp(() { - rightFlipper = Flipper.test(side: BoardSide.right); - leftFlipper = Flipper.test(side: BoardSide.left); + flipperBloc = _MockFlipperCubit(); + whenListen( + flipperBloc, + const Stream.empty(), + initialState: FlipperState.movingDown, + ); }); group('on right Flipper', () { flameTester.test( 'moves upwards when right arrow is pressed', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + flipperBloc: flipperBloc, + side: BoardSide.right, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -55,17 +81,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isNegative); - expect(rightFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveUp).called(1); }, ); flameTester.test( 'moves downwards when right arrow is released', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -74,17 +103,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isPositive); - expect(rightFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveDown).called(1); }, ); flameTester.test( 'moves upwards when D is pressed', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -93,17 +125,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isNegative); - expect(rightFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveUp).called(1); }, ); flameTester.test( 'moves downwards when D is released', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -112,8 +147,8 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isPositive); - expect(rightFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveDown).called(1); }, ); @@ -121,9 +156,12 @@ void main() { flameTester.test( 'left arrow is pressed', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -132,17 +170,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isZero); - expect(rightFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); flameTester.test( 'left arrow is released', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -151,17 +192,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isZero); - expect(rightFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); flameTester.test( 'A is pressed', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -170,17 +214,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isZero); - expect(rightFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); flameTester.test( 'A is released', (game) async { - await game.ensureAdd(rightFlipper); final behavior = FlipperKeyControllingBehavior(); - await rightFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.right, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -189,8 +236,8 @@ void main() { behavior.onKeyEvent(event, {}); - expect(rightFlipper.body.linearVelocity.y, isZero); - expect(rightFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); }); @@ -200,9 +247,12 @@ void main() { flameTester.test( 'moves upwards when left arrow is pressed', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -211,17 +261,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isNegative); - expect(leftFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveUp).called(1); }, ); flameTester.test( 'moves downwards when left arrow is released', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -230,17 +283,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isPositive); - expect(leftFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveDown).called(1); }, ); flameTester.test( 'moves upwards when A is pressed', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -249,17 +305,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isNegative); - expect(leftFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveUp).called(1); }, ); flameTester.test( 'moves downwards when A is released', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -268,8 +327,8 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isPositive); - expect(leftFlipper.body.linearVelocity.x, isZero); + await Future.delayed(Duration.zero); + verify(flipperBloc.moveDown).called(1); }, ); @@ -277,9 +336,12 @@ void main() { flameTester.test( 'right arrow is pressed', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -288,17 +350,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isZero); - expect(leftFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); flameTester.test( 'right arrow is released', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -307,17 +372,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isZero); - expect(leftFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); flameTester.test( 'D is pressed', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn( @@ -326,17 +394,20 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isZero); - expect(leftFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); flameTester.test( 'D is released', (game) async { - await game.ensureAdd(leftFlipper); final behavior = FlipperKeyControllingBehavior(); - await leftFlipper.ensureAdd(behavior); + await game.pump( + behavior, + side: BoardSide.left, + flipperBloc: flipperBloc, + ); final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn( @@ -345,8 +416,8 @@ void main() { behavior.onKeyEvent(event, {}); - expect(leftFlipper.body.linearVelocity.y, isZero); - expect(leftFlipper.body.linearVelocity.x, isZero); + verifyNever(flipperBloc.moveDown); + verifyNever(flipperBloc.moveUp); }, ); }); diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart new file mode 100644 index 00000000..be48d795 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart @@ -0,0 +1,101 @@ +// ignore_for_file: avoid_dynamic_calls, 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( + FlipperMovingBehavior behavior, { + FlipperCubit? flipperBloc, + }) async { + final flipper = Flipper.test(side: BoardSide.left); + await ensureAdd(flipper); + await flipper.ensureAdd( + FlameBlocProvider.value( + value: flipperBloc ?? FlipperCubit(), + children: [behavior], + ), + ); + } +} + +class _MockFlipperCubit extends Mock implements FlipperCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('FlipperMovingBehavior', () { + test('can be instantiated', () { + expect( + FlipperMovingBehavior(strength: 0), + isA(), + ); + }); + + test('throws assertion error when strength is negative', () { + expect( + () => FlipperMovingBehavior(strength: -1), + throwsAssertionError, + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = FlipperMovingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + 'applies vertical velocity to flipper when moving down', + (game) async { + final bloc = _MockFlipperCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: FlipperState.movingUp, + ); + + const strength = 10.0; + final behavior = FlipperMovingBehavior(strength: strength); + await game.pump(behavior, flipperBloc: bloc); + + streamController.add(FlipperState.movingDown); + await Future.delayed(Duration.zero); + + final flipper = behavior.ancestors().whereType().single; + expect(flipper.body.linearVelocity.x, 0); + expect(flipper.body.linearVelocity.y, strength); + }, + ); + + flameTester.test( + 'applies vertical velocity to flipper when moving up', + (game) async { + final bloc = _MockFlipperCubit(); + whenListen( + bloc, + Stream.value(FlipperState.movingUp), + initialState: FlipperState.movingUp, + ); + + const strength = 10.0; + final behavior = FlipperMovingBehavior(strength: strength); + await game.pump(behavior, flipperBloc: bloc); + game.update(0); + + final flipper = behavior.ancestors().whereType().single; + expect(flipper.body.linearVelocity.x, 0); + expect(flipper.body.linearVelocity.y, -strength); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/flipper/cubit/flipper_cubit_test.dart b/packages/pinball_components/test/src/components/flipper/cubit/flipper_cubit_test.dart new file mode 100644 index 00000000..6cd9c591 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/cubit/flipper_cubit_test.dart @@ -0,0 +1,23 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('FlipperCubit', () { + test('can be instantiated', () { + expect(FlipperCubit(), isA()); + }); + + blocTest( + 'moves', + build: FlipperCubit.new, + act: (cubit) => cubit + ..moveUp() + ..moveDown(), + expect: () => [ + FlipperState.movingUp, + FlipperState.movingDown, + ], + ); + }); +} diff --git a/packages/pinball_components/test/src/components/flipper/flipper_test.dart b/packages/pinball_components/test/src/components/flipper/flipper_test.dart index 4569f3ec..0aba00bc 100644 --- a/packages/pinball_components/test/src/components/flipper/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper/flipper_test.dart @@ -128,31 +128,5 @@ void main() { }, ); }); - - flameTester.test( - 'moveDown applies downward velocity', - (game) async { - final flipper = Flipper(side: BoardSide.left); - await game.ensureAdd(flipper); - - expect(flipper.body.linearVelocity, equals(Vector2.zero())); - flipper.moveDown(); - - expect(flipper.body.linearVelocity.y, isPositive); - }, - ); - - flameTester.test( - 'moveUp applies upward velocity', - (game) async { - final flipper = Flipper(side: BoardSide.left); - await game.ensureAdd(flipper); - - expect(flipper.body.linearVelocity, equals(Vector2.zero())); - flipper.moveUp(); - - expect(flipper.body.linearVelocity.y, isNegative); - }, - ); }); } 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 new file mode 100644 index 00000000..6275678c --- /dev/null +++ b/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart @@ -0,0 +1,64 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + GoogleWordAnimatingBehavior child, { + required GoogleWordCubit bloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: bloc, + children: [child], + ), + ); + } +} + +class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('GoogleWordAnimatingBehavior', () { + flameTester.testGameWidget( + 'calls switched after timer period reached', + setUp: (game, tester) async { + final behavior = GoogleWordAnimatingBehavior(); + final bloc = _MockGoogleWordCubit(); + await game.pump(behavior, bloc: bloc); + game.update(behavior.timer.limit); + + verify(bloc.switched).called(1); + }, + ); + + flameTester.testGameWidget( + 'calls onReset and removes itself ' + 'after all blinks complete', + setUp: (game, tester) async { + final behavior = GoogleWordAnimatingBehavior(); + final bloc = _MockGoogleWordCubit(); + + await game.pump(behavior, bloc: bloc); + for (var i = 0; i <= 14; i++) { + game.update(behavior.timer.limit); + } + await game.ready(); + + 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 08acfae8..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 @@ -6,6 +6,21 @@ void main() { group( 'GoogleWordCubit', () { + final litEvens = { + for (int i = 0; i < 6; i++) + if (i.isEven) + i: GoogleLetterSpriteState.lit + else + i: GoogleLetterSpriteState.dimmed + }; + final litOdds = { + for (int i = 0; i < 6; i++) + if (i.isOdd) + i: GoogleLetterSpriteState.lit + else + i: GoogleLetterSpriteState.dimmed + }; + blocTest( 'onRolloverContacted emits first letter lit', build: GoogleWordCubit.new, @@ -25,9 +40,31 @@ void main() { ); blocTest( - 'onBonusAwarded emits initial state', + 'switched emits all even letters lit when first letter is dimmed', + build: GoogleWordCubit.new, + act: (bloc) => bloc.switched(), + expect: () => [GoogleWordState(letterSpriteStates: litEvens)], + ); + + blocTest( + 'switched emits all odd letters lit when first letter is lit', + build: GoogleWordCubit.new, + seed: () => GoogleWordState(letterSpriteStates: litEvens), + act: (bloc) => bloc.switched(), + expect: () => [GoogleWordState(letterSpriteStates: litOdds)], + ); + + blocTest( + 'onBonusAwarded emits all even letters lit', build: GoogleWordCubit.new, act: (bloc) => bloc.onBonusAwarded(), + expect: () => [GoogleWordState(letterSpriteStates: litEvens)], + ); + + blocTest( + 'onReset emits initial state', + build: GoogleWordCubit.new, + 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..6017f255 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/plunger_test.dart @@ -0,0 +1,119 @@ +// 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 asset = Assets.images.plunger.plunger.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + 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.images.load(asset); + 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.images.load(asset); + 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_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart index d1f03ce7..9051059c 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart @@ -1,14 +1,47 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; - -import '../../../../helpers/helpers.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]); + } + + Future pump( + SpaceshipRamp children, { + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: bloc, + children: [ + ZCanvasComponent(children: [children]), + ], + ), + ); + } +} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} @@ -20,20 +53,8 @@ class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.android.ramp.boardOpening.keyName, - Assets.images.android.ramp.railingForeground.keyName, - Assets.images.android.ramp.railingBackground.keyName, - Assets.images.android.ramp.main.keyName, - Assets.images.android.ramp.arrow.inactive.keyName, - Assets.images.android.ramp.arrow.active1.keyName, - Assets.images.android.ramp.arrow.active2.keyName, - Assets.images.android.ramp.arrow.active3.keyName, - Assets.images.android.ramp.arrow.active4.keyName, - Assets.images.android.ramp.arrow.active5.keyName, - ]; - - final flameTester = FlameTester(() => TestGame(assets)); + + final flameTester = FlameTester(_TestGame.new); group( 'RampBallAscendingContactBehavior', @@ -67,16 +88,18 @@ void main() { initialState: const SpaceshipRampState.initial(), ); - final parent = SpaceshipRampBoardOpening.test(); + final opening = SpaceshipRampBoardOpening.test(); final spaceshipRamp = SpaceshipRamp.test( - bloc: bloc, + children: [opening], ); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - await spaceshipRamp.add(parent); - await game.ensureAddAll([spaceshipRamp, ball]); - await parent.add(behavior); + await game.pump( + spaceshipRamp, + bloc: bloc, + ); + await opening.ensureAdd(behavior); behavior.beginContact(ball, _MockContact()); @@ -95,16 +118,18 @@ void main() { initialState: const SpaceshipRampState.initial(), ); - final parent = SpaceshipRampBoardOpening.test(); + final opening = SpaceshipRampBoardOpening.test(); final spaceshipRamp = SpaceshipRamp.test( - bloc: bloc, + children: [opening], ); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - await spaceshipRamp.add(parent); - await game.ensureAddAll([spaceshipRamp, ball]); - await parent.add(behavior); + await game.pump( + spaceshipRamp, + bloc: bloc, + ); + await opening.ensureAdd(behavior); behavior.beginContact(ball, _MockContact()); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart index b7e899fe..1e951ad4 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart @@ -15,9 +15,70 @@ void main() { ..onAscendingBallEntered() ..onAscendingBallEntered(), expect: () => [ - SpaceshipRampState(hits: 1), - SpaceshipRampState(hits: 2), - SpaceshipRampState(hits: 3), + isA().having((state) => state.hits, 'hits', 1), + isA().having((state) => state.hits, 'hits', 2), + isA().having((state) => state.hits, 'hits', 3), + ], + ); + }); + + group('onProgressed', () { + blocTest( + 'emits next arrow lightState', + build: SpaceshipRampCubit.new, + act: (bloc) => bloc + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed(), + expect: () => [ + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active1, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active2, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active3, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active4, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active5, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.inactive, + ), + ], + ); + }); + + group('onReset', () { + blocTest( + 'emits initial state', + build: SpaceshipRampCubit.new, + seed: () => SpaceshipRampState( + hits: 100, + lightState: ArrowLightState.active3, + ), + act: (bloc) => bloc.onReset(), + expect: () => [ + SpaceshipRampState.initial(), ], ); }); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart index 536f4e8e..04142506 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart @@ -7,9 +7,15 @@ void main() { group('SpaceshipRampState', () { test('supports value equality', () { expect( - SpaceshipRampState(hits: 0), + SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ), equals( - SpaceshipRampState(hits: 0), + SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ), ), ); }); @@ -17,19 +23,41 @@ void main() { group('constructor', () { test('can be instantiated', () { expect( - SpaceshipRampState(hits: 0), + SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ), isNotNull, ); }); + + test( + 'throws AssertionError ' + 'when hits is negative', + () { + expect( + () => SpaceshipRampState( + hits: -1, + lightState: ArrowLightState.inactive, + ), + throwsAssertionError, + ); + }, + ); }); test( - 'throws AssertionError ' - 'when hits is negative', + 'arrowFullyLit returns true when lightState is last one', () { expect( - () => SpaceshipRampState(hits: -1), - throwsAssertionError, + SpaceshipRampState.initial().arrowFullyLit, + isFalse, + ); + expect( + SpaceshipRampState.initial() + .copyWith(lightState: ArrowLightState.active5) + .arrowFullyLit, + isTrue, ); }, ); @@ -39,7 +67,10 @@ void main() { 'throws AssertionError ' 'when hits is decreased', () { - const rampState = SpaceshipRampState(hits: 0); + const rampState = SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ); expect( () => rampState.copyWith(hits: rampState.hits - 1), throwsAssertionError, @@ -51,7 +82,10 @@ void main() { 'copies correctly ' 'when no argument specified', () { - const rampState = SpaceshipRampState(hits: 0); + const rampState = SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ); expect( rampState.copyWith(), equals(rampState), @@ -63,12 +97,21 @@ void main() { 'copies correctly ' 'when all arguments specified', () { - const rampState = SpaceshipRampState(hits: 0); - final otherRampState = SpaceshipRampState(hits: rampState.hits + 1); + const rampState = SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ); + final otherRampState = SpaceshipRampState( + hits: rampState.hits + 1, + lightState: ArrowLightState.active1, + ); expect(rampState, isNot(equals(otherRampState))); expect( - rampState.copyWith(hits: rampState.hits + 1), + rampState.copyWith( + hits: otherRampState.hits, + lightState: otherRampState.lightState, + ), equals(otherRampState), ); }, diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index 1c9c968d..e1dafc59 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -1,7 +1,10 @@ -// ignore_for_file: cascade_invocations +// ignore_for_file: cascade_invocations, prefer_const_constructors + +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'; @@ -10,7 +13,38 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]); + } + + Future pump( + SpaceshipRamp children, { + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: bloc, + children: [ + ZCanvasComponent(children: [children]), + ], + ), + ); + } +} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} @@ -22,264 +56,74 @@ class _MockManifold extends Mock implements Manifold {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.android.ramp.boardOpening.keyName, - Assets.images.android.ramp.railingForeground.keyName, - Assets.images.android.ramp.railingBackground.keyName, - Assets.images.android.ramp.main.keyName, - Assets.images.android.ramp.arrow.inactive.keyName, - Assets.images.android.ramp.arrow.active1.keyName, - Assets.images.android.ramp.arrow.active2.keyName, - Assets.images.android.ramp.arrow.active3.keyName, - Assets.images.android.ramp.arrow.active4.keyName, - Assets.images.android.ramp.arrow.active5.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); + + final flameTester = FlameTester(_TestGame.new); group('SpaceshipRamp', () { flameTester.test( 'loads correctly', (game) async { - final spaceshipRamp = SpaceshipRamp(); - await game.ensureAdd(spaceshipRamp); - expect(game.children, contains(spaceshipRamp)); + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final ramp = SpaceshipRamp.test(); + await game.pump(ramp, bloc: bloc); + expect(game.descendants(), contains(ramp)); }, ); - group('renders correctly', () { - const goldenFilePath = '../golden/spaceship_ramp/'; - final centerForSpaceshipRamp = Vector2(-13, -55); - - flameTester.testGameWidget( - 'inactive sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.inactive, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}inactive.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'active1 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc.onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active1, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active1.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'active2 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active2, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active2.png'), - ); - }, - ); + group('adds', () { + flameTester.test('a FlameBlocProvider', (game) async { + final ramp = SpaceshipRamp(); + await game.ensureAdd(ramp); + expect( + ramp.children + .whereType< + FlameBlocProvider>() + .single, + isNotNull, + ); + }); - flameTester.testGameWidget( - 'active3 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); + flameTester.test( + 'a SpaceshipRampBoardOpening', + (game) async { final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered(); + await game.ensureAdd(ramp); - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active3, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active3.png'), + game.descendants().whereType().length, + equals(1), ); }, ); - flameTester.testGameWidget( - 'active4 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); + flameTester.test( + 'a SpaceshipRampArrowSpriteComponent', + (game) async { final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active4, - ); + await game.ensureAdd(ramp); - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active4.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'active5 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active5, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active5.png'), + game + .descendants() + .whereType() + .length, + equals(1), ); }, ); - }); - - flameTester.test('closes bloc when removed', (game) async { - final bloc = _MockSpaceshipRampCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: const SpaceshipRampState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - - final ramp = SpaceshipRamp.test( - bloc: bloc, - ); - - await game.ensureAdd(ramp); - game.remove(ramp); - await game.ready(); - verify(bloc.close).called(1); - }); - - group('adds', () { flameTester.test('new children', (game) async { final component = Component(); final ramp = SpaceshipRamp(children: [component]); await game.ensureAdd(ramp); - expect(ramp.children, contains(component)); + + expect(ramp.descendants(), contains(component)); }); }); }); @@ -332,17 +176,19 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit()); final component = SpaceshipRampBoardOpening(); - await game.ensureAdd(parent); + final parent = SpaceshipRamp.test(); + await game.pump(parent, bloc: _MockSpaceshipRampCubit()); + await parent.ensureAdd(component); expect(parent.children, contains(component)); }); flameTester.test('adds a RampBallAscendingContactBehavior', (game) async { - final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit()); final component = SpaceshipRampBoardOpening(); - await game.ensureAdd(parent); + final parent = SpaceshipRamp.test(); + await game.pump(parent, bloc: _MockSpaceshipRampCubit()); + await parent.ensureAdd(component); expect( component.children.whereType().length, @@ -350,4 +196,36 @@ void main() { ); }); }); + + group('SpaceshipRampArrowSpriteComponent', () { + flameTester.test( + 'changes current state ' + 'when SpaceshipRampState changes lightState', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + final arrow = SpaceshipRampArrowSpriteComponent(); + final ramp = SpaceshipRamp.test(children: [arrow]); + await game.pump( + ramp, + bloc: bloc, + ); + + expect(arrow.current, ArrowLightState.inactive); + + streamController + .add(state.copyWith(lightState: ArrowLightState.active1)); + + await game.ready(); + + expect(arrow.current, ArrowLightState.active1); + }, + ); + }); } 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/packages/pinball_theme/assets/images/android/background.jpg b/packages/pinball_theme/assets/images/android/background.jpg index afe2e3c6..f7326a48 100644 Binary files a/packages/pinball_theme/assets/images/android/background.jpg and b/packages/pinball_theme/assets/images/android/background.jpg differ diff --git a/packages/pinball_theme/assets/images/dash/background.jpg b/packages/pinball_theme/assets/images/dash/background.jpg index 0b70a795..c9e9223d 100644 Binary files a/packages/pinball_theme/assets/images/dash/background.jpg and b/packages/pinball_theme/assets/images/dash/background.jpg differ diff --git a/packages/pinball_theme/assets/images/dino/background.jpg b/packages/pinball_theme/assets/images/dino/background.jpg index 35deff95..8c24a4c8 100644 Binary files a/packages/pinball_theme/assets/images/dino/background.jpg and b/packages/pinball_theme/assets/images/dino/background.jpg differ diff --git a/packages/pinball_theme/assets/images/sparky/background.jpg b/packages/pinball_theme/assets/images/sparky/background.jpg index 9c4fdfc6..aa0ac9a4 100644 Binary files a/packages/pinball_theme/assets/images/sparky/background.jpg and b/packages/pinball_theme/assets/images/sparky/background.jpg differ diff --git a/packages/pinball_ui/lib/src/widgets/pinball_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_button.dart index dd4685c1..febedf4c 100644 --- a/packages/pinball_ui/lib/src/widgets/pinball_button.dart +++ b/packages/pinball_ui/lib/src/widgets/pinball_button.dart @@ -32,6 +32,8 @@ class PinballButton extends StatelessWidget { child: Center( child: InkWell( onTap: onTap, + highlightColor: PinballColors.transparent, + splashColor: PinballColors.transparent, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 32, diff --git a/packages/pinball_ui/lib/src/widgets/pinball_dpad_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_dpad_button.dart index a0c3e653..6fe27b77 100644 --- a/packages/pinball_ui/lib/src/widgets/pinball_dpad_button.dart +++ b/packages/pinball_ui/lib/src/widgets/pinball_dpad_button.dart @@ -55,6 +55,8 @@ class PinballDpadButton extends StatelessWidget { color: PinballColors.transparent, child: InkWell( onTap: onTap, + highlightColor: PinballColors.transparent, + splashColor: PinballColors.transparent, child: Image.asset( direction.toAsset(), width: 60, diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index b9f2ef16..d719767e 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -37,7 +37,7 @@ void main() { shareRepository = _MockShareRepository(); pinballAudioPlayer = _MockPinballAudioPlayer(); platformHelper = _MockPlatformHelper(); - when(pinballAudioPlayer.load).thenAnswer((_) => [Future.value()]); + when(pinballAudioPlayer.load).thenAnswer((_) => [Future.value]); }); testWidgets('renders PinballGamePage', (tester) async { diff --git a/test/assets_manager/cubit/assets_manager_state_test.dart b/test/assets_manager/cubit/assets_manager_state_test.dart index 41e94add..6891ef9b 100644 --- a/test/assets_manager/cubit/assets_manager_state_test.dart +++ b/test/assets_manager/cubit/assets_manager_state_test.dart @@ -7,7 +7,7 @@ void main() { group('AssetsManagerState', () { test('can be instantiated', () { expect( - AssetsManagerState(loadables: const [], loaded: const []), + AssetsManagerState(assetsCount: 0, loaded: 0), isNotNull, ); }); @@ -17,22 +17,19 @@ void main() { AssetsManagerState.initial(), equals( AssetsManagerState( - loadables: const [], - loaded: const [], + assetsCount: 0, + loaded: 0, ), ), ); }); group('progress', () { - final future1 = Future.value(); - final future2 = Future.value(); - test('returns 0 when no future is loaded', () { expect( AssetsManagerState( - loadables: [future1, future2], - loaded: const [], + assetsCount: 2, + loaded: 0, ).progress, equals(0), ); @@ -41,8 +38,8 @@ void main() { test('returns the correct value when some of the futures are loaded', () { expect( AssetsManagerState( - loadables: [future1, future2], - loaded: [future1], + assetsCount: 2, + loaded: 1, ).progress, equals(0.5), ); @@ -51,8 +48,8 @@ void main() { test('returns the 1 when all futures are loaded', () { expect( AssetsManagerState( - loadables: [future1, future2], - loaded: [future1, future2], + assetsCount: 2, + loaded: 2, ).progress, equals(1), ); @@ -60,18 +57,16 @@ void main() { }); group('copyWith', () { - final future = Future.value(); - - test('returns a copy with the updated loadables', () { + test('returns a copy with the updated assetsCount', () { expect( AssetsManagerState( - loadables: const [], - loaded: const [], - ).copyWith(loadables: [future]), + assetsCount: 0, + loaded: 0, + ).copyWith(assetsCount: 1), equals( AssetsManagerState( - loadables: [future], - loaded: const [], + assetsCount: 1, + loaded: 0, ), ), ); @@ -80,13 +75,13 @@ void main() { test('returns a copy with the updated loaded', () { expect( AssetsManagerState( - loadables: const [], - loaded: const [], - ).copyWith(loaded: [future]), + assetsCount: 0, + loaded: 0, + ).copyWith(loaded: 1), equals( AssetsManagerState( - loadables: const [], - loaded: [future], + assetsCount: 0, + loaded: 1, ), ), ); @@ -94,47 +89,29 @@ void main() { }); test('supports value comparison', () { - final future1 = Future.value(); - final future2 = Future.value(); - expect( AssetsManagerState( - loadables: const [], - loaded: const [], + assetsCount: 0, + loaded: 0, ), equals( AssetsManagerState( - loadables: const [], - loaded: const [], - ), - ), - ); - - expect( - AssetsManagerState( - loadables: [future1], - loaded: const [], - ), - isNot( - equals( - AssetsManagerState( - loadables: [future2], - loaded: const [], - ), + assetsCount: 0, + loaded: 0, ), ), ); expect( AssetsManagerState( - loadables: const [], - loaded: [future1], + assetsCount: 1, + loaded: 0, ), isNot( equals( AssetsManagerState( - loadables: const [], - loaded: [future2], + assetsCount: 1, + loaded: 1, ), ), ), diff --git a/test/assets_manager/views/assets_loading_page_test.dart b/test/assets_manager/views/assets_loading_page_test.dart index a6210e0c..7a457e4b 100644 --- a/test/assets_manager/views/assets_loading_page_test.dart +++ b/test/assets_manager/views/assets_loading_page_test.dart @@ -12,9 +12,9 @@ void main() { late AssetsManagerCubit assetsManagerCubit; setUp(() { - final initialAssetsState = AssetsManagerState( - loadables: [Future.value()], - loaded: const [], + const initialAssetsState = AssetsManagerState( + assetsCount: 1, + loaded: 0, ); assetsManagerCubit = _MockAssetsManagerCubit(); whenListen( 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/camera_focusing_behavior_test.dart b/test/game/behaviors/camera_focusing_behavior_test.dart index a856b392..092f3efe 100644 --- a/test/game/behaviors/camera_focusing_behavior_test.dart +++ b/test/game/behaviors/camera_focusing_behavior_test.dart @@ -53,6 +53,20 @@ void main() { }, ); + flameTester.test('sets zoom on resize', (game) async { + final behavior = CameraFocusingBehavior(); + + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); + + game.onGameResize(game.canvasSize * 2); + expect(game.camera.zoom, equals(6.55)); + }); + flameTester.test( 'listenWhen only listens when status changes', (game) async { 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/behaviors/rollover_noise_behavior_test.dart b/test/game/behaviors/rollover_noise_behavior_test.dart new file mode 100644 index 00000000..a196c8b6 --- /dev/null +++ b/test/game/behaviors/rollover_noise_behavior_test.dart @@ -0,0 +1,58 @@ +// 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:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + _TestBodyComponent child, { + required PinballAudioPlayer audioPlayer, + }) { + return ensureAdd( + FlameProvider.value( + audioPlayer, + children: [child], + ), + ); + } +} + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody(BodyDef()); +} + +class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RolloverNoiseBehavior', () { + late PinballAudioPlayer audioPlayer; + final flameTester = FlameTester(_TestGame.new); + + setUp(() { + audioPlayer = _MockPinballAudioPlayer(); + }); + flameTester.testGameWidget( + 'plays rollover sound on contact', + setUp: (game, _) async { + final behavior = RolloverNoiseBehavior(); + final parent = _TestBodyComponent(); + await game.pump(parent, audioPlayer: audioPlayer); + await parent.ensureAdd(behavior); + behavior.beginContact(Object(), _MockContact()); + }, + verify: (_, __) async { + verify(() => audioPlayer.play(PinballAudio.rollover)).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart index cb6c2784..dc0d0e28 100644 --- a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -3,6 +3,7 @@ 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'; @@ -36,14 +37,22 @@ class _TestGame extends Forge2DGame { } Future pump( - SpaceshipRamp child, { + List children, { + required SpaceshipRampCubit bloc, required GameBloc gameBloc, }) async { await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], children: [ - ZCanvasComponent(children: [child]), + ZCanvasComponent(children: children), ], ), ); @@ -54,20 +63,17 @@ class _MockGameBloc extends Mock implements GameBloc {} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} -class _MockStreamSubscription extends Mock - implements StreamSubscription {} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('RampBonusBehavior', () { - const shotPoints = Points.oneMillion; + late GameBloc gameBloc; - late GameBloc gameBloc; + setUp(() { + gameBloc = _MockGameBloc(); + }); - setUp(() { - gameBloc = _MockGameBloc(); - }); + group('RampBonusBehavior', () { + const shotPoints = Points.oneMillion; final flameTester = FlameTester(_TestGame.new); @@ -75,22 +81,23 @@ void main() { 'when hits are multiples of 10 times adds a ScoringBehavior', (game) async { final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); final streamController = StreamController(); whenListen( bloc, streamController.stream, - initialState: SpaceshipRampState(hits: 9), + initialState: state, ); - final behavior = RampBonusBehavior(points: shotPoints); - final parent = SpaceshipRamp.test(bloc: bloc); + final behavior = RampBonusBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(children: [behavior]); await game.pump( - parent, + [parent], + bloc: bloc, gameBloc: gameBloc, ); - await parent.ensureAdd(behavior); - streamController.add(SpaceshipRampState(hits: 10)); + streamController.add(state.copyWith(hits: 10)); final scores = game.descendants().whereType(); await game.ready(); @@ -103,22 +110,23 @@ void main() { "when hits are not multiple of 10 times doesn't add any ScoringBehavior", (game) async { final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); final streamController = StreamController(); whenListen( bloc, streamController.stream, - initialState: SpaceshipRampState.initial(), + initialState: state, ); - final behavior = RampBonusBehavior(points: shotPoints); - final parent = SpaceshipRamp.test(bloc: bloc); + final behavior = RampBonusBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(children: [behavior]); await game.pump( - parent, + [parent], + bloc: bloc, gameBloc: gameBloc, ); - await parent.ensureAdd(behavior); - streamController.add(SpaceshipRampState(hits: 1)); + streamController.add(state.copyWith(hits: 9)); final scores = game.descendants().whereType(); await game.ready(); @@ -126,38 +134,5 @@ void main() { expect(scores.length, 0); }, ); - - flameTester.test( - 'closes subscription when removed', - (game) async { - final bloc = _MockSpaceshipRampCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: SpaceshipRampState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - - final subscription = _MockStreamSubscription(); - when(subscription.cancel).thenAnswer((_) async {}); - - final behavior = RampBonusBehavior.test( - points: shotPoints, - subscription: subscription, - ); - final parent = SpaceshipRamp.test(bloc: bloc); - - await game.pump( - parent, - gameBloc: gameBloc, - ); - await parent.ensureAdd(behavior); - - parent.remove(behavior); - await game.ready(); - - verify(subscription.cancel).called(1); - }, - ); }); } diff --git a/test/game/components/android_acres/behaviors/ramp_multiplier_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_multiplier_behavior_test.dart new file mode 100644 index 00000000..5413c3d3 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_multiplier_behavior_test.dart @@ -0,0 +1,183 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +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/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.fiveThousand.keyName, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _FakeGameEvent extends Fake implements GameEvent {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RampMultiplierBehavior', () { + late GameBloc gameBloc; + + setUp(() { + registerFallbackValue(_FakeGameEvent()); + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'adds MultiplierIncreased ' + 'when hits are multiples of 5 times and multiplier is less than 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + when(() => gameBloc.add(any())).thenAnswer((_) async {}); + + final behavior = RampMultiplierBehavior(); + final parent = SpaceshipRamp.test(children: [behavior]); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verify(() => gameBloc.add(const MultiplierIncreased())).called(1); + }, + ); + + flameTester.test( + "doesn't add MultiplierIncreased " + 'when hits are multiples of 5 times but multiplier is 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampMultiplierBehavior(); + final parent = SpaceshipRamp.test(children: [behavior]); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verifyNever(() => gameBloc.add(const MultiplierIncreased())); + }, + ); + + flameTester.test( + "doesn't add MultiplierIncreased " + "when hits aren't multiples of 5 times", + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + + final behavior = RampMultiplierBehavior(); + final parent = SpaceshipRamp.test(children: [behavior]); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 1)); + + await game.ready(); + + verifyNever(() => gameBloc.add(const MultiplierIncreased())); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart new file mode 100644 index 00000000..29e9f452 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart @@ -0,0 +1,309 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +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/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _FakeGameEvent extends Fake implements GameEvent {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RampProgressBehavior', () { + late GameBloc gameBloc; + + setUp(() { + registerFallbackValue(_FakeGameEvent()); + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'adds onProgressed ' + 'when hits and multiplier are less than 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 1, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + + flameTester.test( + 'adds onProgressed ' + 'when hits and multiplier are 6 but arrow is not fully lit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + + flameTester.test( + "doesn't add onProgressed " + 'when hits and multiplier are 6 and arrow is fully lit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith( + hits: 5, + lightState: ArrowLightState.active5, + ), + ); + await Future.delayed(Duration.zero); + + verifyNever(bloc.onProgressed); + }, + ); + + flameTester.test( + 'adds onProgressed to dim arrow ' + 'when arrow is fully lit after hit and multiplier is less than 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith( + hits: 5, + lightState: ArrowLightState.active5, + ), + ); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(2); + }, + ); + + flameTester.test( + "doesn't add onProgressed to dim arrow " + 'when arrow is not fully lit after hit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith( + hits: 4, + lightState: ArrowLightState.active4, + ), + ); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + + flameTester.test( + "doesn't add onProgressed to dim arrow " + 'when multiplier is 6 after hit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith(hits: 4), + ); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart new file mode 100644 index 00000000..d141c62a --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart @@ -0,0 +1,135 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +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/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.fiveThousand.keyName, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RampResetBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'calls onReset when round lost', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = GameState.initial(); + final streamController = StreamController(); + whenListen( + gameBloc, + streamController.stream, + initialState: state, + ); + final behavior = RampResetBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(rounds: state.rounds - 1)); + await Future.delayed(Duration.zero); + + verify(bloc.onReset).called(1); + }, + ); + + flameTester.test( + "doesn't call onReset when round stays the same", + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = GameState.initial(); + final streamController = StreamController(); + whenListen( + gameBloc, + streamController.stream, + initialState: state, + ); + final behavior = RampResetBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController + .add(state.copyWith(roundScore: state.roundScore + 100)); + await Future.delayed(Duration.zero); + + verifyNever(bloc.onReset); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart index ae072ea4..d5a5ecd6 100644 --- a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -3,6 +3,7 @@ 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'; @@ -36,14 +37,22 @@ class _TestGame extends Forge2DGame { } Future pump( - SpaceshipRamp child, { + List children, { + required SpaceshipRampCubit bloc, required GameBloc gameBloc, }) async { await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], children: [ - ZCanvasComponent(children: [child]), + ZCanvasComponent(children: children), ], ), ); @@ -54,120 +63,47 @@ class _MockGameBloc extends Mock implements GameBloc {} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} -class _MockStreamSubscription extends Mock - implements StreamSubscription {} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('RampShotBehavior', () { - const shotPoints = Points.fiveThousand; + late GameBloc gameBloc; - late GameBloc gameBloc; + setUp(() { + gameBloc = _MockGameBloc(); + }); - setUp(() { - gameBloc = _MockGameBloc(); - }); + group('RampShotBehavior', () { + const shotPoints = Points.fiveThousand; - final flameBlocTester = FlameTester(_TestGame.new); + final flameTester = FlameTester(_TestGame.new); - flameBlocTester.test( - 'when hits are not multiple of 10 times ' - 'increases multiplier and adds a ScoringBehavior', + flameTester.test( + 'adds a ScoringBehavior when hit', (game) async { final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); final streamController = StreamController(); whenListen( bloc, streamController.stream, - initialState: SpaceshipRampState.initial(), + initialState: state, ); - final behavior = RampShotBehavior(points: shotPoints); - final parent = SpaceshipRamp.test(bloc: bloc); + final behavior = RampShotBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(children: [behavior]); await game.pump( - parent, + [parent], + bloc: bloc, gameBloc: gameBloc, ); - await parent.ensureAdd(behavior); - streamController.add(SpaceshipRampState(hits: 1)); + streamController.add(state.copyWith(hits: state.hits + 1)); final scores = game.descendants().whereType(); await game.ready(); - verify(() => gameBloc.add(MultiplierIncreased())).called(1); expect(scores.length, 1); }, ); - - flameBlocTester.test( - 'when hits multiple of 10 times ' - "doesn't increase multiplier, neither ScoringBehavior", - (game) async { - final bloc = _MockSpaceshipRampCubit(); - final streamController = StreamController(); - whenListen( - bloc, - streamController.stream, - initialState: SpaceshipRampState(hits: 9), - ); - final behavior = RampShotBehavior( - points: shotPoints, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); - - await game.pump( - parent, - gameBloc: gameBloc, - ); - await parent.ensureAdd(behavior); - - streamController.add(SpaceshipRampState(hits: 10)); - - final scores = game.children.whereType(); - await game.ready(); - - verifyNever(() => gameBloc.add(MultiplierIncreased())); - expect(scores.length, 0); - }, - ); - - flameBlocTester.test( - 'closes subscription when removed', - (game) async { - final bloc = _MockSpaceshipRampCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: SpaceshipRampState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - - final subscription = _MockStreamSubscription(); - when(subscription.cancel).thenAnswer((_) async {}); - - final behavior = RampShotBehavior.test( - points: shotPoints, - subscription: subscription, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); - - await game.pump( - parent, - gameBloc: gameBloc, - ); - await parent.ensureAdd(behavior); - - parent.remove(behavior); - await game.ready(); - - verify(subscription.cancel).called(1); - }, - ); }); } 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..47a273ba 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,13 @@ 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 _MockFlipperCubit extends Mock implements FlipperCubit {} class _MockAppLocalizations extends Mock implements AppLocalizations { @override @@ -193,10 +201,14 @@ void main() { final behavior = FlipperKeyControllingBehavior(); await game.pump([component, backbox, flipper]); - await flipper.ensureAdd(behavior); + await flipper.ensureAdd( + FlameBlocProvider( + create: _MockFlipperCubit.new, + children: [behavior], + ), + ); expect(state.status, GameStatus.gameOver); - component.onNewState(state); await game.ready(); @@ -207,6 +219,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 +346,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(); @@ -276,18 +373,138 @@ void main() { final flipper = Flipper.test(side: BoardSide.left); await game.pump([component, backbox, flipper]); + await flipper.ensureAdd( + FlameBlocProvider( + create: _MockFlipperCubit.new, + ), + ); component.onNewState(state); await game.ready(); expect( - flipper.children + flipper + .descendants() .whereType() .length, equals(1), ); }, ); + + 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 4b3ec2bd..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,13 +114,13 @@ void main() { verify( () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), ).called(1); - verify(googleWordBloc.onBonusAwarded).called(1); + verify(googleWordBloc.onReset).called(1); }, ); flameTester.testGameWidget( - 'adds BonusBallSpawningBehavior to the game when all letters ' - 'in google word are activated', + 'adds BonusBallSpawningBehavior and GoogleWordAnimatingBehavior ' + 'to the game when all letters in google word are activated', setUp: (game, tester) async { final behavior = GoogleWordBonusBehavior(); final parent = GoogleGallery.test(); @@ -161,6 +161,10 @@ void main() { game.descendants().whereType().length, equals(1), ); + expect( + game.descendants().whereType().length, + equals(1), + ); }, ); }); diff --git a/test/game/components/google_gallery/google_gallery_test.dart b/test/game/components/google_gallery/google_gallery_test.dart index 9551285f..719be2dc 100644 --- a/test/game/components/google_gallery/google_gallery_test.dart +++ b/test/game/components/google_gallery/google_gallery_test.dart @@ -97,6 +97,20 @@ void main() { }, ); + flameTester.test( + 'RolloverNoiseBehavior to GoogleRollovers', + (game) async { + await game.pump(GoogleGallery()); + + game.descendants().whereType().forEach( + (rollover) => expect( + rollover.firstChild(), + isNotNull, + ), + ); + }, + ); + flameTester.test('a GoogleWordBonusBehavior', (game) async { final component = GoogleGallery(); await game.pump(component); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 289fb4fa..05b9442c 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'; @@ -34,7 +35,7 @@ class _TestPinballGame extends PinballGame { @override Future onLoad() async { images.prefix = ''; - final futures = preLoadAssets(); + final futures = preLoadAssets().map((loadableBuilder) => loadableBuilder()); await Future.wait(futures); await super.onLoad(); } @@ -55,7 +56,7 @@ class _TestDebugPinballGame extends DebugPinballGame { @override Future onLoad() async { images.prefix = ''; - final futures = preLoadAssets(); + final futures = preLoadAssets().map((loadableBuilder) => loadableBuilder()); await Future.wait(futures); await super.onLoad(); } @@ -214,7 +215,8 @@ void main() { 'paints sprites with FilterQuality.medium', setUp: (game, tester) async { game.images.prefix = ''; - final futures = game.preLoadAssets(); + final futures = + game.preLoadAssets().map((loadableBuilder) => loadableBuilder()); await Future.wait(futures); await game.ready(); @@ -258,13 +260,19 @@ void main() { when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); - final flippers = game.descendants().whereType().where( - (flipper) => flipper.side == BoardSide.left, - ); - game.onTapDown(0, tapDownEvent); - - expect(flippers.first.body.linearVelocity.y, isNegative); + await Future.delayed(Duration.zero); + + final flipperBloc = game + .descendants() + .whereType() + .where((flipper) => flipper.side == BoardSide.left) + .single + .descendants() + .whereType>() + .first + .bloc; + expect(flipperBloc.state, FlipperState.movingUp); }); flameTester.test('tap down moves right flipper up', (game) async { @@ -281,13 +289,19 @@ void main() { when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); - final flippers = game.descendants().whereType().where( - (flipper) => flipper.side == BoardSide.right, - ); - game.onTapDown(0, tapDownEvent); - - expect(flippers.first.body.linearVelocity.y, isNegative); + final flipperBloc = game + .descendants() + .whereType() + .where((flipper) => flipper.side == BoardSide.right) + .single + .descendants() + .whereType>() + .first + .bloc; + + await Future.delayed(Duration.zero); + expect(flipperBloc.state, FlipperState.movingUp); }); flameTester.test('tap up moves flipper down', (game) async { @@ -297,28 +311,22 @@ void main() { when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); - final raw = _MockTapDownDetails(); - when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - - final tapDownEvent = _MockTapDownInfo(); - when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); - when(() => tapDownEvent.raw).thenReturn(raw); - - final flippers = game.descendants().whereType().where( - (flipper) => flipper.side == BoardSide.left, - ); - - game.onTapDown(0, tapDownEvent); - - expect(flippers.first.body.linearVelocity.y, isNegative); - final tapUpEvent = _MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); game.onTapUp(0, tapUpEvent); await game.ready(); - expect(flippers.first.body.linearVelocity.y, isPositive); + final flipperBloc = game + .descendants() + .whereType() + .where((flipper) => flipper.side == BoardSide.left) + .single + .descendants() + .whereType>() + .first + .bloc; + expect(flipperBloc.state, FlipperState.movingDown); }); flameTester.test('tap cancel moves flipper down', (game) async { @@ -335,17 +343,19 @@ void main() { when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); - final flippers = game.descendants().whereType().where( - (flipper) => flipper.side == BoardSide.left, - ); + final flipperBloc = game + .descendants() + .whereType() + .where((flipper) => flipper.side == BoardSide.left) + .single + .descendants() + .whereType>() + .first + .bloc; game.onTapDown(0, tapDownEvent); - - expect(flippers.first.body.linearVelocity.y, isNegative); - game.onTapCancel(0); - - expect(flippers.first.body.linearVelocity.y, isPositive); + expect(flipperBloc.state, FlipperState.movingDown); }); flameTester.test( @@ -374,17 +384,25 @@ void main() { .thenReturn(rightEventPosition); when(() => rightTapDownEvent.raw).thenReturn(raw); - final flippers = game.descendants().whereType(); - final rightFlipper = flippers.elementAt(0); - final leftFlipper = flippers.elementAt(1); - game.onTapDown(0, leftTapDownEvent); game.onTapDown(1, rightTapDownEvent); - expect(leftFlipper.body.linearVelocity.y, isNegative); - expect(leftFlipper.side, equals(BoardSide.left)); - expect(rightFlipper.body.linearVelocity.y, isNegative); - expect(rightFlipper.side, equals(BoardSide.right)); + final flippers = game.descendants().whereType(); + final rightFlipper = flippers.elementAt(0); + final leftFlipper = flippers.elementAt(1); + final leftFlipperBloc = leftFlipper + .descendants() + .whereType>() + .first + .bloc; + final rightFlipperBloc = rightFlipper + .descendants() + .whereType>() + .first + .bloc; + + expect(leftFlipperBloc.state, equals(FlipperState.movingUp)); + expect(rightFlipperBloc.state, equals(FlipperState.movingUp)); expect( game.focusedBoardSide, @@ -395,7 +413,7 @@ void main() { }); group('plunger control', () { - flameTester.test('tap down moves plunger down', (game) async { + flameTester.test('plunger control tap down emits plunging', (game) async { await game.ready(); final eventPosition = _MockEventPosition(); @@ -408,13 +426,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..da52bbd0 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -37,9 +37,13 @@ class _TestPinballGame extends PinballGame { images.prefix = ''; final futures = [ ...preLoadAssets(), - preFetchLeaderboard(), + ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(), + preFetchLeaderboard, ]; - await Future.wait(futures); + await Future.wait( + futures.map((loadableBuilder) => loadableBuilder()).toList(), + ); return super.onLoad(); } @@ -78,7 +82,9 @@ void main() { late GameBloc gameBloc; setUp(() async { - await Future.wait(game.preLoadAssets()); + await Future.wait( + game.preLoadAssets().map((loadableBuilder) => loadableBuilder()), + ); characterThemeCubit = _MockCharacterThemeCubit(); gameBloc = _MockGameBloc(); @@ -122,8 +128,8 @@ void main() { (tester) async { final assetsManagerCubit = _MockAssetsManagerCubit(); final initialAssetsState = AssetsManagerState( - loadables: [Future.value()], - loaded: const [], + assetsCount: 1, + loaded: 0, ); whenListen( assetsManagerCubit, @@ -146,8 +152,8 @@ void main() { final startGameBloc = _MockStartGameBloc(); final loadedAssetsState = AssetsManagerState( - loadables: [Future.value()], - loaded: [Future.value()], + assetsCount: 1, + loaded: 1, ); whenListen( assetsManagerCubit, @@ -179,7 +185,9 @@ void main() { final startGameBloc = _MockStartGameBloc(); setUp(() async { - await Future.wait(game.preLoadAssets()); + await Future.wait( + game.preLoadAssets().map((loadableBuilder) => loadableBuilder()), + ); whenListen( gameBloc, @@ -307,6 +315,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; -} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index d136487c..c057bc83 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -34,15 +34,15 @@ class _MockPlatformHelper extends Mock implements PlatformHelper {} PinballAudioPlayer _buildDefaultPinballAudioPlayer() { final audioPlayer = _MockPinballAudioPlayer(); - when(audioPlayer.load).thenAnswer((_) => [Future.value()]); + when(audioPlayer.load).thenAnswer((_) => [Future.value]); return audioPlayer; } AssetsManagerCubit _buildDefaultAssetsManagerCubit() { final cubit = _MockAssetsManagerCubit(); - final state = AssetsManagerState( - loadables: [Future.value()], - loaded: [Future.value()], + const state = AssetsManagerState( + assetsCount: 1, + loaded: 1, ); whenListen( cubit,