diff --git a/.github/workflows/geometry.yaml b/.github/workflows/geometry.yaml index 8bf55107..ccd41914 100644 --- a/.github/workflows/geometry.yaml +++ b/.github/workflows/geometry.yaml @@ -1,5 +1,9 @@ name: geometry +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/leaderboard_repository.yaml b/.github/workflows/leaderboard_repository.yaml index 6eddf283..327f70b3 100644 --- a/.github/workflows/leaderboard_repository.yaml +++ b/.github/workflows/leaderboard_repository.yaml @@ -1,5 +1,9 @@ name: leaderboard_repository +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f3e3fd99..8ed906a2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,5 +1,9 @@ name: pinball +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: [pull_request, push] jobs: diff --git a/.github/workflows/pinball_audio.yaml b/.github/workflows/pinball_audio.yaml index 7a43413a..6ba3adde 100644 --- a/.github/workflows/pinball_audio.yaml +++ b/.github/workflows/pinball_audio.yaml @@ -1,5 +1,9 @@ name: pinball_audio +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/pinball_components.yaml b/.github/workflows/pinball_components.yaml index d75553f5..19f13044 100644 --- a/.github/workflows/pinball_components.yaml +++ b/.github/workflows/pinball_components.yaml @@ -1,5 +1,9 @@ name: pinball_components +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/pinball_flame.yaml b/.github/workflows/pinball_flame.yaml new file mode 100644 index 00000000..297b792e --- /dev/null +++ b/.github/workflows/pinball_flame.yaml @@ -0,0 +1,24 @@ +name: pinball_flame + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/pinball_flame/**" + - ".github/workflows/pinball_flame.yaml" + + pull_request: + paths: + - "packages/pinball_flame/**" + - ".github/workflows/pinball_flame.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_flame + coverage_excludes: "lib/gen/*.dart" + test_optimization: false diff --git a/.github/workflows/pinball_theme.yaml b/.github/workflows/pinball_theme.yaml index 83206de5..15280761 100644 --- a/.github/workflows/pinball_theme.yaml +++ b/.github/workflows/pinball_theme.yaml @@ -1,5 +1,9 @@ name: pinball_theme +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/share_repository.yaml b/.github/workflows/share_repository.yaml new file mode 100644 index 00000000..2860902b --- /dev/null +++ b/.github/workflows/share_repository.yaml @@ -0,0 +1,18 @@ +name: share_repository + +on: + push: + paths: + - "packages/share_repository/**" + - ".github/workflows/share_repository.yaml" + + pull_request: + paths: + - "packages/share_repository/**" + - ".github/workflows/share_repository.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/share_repository diff --git a/.gitignore b/.gitignore index a7531405..2d9c4dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,3 @@ app.*.map.json test/.test_runner.dart web/__/firebase/init.js - -# Application exceptions -!/packages/pinball_components/assets/images/flutter_sign_post.png diff --git a/assets/images/bonus_animation/android_spaceship.png b/assets/images/bonus_animation/android_spaceship.png new file mode 100644 index 00000000..200e1c6c Binary files /dev/null and b/assets/images/bonus_animation/android_spaceship.png differ diff --git a/assets/images/bonus_animation/dash_nest.png b/assets/images/bonus_animation/dash_nest.png new file mode 100644 index 00000000..210f17cb Binary files /dev/null and b/assets/images/bonus_animation/dash_nest.png differ diff --git a/assets/images/bonus_animation/dino_chomp.png b/assets/images/bonus_animation/dino_chomp.png new file mode 100644 index 00000000..a8a9cfe3 Binary files /dev/null and b/assets/images/bonus_animation/dino_chomp.png differ diff --git a/assets/images/bonus_animation/google_word.png b/assets/images/bonus_animation/google_word.png new file mode 100644 index 00000000..7adab3b4 Binary files /dev/null and b/assets/images/bonus_animation/google_word.png differ diff --git a/assets/images/bonus_animation/sparky_turbo_charge.png b/assets/images/bonus_animation/sparky_turbo_charge.png new file mode 100644 index 00000000..8b3491e8 Binary files /dev/null and b/assets/images/bonus_animation/sparky_turbo_charge.png differ diff --git a/assets/images/components/background.png b/assets/images/components/background.png index 8b8fdf77..77a8542c 100644 Binary files a/assets/images/components/background.png and b/assets/images/components/background.png differ diff --git a/assets/images/components/plunger.png b/assets/images/components/plunger.png deleted file mode 100644 index f3cbdf0f..00000000 Binary files a/assets/images/components/plunger.png and /dev/null differ diff --git a/assets/images/score/mini_score_background.png b/assets/images/score/mini_score_background.png new file mode 100644 index 00000000..781f7349 Binary files /dev/null and b/assets/images/score/mini_score_background.png differ diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 521d575e..97cfec9b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -11,8 +11,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/landing/landing.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; class App extends StatelessWidget { @@ -34,20 +35,17 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], - child: MaterialApp( - title: 'I/O Pinball', - theme: ThemeData( - appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)), - colorScheme: ColorScheme.fromSwatch( - accentColor: const Color(0xFF13B9FF), - ), + child: BlocProvider( + create: (context) => CharacterThemeCubit(), + child: const MaterialApp( + title: 'I/O Pinball', + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: PinballGamePage(), ), - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - ], - supportedLocales: AppLocalizations.supportedLocales, - home: const LandingPage(), ), ); } diff --git a/lib/flame/flame.dart b/lib/flame/flame.dart deleted file mode 100644 index 9264c0f4..00000000 --- a/lib/flame/flame.dart +++ /dev/null @@ -1 +0,0 @@ -export 'component_controller.dart'; diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index ce1a78b4..4ba63092 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -11,14 +11,10 @@ class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { on(_onBallLost); on(_onScored); - on(_onBonusLetterActivated); - on(_onDashNestActivated); + on(_onBonusActivated); on(_onSparkyTurboChargeActivated); } - static const bonusWord = 'GOOGLE'; - static const bonusWordScore = 10000; - void _onBallLost(BallLost event, Emitter emit) { emit(state.copyWith(balls: state.balls - 1)); } @@ -29,54 +25,12 @@ class GameBloc extends Bloc { } } - void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { - final newBonusLetters = [ - ...state.activatedBonusLetters, - event.letterIndex, - ]; - - final achievedBonus = newBonusLetters.length == bonusWord.length; - if (achievedBonus) { - emit( - state.copyWith( - activatedBonusLetters: [], - bonusHistory: [ - ...state.bonusHistory, - GameBonus.word, - ], - ), - ); - add(const Scored(points: bonusWordScore)); - } else { - emit( - state.copyWith(activatedBonusLetters: newBonusLetters), - ); - } - } - - void _onDashNestActivated(DashNestActivated event, Emitter emit) { - final newNests = { - ...state.activatedDashNests, - event.nestId, - }; - - final achievedBonus = newNests.length == 3; - if (achievedBonus) { - emit( - state.copyWith( - balls: state.balls + 1, - activatedDashNests: {}, - bonusHistory: [ - ...state.bonusHistory, - GameBonus.dashNest, - ], - ), - ); - } else { - emit( - state.copyWith(activatedDashNests: newNests), - ); - } + void _onBonusActivated(BonusActivated event, Emitter emit) { + emit( + state.copyWith( + bonusHistory: [...state.bonusHistory, event.bonus], + ), + ); } Future _onSparkyTurboChargeActivated( diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index ee5315ad..bbb89028 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -33,26 +33,13 @@ class Scored extends GameEvent { List get props => [points]; } -class BonusLetterActivated extends GameEvent { - const BonusLetterActivated(this.letterIndex) - : assert( - letterIndex < GameBloc.bonusWord.length, - 'Index must be smaller than the length of the word', - ); +class BonusActivated extends GameEvent { + const BonusActivated(this.bonus); - final int letterIndex; + final GameBonus bonus; @override - List get props => [letterIndex]; -} - -class DashNestActivated extends GameEvent { - const DashNestActivated(this.nestId); - - final String nestId; - - @override - List get props => [nestId]; + List get props => [bonus]; } class SparkyTurboChargeActivated extends GameEvent { diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 0d9485e9..772bfea3 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -4,15 +4,20 @@ part of 'game_bloc.dart'; /// Defines bonuses that a player can gain during a PinballGame. enum GameBonus { - /// Bonus achieved when the user activate all of the bonus - /// letters on the board, forming the bonus word. - word, + /// Bonus achieved when the ball activates all Google letters. + googleWord, /// Bonus achieved when the user activates all dash nest bumpers. dashNest, /// Bonus achieved when a ball enters Sparky's computer. sparkyTurboCharge, + + /// Bonus achieved when the ball goes in the dino mouth. + dinoChomp, + + /// Bonus achieved when a ball enters the android spaceship. + androidSpaceship, } /// {@template game_state} @@ -23,17 +28,13 @@ class GameState extends Equatable { const GameState({ required this.score, required this.balls, - required this.activatedBonusLetters, required this.bonusHistory, - required this.activatedDashNests, }) : assert(score >= 0, "Score can't be negative"), assert(balls >= 0, "Number of balls can't be negative"); const GameState.initial() : score = 0, balls = 3, - activatedBonusLetters = const [], - activatedDashNests = const {}, bonusHistory = const []; /// The current score of the game. @@ -44,12 +45,6 @@ class GameState extends Equatable { /// When the number of balls is 0, the game is over. final int balls; - /// Active bonus letters. - final List activatedBonusLetters; - - /// Active dash nests. - final Set activatedDashNests; - /// Holds the history of all the [GameBonus]es earned by the player during a /// PinballGame. final List bonusHistory; @@ -57,15 +52,9 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; - /// Shortcut method to check if the given [i] - /// is activated. - bool isLetterActivated(int i) => activatedBonusLetters.contains(i); - GameState copyWith({ int? score, int? balls, - List? activatedBonusLetters, - Set? activatedDashNests, List? bonusHistory, }) { assert( @@ -76,9 +65,6 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, balls: balls ?? this.balls, - activatedBonusLetters: - activatedBonusLetters ?? this.activatedBonusLetters, - activatedDashNests: activatedDashNests ?? this.activatedDashNests, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -87,8 +73,6 @@ class GameState extends Equatable { List get props => [ score, balls, - activatedBonusLetters, - activatedDashNests, bonusHistory, ]; } diff --git a/lib/game/components/alien_zone.dart b/lib/game/components/alien_zone.dart new file mode 100644 index 00000000..dadc5ba4 --- /dev/null +++ b/lib/game/components/alien_zone.dart @@ -0,0 +1,29 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template alien_zone} +/// Area positioned below [Spaceship] where the [Ball] +/// can bounce off [AlienBumper]s. +/// {@endtemplate} +class AlienZone extends Blueprint { + /// {@macro alien_zone} + AlienZone() + : super( + components: [ + AlienBumper.a( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(-32.52, -9.1), + AlienBumper.b( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(-22.89, -17.35), + ], + ); +} diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 41581cc3..8ee4128f 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -23,7 +23,7 @@ class Board extends Component { final dino = ChromeDino() ..initialPosition = Vector2( BoardDimensions.bounds.center.dx + 25, - BoardDimensions.bounds.center.dy + 10, + BoardDimensions.bounds.center.dy - 10, ); await addAll([ @@ -42,7 +42,7 @@ class Board extends Component { // TODO(alestiago): Consider renaming once entire Board is defined. class _BottomGroup extends Component { /// {@macro bottom_group} - _BottomGroup(); + _BottomGroup() : super(priority: RenderPriority.bottomGroup); @override Future onLoad() async { @@ -77,17 +77,17 @@ class _BottomGroupSide extends Component { final flipper = ControlledFlipper( side: _side, - )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, -43.6); + )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); final baseboard = Baseboard(side: _side) ..initialPosition = Vector2( (25.58 * direction) + centerXAdjustment, - -28.69, + 28.69, ); final kicker = Kicker( side: _side, )..initialPosition = Vector2( (22.4 * direction) + centerXAdjustment, - -25, + 25, ); await addAll([flipper, baseboard, kicker]); diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart deleted file mode 100644 index 3457e84c..00000000 --- a/lib/game/components/bonus_word.dart +++ /dev/null @@ -1,208 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flame/effects.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template bonus_word} -/// Loads all [BonusLetter]s to compose a [BonusWord]. -/// {@endtemplate} -class BonusWord extends Component - with BlocComponent, HasGameRef { - /// {@macro bonus_word} - BonusWord({required Vector2 position}) : _position = position; - - final Vector2 _position; - - @override - bool listenWhen(GameState? previousState, GameState newState) { - return (previousState?.bonusHistory.length ?? 0) < - newState.bonusHistory.length && - newState.bonusHistory.last == GameBonus.word; - } - - @override - void onNewState(GameState state) { - if (state.bonusHistory.last == GameBonus.word) { - gameRef.audio.googleBonus(); - - final letters = children.whereType().toList(); - - for (var i = 0; i < letters.length; i++) { - final letter = letters[i]; - letter - ..isEnabled = false - ..add( - SequenceEffect( - [ - ColorEffect( - i.isOdd - ? BonusLetter._activeColor - : BonusLetter._disableColor, - const Offset(0, 1), - EffectController(duration: 0.25), - ), - ColorEffect( - i.isOdd - ? BonusLetter._disableColor - : BonusLetter._activeColor, - const Offset(0, 1), - EffectController(duration: 0.25), - ), - ], - repeatCount: 4, - )..onFinishCallback = () { - letter - ..isEnabled = true - ..add( - ColorEffect( - BonusLetter._disableColor, - const Offset(0, 1), - EffectController(duration: 0.25), - ), - ); - }, - ); - } - } - } - - @override - Future onLoad() async { - await super.onLoad(); - - final offsets = [ - Vector2(-12.92, -1.82), - Vector2(-8.33, 0.65), - Vector2(-2.88, 1.75), - ]; - offsets.addAll( - offsets.reversed - .map( - (offset) => Vector2(-offset.x, offset.y), - ) - .toList(), - ); - assert(offsets.length == GameBloc.bonusWord.length, 'Invalid positions'); - - final letters = []; - for (var i = 0; i < GameBloc.bonusWord.length; i++) { - letters.add( - BonusLetter( - letter: GameBloc.bonusWord[i], - index: i, - )..initialPosition = _position + offsets[i], - ); - } - - await addAll(letters); - } -} - -/// {@template bonus_letter} -/// [BodyType.static] sensor component, part of a word bonus, -/// which will activate its letter after contact with a [Ball]. -/// {@endtemplate} -class BonusLetter extends BodyComponent - with BlocComponent, InitialPosition { - /// {@macro bonus_letter} - BonusLetter({ - required String letter, - required int index, - }) : _letter = letter, - _index = index { - paint = Paint()..color = _disableColor; - } - - /// The size of the [BonusLetter]. - static final size = Vector2.all(3.7); - - static const _activeColor = Colors.green; - static const _disableColor = Colors.red; - - final String _letter; - final int _index; - - /// Indicates if a [BonusLetter] can be activated on [Ball] contact. - /// - /// It is disabled whilst animating and enabled again once the animation - /// completes. The animation is triggered when [GameBonus.word] is - /// awarded. - bool isEnabled = true; - - @override - Future onLoad() async { - await super.onLoad(); - - await add( - TextComponent( - position: Vector2(-1, -1), - text: _letter, - textRenderer: TextPaint( - style: const TextStyle(fontSize: 2, color: Colors.white), - ), - ), - ); - } - - @override - Body createBody() { - final shape = CircleShape()..radius = size.x / 2; - - final fixtureDef = FixtureDef(shape)..isSensor = true; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - @override - bool listenWhen(GameState? previousState, GameState newState) { - final wasActive = previousState?.isLetterActivated(_index) ?? false; - final isActive = newState.isLetterActivated(_index); - - return wasActive != isActive; - } - - @override - void onNewState(GameState state) { - final isActive = state.isLetterActivated(_index); - - add( - ColorEffect( - isActive ? _activeColor : _disableColor, - const Offset(0, 1), - EffectController(duration: 0.25), - ), - ); - } - - /// Activates this [BonusLetter], if it's not already activated. - void activate() { - final isActive = state?.isLetterActivated(_index) ?? false; - if (!isActive) { - gameRef.read().add(BonusLetterActivated(_index)); - } - } -} - -/// Triggers [BonusLetter.activate] method when a [BonusLetter] and a [Ball] -/// come in contact. -class BonusLetterBallContactCallback - extends ContactCallback { - @override - void begin(Ball ball, BonusLetter bonusLetter, Contact contact) { - if (bonusLetter.isEnabled) { - bonusLetter.activate(); - } - } -} diff --git a/lib/game/components/camera_controller.dart b/lib/game/components/camera_controller.dart index aa963e9a..a411942e 100644 --- a/lib/game/components/camera_controller.dart +++ b/lib/game/components/camera_controller.dart @@ -1,7 +1,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; -import 'package:pinball/flame/flame.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// Adds helpers methods to Flame's [Camera] extension CameraX on Camera { diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index f91d9baf..321be988 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,13 +1,13 @@ +export 'alien_zone.dart'; export 'board.dart'; -export 'bonus_word.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; -export 'controlled_sparky_computer.dart'; -export 'flutter_forest.dart'; +export 'controlled_plunger.dart'; +export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; -export 'plunger.dart'; -export 'score_effect_controller.dart'; -export 'score_points.dart'; +export 'google_word/google_word.dart'; +export 'launcher.dart'; +export 'scoring_behavior.dart'; export 'sparky_fire_zone.dart'; export 'wall.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index d48ebe66..f36cfef2 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,9 +1,9 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; -import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template controlled_ball} @@ -15,9 +15,11 @@ class ControlledBall extends Ball with Controls { /// When a launched [Ball] is lost, it will decrease the [GameState.balls] /// count, and a new [Ball] is spawned. ControlledBall.launch({ - required PinballTheme theme, - }) : super(baseColor: theme.characterTheme.ballColor) { + required CharacterTheme characterTheme, + }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); + priority = RenderPriority.ballOnLaunchRamp; + layer = Layer.launcher; } /// {@template bonus_ball} @@ -26,14 +28,16 @@ class ControlledBall extends Ball with Controls { /// When a bonus [Ball] is lost, the [GameState.balls] doesn't change. /// {@endtemplate} ControlledBall.bonus({ - required PinballTheme theme, - }) : super(baseColor: theme.characterTheme.ballColor) { + required CharacterTheme characterTheme, + }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); + priority = RenderPriority.ballOnBoard; } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { controller = DebugBallController(this); + priority = RenderPriority.ballOnBoard; } } @@ -45,10 +49,8 @@ class BallController extends ComponentController /// {@macro ball_controller} BallController(Ball ball) : super(ball); - /// Removes the [Ball] from a [PinballGame]. - /// - /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into - /// a [BottomWall]. + /// Event triggered when the ball is lost. + // TODO(alestiago): Refactor using behaviors. void lost() { component.shouldRemove = true; } @@ -58,13 +60,15 @@ class BallController extends ComponentController Future turboCharge() async { gameRef.read().add(const SparkyTurboChargeActivated()); - // TODO(allisonryan0002): adjust delay to match animation duration once - // given animations. component.stop(); - await Future.delayed(const Duration(seconds: 1)); - component - ..resume() - ..boost(Vector2(200, -500)); + // TODO(alestiago): Refactor this hard coded duration once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1564 + await Future.delayed( + const Duration(milliseconds: 2583), + ); + component.resume(); + await component.boost(Vector2(40, 110)); } @override diff --git a/lib/game/components/controlled_flipper.dart b/lib/game/components/controlled_flipper.dart index 9b73b6d3..3c82e719 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/lib/game/components/controlled_flipper.dart @@ -1,7 +1,9 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; -import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template controlled_flipper} /// A [Flipper] with a [FlipperController] attached. @@ -19,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls { /// A [ComponentController] that controls a [Flipper]s movement. /// {@endtemplate} class FlipperController extends ComponentController - with KeyboardHandler { + with KeyboardHandler, BlocComponent { /// {@macro flipper_controller} FlipperController(Flipper flipper) : _keys = flipper.side.flipperKeys, @@ -35,6 +37,7 @@ class FlipperController extends ComponentController RawKeyEvent event, Set keysPressed, ) { + if (state?.isGameOver ?? false) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart new file mode 100644 index 00000000..d6c622f7 --- /dev/null +++ b/lib/game/components/controlled_plunger.dart @@ -0,0 +1,52 @@ +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_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); + } +} + +/// {@template plunger_controller} +/// A [ComponentController] that controls a [Plunger]s movement. +/// {@endtemplate} +class PlungerController extends ComponentController + with KeyboardHandler, BlocComponent { + /// {@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 (state?.isGameOver ?? false) 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/controlled_sparky_computer.dart b/lib/game/components/controlled_sparky_computer.dart deleted file mode 100644 index 699ebae2..00000000 --- a/lib/game/components/controlled_sparky_computer.dart +++ /dev/null @@ -1,84 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/flame/flame.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template controlled_sparky_computer} -/// [SparkyComputer] with a [SparkyComputerController] attached. -/// {@endtemplate} -class ControlledSparkyComputer extends SparkyComputer - with Controls, HasGameRef { - /// {@macro controlled_sparky_computer} - ControlledSparkyComputer() { - controller = SparkyComputerController(this); - } - - @override - void build(Forge2DGame _) { - addContactCallback(SparkyTurboChargeSensorBallContactCallback()); - final sparkyTurboChargeSensor = SparkyTurboChargeSensor() - ..initialPosition = Vector2(-13, 49.8); - add(sparkyTurboChargeSensor); - super.build(_); - } -} - -/// {@template sparky_computer_controller} -/// Controller attached to a [SparkyComputer] that handles its game related -/// logic. -/// {@endtemplate} -// TODO(allisonryan0002): listen for turbo charge game bonus and animate Sparky. -class SparkyComputerController - extends ComponentController { - /// {@macro sparky_computer_controller} - SparkyComputerController(ControlledSparkyComputer controlledComputer) - : super(controlledComputer); -} - -/// {@template sparky_turbo_charge_sensor} -/// Small sensor body used to detect when a ball has entered the -/// [SparkyComputer] with the [SparkyTurboChargeSensorBallContactCallback]. -/// {@endtemplate} -@visibleForTesting -class SparkyTurboChargeSensor extends BodyComponent with InitialPosition { - /// {@macro sparky_turbo_charge_sensor} - SparkyTurboChargeSensor() { - renderBody = false; - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 0.1; - - final fixtureDef = FixtureDef(shape)..isSensor = true; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template sparky_turbo_charge_sensor_ball_contact_callback} -/// Turbo charges the [Ball] on contact with [SparkyTurboChargeSensor]. -/// {@endtemplate} -@visibleForTesting -class SparkyTurboChargeSensorBallContactCallback - extends ContactCallback { - /// {@macro sparky_turbo_charge_sensor_ball_contact_callback} - SparkyTurboChargeSensorBallContactCallback(); - - @override - void begin( - SparkyTurboChargeSensor sparkyTurboChargeSensor, - ControlledBall ball, - _, - ) { - ball.controller.turboCharge(); - } -} diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart deleted file mode 100644 index 966b0a6b..00000000 --- a/lib/game/components/flutter_forest.dart +++ /dev/null @@ -1,157 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -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/flame/flame.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template flutter_forest} -/// Area positioned at the top right of the [Board] where the [Ball] -/// can bounce off [DashNestBumper]s. -/// -/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] -/// is awarded, and the [BigDashNestBumper] releases a new [Ball]. -/// {@endtemplate} -// TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from -// [Component]. -class FlutterForest extends Component with Controls<_FlutterForestController> { - /// {@macro flutter_forest} - FlutterForest() { - controller = _FlutterForestController(this); - } - - @override - Future onLoad() async { - await super.onLoad(); - final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); - - final bigNest = _ControlledBigDashNestBumper( - id: 'big_nest_bumper', - )..initialPosition = Vector2(18.55, 59.35); - final smallLeftNest = _ControlledSmallDashNestBumper.a( - id: 'small_nest_bumper_a', - )..initialPosition = Vector2(8.95, 51.95); - final smallRightNest = _ControlledSmallDashNestBumper.b( - id: 'small_nest_bumper_b', - )..initialPosition = Vector2(23.3, 46.75); - - await addAll([ - signPost, - smallLeftNest, - smallRightNest, - bigNest, - ]); - } -} - -class _FlutterForestController extends ComponentController - with BlocComponent, HasGameRef { - _FlutterForestController(FlutterForest flutterForest) : super(flutterForest); - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback()); - } - - @override - bool listenWhen(GameState? previousState, GameState newState) { - return (previousState?.bonusHistory.length ?? 0) < - newState.bonusHistory.length && - newState.bonusHistory.last == GameBonus.dashNest; - } - - @override - void onNewState(GameState state) { - super.onNewState(state); - - gameRef.add( - ControlledBall.bonus(theme: gameRef.theme) - ..initialPosition = Vector2(17.2, 52.7), - ); - } -} - -class _ControlledBigDashNestBumper extends BigDashNestBumper - with Controls, ScorePoints { - _ControlledBigDashNestBumper({required String id}) : super() { - controller = DashNestBumperController(this, id: id); - } - - @override - int get points => 20; -} - -class _ControlledSmallDashNestBumper extends SmallDashNestBumper - with Controls, ScorePoints { - _ControlledSmallDashNestBumper.a({required String id}) : super.a() { - controller = DashNestBumperController(this, id: id); - } - - _ControlledSmallDashNestBumper.b({required String id}) : super.b() { - controller = DashNestBumperController(this, id: id); - } - - @override - int get points => 10; -} - -/// {@template dash_nest_bumper_controller} -/// Controls a [DashNestBumper]. -/// {@endtemplate} -@visibleForTesting -class DashNestBumperController extends ComponentController - with BlocComponent, HasGameRef { - /// {@macro dash_nest_bumper_controller} - DashNestBumperController( - DashNestBumper dashNestBumper, { - required this.id, - }) : super(dashNestBumper); - - /// Unique identifier for the controlled [DashNestBumper]. - /// - /// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. - final String id; - - @override - bool listenWhen(GameState? previousState, GameState newState) { - final wasActive = previousState?.activatedDashNests.contains(id) ?? false; - final isActive = newState.activatedDashNests.contains(id); - - return wasActive != isActive; - } - - @override - void onNewState(GameState state) { - super.onNewState(state); - - if (state.activatedDashNests.contains(id)) { - component.activate(); - } else { - component.deactivate(); - } - } - - /// Registers when a [DashNestBumper] is hit by a [Ball]. - /// - /// Triggered by [_ControlledDashNestBumperBallContactCallback]. - void hit() { - gameRef.read().add(DashNestActivated(id)); - } -} - -/// Listens when a [Ball] bounces bounces against a [DashNestBumper]. -class _ControlledDashNestBumperBallContactCallback - extends ContactCallback, Ball> { - @override - void begin( - Controls controlledDashNestBumper, - Ball _, - Contact __, - ) { - controlledDashNestBumper.controller.hit(); - } -} diff --git a/lib/game/components/flutter_forest/behaviors/behaviors.dart b/lib/game/components/flutter_forest/behaviors/behaviors.dart new file mode 100644 index 00000000..c0f39810 --- /dev/null +++ b/lib/game/components/flutter_forest/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'flutter_forest_bonus_behavior.dart'; diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart new file mode 100644 index 00000000..949fead1 --- /dev/null +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -0,0 +1,41 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] +/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. +class FlutterForestBonusBehavior extends Component + with ParentIsA, HasGameRef { + @override + void onMount() { + super.onMount(); + + final bumpers = parent.children.whereType(); + for (final bumper in bumpers) { + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + bumper.bloc.stream.listen((state) { + final achievedBonus = bumpers.every( + (bumper) => bumper.bloc.state == DashNestBumperState.active, + ); + + if (achievedBonus) { + gameRef + .read() + .add(const BonusActivated(GameBonus.dashNest)); + gameRef.add( + ControlledBall.bonus(characterTheme: gameRef.characterTheme) + ..initialPosition = Vector2(17.2, -52.7), + ); + parent.firstChild()?.playing = true; + + for (final bumper in bumpers) { + bumper.bloc.onReset(); + } + } + }); + } + } +} diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart new file mode 100644 index 00000000..02483159 --- /dev/null +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -0,0 +1,49 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template flutter_forest} +/// Area positioned at the top right of the [Board] where the [Ball] can bounce +/// off [DashNestBumper]s. +/// {@endtemplate} +class FlutterForest extends Component { + /// {@macro flutter_forest} + FlutterForest() + : super( + priority: RenderPriority.flutterForest, + children: [ + Signpost( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(8.35, -58.3), + DashNestBumper.main( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(18.55, -59.35), + DashNestBumper.a( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(8.95, -51.95), + DashNestBumper.b( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(23.3, -46.75), + DashAnimatronic()..position = Vector2(20, -66), + FlutterForestBonusBehavior(), + ], + ); + + /// Creates a [FlutterForest] without any children. + /// + /// This can be used for testing [FlutterForest]'s behaviors in isolation. + @visibleForTesting + FlutterForest.test(); +} diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index b0f6f514..48dd5518 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -1,8 +1,8 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; -import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template game_flow_controller} /// A [Component] that controls the game over and game restart logic @@ -28,7 +28,12 @@ class GameFlowController extends ComponentController /// Puts the game on a game over state void gameOver() { - component.firstChild()?.gameOverMode(); + // TODO(erickzanardo): implement score submission and "navigate" to the + // next page + component.firstChild()?.gameOverMode( + score: state?.score ?? 0, + characterIconPath: component.characterTheme.leaderboardIcon.keyName, + ); component.firstChild()?.focusOnBackboard(); } diff --git a/lib/game/components/google_word/behaviors/behaviors.dart b/lib/game/components/google_word/behaviors/behaviors.dart new file mode 100644 index 00000000..4ebf817c --- /dev/null +++ b/lib/game/components/google_word/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'google_word_bonus_behavior.dart'; diff --git a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart new file mode 100644 index 00000000..92664531 --- /dev/null +++ b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated. +class GoogleWordBonusBehavior extends Component + with HasGameRef, ParentIsA { + @override + void onMount() { + super.onMount(); + + final googleLetters = parent.children.whereType(); + for (final letter in googleLetters) { + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + letter.bloc.stream.listen((_) { + final achievedBonus = googleLetters + .every((letter) => letter.bloc.state == GoogleLetterState.active); + + if (achievedBonus) { + gameRef.audio.googleBonus(); + gameRef + .read() + .add(const BonusActivated(GameBonus.googleWord)); + for (final letter in googleLetters) { + letter.bloc.onReset(); + } + } + }); + } + } +} diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart new file mode 100644 index 00000000..9a9faa9a --- /dev/null +++ b/lib/game/components/google_word/google_word.dart @@ -0,0 +1,30 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template google_word} +/// Loads all [GoogleLetter]s to compose a [GoogleWord]. +/// {@endtemplate} +class GoogleWord extends Component { + /// {@macro google_word} + GoogleWord({ + required Vector2 position, + }) : super( + children: [ + GoogleLetter(0)..initialPosition = position + Vector2(-12.92, 1.82), + GoogleLetter(1)..initialPosition = position + Vector2(-8.33, -0.65), + GoogleLetter(2)..initialPosition = position + Vector2(-2.88, -1.75), + GoogleLetter(3)..initialPosition = position + Vector2(2.88, -1.75), + GoogleLetter(4)..initialPosition = position + Vector2(8.33, -0.65), + GoogleLetter(5)..initialPosition = position + Vector2(12.92, 1.82), + GoogleWordBonusBehavior(), + ], + ); + + /// Creates a [GoogleWord] without any children. + /// + /// This can be used for testing [GoogleWord]'s behaviors in isolation. + @visibleForTesting + GoogleWord.test(); +} diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart new file mode 100644 index 00000000..7aef09d2 --- /dev/null +++ b/lib/game/components/launcher.dart @@ -0,0 +1,21 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/components/components.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template launcher} +/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and +/// [LaunchRamp]. +/// {@endtemplate} +class Launcher extends Blueprint { + /// {@macro launcher} + Launcher() + : super( + components: [ + ControlledPlunger(compressionDistance: 14) + ..initialPosition = Vector2(40.7, 38), + RocketSpriteComponent()..position = Vector2(43, 62), + ], + blueprints: [LaunchRamp()], + ); +} diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart deleted file mode 100644 index b8c079b5..00000000 --- a/lib/game/components/plunger.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/services.dart'; -import 'package:pinball/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -/// {@template plunger} -/// [Plunger] serves as a spring, that shoots the ball on the right side of the -/// playfield. -/// -/// [Plunger] ignores gravity so the player controls its downward [_pull]. -/// {@endtemplate} -class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { - /// {@macro plunger} - Plunger({ - required this.compressionDistance, - // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities - // are fixed. - }) : super(priority: 0); - - /// Distance the plunger can lower. - final double compressionDistance; - - @override - Body createBody() { - final shape = PolygonShape() - ..setAsBox( - 1.35, - 0.5, - Vector2.zero(), - BoardDimensions.perspectiveAngle, - ); - - final fixtureDef = FixtureDef(shape)..density = 80; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this - ..type = BodyType.dynamic - ..gravityScale = 0; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - /// Set a constant downward velocity on the [Plunger]. - void _pull() { - body.linearVelocity = Vector2(0, -7); - } - - /// 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 velocity = (initialPosition.y - body.position.y) * 5; - body.linearVelocity = Vector2(0, velocity); - } - - @override - bool onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - final keys = [ - LogicalKeyboardKey.space, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.keyS, - ]; - if (!keys.contains(event.logicalKey)) return true; - - if (event is RawKeyDownEvent) { - _pull(); - } else if (event is RawKeyUpEvent) { - _release(); - } - - return false; - } - - /// 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(); - - renderBody = false; - - await _loadSprite(); - } - - Future _loadSprite() async { - final sprite = await gameRef.loadSprite( - Assets.images.components.plunger.path, - ); - - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(5.5, 40), - anchor: Anchor.center, - position: Vector2(2, 19), - angle: -0.033, - ), - ); - } -} - -/// {@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, - ); - } - - @override - Body createBody() { - final bodyDef = BodyDef() - ..position = initialPosition - ..type = BodyType.static; - return world.createBody(bodyDef); - } -} - -/// {@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(18.6, BoardDimensions.bounds.height), - ); - enableLimit = true; - lowerTranslation = double.negativeInfinity; - enableMotor = true; - motorSpeed = 1000; - maxMotorForce = motorSpeed; - collideConnected = true; - } -} diff --git a/lib/game/components/score_effect_controller.dart b/lib/game/components/score_effect_controller.dart deleted file mode 100644 index 7fafd4b5..00000000 --- a/lib/game/components/score_effect_controller.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:math'; - -import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:pinball/flame/flame.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template score_effect_controller} -/// A [ComponentController] responsible for adding [ScoreText]s -/// on the game screen when the user earns points. -/// {@endtemplate} -class ScoreEffectController extends ComponentController - with BlocComponent { - /// {@macro score_effect_controller} - ScoreEffectController(PinballGame component) : super(component); - - int _lastScore = 0; - final _random = Random(); - - double _noise() { - return _random.nextDouble() * 5 * (_random.nextBool() ? -1 : 1); - } - - @override - bool listenWhen(GameState? previousState, GameState newState) { - return previousState?.score != newState.score; - } - - @override - void onNewState(GameState state) { - final newScore = state.score - _lastScore; - _lastScore = state.score; - - component.add( - ScoreText( - text: newScore.toString(), - position: Vector2( - _noise(), - _noise() + (-BoardDimensions.bounds.topCenter.dy + 10), - ), - ), - ); - } -} diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart deleted file mode 100644 index ce13c718..00000000 --- a/lib/game/components/score_points.dart +++ /dev/null @@ -1,43 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template score_points} -/// Specifies the amount of points received on [Ball] collision. -/// {@endtemplate} -mixin ScorePoints on BodyComponent { - /// {@macro score_points} - int get points; - - @override - Future onLoad() async { - await super.onLoad(); - body.userData = this; - } -} - -/// {@template ball_score_points_callbacks} -/// Adds points to the score when a [Ball] collides with a [BodyComponent] that -/// implements [ScorePoints]. -/// {@endtemplate} -class BallScorePointsCallback extends ContactCallback { - /// {@macro ball_score_points_callbacks} - BallScorePointsCallback(PinballGame game) : _gameRef = game; - - final PinballGame _gameRef; - - @override - void begin( - Ball _, - ScorePoints scorePoints, - Contact __, - ) { - _gameRef.read().add( - Scored(points: scorePoints.points), - ); - - _gameRef.audio.score(); - } -} diff --git a/lib/game/components/scoring_behavior.dart b/lib/game/components/scoring_behavior.dart new file mode 100644 index 00000000..3ef82bb5 --- /dev/null +++ b/lib/game/components/scoring_behavior.dart @@ -0,0 +1,34 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template scoring_behavior} +/// Adds points to the score when the ball contacts the [parent]. +/// {@endtemplate} +class ScoringBehavior extends ContactBehavior with HasGameRef { + /// {@macro scoring_behavior} + ScoringBehavior({ + required int points, + }) : _points = points; + + final int _points; + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + gameRef.read().add(Scored(points: _points)); + gameRef.audio.score(); + gameRef.add( + ScoreText( + text: _points.toString(), + position: other.body.position, + ), + ); + } +} diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index ee8da614..a23a4fbc 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -1,103 +1,71 @@ // ignore_for_file: avoid_renaming_method_parameters -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_fire_zone} /// Area positioned at the top left of the [Board] where the [Ball] /// can bounce off [SparkyBumper]s. /// -/// When a [Ball] hits [SparkyBumper]s, they toggle between activated and -/// deactivated states. +/// When a [Ball] hits [SparkyBumper]s, the bumper animates. /// {@endtemplate} -class SparkyFireZone extends Component with HasGameRef { +class SparkyFireZone extends Blueprint { /// {@macro sparky_fire_zone} - SparkyFireZone(); - - @override - Future onLoad() async { - await super.onLoad(); - - gameRef.addContactCallback(_ControlledSparkyBumperBallContactCallback()); - - final lowerLeftBumper = ControlledSparkyBumper.a() - ..initialPosition = Vector2(-23.15, 41.65); - final upperLeftBumper = ControlledSparkyBumper.b() - ..initialPosition = Vector2(-21.25, 58.15); - final rightBumper = ControlledSparkyBumper.c() - ..initialPosition = Vector2(-3.56, 53.051); - - await addAll([ - lowerLeftBumper, - upperLeftBumper, - rightBumper, - ]); - } + SparkyFireZone() + : super( + components: [ + SparkyBumper.a( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(-22.9, -41.65), + SparkyBumper.b( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(-21.25, -57.9), + SparkyBumper.c( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(-3.3, -52.55), + SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), + SparkyAnimatronic()..position = Vector2(-13.8, -58.2), + ], + blueprints: [ + SparkyComputer(), + ], + ); } -/// {@template controlled_sparky_bumper} -/// [SparkyBumper] with [_SparkyBumperController] attached. +/// {@template sparky_computer_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SparkyComputer]. /// {@endtemplate} -@visibleForTesting -class ControlledSparkyBumper extends SparkyBumper - with Controls<_SparkyBumperController>, ScorePoints { - ///{@macro controlled_sparky_bumper} - ControlledSparkyBumper.a() : super.a() { - controller = _SparkyBumperController(this); - } - - ///{@macro controlled_sparky_bumper} - ControlledSparkyBumper.b() : super.b() { - controller = _SparkyBumperController(this); - } - - ///{@macro controlled_sparky_bumper} - ControlledSparkyBumper.c() : super.c() { - controller = _SparkyBumperController(this); - } +class SparkyComputerSensor extends BodyComponent + with InitialPosition, ContactCallbacks { + /// {@macro sparky_computer_sensor} + SparkyComputerSensor() : super(renderBody: false); @override - int get points => 20; -} - -/// {@template sparky_bumper_controller} -/// Controls a [SparkyBumper]. -/// {@endtemplate} -class _SparkyBumperController extends ComponentController - with HasGameRef { - /// {@macro sparky_bumper_controller} - _SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper) - : super(controlledSparkyBumper); - - /// Flag for activated state of the [SparkyBumper]. - /// - /// Used to toggle [SparkyBumper]s' state between activated and deactivated. - bool isActivated = false; - - /// Registers when a [SparkyBumper] is hit by a [Ball]. - void hit() { - if (isActivated) { - component.deactivate(); - } else { - component.activate(); - } - isActivated = !isActivated; + Body createBody() { + final shape = CircleShape()..radius = 0.1; + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + return world.createBody(bodyDef)..createFixture(fixtureDef); } -} -/// Listens when a [Ball] bounces bounces against a [SparkyBumper]. -class _ControlledSparkyBumperBallContactCallback - extends ContactCallback, Ball> { @override - void begin( - Controls<_SparkyBumperController> controlledSparkyBumper, - Ball _, - Contact __, - ) { - controlledSparkyBumper.controller.hit(); + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + + other.controller.turboCharge(); + gameRef.firstChild()?.playing = true; } } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index ba8af5e7..2f180d61 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -39,44 +39,22 @@ class Wall extends BodyComponent { } } -/// Create top, left, and right [Wall]s for the game board. -List createBoundaries(Forge2DGame game) { - final topLeft = BoardDimensions.bounds.topLeft.toVector2() + Vector2(18.6, 0); - final bottomRight = BoardDimensions.bounds.bottomRight.toVector2(); - - final topRight = - BoardDimensions.bounds.topRight.toVector2() - Vector2(18.6, 0); - final bottomLeft = BoardDimensions.bounds.bottomLeft.toVector2(); - - return [ - Wall(start: topLeft, end: topRight), - Wall(start: topRight, end: bottomRight), - Wall(start: topLeft, end: bottomLeft), - ]; -} - /// {@template bottom_wall} /// [Wall] located at the bottom of the board. /// -/// Collisions with [BottomWall] are listened by -/// [BottomWallBallContactCallback]. /// {@endtemplate} -class BottomWall extends Wall { +class BottomWall extends Wall with ContactCallbacks { /// {@macro bottom_wall} BottomWall() : super( start: BoardDimensions.bounds.bottomLeft.toVector2(), end: BoardDimensions.bounds.bottomRight.toVector2(), ); -} -/// {@template bottom_wall_ball_contact_callback} -/// Listens when a [ControlledBall] falls into a [BottomWall]. -/// {@endtemplate} -class BottomWallBallContactCallback - extends ContactCallback { @override - void begin(ControlledBall ball, BottomWall wall, Contact contact) { - ball.controller.lost(); + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + other.controller.lost(); } } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 22605904..9dc88562 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -1,61 +1,110 @@ import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' as components; +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() { + const dashTheme = DashTheme(); + const sparkyTheme = SparkyTheme(); + const androidTheme = AndroidTheme(); + const dinoTheme = DinoTheme(); + return [ - images.load(components.Assets.images.ball.keyName), - images.load(components.Assets.images.flutterSignPost.keyName), + images.load(components.Assets.images.ball.ball.keyName), + images.load(components.Assets.images.ball.flameEffect.keyName), + images.load(components.Assets.images.signpost.inactive.keyName), + images.load(components.Assets.images.signpost.active1.keyName), + 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.keyName), images.load(components.Assets.images.kicker.right.keyName), - images.load(components.Assets.images.slingshot.leftUpper.keyName), - images.load(components.Assets.images.slingshot.leftLower.keyName), - images.load(components.Assets.images.slingshot.rightUpper.keyName), - images.load(components.Assets.images.slingshot.rightLower.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.dinoLandTop.keyName), images.load(components.Assets.images.dino.dinoLandBottom.keyName), - images.load(components.Assets.images.dashBumper.a.active.keyName), - images.load(components.Assets.images.dashBumper.a.inactive.keyName), - images.load(components.Assets.images.dashBumper.b.active.keyName), - images.load(components.Assets.images.dashBumper.b.inactive.keyName), - images.load(components.Assets.images.dashBumper.main.active.keyName), - images.load(components.Assets.images.dashBumper.main.inactive.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.spaceship.saucer.keyName), images.load(components.Assets.images.spaceship.bridge.keyName), - images.load(components.Assets.images.spaceship.ramp.main.keyName), + images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), + images.load( + components.Assets.images.spaceship.ramp.railingForeground.keyName, + ), images.load( components.Assets.images.spaceship.ramp.railingBackground.keyName, ), + images.load(components.Assets.images.spaceship.ramp.main.keyName), + images + .load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName), images.load( - components.Assets.images.spaceship.ramp.railingForeground.keyName, + components.Assets.images.spaceship.ramp.arrow.active1.keyName, + ), + images.load( + components.Assets.images.spaceship.ramp.arrow.active2.keyName, + ), + images.load( + components.Assets.images.spaceship.ramp.arrow.active3.keyName, + ), + images.load( + components.Assets.images.spaceship.ramp.arrow.active4.keyName, + ), + images.load( + components.Assets.images.spaceship.ramp.arrow.active5.keyName, ), images.load(components.Assets.images.spaceship.rail.main.keyName), images.load(components.Assets.images.spaceship.rail.foreground.keyName), + images.load(components.Assets.images.alienBumper.a.active.keyName), + images.load(components.Assets.images.alienBumper.a.inactive.keyName), + images.load(components.Assets.images.alienBumper.b.active.keyName), + images.load(components.Assets.images.alienBumper.b.inactive.keyName), images.load(components.Assets.images.chromeDino.mouth.keyName), images.load(components.Assets.images.chromeDino.head.keyName), - images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.computer.top.keyName), - images.load(components.Assets.images.sparky.bumper.a.active.keyName), + images.load(components.Assets.images.sparky.computer.base.keyName), + images.load(components.Assets.images.sparky.animatronic.keyName), images.load(components.Assets.images.sparky.bumper.a.inactive.keyName), + images.load(components.Assets.images.sparky.bumper.a.active.keyName), images.load(components.Assets.images.sparky.bumper.b.active.keyName), images.load(components.Assets.images.sparky.bumper.b.inactive.keyName), images.load(components.Assets.images.sparky.bumper.c.active.keyName), images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backboard.backboardGameOver.keyName), + images.load(components.Assets.images.googleWord.letter1.keyName), + images.load(components.Assets.images.googleWord.letter2.keyName), + images.load(components.Assets.images.googleWord.letter3.keyName), + images.load(components.Assets.images.googleWord.letter4.keyName), + images.load(components.Assets.images.googleWord.letter5.keyName), + images.load(components.Assets.images.googleWord.letter6.keyName), + images.load(components.Assets.images.backboard.display.keyName), + images.load(dashTheme.leaderboardIcon.keyName), + images.load(sparkyTheme.leaderboardIcon.keyName), + images.load(androidTheme.leaderboardIcon.keyName), + images.load(dinoTheme.leaderboardIcon.keyName), images.load(Assets.images.components.background.path), ]; } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 8d080b22..1d483fe2 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -2,14 +2,16 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/flame/flame.dart'; +import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' hide Assets; class PinballGame extends Forge2DGame @@ -18,7 +20,7 @@ class PinballGame extends Forge2DGame HasKeyboardHandlerComponents, Controls<_GameBallsController> { PinballGame({ - required this.theme, + required this.characterTheme, required this.audio, }) { images.prefix = ''; @@ -28,7 +30,10 @@ class PinballGame extends Forge2DGame /// Identifier of the play button overlay static const playButtonOverlay = 'play_button'; - final PinballTheme theme; + @override + Color backgroundColor() => Colors.transparent; + + final CharacterTheme characterTheme; final PinballAudio audio; @@ -36,65 +41,43 @@ class PinballGame extends Forge2DGame @override Future onLoad() async { - _addContactCallbacks(); - - unawaited(add(ScoreEffectController(this))); unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); - unawaited(add(Backboard(position: Vector2(0, -88)))); + unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); - await _addGameBoundaries(); + // TODO(allisonryan0002): banish Wall and Board classes in later PR. + await add(BottomWall()); unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(LaunchRamp())); - unawaited(addFromBlueprint(ControlledSparkyComputer())); - - final plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(38, -19); - await add(plunger); + final launcher = Launcher(); + unawaited(addFromBlueprint(launcher)); unawaited(add(Board())); - unawaited(add(SparkyFireZone())); + await addFromBlueprint(AlienZone()); + + await addFromBlueprint(SparkyFireZone()); unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); - unawaited(_addBonusWord()); unawaited(addFromBlueprint(SpaceshipRamp())); unawaited( addFromBlueprint( Spaceship( - position: Vector2(-26.5, 28.5), + position: Vector2(-26.5, -28.5), ), ), ); - unawaited( - addFromBlueprint( - SpaceshipRail(), - ), - ); - - controller.attachTo(plunger); - await super.onLoad(); - } - - void _addContactCallbacks() { - addContactCallback(BallScorePointsCallback(this)); - addContactCallback(BottomWallBallContactCallback()); - addContactCallback(BonusLetterBallContactCallback()); - } - - Future _addGameBoundaries() async { - await add(BottomWall()); - createBoundaries(this).forEach(add); - } - - Future _addBonusWord() async { + unawaited(addFromBlueprint(SpaceshipRail())); await add( - BonusWord( + GoogleWord( position: Vector2( - BoardDimensions.bounds.center.dx - 3.07, - BoardDimensions.bounds.center.dy - 2.4, + BoardDimensions.bounds.center.dx - 4.1, + BoardDimensions.bounds.center.dy + 1.8, ), ), ); + + controller.attachTo(launcher.components.whereType().first); + await super.onLoad(); } } @@ -126,10 +109,10 @@ class _GameBallsController extends ComponentController void _spawnBall() { final ball = ControlledBall.launch( - theme: gameRef.theme, + characterTheme: gameRef.characterTheme, )..initialPosition = Vector2( _plunger.body.position.x, - _plunger.body.position.y + Ball.size.y, + _plunger.body.position.y - Ball.size.y, ); component.add(ball); } @@ -142,12 +125,12 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { DebugPinballGame({ - required PinballTheme theme, + required CharacterTheme characterTheme, required PinballAudio audio, }) : super( - theme: theme, + characterTheme: characterTheme, audio: audio, ) { controller = _DebugGameBallsController(this); @@ -157,6 +140,7 @@ class DebugPinballGame extends PinballGame with TapDetector { Future onLoad() async { await super.onLoad(); await _loadBackground(); + await add(_DebugInformation()); } // TODO(alestiago): Move to PinballGame once we have the real background @@ -171,7 +155,7 @@ class DebugPinballGame extends PinballGame with TapDetector { anchor: Anchor.center, ) ..position = Vector2(0, -7.8) - ..priority = -2; + ..priority = RenderPriority.background; await add(spriteComponent); } @@ -199,3 +183,35 @@ class _DebugGameBallsController extends _GameBallsController { return noBallsLeft && canBallRespawn; } } + +class _DebugInformation extends Component with HasGameRef { + _DebugInformation() : super(priority: RenderPriority.debugInfo); + + @override + PositionType get positionType => PositionType.widget; + + final _debugTextPaint = TextPaint( + style: const TextStyle( + color: Colors.green, + fontSize: 10, + ), + ); + + final _debugBackgroundPaint = Paint()..color = Colors.white; + + @override + void render(Canvas canvas) { + final debugText = [ + 'FPS: ${gameRef.fps().toStringAsFixed(1)}', + 'BALLS: ${gameRef.descendants().whereType().length}', + ].join(' | '); + + final height = _debugTextPaint.measureTextHeight(debugText); + final position = Vector2(0, gameRef.camera.canvasSize.y - height); + canvas.drawRect( + position & Vector2(gameRef.camera.canvasSize.x, height), + _debugBackgroundPaint, + ); + _debugTextPaint.render(canvas, debugText, position); + } +} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index f6b7ee81..be11a15c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,45 +5,25 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_theme/pinball_theme.dart'; class PinballGamePage extends StatelessWidget { const PinballGamePage({ Key? key, - required this.theme, - required this.game, + this.isDebugMode = kDebugMode, }) : super(key: key); - final PinballTheme theme; - final PinballGame game; + final bool isDebugMode; static Route route({ - required PinballTheme theme, bool isDebugMode = kDebugMode, }) { return MaterialPageRoute( builder: (context) { - final audio = context.read(); - - final game = isDebugMode - ? DebugPinballGame(theme: theme, audio: audio) - : PinballGame(theme: theme, audio: audio); - - final pinballAudio = context.read(); - final loadables = [ - ...game.preLoadAssets(), - pinballAudio.load(), - ]; - - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => GameBloc()), - BlocProvider( - create: (_) => AssetsManagerCubit(loadables)..load(), - ), - ], - child: PinballGamePage(theme: theme, game: game), + return PinballGamePage( + isDebugMode: isDebugMode, ); }, ); @@ -51,7 +31,31 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - return PinballGameView(game: game); + final characterTheme = + context.read().state.characterTheme; + final audio = context.read(); + final pinballAudio = context.read(); + + final game = isDebugMode + ? DebugPinballGame(characterTheme: characterTheme, audio: audio) + : PinballGame(characterTheme: characterTheme, audio: audio); + + final loadables = [ + ...game.preLoadAssets(), + pinballAudio.load(), + ...BonusAnimation.loadAssets(), + ]; + + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => StartGameBloc(game: game)), + BlocProvider(create: (_) => GameBloc()), + BlocProvider( + create: (_) => AssetsManagerCubit(loadables)..load(), + ), + ], + child: PinballGameView(game: game), + ); } } @@ -65,17 +69,54 @@ class PinballGameView extends StatelessWidget { @override Widget build(BuildContext context) { - final loadingProgress = context.watch().state.progress; + final isLoading = context.select( + (AssetsManagerCubit bloc) => bloc.state.progress != 1, + ); - if (loadingProgress != 1) { - return Scaffold( - body: Center( - child: Text( - loadingProgress.toString(), - ), + return Scaffold( + backgroundColor: Colors.blue, + body: isLoading + ? const _PinballGameLoadingView() + : PinballGameLoadedView(game: game), + ); + } +} + +class _PinballGameLoadingView extends StatelessWidget { + const _PinballGameLoadingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final loadingProgress = context.select( + (AssetsManagerCubit bloc) => bloc.state.progress, + ); + + return Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: LinearProgressIndicator( + color: Colors.white, + value: loadingProgress, ), - ); - } + ), + ); + } +} + +@visibleForTesting +class PinballGameLoadedView extends StatelessWidget { + const PinballGameLoadedView({ + Key? key, + required this.game, + }) : super(key: key); + + final PinballGame game; + + @override + Widget build(BuildContext context) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + final screenWidth = MediaQuery.of(context).size.width; + final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); return Stack( children: [ @@ -95,10 +136,12 @@ class PinballGameView extends StatelessWidget { }, ), ), - const Positioned( - top: 8, - left: 8, - child: GameHud(), + // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc + // status + Positioned( + top: 16, + left: leftMargin, + child: const GameHud(), ), ], ); diff --git a/lib/game/view/widgets/bonus_animation.dart b/lib/game/view/widgets/bonus_animation.dart new file mode 100644 index 00000000..da67e1aa --- /dev/null +++ b/lib/game/view/widgets/bonus_animation.dart @@ -0,0 +1,156 @@ +import 'package:flame/flame.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart' hide Image; +import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template bonus_animation} +/// [Widget] that displays bonus animations. +/// {@endtemplate} +class BonusAnimation extends StatefulWidget { + /// {@macro bonus_animation} + const BonusAnimation._( + String imagePath, { + VoidCallback? onCompleted, + Key? key, + }) : _imagePath = imagePath, + _onCompleted = onCompleted, + super(key: key); + + /// [Widget] that displays the dash nest animation. + BonusAnimation.dashNest({ + Key? key, + VoidCallback? onCompleted, + }) : this._( + Assets.images.bonusAnimation.dashNest.keyName, + onCompleted: onCompleted, + key: key, + ); + + /// [Widget] that displays the sparky turbo charge animation. + BonusAnimation.sparkyTurboCharge({ + Key? key, + VoidCallback? onCompleted, + }) : this._( + Assets.images.bonusAnimation.sparkyTurboCharge.keyName, + onCompleted: onCompleted, + key: key, + ); + + /// [Widget] that displays the dino chomp animation. + BonusAnimation.dinoChomp({ + Key? key, + VoidCallback? onCompleted, + }) : this._( + Assets.images.bonusAnimation.dinoChomp.keyName, + onCompleted: onCompleted, + key: key, + ); + + /// [Widget] that displays the android spaceship animation. + BonusAnimation.androidSpaceship({ + Key? key, + VoidCallback? onCompleted, + }) : this._( + Assets.images.bonusAnimation.androidSpaceship.keyName, + onCompleted: onCompleted, + key: key, + ); + + /// [Widget] that displays the google word animation. + BonusAnimation.googleWord({ + Key? key, + VoidCallback? onCompleted, + }) : this._( + Assets.images.bonusAnimation.googleWord.keyName, + onCompleted: onCompleted, + key: key, + ); + + final String _imagePath; + + final VoidCallback? _onCompleted; + + /// Returns a list of assets to be loaded for animations. + 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), + ]; + } + + @override + State createState() => _BonusAnimationState(); +} + +class _BonusAnimationState extends State + with TickerProviderStateMixin { + late SpriteAnimationController controller; + late SpriteAnimation animation; + bool shouldRunBuildCallback = true; + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + // When the animation is overwritten by another animation, we need to stop + // the callback in the build method as it will break the new animation. + // Otherwise we need to set up a new callback when a new animation starts to + // show the score view at the end of the animation. + @override + void didUpdateWidget(BonusAnimation oldWidget) { + shouldRunBuildCallback = oldWidget._imagePath == widget._imagePath; + + Future.delayed( + Duration(seconds: animation.totalDuration().ceil()), + () { + widget._onCompleted?.call(); + }, + ); + + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final spriteSheet = SpriteSheet.fromColumnsAndRows( + image: Flame.images.fromCache(widget._imagePath), + columns: 8, + rows: 9, + ); + animation = spriteSheet.createAnimation( + row: 0, + stepTime: 1 / 24, + to: spriteSheet.rows * spriteSheet.columns, + loop: false, + ); + + Future.delayed( + Duration(seconds: animation.totalDuration().ceil()), + () { + if (shouldRunBuildCallback) { + widget._onCompleted?.call(); + } + }, + ); + + controller = SpriteAnimationController( + animation: animation, + vsync: this, + )..forward(); + + return SizedBox( + width: double.infinity, + height: double.infinity, + child: SpriteAnimationWidget( + controller: controller, + ), + ); + } +} diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 00eedd2b..3623e21f 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -1,46 +1,122 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/gen.dart'; +import 'package:pinball/theme/app_colors.dart'; /// {@template game_hud} -/// Overlay of a [PinballGame] that displays the current [GameState.score] and -/// [GameState.balls]. +/// Overlay on the [PinballGame]. +/// +/// Displays the current [GameState.score], [GameState.balls] and animates when +/// the player gets a [GameBonus]. /// {@endtemplate} -class GameHud extends StatelessWidget { +class GameHud extends StatefulWidget { /// {@macro game_hud} const GameHud({Key? key}) : super(key: key); + @override + State createState() => _GameHudState(); +} + +class _GameHudState extends State { + bool showAnimation = false; + + /// Ratio from sprite frame (width 500, height 144) w / h = ratio + static const _ratio = 3.47; + static const _width = 265.0; + @override Widget build(BuildContext context) { - final state = context.watch().state; - - return Container( - color: Colors.redAccent, - width: 200, - height: 100, - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${state.score}', - style: Theme.of(context).textTheme.headline3, + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return _ScoreViewDecoration( + child: SizedBox( + height: _width / _ratio, + width: _width, + child: BlocListener( + listenWhen: (previous, current) => + previous.bonusHistory.length != current.bonusHistory.length, + listener: (_, __) => setState(() => showAnimation = true), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: showAnimation && !isGameOver + ? _AnimationView( + onComplete: () { + if (mounted) { + setState(() => showAnimation = false); + } + }, + ) + : const ScoreView(), ), - Wrap( - direction: Axis.vertical, - children: [ - for (var i = 0; i < state.balls; i++) - const Padding( - padding: EdgeInsets.only(top: 6, right: 6), - child: CircleAvatar( - radius: 8, - backgroundColor: Colors.black, - ), - ), - ], + ), + ), + ); + } +} + +class _ScoreViewDecoration extends StatelessWidget { + const _ScoreViewDecoration({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(12)); + const boardWidth = 5.0; + + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + border: Border.all( + color: AppColors.white, + width: boardWidth, + ), + image: DecorationImage( + fit: BoxFit.cover, + image: AssetImage( + Assets.images.score.miniScoreBackground.path, ), - ], + ), ), + child: Padding( + padding: const EdgeInsets.all(boardWidth - 1), + child: ClipRRect( + borderRadius: radius, + child: child, + ), + ), + ); + } +} + +class _AnimationView extends StatelessWidget { + const _AnimationView({ + Key? key, + required this.onComplete, + }) : super(key: key); + + final VoidCallback onComplete; + + @override + Widget build(BuildContext context) { + final lastBonus = context.select( + (GameBloc bloc) => bloc.state.bonusHistory.last, ); + switch (lastBonus) { + case GameBonus.dashNest: + return BonusAnimation.dashNest(onCompleted: onComplete); + case GameBonus.sparkyTurboCharge: + return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete); + case GameBonus.dinoChomp: + return BonusAnimation.dinoChomp(onCompleted: onComplete); + case GameBonus.googleWord: + return BonusAnimation.googleWord(onCompleted: onComplete); + case GameBonus.androidSpaceship: + return BonusAnimation.androidSpaceship(onCompleted: onComplete); + } } } diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 6f039124..f90ebb98 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/select_character/select_character.dart'; /// {@template play_button_overlay} /// [Widget] that renders the button responsible to starting the game @@ -18,9 +19,27 @@ class PlayButtonOverlay extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + return Center( child: ElevatedButton( - onPressed: _game.gameFlowController.start, + onPressed: () { + _game.gameFlowController.start(); + showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + final height = MediaQuery.of(context).size.height * 0.5; + + return Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: const CharacterSelectionDialog(), + ), + ); + }, + ); + }, child: Text(l10n.play), ), ); diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart new file mode 100644 index 00000000..98776764 --- /dev/null +++ b/lib/game/view/widgets/round_count_display.dart @@ -0,0 +1,70 @@ +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/theme/theme.dart'; + +/// {@template round_count_display} +/// Colored square indicating if a round is available. +/// {@endtemplate} +class RoundCountDisplay extends StatelessWidget { + /// {@macro round_count_display} + const RoundCountDisplay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + // TODO(arturplaczek): refactor when GameState handle balls and rounds and + // select state.rounds property instead of state.ball + final balls = context.select((GameBloc bloc) => bloc.state.balls); + + return Row( + children: [ + Text( + l10n.rounds, + style: AppTextStyle.subtitle1.copyWith( + color: AppColors.orange, + ), + ), + const SizedBox(width: 8), + Row( + children: [ + RoundIndicator(isActive: balls >= 1), + RoundIndicator(isActive: balls >= 2), + RoundIndicator(isActive: balls >= 3), + ], + ), + ], + ); + } +} + +/// {@template round_indicator} +/// [Widget] that displays the round indicator. +/// {@endtemplate} +@visibleForTesting +class RoundIndicator extends StatelessWidget { + /// {@macro round_indicator} + const RoundIndicator({ + Key? key, + required this.isActive, + }) : super(key: key); + + /// A value that describes whether the indicator is active. + final bool isActive; + + @override + Widget build(BuildContext context) { + final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128); + const size = 8.0; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + color: color, + height: size, + width: size, + ), + ); + } +} diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart new file mode 100644 index 00000000..288ea05c --- /dev/null +++ b/lib/game/view/widgets/score_view.dart @@ -0,0 +1,86 @@ +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/theme/theme.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template score_view} +/// [Widget] that displays the score. +/// {@endtemplate} +class ScoreView extends StatelessWidget { + /// {@macro score_view} + const ScoreView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: isGameOver ? const _GameOver() : const _ScoreDisplay(), + ), + ); + } +} + +class _GameOver extends StatelessWidget { + const _GameOver({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Text( + l10n.gameOver, + style: AppTextStyle.headline1.copyWith( + color: AppColors.white, + ), + ); + } +} + +class _ScoreDisplay extends StatelessWidget { + const _ScoreDisplay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + l10n.score.toLowerCase(), + style: AppTextStyle.subtitle1.copyWith( + color: AppColors.orange, + ), + ), + const _ScoreText(), + const RoundCountDisplay(), + ], + ); + } +} + +class _ScoreText extends StatelessWidget { + const _ScoreText({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final score = context.select((GameBloc bloc) => bloc.state.score); + + return Text( + score.formatScore(), + style: AppTextStyle.headline1.copyWith( + color: AppColors.white, + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart index 7e9db5c3..5d1fccf8 100644 --- a/lib/game/view/widgets/widgets.dart +++ b/lib/game/view/widgets/widgets.dart @@ -1,2 +1,5 @@ +export 'bonus_animation.dart'; export 'game_hud.dart'; export 'play_button_overlay.dart'; +export 'round_count_display.dart'; +export 'score_view.dart'; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index b3b964f3..f5b935a5 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,22 +3,58 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); + $AssetsImagesBonusAnimationGen get bonusAnimation => + const $AssetsImagesBonusAnimationGen(); $AssetsImagesComponentsGen get components => const $AssetsImagesComponentsGen(); + $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); +} + +class $AssetsImagesBonusAnimationGen { + const $AssetsImagesBonusAnimationGen(); + + /// File path: assets/images/bonus_animation/android_spaceship.png + AssetGenImage get androidSpaceship => const AssetGenImage( + 'assets/images/bonus_animation/android_spaceship.png'); + + /// File path: assets/images/bonus_animation/dash_nest.png + AssetGenImage get dashNest => + const AssetGenImage('assets/images/bonus_animation/dash_nest.png'); + + /// File path: assets/images/bonus_animation/dino_chomp.png + AssetGenImage get dinoChomp => + const AssetGenImage('assets/images/bonus_animation/dino_chomp.png'); + + /// File path: assets/images/bonus_animation/google_word.png + AssetGenImage get googleWord => + const AssetGenImage('assets/images/bonus_animation/google_word.png'); + + /// File path: assets/images/bonus_animation/sparky_turbo_charge.png + AssetGenImage get sparkyTurboCharge => const AssetGenImage( + 'assets/images/bonus_animation/sparky_turbo_charge.png'); } class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); + /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); - AssetGenImage get plunger => - const AssetGenImage('assets/images/components/plunger.png'); +} + +class $AssetsImagesScoreGen { + const $AssetsImagesScoreGen(); + + /// File path: assets/images/score/mini_score_background.png + AssetGenImage get miniScoreBackground => + const AssetGenImage('assets/images/score/mini_score_background.png'); } class Assets { diff --git a/lib/gen/gen.dart b/lib/gen/gen.dart new file mode 100644 index 00000000..e7ad4c54 --- /dev/null +++ b/lib/gen/gen.dart @@ -0,0 +1 @@ +export 'assets.gen.dart'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index aa56e015..9655d8be 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,71 +1,83 @@ { - "@@locale": "en", - "play": "Play", - "@play": { - "description": "Text displayed on the landing page play button" - }, - "howToPlay": "How to Play", - "@howToPlay": { - "description": "Text displayed on the landing page how to play button" - }, - "launchControls": "Launch Controls", - "@launchControls": { - "description": "Text displayed on the how to play dialog with the launch controls" - }, - "flipperControls": "Flipper Controls", - "@flipperControls": { - "description": "Text displayed on the how to play dialog with the flipper controls" - }, - "start": "Start", - "@start": { - "description": "Text displayed on the character selection page start button" - }, - "characterSelectionTitle": "Choose your character!", - "@characterSelectionTitle": { - "description": "Title text displayed on the character selection page" - }, - "gameOver": "Game Over", - "@gameOver": { - "description": "Text displayed on the ending dialog when game finishes" - }, - "leaderboard": "Leaderboard", - "@leaderboard": { - "description": "Text displayed on the ending dialog leaderboard button" - }, - "rank": "Rank", - "@rank": { - "description": "Text displayed on the leaderboard page header rank column" - }, - "character": "Character", - "@character": { - "description": "Text displayed on the leaderboard page header character column" - }, - "username": "Username", - "@username": { - "description": "Text displayed on the leaderboard page header userName column" - }, - "score": "Score", - "@score": { - "description": "Text displayed on the leaderboard page header score column" - }, - "retry": "Retry", - "@retry": { - "description": "Text displayed on the retry button leaders board page" - }, - "addUser": "Add User", - "@addUser": { - "description": "Text displayed on the add user button at ending dialog" - }, - "error": "Error", - "@error": { - "description": "Text displayed on the ending dialog when there is any error on sending user" - }, - "yourScore": "Your score is", - "@yourScore": { - "description": "Text displayed on the ending dialog when game finishes to show the final score" - }, - "enterInitials": "Enter your initials", - "@enterInitials": { - "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" - } -} \ No newline at end of file + "@@locale": "en", + "play": "Play", + "@play": { + "description": "Text displayed on the landing page play button" + }, + "howToPlay": "How to Play", + "@howToPlay": { + "description": "Text displayed on the landing page how to play button" + }, + "launchControls": "Launch Controls", + "@launchControls": { + "description": "Text displayed on the how to play dialog with the launch controls" + }, + "flipperControls": "Flipper Controls", + "@flipperControls": { + "description": "Text displayed on the how to play dialog with the flipper controls" + }, + "start": "Start", + "@start": { + "description": "Text displayed on the character selection page start button" + }, + "select": "Select", + "@select": { + "description": "Text displayed on the character selection page select button" + }, + "characterSelectionTitle": "Choose your character!", + "@characterSelectionTitle": { + "description": "Title text displayed on the character selection page" + }, + "characterSelectionSubtitle": "There’s no wrong answer", + "@characterSelectionSubtitle": { + "description": "Text displayed on the selecting character dialog at game beginning" + }, + "gameOver": "Game Over", + "@gameOver": { + "description": "Text displayed on the ending dialog when game finishes" + }, + "leaderboard": "Leaderboard", + "@leaderboard": { + "description": "Text displayed on the ending dialog leaderboard button" + }, + "rank": "Rank", + "@rank": { + "description": "Text displayed on the leaderboard page header rank column" + }, + "character": "Character", + "@character": { + "description": "Text displayed on the leaderboard page header character column" + }, + "username": "Username", + "@username": { + "description": "Text displayed on the leaderboard page header userName column" + }, + "score": "Score", + "@score": { + "description": "Text displayed on the leaderboard page header score column" + }, + "retry": "Retry", + "@retry": { + "description": "Text displayed on the retry button leaders board page" + }, + "addUser": "Add User", + "@addUser": { + "description": "Text displayed on the add user button at ending dialog" + }, + "error": "Error", + "@error": { + "description": "Text displayed on the ending dialog when there is any error on sending user" + }, + "yourScore": "Your score is", + "@yourScore": { + "description": "Text displayed on the ending dialog when game finishes to show the final score" + }, + "enterInitials": "Enter your initials", + "@enterInitials": { + "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" + }, + "rounds": "Ball Ct:", + "@rounds": { + "description": "Text displayed on the scoreboard widget to indicate rounds left" + } +} diff --git a/lib/landing/landing.dart b/lib/landing/landing.dart deleted file mode 100644 index b7da30c3..00000000 --- a/lib/landing/landing.dart +++ /dev/null @@ -1 +0,0 @@ -export 'view/landing_page.dart'; diff --git a/lib/leaderboard/models/leader_board_entry.dart b/lib/leaderboard/models/leader_board_entry.dart index 194f7cb6..a86975dd 100644 --- a/lib/leaderboard/models/leader_board_entry.dart +++ b/lib/leaderboard/models/leader_board_entry.dart @@ -36,7 +36,7 @@ extension LeaderboardEntryDataX on LeaderboardEntryData { rank: position.toString(), playerInitials: playerInitials, score: score, - character: character.toTheme.characterAsset, + character: character.toTheme.leaderboardIcon, ); } } diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart index 54b364e9..b9866111 100644 --- a/lib/leaderboard/view/leaderboard_page.dart +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_theme/pinball_theme.dart'; class LeaderboardPage extends StatelessWidget { @@ -69,7 +69,7 @@ class LeaderboardView extends StatelessWidget { const SizedBox(height: 20), TextButton( onPressed: () => Navigator.of(context).push( - CharacterSelectionPage.route(), + CharacterSelectionDialog.route(), ), child: Text(l10n.retry), ), diff --git a/lib/theme/cubit/theme_cubit.dart b/lib/select_character/cubit/character_theme_cubit.dart similarity index 59% rename from lib/theme/cubit/theme_cubit.dart rename to lib/select_character/cubit/character_theme_cubit.dart index 94eba4a6..84792a71 100644 --- a/lib/theme/cubit/theme_cubit.dart +++ b/lib/select_character/cubit/character_theme_cubit.dart @@ -5,12 +5,12 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:pinball_theme/pinball_theme.dart'; -part 'theme_state.dart'; +part 'character_theme_state.dart'; -class ThemeCubit extends Cubit { - ThemeCubit() : super(const ThemeState.initial()); +class CharacterThemeCubit extends Cubit { + CharacterThemeCubit() : super(const CharacterThemeState.initial()); void characterSelected(CharacterTheme characterTheme) { - emit(ThemeState(PinballTheme(characterTheme: characterTheme))); + emit(CharacterThemeState(characterTheme)); } } diff --git a/lib/select_character/cubit/character_theme_state.dart b/lib/select_character/cubit/character_theme_state.dart new file mode 100644 index 00000000..ffe5667c --- /dev/null +++ b/lib/select_character/cubit/character_theme_state.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs +// TODO(allisonryan0002): Document this section when the API is stable. + +part of 'character_theme_cubit.dart'; + +class CharacterThemeState extends Equatable { + const CharacterThemeState(this.characterTheme); + + const CharacterThemeState.initial() : characterTheme = const DashTheme(); + + final CharacterTheme characterTheme; + + @override + List get props => [characterTheme]; +} diff --git a/lib/select_character/select_character.dart b/lib/select_character/select_character.dart new file mode 100644 index 00000000..40699840 --- /dev/null +++ b/lib/select_character/select_character.dart @@ -0,0 +1,2 @@ +export 'cubit/character_theme_cubit.dart'; +export 'view/view.dart'; diff --git a/lib/theme/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart similarity index 77% rename from lib/theme/view/character_selection_page.dart rename to lib/select_character/view/character_selection_page.dart index 9569760a..0e83db8d 100644 --- a/lib/theme/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -2,24 +2,24 @@ 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/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; -class CharacterSelectionPage extends StatelessWidget { - const CharacterSelectionPage({Key? key}) : super(key: key); +class CharacterSelectionDialog extends StatelessWidget { + const CharacterSelectionDialog({Key? key}) : super(key: key); static Route route() { return MaterialPageRoute( - builder: (_) => const CharacterSelectionPage(), + builder: (_) => const CharacterSelectionDialog(), ); } @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ThemeCubit(), + create: (_) => CharacterThemeCubit(), child: const CharacterSelectionView(), ); } @@ -46,11 +46,13 @@ class CharacterSelectionView extends StatelessWidget { const _CharacterSelectionGridView(), const SizedBox(height: 20), TextButton( - onPressed: () => Navigator.of(context).push( - PinballGamePage.route( - theme: context.read().state.theme, - ), - ), + onPressed: () { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: (_) => const HowToPlayDialog(), + ); + }, child: Text(l10n.start), ), ], @@ -107,12 +109,14 @@ class CharacterImageButton extends StatelessWidget { @override Widget build(BuildContext context) { - final currentCharacterTheme = context.select( - (cubit) => cubit.state.theme.characterTheme, + final currentCharacterTheme = + context.select( + (cubit) => cubit.state.characterTheme, ); return GestureDetector( - onTap: () => context.read().characterSelected(characterTheme), + onTap: () => + context.read().characterSelected(characterTheme), child: DecoratedBox( decoration: BoxDecoration( color: (currentCharacterTheme == characterTheme) @@ -122,7 +126,7 @@ class CharacterImageButton extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(8), - child: characterTheme.characterAsset.image(), + child: characterTheme.icon.image(), ), ), ); diff --git a/lib/theme/view/view.dart b/lib/select_character/view/view.dart similarity index 100% rename from lib/theme/view/view.dart rename to lib/select_character/view/view.dart diff --git a/lib/start_game/bloc/start_game_bloc.dart b/lib/start_game/bloc/start_game_bloc.dart new file mode 100644 index 00000000..ba44d88c --- /dev/null +++ b/lib/start_game/bloc/start_game_bloc.dart @@ -0,0 +1,58 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball/game/game.dart'; + +part 'start_game_event.dart'; +part 'start_game_state.dart'; + +/// {@template start_game_bloc} +/// Bloc that manages the app flow before the game starts. +/// {@endtemplate} +class StartGameBloc extends Bloc { + /// {@macro start_game_bloc} + StartGameBloc({ + required PinballGame game, + }) : _game = game, + super(const StartGameState.initial()) { + on(_onPlayTapped); + on(_onCharacterSelected); + on(_onHowToPlayFinished); + } + + final PinballGame _game; + + void _onPlayTapped( + PlayTapped event, + Emitter emit, + ) { + _game.gameFlowController.start(); + + emit( + state.copyWith( + status: StartGameStatus.selectCharacter, + ), + ); + } + + void _onCharacterSelected( + CharacterSelected event, + Emitter emit, + ) { + emit( + state.copyWith( + status: StartGameStatus.howToPlay, + ), + ); + } + + void _onHowToPlayFinished( + HowToPlayFinished event, + Emitter emit, + ) { + emit( + state.copyWith( + status: StartGameStatus.play, + ), + ); + } +} diff --git a/lib/start_game/bloc/start_game_event.dart b/lib/start_game/bloc/start_game_event.dart new file mode 100644 index 00000000..ce164e97 --- /dev/null +++ b/lib/start_game/bloc/start_game_event.dart @@ -0,0 +1,42 @@ +part of 'start_game_bloc.dart'; + +/// {@template start_game_event} +/// Event added during the start game flow. +/// {@endtemplate} +abstract class StartGameEvent extends Equatable { + /// {@macro start_game_event} + const StartGameEvent(); +} + +/// {@template play_tapped} +/// Play tapped event. +/// {@endtemplate} +class PlayTapped extends StartGameEvent { + /// {@macro play_tapped} + const PlayTapped(); + + @override + List get props => []; +} + +/// {@template character_selected} +/// Character selected event. +/// {@endtemplate} +class CharacterSelected extends StartGameEvent { + /// {@macro character_selected} + const CharacterSelected(); + + @override + List get props => []; +} + +/// {@template how_to_play_finished} +/// How to play finished event. +/// {@endtemplate} +class HowToPlayFinished extends StartGameEvent { + /// {@macro how_to_play_finished} + const HowToPlayFinished(); + + @override + List get props => []; +} diff --git a/lib/start_game/bloc/start_game_state.dart b/lib/start_game/bloc/start_game_state.dart new file mode 100644 index 00000000..ad7c7cbe --- /dev/null +++ b/lib/start_game/bloc/start_game_state.dart @@ -0,0 +1,44 @@ +part of 'start_game_bloc.dart'; + +/// Defines status of start game flow. +enum StartGameStatus { + /// Initial status. + initial, + + /// Selection characters status. + selectCharacter, + + /// How to play status. + howToPlay, + + /// Play status. + play, +} + +/// {@template start_game_state} +/// Represents the state of flow before the game starts. +/// {@endtemplate} +class StartGameState extends Equatable { + /// {@macro start_game_state} + const StartGameState({ + required this.status, + }); + + /// Initial [StartGameState]. + const StartGameState.initial() : this(status: StartGameStatus.initial); + + /// Status of [StartGameState]. + final StartGameStatus status; + + /// Creates a copy of [StartGameState]. + StartGameState copyWith({ + StartGameStatus? status, + }) { + return StartGameState( + status: status ?? this.status, + ); + } + + @override + List get props => [status]; +} diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart new file mode 100644 index 00000000..1556b533 --- /dev/null +++ b/lib/start_game/start_game.dart @@ -0,0 +1,2 @@ +export 'bloc/start_game_bloc.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/landing/view/landing_page.dart b/lib/start_game/widgets/how_to_play_dialog.dart similarity index 81% rename from lib/landing/view/landing_page.dart rename to lib/start_game/widgets/how_to_play_dialog.dart index 5b0474b6..aed7a3e3 100644 --- a/lib/landing/view/landing_page.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -2,42 +2,9 @@ import 'package:flutter/material.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; -class LandingPage extends StatelessWidget { - const LandingPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () => Navigator.of(context).push( - CharacterSelectionPage.route(), - ), - child: Text(l10n.play), - ), - TextButton( - onPressed: () => showDialog( - context: context, - builder: (_) => const _HowToPlayDialog(), - ), - child: Text(l10n.howToPlay), - ), - ], - ), - ), - ); - } -} - -class _HowToPlayDialog extends StatelessWidget { - const _HowToPlayDialog({Key? key}) : super(key: key); +class HowToPlayDialog extends StatelessWidget { + const HowToPlayDialog({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/start_game/widgets/widgets.dart b/lib/start_game/widgets/widgets.dart new file mode 100644 index 00000000..bad2c6b5 --- /dev/null +++ b/lib/start_game/widgets/widgets.dart @@ -0,0 +1 @@ +export 'how_to_play_dialog.dart'; diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart new file mode 100644 index 00000000..2d3899a6 --- /dev/null +++ b/lib/theme/app_colors.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +abstract class AppColors { + static const Color white = Color(0xFFFFFFFF); + + static const Color darkBlue = Color(0xFF0C32A4); + + static const Color orange = Color(0xFFFFEE02); + + static const Color blue = Color(0xFF4B94F6); + + static const Color transparent = Color(0x00000000); +} diff --git a/lib/theme/app_text_style.dart b/lib/theme/app_text_style.dart new file mode 100644 index 00000000..8104ca11 --- /dev/null +++ b/lib/theme/app_text_style.dart @@ -0,0 +1,35 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/widgets.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_components/pinball_components.dart'; + +const _fontPackage = 'pinball_components'; +const _primaryFontFamily = FontFamily.pixeloidSans; + +abstract class AppTextStyle { + static const headline1 = TextStyle( + fontSize: 28, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const headline2 = TextStyle( + fontSize: 24, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const headline3 = TextStyle( + color: AppColors.white, + fontSize: 20, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle1 = TextStyle( + fontSize: 10, + fontFamily: _primaryFontFamily, + package: _fontPackage, + ); +} diff --git a/lib/theme/cubit/theme_state.dart b/lib/theme/cubit/theme_state.dart deleted file mode 100644 index 078f5c84..00000000 --- a/lib/theme/cubit/theme_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -// ignore_for_file: public_member_api_docs -// TODO(allisonryan0002): Document this section when the API is stable. - -part of 'theme_cubit.dart'; - -class ThemeState extends Equatable { - const ThemeState(this.theme); - - const ThemeState.initial() - : theme = const PinballTheme(characterTheme: DashTheme()); - - final PinballTheme theme; - - @override - List get props => [theme]; -} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index f6318400..c9e2f9e1 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,2 +1,2 @@ -export 'cubit/theme_cubit.dart'; -export 'view/view.dart'; +export 'app_colors.dart'; +export 'app_text_style.dart'; diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 30f6810f..9d8b2434 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -72,6 +72,20 @@ class FetchPlayerRankingException extends LeaderboardException { ); } +/// {@template fetch_prohibited_initials_exception} +/// Exception thrown when failure occurs while fetching prohibited initials. +/// {@endtemplate} +class FetchProhibitedInitialsException extends LeaderboardException { + /// {@macro fetch_prohibited_initials_exception} + const FetchProhibitedInitialsException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + /// {@template leaderboard_repository} /// Repository to access leaderboard data in Firebase Cloud Firestore. /// {@endtemplate} @@ -152,4 +166,25 @@ class LeaderboardRepository { throw FetchPlayerRankingException(error, stackTrace); } } + + /// Determines if the given [initials] are allowed. + Future areInitialsAllowed({required String initials}) async { + // Initials can only be three uppercase A-Z letters + final initialsRegex = RegExp(r'^[A-Z]{3}$'); + if (!initialsRegex.hasMatch(initials)) { + return false; + } + + try { + final document = await _firebaseFirestore + .collection('prohibitedInitials') + .doc('list') + .get(); + final prohibitedInitials = + document.get('prohibitedInitials') as List; + return !prohibitedInitials.contains(initials); + } on Exception catch (error, stackTrace) { + throw FetchProhibitedInitialsException(error, stackTrace); + } + } } diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index 1341d3f4..9d31983f 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -21,6 +21,9 @@ class MockQueryDocumentSnapshot extends Mock class MockDocumentReference extends Mock implements DocumentReference> {} +class MockDocumentSnapshot extends Mock + implements DocumentSnapshot> {} + void main() { group('LeaderboardRepository', () { late FirebaseFirestore firestore; @@ -223,5 +226,94 @@ void main() { ); }); }); + + group('areInitialsAllowed', () { + late LeaderboardRepository leaderboardRepository; + late CollectionReference> collectionReference; + late DocumentReference> documentReference; + late DocumentSnapshot> documentSnapshot; + + setUp(() async { + collectionReference = MockCollectionReference(); + documentReference = MockDocumentReference(); + documentSnapshot = MockDocumentSnapshot(); + leaderboardRepository = LeaderboardRepository(firestore); + + when(() => firestore.collection('prohibitedInitials')) + .thenReturn(collectionReference); + when(() => collectionReference.doc('list')) + .thenReturn(documentReference); + when(() => documentReference.get()) + .thenAnswer((_) async => documentSnapshot); + when(() => documentSnapshot.get('prohibitedInitials')) + .thenReturn(['BAD']); + }); + + test('returns true if initials are three letters and allowed', () async { + final isUsernameAllowedResponse = + await leaderboardRepository.areInitialsAllowed( + initials: 'ABC', + ); + expect( + isUsernameAllowedResponse, + isTrue, + ); + }); + + test( + 'returns false if initials are shorter than 3 characters', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'AB'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials are longer than 3 characters', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'ABCD'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials contain a lowercase letter', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'AbC'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials contain a special character', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'A@C'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test('returns false if initials are forbidden', () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'BAD'); + expect(areInitialsAllowedResponse, isFalse); + }); + + test( + 'throws FetchProhibitedInitialsException when Exception occurs ' + 'when trying to retrieve information from firestore', + () async { + when(() => firestore.collection('prohibitedInitials')) + .thenThrow(Exception('oops')); + expect( + () => leaderboardRepository.areInitialsAllowed(initials: 'ABC'), + throwsA(isA()), + ); + }, + ); + }); }); } diff --git a/packages/pinball_components/assets/images/alien_bumper/a/active.png b/packages/pinball_components/assets/images/alien_bumper/a/active.png new file mode 100644 index 00000000..4bb38a74 Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/alien_bumper/a/inactive.png b/packages/pinball_components/assets/images/alien_bumper/a/inactive.png new file mode 100644 index 00000000..d693f1c1 Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/alien_bumper/b/active.png b/packages/pinball_components/assets/images/alien_bumper/b/active.png new file mode 100644 index 00000000..fbfd36e1 Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/alien_bumper/b/inactive.png b/packages/pinball_components/assets/images/alien_bumper/b/inactive.png new file mode 100644 index 00000000..fd23693d Binary files /dev/null and b/packages/pinball_components/assets/images/alien_bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/backboard/backboard_scores.png b/packages/pinball_components/assets/images/backboard/backboard_scores.png index bb591b14..dab850d2 100644 Binary files a/packages/pinball_components/assets/images/backboard/backboard_scores.png and b/packages/pinball_components/assets/images/backboard/backboard_scores.png differ diff --git a/packages/pinball_components/assets/images/backboard/display.png b/packages/pinball_components/assets/images/backboard/display.png new file mode 100644 index 00000000..97dbb50b Binary files /dev/null and b/packages/pinball_components/assets/images/backboard/display.png differ diff --git a/packages/pinball_components/assets/images/ball.png b/packages/pinball_components/assets/images/ball/ball.png similarity index 100% rename from packages/pinball_components/assets/images/ball.png rename to packages/pinball_components/assets/images/ball/ball.png diff --git a/packages/pinball_components/assets/images/ball/flame_effect.png b/packages/pinball_components/assets/images/ball/flame_effect.png new file mode 100644 index 00000000..03a6fca6 Binary files /dev/null and b/packages/pinball_components/assets/images/ball/flame_effect.png differ diff --git a/packages/pinball_components/assets/images/baseboard/left.png b/packages/pinball_components/assets/images/baseboard/left.png index 17253554..d13b4e31 100644 Binary files a/packages/pinball_components/assets/images/baseboard/left.png and b/packages/pinball_components/assets/images/baseboard/left.png differ diff --git a/packages/pinball_components/assets/images/baseboard/right.png b/packages/pinball_components/assets/images/baseboard/right.png index 081a1782..8ad93045 100644 Binary files a/packages/pinball_components/assets/images/baseboard/right.png and b/packages/pinball_components/assets/images/baseboard/right.png differ diff --git a/packages/pinball_components/assets/images/boundary/bottom.png b/packages/pinball_components/assets/images/boundary/bottom.png index 2effb7ac..806f7051 100644 Binary files a/packages/pinball_components/assets/images/boundary/bottom.png and b/packages/pinball_components/assets/images/boundary/bottom.png differ diff --git a/packages/pinball_components/assets/images/boundary/outer-bottom.png b/packages/pinball_components/assets/images/boundary/outer-bottom.png new file mode 100644 index 00000000..508bcee8 Binary files /dev/null and b/packages/pinball_components/assets/images/boundary/outer-bottom.png differ diff --git a/packages/pinball_components/assets/images/boundary/outer.png b/packages/pinball_components/assets/images/boundary/outer.png index c0b81e96..3c06cb6c 100644 Binary files a/packages/pinball_components/assets/images/boundary/outer.png and b/packages/pinball_components/assets/images/boundary/outer.png differ diff --git a/packages/pinball_components/assets/images/dash/animatronic.png b/packages/pinball_components/assets/images/dash/animatronic.png new file mode 100644 index 00000000..13f7b794 Binary files /dev/null and b/packages/pinball_components/assets/images/dash/animatronic.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/a/active.png b/packages/pinball_components/assets/images/dash/bumper/a/active.png new file mode 100644 index 00000000..57330eb4 Binary files /dev/null and b/packages/pinball_components/assets/images/dash/bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/a/inactive.png b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png new file mode 100644 index 00000000..aead95ec Binary files /dev/null and b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/b/active.png b/packages/pinball_components/assets/images/dash/bumper/b/active.png new file mode 100644 index 00000000..fe871847 Binary files /dev/null and b/packages/pinball_components/assets/images/dash/bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/b/inactive.png b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png new file mode 100644 index 00000000..3d53b743 Binary files /dev/null and b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/main/active.png b/packages/pinball_components/assets/images/dash/bumper/main/active.png new file mode 100644 index 00000000..9508b56c Binary files /dev/null and b/packages/pinball_components/assets/images/dash/bumper/main/active.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/main/inactive.png b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png new file mode 100644 index 00000000..b1d0ae7d Binary files /dev/null and b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/a/active.png b/packages/pinball_components/assets/images/dash_bumper/a/active.png deleted file mode 100644 index feeee11f..00000000 Binary files a/packages/pinball_components/assets/images/dash_bumper/a/active.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dash_bumper/a/inactive.png b/packages/pinball_components/assets/images/dash_bumper/a/inactive.png deleted file mode 100644 index 58ab8c56..00000000 Binary files a/packages/pinball_components/assets/images/dash_bumper/a/inactive.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dash_bumper/b/active.png b/packages/pinball_components/assets/images/dash_bumper/b/active.png deleted file mode 100644 index 4bc2897f..00000000 Binary files a/packages/pinball_components/assets/images/dash_bumper/b/active.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dash_bumper/b/inactive.png b/packages/pinball_components/assets/images/dash_bumper/b/inactive.png deleted file mode 100644 index eddc7693..00000000 Binary files a/packages/pinball_components/assets/images/dash_bumper/b/inactive.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dash_bumper/main/active.png b/packages/pinball_components/assets/images/dash_bumper/main/active.png deleted file mode 100644 index bef56684..00000000 Binary files a/packages/pinball_components/assets/images/dash_bumper/main/active.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dash_bumper/main/inactive.png b/packages/pinball_components/assets/images/dash_bumper/main/inactive.png deleted file mode 100644 index e6f15b38..00000000 Binary files a/packages/pinball_components/assets/images/dash_bumper/main/inactive.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-bottom.png b/packages/pinball_components/assets/images/dino/dino-land-bottom.png index 1839dda3..6a20f1a7 100644 Binary files a/packages/pinball_components/assets/images/dino/dino-land-bottom.png and b/packages/pinball_components/assets/images/dino/dino-land-bottom.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-top.png b/packages/pinball_components/assets/images/dino/dino-land-top.png index 85c2619a..cb4c82f2 100644 Binary files a/packages/pinball_components/assets/images/dino/dino-land-top.png and b/packages/pinball_components/assets/images/dino/dino-land-top.png differ diff --git a/packages/pinball_components/assets/images/flipper/left.png b/packages/pinball_components/assets/images/flipper/left.png index 42798f28..3aefa225 100644 Binary files a/packages/pinball_components/assets/images/flipper/left.png and b/packages/pinball_components/assets/images/flipper/left.png differ diff --git a/packages/pinball_components/assets/images/flipper/right.png b/packages/pinball_components/assets/images/flipper/right.png index 86fbc81d..3627c86c 100644 Binary files a/packages/pinball_components/assets/images/flipper/right.png and b/packages/pinball_components/assets/images/flipper/right.png differ diff --git a/packages/pinball_components/assets/images/flutter_sign_post.png b/packages/pinball_components/assets/images/flutter_sign_post.png deleted file mode 100644 index 28a3facb..00000000 Binary files a/packages/pinball_components/assets/images/flutter_sign_post.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/launch_ramp/background-railing.png b/packages/pinball_components/assets/images/launch_ramp/background-railing.png new file mode 100644 index 00000000..aa7d5774 Binary files /dev/null and b/packages/pinball_components/assets/images/launch_ramp/background-railing.png differ diff --git a/packages/pinball_components/assets/images/launch_ramp/foreground-railing.png b/packages/pinball_components/assets/images/launch_ramp/foreground-railing.png index 7dd0de96..f953fdf5 100644 Binary files a/packages/pinball_components/assets/images/launch_ramp/foreground-railing.png and b/packages/pinball_components/assets/images/launch_ramp/foreground-railing.png differ diff --git a/packages/pinball_components/assets/images/launch_ramp/ramp.png b/packages/pinball_components/assets/images/launch_ramp/ramp.png index c811dd83..b024860a 100644 Binary files a/packages/pinball_components/assets/images/launch_ramp/ramp.png and b/packages/pinball_components/assets/images/launch_ramp/ramp.png differ diff --git a/packages/pinball_components/assets/images/plunger/plunger.png b/packages/pinball_components/assets/images/plunger/plunger.png new file mode 100644 index 00000000..2ec6e001 Binary files /dev/null and b/packages/pinball_components/assets/images/plunger/plunger.png differ diff --git a/packages/pinball_components/assets/images/plunger/rocket.png b/packages/pinball_components/assets/images/plunger/rocket.png new file mode 100644 index 00000000..ee5eef5b Binary files /dev/null and b/packages/pinball_components/assets/images/plunger/rocket.png differ diff --git a/packages/pinball_components/assets/images/signpost/active1.png b/packages/pinball_components/assets/images/signpost/active1.png new file mode 100644 index 00000000..1addb228 Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/active1.png differ diff --git a/packages/pinball_components/assets/images/signpost/active2.png b/packages/pinball_components/assets/images/signpost/active2.png new file mode 100644 index 00000000..081a936c Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/active2.png differ diff --git a/packages/pinball_components/assets/images/signpost/active3.png b/packages/pinball_components/assets/images/signpost/active3.png new file mode 100644 index 00000000..8d781dfb Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/active3.png differ diff --git a/packages/pinball_components/assets/images/signpost/inactive.png b/packages/pinball_components/assets/images/signpost/inactive.png new file mode 100644 index 00000000..6043454b Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/inactive.png differ diff --git a/packages/pinball_components/assets/images/slingshot/left_lower.png b/packages/pinball_components/assets/images/slingshot/left_lower.png deleted file mode 100644 index b44b58fb..00000000 Binary files a/packages/pinball_components/assets/images/slingshot/left_lower.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/slingshot/left_upper.png b/packages/pinball_components/assets/images/slingshot/left_upper.png deleted file mode 100644 index c74267ca..00000000 Binary files a/packages/pinball_components/assets/images/slingshot/left_upper.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/slingshot/lower.png b/packages/pinball_components/assets/images/slingshot/lower.png new file mode 100644 index 00000000..61e01ba1 Binary files /dev/null and b/packages/pinball_components/assets/images/slingshot/lower.png differ diff --git a/packages/pinball_components/assets/images/slingshot/right_lower.png b/packages/pinball_components/assets/images/slingshot/right_lower.png deleted file mode 100644 index 71a6a277..00000000 Binary files a/packages/pinball_components/assets/images/slingshot/right_lower.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/slingshot/right_upper.png b/packages/pinball_components/assets/images/slingshot/right_upper.png deleted file mode 100644 index e6b42ded..00000000 Binary files a/packages/pinball_components/assets/images/slingshot/right_upper.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/slingshot/upper.png b/packages/pinball_components/assets/images/slingshot/upper.png new file mode 100644 index 00000000..d86bd925 Binary files /dev/null and b/packages/pinball_components/assets/images/slingshot/upper.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png new file mode 100644 index 00000000..c649d64a Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png new file mode 100644 index 00000000..8ee71e36 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png new file mode 100644 index 00000000..023e86e9 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png new file mode 100644 index 00000000..f5a413f5 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png new file mode 100644 index 00000000..c0b76b4d Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png b/packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png new file mode 100644 index 00000000..6ad09460 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/board-opening.png b/packages/pinball_components/assets/images/spaceship/ramp/board-opening.png new file mode 100644 index 00000000..1c6b78f2 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship/ramp/board-opening.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/main.png b/packages/pinball_components/assets/images/spaceship/ramp/main.png index 81498965..b885753b 100644 Binary files a/packages/pinball_components/assets/images/spaceship/ramp/main.png and b/packages/pinball_components/assets/images/spaceship/ramp/main.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/railing-background.png b/packages/pinball_components/assets/images/spaceship/ramp/railing-background.png index 2298f799..e5630446 100644 Binary files a/packages/pinball_components/assets/images/spaceship/ramp/railing-background.png and b/packages/pinball_components/assets/images/spaceship/ramp/railing-background.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png b/packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png index e788fde0..fa25d124 100644 Binary files a/packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png and b/packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png differ diff --git a/packages/pinball_components/assets/images/sparky/animatronic.png b/packages/pinball_components/assets/images/sparky/animatronic.png new file mode 100644 index 00000000..cc57e405 Binary files /dev/null and b/packages/pinball_components/assets/images/sparky/animatronic.png differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/a/active.png b/packages/pinball_components/assets/images/sparky/bumper/a/active.png index 6e84d8ef..81a91a52 100644 Binary files a/packages/pinball_components/assets/images/sparky/bumper/a/active.png and b/packages/pinball_components/assets/images/sparky/bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/a/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/a/inactive.png index 9157a73f..a81c7915 100644 Binary files a/packages/pinball_components/assets/images/sparky/bumper/a/inactive.png and b/packages/pinball_components/assets/images/sparky/bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/b/active.png b/packages/pinball_components/assets/images/sparky/bumper/b/active.png index 02371ce1..a00f3f33 100644 Binary files a/packages/pinball_components/assets/images/sparky/bumper/b/active.png and b/packages/pinball_components/assets/images/sparky/bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/b/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/b/inactive.png index 20b4f092..0eac905c 100644 Binary files a/packages/pinball_components/assets/images/sparky/bumper/b/inactive.png and b/packages/pinball_components/assets/images/sparky/bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/c/active.png b/packages/pinball_components/assets/images/sparky/bumper/c/active.png index 85748375..265f35aa 100644 Binary files a/packages/pinball_components/assets/images/sparky/bumper/c/active.png and b/packages/pinball_components/assets/images/sparky/bumper/c/active.png differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/c/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/c/inactive.png index b5b3584d..50a69d54 100644 Binary files a/packages/pinball_components/assets/images/sparky/bumper/c/inactive.png and b/packages/pinball_components/assets/images/sparky/bumper/c/inactive.png differ diff --git a/packages/pinball_components/assets/images/sparky/computer/base.png b/packages/pinball_components/assets/images/sparky/computer/base.png index 2e8fe362..188e4329 100644 Binary files a/packages/pinball_components/assets/images/sparky/computer/base.png and b/packages/pinball_components/assets/images/sparky/computer/base.png differ diff --git a/packages/pinball_components/assets/images/sparky/computer/top.png b/packages/pinball_components/assets/images/sparky/computer/top.png index d9f3bc6c..085771cd 100644 Binary files a/packages/pinball_components/assets/images/sparky/computer/top.png and b/packages/pinball_components/assets/images/sparky/computer/top.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index b52ca694..4d4cda7d 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -10,34 +10,36 @@ import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); + $AssetsImagesAlienBumperGen get alienBumper => + const $AssetsImagesAlienBumperGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); - - /// File path: assets/images/ball.png - AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); - + $AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => const $AssetsImagesChromeDinoGen(); - $AssetsImagesDashBumperGen get dashBumper => - const $AssetsImagesDashBumperGen(); + $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); - - /// File path: assets/images/flutter_sign_post.png - AssetGenImage get flutterSignPost => - const AssetGenImage('assets/images/flutter_sign_post.png'); - $AssetsImagesGoogleWordGen get googleWord => const $AssetsImagesGoogleWordGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); + $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); } +class $AssetsImagesAlienBumperGen { + const $AssetsImagesAlienBumperGen(); + + $AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen(); + $AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen(); +} + class $AssetsImagesBackboardGen { const $AssetsImagesBackboardGen(); @@ -48,6 +50,21 @@ class $AssetsImagesBackboardGen { /// File path: assets/images/backboard/backboard_scores.png AssetGenImage get backboardScores => const AssetGenImage('assets/images/backboard/backboard_scores.png'); + + /// File path: assets/images/backboard/display.png + AssetGenImage get display => + const AssetGenImage('assets/images/backboard/display.png'); +} + +class $AssetsImagesBallGen { + const $AssetsImagesBallGen(); + + /// File path: assets/images/ball/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/ball/ball.png'); + + /// File path: assets/images/ball/flame_effect.png + AssetGenImage get flameEffect => + const AssetGenImage('assets/images/ball/flame_effect.png'); } class $AssetsImagesBaseboardGen { @@ -69,6 +86,10 @@ class $AssetsImagesBoundaryGen { AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); + /// File path: assets/images/boundary/outer-bottom.png + AssetGenImage get outerBottom => + const AssetGenImage('assets/images/boundary/outer-bottom.png'); + /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); @@ -86,13 +107,14 @@ class $AssetsImagesChromeDinoGen { const AssetGenImage('assets/images/chrome_dino/mouth.png'); } -class $AssetsImagesDashBumperGen { - const $AssetsImagesDashBumperGen(); +class $AssetsImagesDashGen { + const $AssetsImagesDashGen(); - $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); - $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); - $AssetsImagesDashBumperMainGen get main => - const $AssetsImagesDashBumperMainGen(); + /// File path: assets/images/dash/animatronic.png + AssetGenImage get animatronic => + const AssetGenImage('assets/images/dash/animatronic.png'); + + $AssetsImagesDashBumperGen get bumper => const $AssetsImagesDashBumperGen(); } class $AssetsImagesDinoGen { @@ -162,6 +184,10 @@ class $AssetsImagesKickerGen { class $AssetsImagesLaunchRampGen { const $AssetsImagesLaunchRampGen(); + /// File path: assets/images/launch_ramp/background-railing.png + AssetGenImage get backgroundRailing => + const AssetGenImage('assets/images/launch_ramp/background-railing.png'); + /// File path: assets/images/launch_ramp/foreground-railing.png AssetGenImage get foregroundRailing => const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); @@ -171,24 +197,48 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } -class $AssetsImagesSlingshotGen { - const $AssetsImagesSlingshotGen(); +class $AssetsImagesPlungerGen { + const $AssetsImagesPlungerGen(); + + /// File path: assets/images/plunger/plunger.png + AssetGenImage get plunger => + const AssetGenImage('assets/images/plunger/plunger.png'); + + /// File path: assets/images/plunger/rocket.png + AssetGenImage get rocket => + const AssetGenImage('assets/images/plunger/rocket.png'); +} + +class $AssetsImagesSignpostGen { + const $AssetsImagesSignpostGen(); - /// File path: assets/images/slingshot/left_lower.png - AssetGenImage get leftLower => - const AssetGenImage('assets/images/slingshot/left_lower.png'); + /// File path: assets/images/signpost/active1.png + AssetGenImage get active1 => + const AssetGenImage('assets/images/signpost/active1.png'); - /// File path: assets/images/slingshot/left_upper.png - AssetGenImage get leftUpper => - const AssetGenImage('assets/images/slingshot/left_upper.png'); + /// File path: assets/images/signpost/active2.png + AssetGenImage get active2 => + const AssetGenImage('assets/images/signpost/active2.png'); - /// File path: assets/images/slingshot/right_lower.png - AssetGenImage get rightLower => - const AssetGenImage('assets/images/slingshot/right_lower.png'); + /// File path: assets/images/signpost/active3.png + AssetGenImage get active3 => + const AssetGenImage('assets/images/signpost/active3.png'); - /// File path: assets/images/slingshot/right_upper.png - AssetGenImage get rightUpper => - const AssetGenImage('assets/images/slingshot/right_upper.png'); + /// File path: assets/images/signpost/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/signpost/inactive.png'); +} + +class $AssetsImagesSlingshotGen { + const $AssetsImagesSlingshotGen(); + + /// File path: assets/images/slingshot/lower.png + AssetGenImage get lower => + const AssetGenImage('assets/images/slingshot/lower.png'); + + /// File path: assets/images/slingshot/upper.png + AssetGenImage get upper => + const AssetGenImage('assets/images/slingshot/upper.png'); } class $AssetsImagesSpaceshipGen { @@ -211,46 +261,45 @@ class $AssetsImagesSpaceshipGen { class $AssetsImagesSparkyGen { const $AssetsImagesSparkyGen(); + AssetGenImage get animatronic => + const AssetGenImage('assets/images/sparky/animatronic.png'); $AssetsImagesSparkyBumperGen get bumper => const $AssetsImagesSparkyBumperGen(); $AssetsImagesSparkyComputerGen get computer => const $AssetsImagesSparkyComputerGen(); } -class $AssetsImagesDashBumperAGen { - const $AssetsImagesDashBumperAGen(); +class $AssetsImagesAlienBumperAGen { + const $AssetsImagesAlienBumperAGen(); - /// File path: assets/images/dash_bumper/a/active.png + /// File path: assets/images/alien_bumper/a/active.png AssetGenImage get active => - const AssetGenImage('assets/images/dash_bumper/a/active.png'); + const AssetGenImage('assets/images/alien_bumper/a/active.png'); - /// File path: assets/images/dash_bumper/a/inactive.png + /// File path: assets/images/alien_bumper/a/inactive.png AssetGenImage get inactive => - const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); + const AssetGenImage('assets/images/alien_bumper/a/inactive.png'); } -class $AssetsImagesDashBumperBGen { - const $AssetsImagesDashBumperBGen(); +class $AssetsImagesAlienBumperBGen { + const $AssetsImagesAlienBumperBGen(); - /// File path: assets/images/dash_bumper/b/active.png + /// File path: assets/images/alien_bumper/b/active.png AssetGenImage get active => - const AssetGenImage('assets/images/dash_bumper/b/active.png'); + const AssetGenImage('assets/images/alien_bumper/b/active.png'); - /// File path: assets/images/dash_bumper/b/inactive.png + /// File path: assets/images/alien_bumper/b/inactive.png AssetGenImage get inactive => - const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); + const AssetGenImage('assets/images/alien_bumper/b/inactive.png'); } -class $AssetsImagesDashBumperMainGen { - const $AssetsImagesDashBumperMainGen(); - - /// File path: assets/images/dash_bumper/main/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/dash_bumper/main/active.png'); +class $AssetsImagesDashBumperGen { + const $AssetsImagesDashBumperGen(); - /// File path: assets/images/dash_bumper/main/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); + $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); + $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); + $AssetsImagesDashBumperMainGen get main => + const $AssetsImagesDashBumperMainGen(); } class $AssetsImagesSpaceshipRailGen { @@ -268,6 +317,13 @@ class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRampGen { const $AssetsImagesSpaceshipRampGen(); + $AssetsImagesSpaceshipRampArrowGen get arrow => + const $AssetsImagesSpaceshipRampArrowGen(); + + /// File path: assets/images/spaceship/ramp/board-opening.png + AssetGenImage get boardOpening => + const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); + /// File path: assets/images/spaceship/ramp/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/ramp/main.png'); @@ -301,6 +357,70 @@ class $AssetsImagesSparkyComputerGen { const AssetGenImage('assets/images/sparky/computer/top.png'); } +class $AssetsImagesDashBumperAGen { + const $AssetsImagesDashBumperAGen(); + + /// File path: assets/images/dash/bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash/bumper/a/active.png'); + + /// File path: assets/images/dash/bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash/bumper/a/inactive.png'); +} + +class $AssetsImagesDashBumperBGen { + const $AssetsImagesDashBumperBGen(); + + /// File path: assets/images/dash/bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash/bumper/b/active.png'); + + /// File path: assets/images/dash/bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash/bumper/b/inactive.png'); +} + +class $AssetsImagesDashBumperMainGen { + const $AssetsImagesDashBumperMainGen(); + + /// File path: assets/images/dash/bumper/main/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash/bumper/main/active.png'); + + /// File path: assets/images/dash/bumper/main/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); +} + +class $AssetsImagesSpaceshipRampArrowGen { + const $AssetsImagesSpaceshipRampArrowGen(); + + /// File path: assets/images/spaceship/ramp/arrow/active1.png + AssetGenImage get active1 => + const AssetGenImage('assets/images/spaceship/ramp/arrow/active1.png'); + + /// File path: assets/images/spaceship/ramp/arrow/active2.png + AssetGenImage get active2 => + const AssetGenImage('assets/images/spaceship/ramp/arrow/active2.png'); + + /// File path: assets/images/spaceship/ramp/arrow/active3.png + AssetGenImage get active3 => + const AssetGenImage('assets/images/spaceship/ramp/arrow/active3.png'); + + /// File path: assets/images/spaceship/ramp/arrow/active4.png + AssetGenImage get active4 => + const AssetGenImage('assets/images/spaceship/ramp/arrow/active4.png'); + + /// File path: assets/images/spaceship/ramp/arrow/active5.png + AssetGenImage get active5 => + const AssetGenImage('assets/images/spaceship/ramp/arrow/active5.png'); + + /// File path: assets/images/spaceship/ramp/arrow/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/spaceship/ramp/arrow/inactive.png'); +} + class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); diff --git a/packages/pinball_components/lib/gen/fonts.gen.dart b/packages/pinball_components/lib/gen/fonts.gen.dart index b15f2dd0..5f77da16 100644 --- a/packages/pinball_components/lib/gen/fonts.gen.dart +++ b/packages/pinball_components/lib/gen/fonts.gen.dart @@ -3,9 +3,14 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + class FontFamily { FontFamily._(); + /// Font family: PixeloidMono static const String pixeloidMono = 'PixeloidMono'; + + /// Font family: PixeloidSans static const String pixeloidSans = 'PixeloidSans'; } diff --git a/packages/pinball_components/lib/gen/gen.dart b/packages/pinball_components/lib/gen/gen.dart new file mode 100644 index 00000000..ada8b777 --- /dev/null +++ b/packages/pinball_components/lib/gen/gen.dart @@ -0,0 +1,3 @@ +export 'assets.gen.dart'; +export 'fonts.gen.dart'; +export 'pinball_fonts.dart'; diff --git a/packages/pinball_components/lib/gen/pinball_fonts.dart b/packages/pinball_components/lib/gen/pinball_fonts.dart index c1b3c6fa..de6c5782 100644 --- a/packages/pinball_components/lib/gen/pinball_fonts.dart +++ b/packages/pinball_components/lib/gen/pinball_fonts.dart @@ -1,16 +1,14 @@ import 'package:pinball_components/gen/fonts.gen.dart'; -String _prefixFont(String font) { - return 'packages/pinball_components/$font'; -} +const String _fontPath = 'packages/pinball_components'; /// Class with the fonts available on the pinball game class PinballFonts { PinballFonts._(); /// Mono variation of the Pixeloid font - static final String pixeloidMono = _prefixFont(FontFamily.pixeloidMono); + static const String pixeloidMono = '$_fontPath/${FontFamily.pixeloidMono}'; /// Sans variation of the Pixeloid font - static final String pixeloidSans = _prefixFont(FontFamily.pixeloidMono); + static const String pixeloidSans = '$_fontPath/${FontFamily.pixeloidSans}'; } diff --git a/packages/pinball_components/lib/pinball_components.dart b/packages/pinball_components/lib/pinball_components.dart index 2551b54e..cf394b0e 100644 --- a/packages/pinball_components/lib/pinball_components.dart +++ b/packages/pinball_components/lib/pinball_components.dart @@ -1,5 +1,4 @@ library pinball_components; -export 'gen/assets.gen.dart'; -export 'gen/pinball_fonts.dart'; +export 'gen/gen.dart'; export 'src/pinball_components.dart'; diff --git a/packages/pinball_components/lib/src/components/alien_bumper/alien_bumper.dart b/packages/pinball_components/lib/src/components/alien_bumper/alien_bumper.dart new file mode 100644 index 00000000..abe39ae5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper/alien_bumper.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +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/alien_bumper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/alien_bumper_cubit.dart'; + +/// {@template alien_bumper} +/// Bumper for area under the [Spaceship]. +/// {@endtemplate} +class AlienBumper extends BodyComponent with InitialPosition { + /// {@macro alien_bumper} + AlienBumper._({ + required double majorRadius, + required double minorRadius, + required String onAssetPath, + required String offAssetPath, + Iterable? children, + required this.bloc, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + super( + priority: RenderPriority.alienBumper, + renderBody: false, + children: [ + AlienBumperBallContactBehavior(), + AlienBumperBlinkingBehavior(), + _AlienBumperSpriteGroupComponent( + offAssetPath: offAssetPath, + onAssetPath: onAssetPath, + state: bloc.state, + ), + ...?children, + ], + ); + + /// {@macro alien_bumper} + AlienBumper.a({ + Iterable? children, + }) : this._( + majorRadius: 3.52, + minorRadius: 2.97, + onAssetPath: Assets.images.alienBumper.a.active.keyName, + offAssetPath: Assets.images.alienBumper.a.inactive.keyName, + bloc: AlienBumperCubit(), + children: children, + ); + + /// {@macro alien_bumper} + AlienBumper.b({ + Iterable? children, + }) : this._( + majorRadius: 3.19, + minorRadius: 2.79, + onAssetPath: Assets.images.alienBumper.b.active.keyName, + offAssetPath: Assets.images.alienBumper.b.inactive.keyName, + bloc: AlienBumperCubit(), + children: children, + ); + + /// Creates an [AlienBumper] without any children. + /// + /// This can be used for testing [AlienBumper]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + AlienBumper.test({ + required this.bloc, + }) : _majorRadius = 3.52, + _minorRadius = 2.97; + + final double _majorRadius; + + final double _minorRadius; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final AlienBumperCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(1.29); + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); + final bodyDef = BodyDef( + position: initialPosition, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _AlienBumperSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _AlienBumperSpriteGroupComponent({ + required String onAssetPath, + required String offAssetPath, + required AlienBumperState state, + }) : _onAssetPath = onAssetPath, + _offAssetPath = offAssetPath, + super( + anchor: Anchor.center, + position: Vector2(0, -0.1), + current: state, + ); + + final String _onAssetPath; + final String _offAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + AlienBumperState.active: Sprite( + gameRef.images.fromCache(_onAssetPath), + ), + AlienBumperState.inactive: + Sprite(gameRef.images.fromCache(_offAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/alien_bumper/behaviors/alien_bumper_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/alien_bumper/behaviors/alien_bumper_ball_contact_behavior.dart new file mode 100644 index 00000000..effa3221 --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper/behaviors/alien_bumper_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class AlienBumperBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/alien_bumper/behaviors/alien_bumper_blinking_behavior.dart b/packages/pinball_components/lib/src/components/alien_bumper/behaviors/alien_bumper_blinking_behavior.dart new file mode 100644 index 00000000..f606ec70 --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper/behaviors/alien_bumper_blinking_behavior.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template alien_bumper_blinking_behavior} +/// Makes a [AlienBumper] blink back to [AlienBumperState.active] when +/// [AlienBumperState.inactive]. +/// {@endtemplate} +class AlienBumperBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro alien_bumper_blinking_behavior} + AlienBumperBlinkingBehavior() : super(period: 0.05); + + void _onNewState(AlienBumperState state) { + switch (state) { + case AlienBumperState.active: + break; + case AlienBumperState.inactive: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/alien_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/alien_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..14762ea2 --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'alien_bumper_ball_contact_behavior.dart'; +export 'alien_bumper_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/alien_bumper/cubit/alien_bumper_cubit.dart b/packages/pinball_components/lib/src/components/alien_bumper/cubit/alien_bumper_cubit.dart new file mode 100644 index 00000000..d4319a3b --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper/cubit/alien_bumper_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'alien_bumper_state.dart'; + +class AlienBumperCubit extends Cubit { + AlienBumperCubit() : super(AlienBumperState.active); + + void onBallContacted() { + emit(AlienBumperState.inactive); + } + + void onBlinked() { + emit(AlienBumperState.active); + } +} diff --git a/packages/pinball_components/lib/src/components/alien_bumper/cubit/alien_bumper_state.dart b/packages/pinball_components/lib/src/components/alien_bumper/cubit/alien_bumper_state.dart new file mode 100644 index 00000000..cbec959e --- /dev/null +++ b/packages/pinball_components/lib/src/components/alien_bumper/cubit/alien_bumper_state.dart @@ -0,0 +1,10 @@ +part of 'alien_bumper_cubit.dart'; + +/// Indicates the [AlienBumperCubit]'s current state. +enum AlienBumperState { + /// A lit up bumper. + active, + + /// A dimmed bumper. + inactive, +} diff --git a/packages/pinball_components/lib/src/components/backboard.dart b/packages/pinball_components/lib/src/components/backboard.dart deleted file mode 100644 index 613cbc05..00000000 --- a/packages/pinball_components/lib/src/components/backboard.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template backboard} -/// The [Backboard] of the pinball machine. -/// {@endtemplate} -class Backboard extends SpriteComponent with HasGameRef { - /// {@macro backboard} - Backboard({ - required Vector2 position, - }) : super( - // TODO(erickzanardo): remove multiply after - // https://github.com/flame-engine/flame/pull/1506 is merged - position: position..clone().multiply(Vector2(1, -1)), - anchor: Anchor.bottomCenter, - ); - - @override - Future onLoad() async { - await waitingMode(); - } - - /// Puts the Backboard in waiting mode, where the scoreboard is shown. - Future waitingMode() async { - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardScores.keyName, - ); - size = sprite.originalSize / 10; - this.sprite = sprite; - } - - /// Puts the Backboard in game over mode, where the score input is shown. - Future gameOverMode() async { - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardGameOver.keyName, - ); - size = sprite.originalSize / 10; - this.sprite = sprite; - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard.dart b/packages/pinball_components/lib/src/components/backboard/backboard.dart new file mode 100644 index 00000000..fe5fd37c --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; + +export 'backboard_game_over.dart'; +export 'backboard_letter_prompt.dart'; +export 'backboard_waiting.dart'; + +/// {@template backboard} +/// The [Backboard] of the pinball machine. +/// {@endtemplate} +class Backboard extends PositionComponent with HasGameRef { + /// {@macro backboard} + Backboard({ + required Vector2 position, + }) : super( + position: position, + anchor: Anchor.bottomCenter, + ); + + /// {@macro backboard} + /// + /// Returns a [Backboard] initialized in the waiting mode + factory Backboard.waiting({ + required Vector2 position, + }) { + return Backboard(position: position)..waitingMode(); + } + + /// {@macro backboard} + /// + /// Returns a [Backboard] initialized in the game over mode + factory Backboard.gameOver({ + required Vector2 position, + required String characterIconPath, + required int score, + required BackboardOnSubmit onSubmit, + }) { + return Backboard(position: position) + ..gameOverMode( + score: score, + characterIconPath: characterIconPath, + onSubmit: onSubmit, + ); + } + + /// [TextPaint] used on the [Backboard] + static final textPaint = TextPaint( + style: const TextStyle( + fontSize: 6, + color: Colors.white, + fontFamily: PinballFonts.pixeloidSans, + ), + ); + + /// Puts the Backboard in waiting mode, where the scoreboard is shown. + Future waitingMode() async { + children.removeWhere((_) => true); + await add(BackboardWaiting()); + } + + /// Puts the Backboard in game over mode, where the score input is shown. + Future gameOverMode({ + required int score, + required String characterIconPath, + BackboardOnSubmit? onSubmit, + }) async { + children.removeWhere((_) => true); + await add( + BackboardGameOver( + score: score, + characterIconPath: characterIconPath, + onSubmit: onSubmit, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart new file mode 100644 index 00000000..cfea0bc6 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Signature for the callback called when the used has +/// submettied their initials on the [BackboardGameOver] +typedef BackboardOnSubmit = void Function(String); + +/// {@template backboard_game_over} +/// [PositionComponent] that handles the user input on the +/// game over display view. +/// {@endtemplate} +class BackboardGameOver extends PositionComponent with HasGameRef { + /// {@macro backboard_game_over} + BackboardGameOver({ + required int score, + required String characterIconPath, + BackboardOnSubmit? onSubmit, + }) : _onSubmit = onSubmit, + super( + children: [ + _BackboardSpriteComponent(), + _BackboardDisplaySpriteComponent(), + _ScoreTextComponent(score.formatScore()), + _CharacterIconSpriteComponent(characterIconPath), + ], + ); + + final BackboardOnSubmit? _onSubmit; + + @override + Future onLoad() async { + for (var i = 0; i < 3; i++) { + await add( + BackboardLetterPrompt( + position: Vector2( + 24.3 + (4.5 * i), + -45, + ), + hasFocus: i == 0, + ), + ); + } + + await add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowLeft: () => _movePrompt(true), + LogicalKeyboardKey.arrowRight: () => _movePrompt(false), + LogicalKeyboardKey.enter: _submit, + }, + ), + ); + } + + /// Returns the current inputed initials + String get initials => children + .whereType() + .map((prompt) => prompt.char) + .join(); + + bool _submit() { + _onSubmit?.call(initials); + return true; + } + + bool _movePrompt(bool left) { + final prompts = children.whereType().toList(); + + final current = prompts.firstWhere((prompt) => prompt.hasFocus) + ..hasFocus = false; + var index = prompts.indexOf(current) + (left ? -1 : 1); + index = min(max(0, index), prompts.length - 1); + + prompts[index].hasFocus = true; + + return false; + } +} + +class _BackboardSpriteComponent extends SpriteComponent with HasGameRef { + _BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.backboard.backboardGameOver.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef { + _BackboardDisplaySpriteComponent() + : super( + anchor: Anchor.bottomCenter, + position: Vector2(0, -11.5), + ); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.backboard.display.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _ScoreTextComponent extends TextComponent { + _ScoreTextComponent(String score) + : super( + text: score, + anchor: Anchor.centerLeft, + position: Vector2(-34, -45), + textRenderer: Backboard.textPaint, + ); +} + +class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef { + _CharacterIconSpriteComponent(String characterIconPath) + : _characterIconPath = characterIconPath, + super( + anchor: Anchor.center, + position: Vector2(18.4, -45), + ); + + final String _characterIconPath; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite(gameRef.images.fromCache(_characterIconPath)); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart new file mode 100644 index 00000000..fe582210 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template backboard_letter_prompt} +/// A [PositionComponent] that renders a letter prompt used +/// on the [BackboardGameOver] +/// {@endtemplate} +class BackboardLetterPrompt extends PositionComponent { + /// {@macro backboard_letter_prompt} + BackboardLetterPrompt({ + required Vector2 position, + bool hasFocus = false, + }) : _hasFocus = hasFocus, + super( + position: position, + ); + + static const _alphabetCode = 65; + static const _alphabetLength = 25; + var _charIndex = 0; + + bool _hasFocus; + + late RectangleComponent _underscore; + late TextComponent _input; + late TimerComponent _underscoreBlinker; + + @override + Future onLoad() async { + _underscore = RectangleComponent( + size: Vector2(3.8, 0.8), + anchor: Anchor.center, + position: Vector2(-0.3, 4), + ); + + await add(_underscore); + + _input = TextComponent( + text: 'A', + textRenderer: Backboard.textPaint, + anchor: Anchor.center, + ); + await add(_input); + + _underscoreBlinker = TimerComponent( + period: 0.6, + repeat: true, + autoStart: _hasFocus, + onTick: () { + _underscore.paint.color = (_underscore.paint.color == Colors.white) + ? Colors.transparent + : Colors.white; + }, + ); + + await add(_underscoreBlinker); + + await add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: () => _cycle(true), + LogicalKeyboardKey.arrowDown: () => _cycle(false), + }, + ), + ); + } + + /// Returns the current selected character + String get char => String.fromCharCode(_alphabetCode + _charIndex); + + bool _cycle(bool up) { + if (_hasFocus) { + final newCharCode = + min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); + _input.text = String.fromCharCode(_alphabetCode + newCharCode); + _charIndex = newCharCode; + + return false; + } + return true; + } + + /// Returns if this prompt has focus on it + bool get hasFocus => _hasFocus; + + /// Updates this prompt focus + set hasFocus(bool hasFocus) { + if (hasFocus) { + _underscoreBlinker.timer.resume(); + } else { + _underscoreBlinker.timer.pause(); + } + _underscore.paint.color = Colors.white; + _hasFocus = hasFocus; + } +} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart b/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart new file mode 100644 index 00000000..f7fa84bf --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart @@ -0,0 +1,17 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// [PositionComponent] that shows the leaderboard while the player +/// has not started the game yet. +class BackboardWaiting extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + final sprite = await gameRef.loadSprite( + Assets.images.backboard.backboardScores.keyName, + ); + + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.bottomCenter; + } +} diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index c32b8b18..1c9c1270 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:math' as math; import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template ball} @@ -10,10 +12,15 @@ import 'package:pinball_components/pinball_components.dart'; /// {@endtemplate} class Ball extends BodyComponent with Layered, InitialPosition { - /// {@macro ball_body} + /// {@macro ball} Ball({ required this.baseColor, - }) { + }) : super( + renderBody: false, + children: [ + _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + ], + ) { // TODO(ruimiguel): while developing Ball can be launched by clicking mouse, // and default layer is Layer.all. But on final game Ball will be always be // be launched from Plunger and LauncherRamp will modify it to Layer.board. @@ -28,31 +35,18 @@ class Ball extends BodyComponent /// The base [Color] used to tint this [Ball]. final Color baseColor; - double _boostTimer = 0; - static const _boostDuration = 2.0; - - final _BallSpriteComponent _spriteComponent = _BallSpriteComponent(); - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add( - _spriteComponent..tint(baseColor.withOpacity(0.5)), - ); - - renderBody = false; - } - @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; - final fixtureDef = FixtureDef(shape)..density = 1; - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this - ..type = BodyType.dynamic; + final fixtureDef = FixtureDef( + shape, + density: 1, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + type: BodyType.dynamic, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } @@ -64,7 +58,7 @@ class Ball extends BodyComponent // TODO(allisonryan0002): prevent motion from contact with other balls. void stop() { body - ..gravityScale = 0 + ..gravityScale = Vector2.zero() ..linearVelocity = Vector2.zero() ..angularVelocity = 0; } @@ -73,46 +67,55 @@ class Ball extends BodyComponent /// /// If previously [stop]ped, the previous ball's velocity is not kept. void resume() { - body.gravityScale = 1; + body.gravityScale = Vector2(0, 1); + } + + /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. + Future boost(Vector2 impulse) async { + body.linearVelocity = impulse; + await add(_TurboChargeSpriteAnimationComponent()); } @override void update(double dt) { super.update(dt); - if (_boostTimer > 0) { - _boostTimer -= dt; - final direction = body.linearVelocity.normalized(); - final effect = FireEffect( - burstPower: _boostTimer, - direction: -direction, - position: Vector2(body.position.x, -body.position.y), - priority: priority - 1, - ); - - unawaited(gameRef.add(effect)); - } - _rescale(); + _rescaleSize(); + _setPositionalGravity(); } - /// Applies a boost on this [Ball]. - void boost(Vector2 impulse) { - body.linearVelocity = impulse; - _boostTimer = _boostDuration; + void _rescaleSize() { + final boardHeight = BoardDimensions.bounds.height; + const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; + + final standardizedYPosition = body.position.y + (boardHeight / 2); + + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); + + body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; + + // TODO(alestiago): Revisit and see if there's a better way to do this. + final spriteComponent = firstChild<_BallSpriteComponent>(); + spriteComponent?.scale = Vector2.all(scaleFactor); } - void _rescale() { - final boardHeight = BoardDimensions.size.y; - const maxShrinkAmount = BoardDimensions.perspectiveShrinkFactor; + void _setPositionalGravity() { + final defaultGravity = gameRef.world.gravity.y; + final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2; + const maxXGravityPercentage = + (1 - BoardDimensions.perspectiveShrinkFactor) / 2; + final xDeviationFromCenter = body.position.x; - final adjustedYPosition = body.position.y + (boardHeight / 2); + final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) * + maxXGravityPercentage) * + defaultGravity; - final scaleFactor = ((boardHeight - adjustedYPosition) / - BoardDimensions.shrinkAdjustedHeight) + - maxShrinkAmount; + final positionalYForce = math.sqrt( + math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2), + ); - body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; - _spriteComponent.scale = Vector2.all(scaleFactor); + body.gravityOverride = Vector2(positionalXForce, positionalYForce); } } @@ -121,10 +124,61 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef { Future onLoad() async { await super.onLoad(); final sprite = await gameRef.loadSprite( - Assets.images.ball.keyName, + Assets.images.ball.ball.keyName, ); this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; } } + +class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef { + _TurboChargeSpriteAnimationComponent() + : super( + anchor: const Anchor(0.53, 0.72), + priority: RenderPriority.turboChargeFlame, + removeOnFinish: true, + ); + + late final Vector2 _textureSize; + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = await gameRef.images.load( + Assets.images.ball.flameEffect.keyName, + ); + + const amountPerRow = 8; + const amountPerColumn = 4; + _textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: _textureSize, + loop: false, + ), + ); + } + + @override + void update(double dt) { + super.update(dt); + + if (parent != null) { + final body = (parent! as BodyComponent).body; + final direction = -body.linearVelocity.normalized(); + angle = math.atan2(direction.x, -direction.y); + size = (_textureSize / 45) * body.fixtures.first.shape.radius; + } + } +} diff --git a/packages/pinball_components/lib/src/components/baseboard.dart b/packages/pinball_components/lib/src/components/baseboard.dart index 56b7c978..47ba4666 100644 --- a/packages/pinball_components/lib/src/components/baseboard.dart +++ b/packages/pinball_components/lib/src/components/baseboard.dart @@ -11,88 +11,85 @@ class Baseboard extends BodyComponent with InitialPosition { /// {@macro baseboard} Baseboard({ required BoardSide side, - }) : _side = side; + }) : _side = side, + super( + renderBody: false, + children: [_BaseboardSpriteComponent(side: side)], + ); /// Whether the [Baseboard] is on the left or right side of the board. final BoardSide _side; List _createFixtureDefs() { - final fixturesDef = []; final direction = _side.direction; - final arcsAngle = -1.11 * direction; - const arcsRotation = math.pi / 2.08; + const arcsAngle = 1.11; + final arcsRotation = (_side.isLeft) ? -2.7 : -1.6; final pegBumperShape = CircleShape()..radius = 0.7; - pegBumperShape.position.setValues(11.11 * direction, 7.15); + pegBumperShape.position.setValues(11.11 * direction, -7.15); final pegBumperFixtureDef = FixtureDef(pegBumperShape); - fixturesDef.add(pegBumperFixtureDef); final topCircleShape = CircleShape()..radius = 0.7; - topCircleShape.position.setValues(9.71 * direction, 4.95); + topCircleShape.position.setValues(9.71 * direction, -4.95); final topCircleFixtureDef = FixtureDef(topCircleShape); - fixturesDef.add(topCircleFixtureDef); final innerEdgeShape = EdgeShape() ..set( - Vector2(9.01 * direction, 5.35), - Vector2(5.29 * direction, -0.95), + Vector2(9.01 * direction, -5.35), + Vector2(5.29 * direction, 0.95), ); final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape); - fixturesDef.add(innerEdgeShapeFixtureDef); final outerEdgeShape = EdgeShape() ..set( - Vector2(10.41 * direction, 4.75), - Vector2(3.79 * direction, -5.95), + Vector2(10.41 * direction, -4.75), + Vector2(3.79 * direction, 5.95), ); final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape); - fixturesDef.add(outerEdgeShapeFixtureDef); final upperArcShape = ArcShape( - center: Vector2(0.09 * direction, 2.15), + center: Vector2(0.09 * direction, -2.15), arcRadius: 6.1, angle: arcsAngle, rotation: arcsRotation, ); final upperArcFixtureDef = FixtureDef(upperArcShape); - fixturesDef.add(upperArcFixtureDef); final lowerArcShape = ArcShape( - center: Vector2(0.09 * direction, -3.35), + center: Vector2(0.09 * direction, 3.35), arcRadius: 4.5, angle: arcsAngle, rotation: arcsRotation, ); final lowerArcFixtureDef = FixtureDef(lowerArcShape); - fixturesDef.add(lowerArcFixtureDef); final bottomRectangle = PolygonShape() ..setAsBox( 6.8, 2, - Vector2(-6.3 * direction, -5.85), + Vector2(-6.3 * direction, 5.85), 0, ); final bottomRectangleFixtureDef = FixtureDef(bottomRectangle); - fixturesDef.add(bottomRectangleFixtureDef); - return fixturesDef; - } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - await add(_BaseboardSpriteComponent(side: _side)); + return [ + pegBumperFixtureDef, + topCircleFixtureDef, + innerEdgeShapeFixtureDef, + outerEdgeShapeFixtureDef, + upperArcFixtureDef, + lowerArcFixtureDef, + bottomRectangleFixtureDef, + ]; } @override Body createBody() { const angle = 37.1 * (math.pi / 180); - - final bodyDef = BodyDef() - ..position = initialPosition - ..angle = _side.isLeft ? -angle : angle; + final bodyDef = BodyDef( + position: initialPosition, + angle: -angle * _side.direction, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); @@ -102,21 +99,26 @@ class Baseboard extends BodyComponent with InitialPosition { } class _BaseboardSpriteComponent extends SpriteComponent with HasGameRef { - _BaseboardSpriteComponent({required BoardSide side}) : _side = side; + _BaseboardSpriteComponent({required BoardSide side}) + : _side = side, + super( + anchor: Anchor.center, + position: Vector2(0.4 * -side.direction, 0), + ); final BoardSide _side; @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - (_side.isLeft) - ? Assets.images.baseboard.left.keyName - : Assets.images.baseboard.right.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + (_side.isLeft) + ? Assets.images.baseboard.left.keyName + : Assets.images.baseboard.right.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(0.4 * -_side.direction, 0); - anchor = Anchor.center; } } diff --git a/packages/pinball_components/lib/src/components/board_dimensions.dart b/packages/pinball_components/lib/src/components/board_dimensions.dart index b4db8c3c..3d547996 100644 --- a/packages/pinball_components/lib/src/components/board_dimensions.dart +++ b/packages/pinball_components/lib/src/components/board_dimensions.dart @@ -14,7 +14,7 @@ class BoardDimensions { static final bounds = Rect.fromCenter( center: Offset.zero, width: size.x, - height: -size.y, + height: size.y, ); /// 3D perspective angle of the board in radians. @@ -22,8 +22,4 @@ class BoardDimensions { /// Factor the board shrinks by from the closest point to the farthest. static const perspectiveShrinkFactor = 0.63; - - /// Board height based on the [perspectiveShrinkFactor]. - static final shrinkAdjustedHeight = - (1 / (1 - perspectiveShrinkFactor)) * size.y; } diff --git a/packages/pinball_components/lib/src/components/board_side.dart b/packages/pinball_components/lib/src/components/board_side.dart index ac530567..9f6be71c 100644 --- a/packages/pinball_components/lib/src/components/board_side.dart +++ b/packages/pinball_components/lib/src/components/board_side.dart @@ -1,12 +1,8 @@ -// ignore_for_file: comment_references -// TODO(alestiago): Revisit ignore lint rule once Kicker is moved to this -// package. - import 'package:pinball_components/pinball_components.dart'; /// Indicates a side of the board. /// -/// Usually used to position or mirror elements of a [PinballGame]; such as a +/// Usually used to position or mirror elements of a pinball game; such as a /// [Flipper] or [Kicker]. enum BoardSide { /// The left side of the board. diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 16c0f7b7..2ba5ab3f 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -1,20 +1,21 @@ -// ignore_for_file: avoid_renaming_method_parameters - import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template boundaries} /// A [Blueprint] which creates the [_BottomBoundary] and [_OuterBoundary]. ///{@endtemplate boundaries} -class Boundaries extends Forge2DBlueprint { - @override - void build(_) { - final bottomBoundary = _BottomBoundary(); - final outerBoundary = _OuterBoundary(); - - addAll([bottomBoundary, outerBoundary]); - } +class Boundaries extends Blueprint { + /// {@macro boundaries} + Boundaries() + : super( + components: [ + _BottomBoundary(), + _OuterBoundary(), + _OuterBottomBoundarySpriteComponent(), + ], + ); } /// {@template bottom_boundary} @@ -23,32 +24,33 @@ class Boundaries extends Forge2DBlueprint { /// {@endtemplate bottom_boundary} class _BottomBoundary extends BodyComponent with InitialPosition { /// {@macro bottom_boundary} - _BottomBoundary() : super(priority: 2); + _BottomBoundary() + : super( + renderBody: false, + priority: RenderPriority.bottomBoundary, + children: [_BottomBoundarySpriteComponent()], + ); List _createFixtureDefs() { - final fixturesDefs = []; - final bottomLeftCurve = BezierCurveShape( controlPoints: [ - Vector2(-43.9, -41.8), - Vector2(-35.7, -43), - Vector2(-19.9, -51), + Vector2(-43.9, 41.8), + Vector2(-35.7, 43), + Vector2(-19.9, 51), ], ); final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurve); - fixturesDefs.add(bottomLeftCurveFixtureDef); final bottomRightCurve = BezierCurveShape( controlPoints: [ - Vector2(31.8, -44.8), - Vector2(21.95, -47.7), - Vector2(12.3, -52.1), + Vector2(31.8, 44.8), + Vector2(21.95, 47.7), + Vector2(12.3, 52.1), ], ); final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurve); - fixturesDefs.add(bottomRightCurveFixtureDef); - return fixturesDefs; + return [bottomLeftCurveFixtureDef, bottomRightCurveFixtureDef]; } @override @@ -59,26 +61,25 @@ class _BottomBoundary extends BodyComponent with InitialPosition { return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - await add(_BottomBoundarySpriteComponent()); - } } class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { + _BottomBoundarySpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-5.4, 55.6), + ); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.boundary.bottom.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.boundary.bottom.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-5.4, 55.8); } } @@ -88,38 +89,42 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { /// {@endtemplate outer_boundary} class _OuterBoundary extends BodyComponent with InitialPosition { /// {@macro outer_boundary} - _OuterBoundary() : super(priority: -1); + _OuterBoundary() + : super( + renderBody: false, + priority: RenderPriority.outerBoundary, + children: [_OuterBoundarySpriteComponent()], + ); List _createFixtureDefs() { - final fixturesDefs = []; - final topWall = EdgeShape() ..set( - Vector2(3.6, 70.2), - Vector2(-14.1, 70.2), + Vector2(3.6, -70.2), + Vector2(-14.1, -70.2), ); final topWallFixtureDef = FixtureDef(topWall); - fixturesDefs.add(topWallFixtureDef); final topLeftCurve = BezierCurveShape( controlPoints: [ - Vector2(-32.3, 57.2), - Vector2(-31.5, 69.9), - Vector2(-14.1, 70.2), + Vector2(-32.3, -57.2), + Vector2(-31.5, -69.9), + Vector2(-14.1, -70.2), ], ); final topLeftCurveFixtureDef = FixtureDef(topLeftCurve); - fixturesDefs.add(topLeftCurveFixtureDef); final leftWall = EdgeShape() ..set( - Vector2(-32.3, 57.2), - Vector2(-43.9, -41.8), + Vector2(-32.3, -57.2), + Vector2(-43.9, 41.8), ); final leftWallFixtureDef = FixtureDef(leftWall); - fixturesDefs.add(leftWallFixtureDef); - return fixturesDefs; + return [ + topWallFixtureDef, + topLeftCurveFixtureDef, + leftWallFixtureDef, + ]; } @override @@ -130,25 +135,46 @@ class _OuterBoundary extends BodyComponent with InitialPosition { return body; } +} + +class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { + _OuterBoundarySpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(0, -7.8), + ); @override Future onLoad() async { await super.onLoad(); - renderBody = false; - await add(_OuterBoundarySpriteComponent()); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.boundary.outer.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; } } -class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { +class _OuterBottomBoundarySpriteComponent extends SpriteComponent + with HasGameRef { + _OuterBottomBoundarySpriteComponent() + : super( + priority: RenderPriority.outerBottomBoundary, + anchor: Anchor.center, + position: Vector2(0, 71), + ); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.boundary.outer.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.boundary.outerBottom.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-0.2, -1.4); } } diff --git a/packages/pinball_components/lib/src/components/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart index 327e14f5..7846f140 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino.dart @@ -12,10 +12,12 @@ import 'package:pinball_components/pinball_components.dart'; /// {@endtemplate} class ChromeDino extends BodyComponent with InitialPosition { /// {@macro chrome_dino} - ChromeDino() { - // TODO(alestiago): Remove once sprites are defined. - paint = Paint()..color = Colors.blue; - } + ChromeDino() + : super( + // TODO(alestiago): Remove once sprites are defined. + paint: Paint()..color = Colors.blue, + priority: RenderPriority.dino, + ); /// The size of the dinosaur mouth. static final size = Vector2(5, 2.5); @@ -54,12 +56,14 @@ class ChromeDino extends BodyComponent with InitialPosition { // TODO(alestiago): Subject to change when sprites are added. final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); - final fixtureDef = FixtureDef(box) - ..shape = box - ..density = 999 - ..friction = 0.3 - ..restitution = 0.1 - ..isSensor = true; + final fixtureDef = FixtureDef( + box, + density: 999, + friction: 0.3, + restitution: 0.1, + isSensor: true, + ); + fixtureDefs.add(fixtureDef); // FIXME(alestiago): Investigate why adding these fixtures is considered as @@ -93,10 +97,11 @@ class ChromeDino extends BodyComponent with InitialPosition { @override Body createBody() { - final bodyDef = BodyDef() - ..gravityScale = 0 - ..position = initialPosition - ..type = BodyType.dynamic; + final bodyDef = BodyDef( + position: initialPosition, + type: BodyType.dynamic, + gravityScale: Vector2.zero(), + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); @@ -111,10 +116,7 @@ class ChromeDino extends BodyComponent with InitialPosition { class _ChromeDinoAnchor extends JointAnchor { /// {@macro flipper_anchor} _ChromeDinoAnchor() { - initialPosition = Vector2( - ChromeDino.size.x / 2, - 0, - ); + initialPosition = Vector2(ChromeDino.size.x / 2, 0); } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 8dca7549..5d6f5744 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,4 +1,5 @@ -export 'backboard.dart'; +export 'alien_bumper/alien_bumper.dart'; +export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; export 'board_dimensions.dart'; @@ -6,23 +7,28 @@ export 'board_side.dart'; export 'boundaries.dart'; export 'camera_zoom.dart'; export 'chrome_dino.dart'; -export 'dash_nest_bumper.dart'; +export 'dash_animatronic.dart'; +export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; -export 'flutter_sign_post.dart'; -export 'google_letter.dart'; +export 'google_letter/google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; export 'kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; -export 'ramp_opening.dart'; +export 'layer_sensor.dart'; +export 'plunger.dart'; +export 'render_priority.dart'; +export 'rocket.dart'; export 'score_text.dart'; export 'shapes/shapes.dart'; +export 'signpost.dart'; export 'slingshot.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; -export 'sparky_bumper.dart'; +export 'sparky_animatronic.dart'; +export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_animatronic.dart b/packages/pinball_components/lib/src/components/dash_animatronic.dart new file mode 100644 index 00000000..faa604e9 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_animatronic.dart @@ -0,0 +1,45 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template dash_animatronic} +/// Animated Dash that sits on top of the [DashNestBumper.main]. +/// {@endtemplate} +class DashAnimatronic extends SpriteAnimationComponent with HasGameRef { + /// {@macro dash_animatronic} + DashAnimatronic() + : super( + anchor: Anchor.center, + playing: false, + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.dash.animatronic.keyName, + ); + + const amountPerRow = 13; + const amountPerColumn = 6; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + loop: false, + ), + )..onComplete = () { + animation?.reset(); + playing = false; + }; + } +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart deleted file mode 100644 index 447b4156..00000000 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'dart:math' as math; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template dash_nest_bumper} -/// Bumper with a nest appearance. -/// {@endtemplate} -abstract class DashNestBumper extends BodyComponent with InitialPosition { - /// {@macro dash_nest_bumper} - DashNestBumper._({ - required String activeAssetPath, - required String inactiveAssetPath, - required SpriteComponent spriteComponent, - }) : _activeAssetPath = activeAssetPath, - _inactiveAssetPath = inactiveAssetPath, - _spriteComponent = spriteComponent; - - final String _activeAssetPath; - late final Sprite _activeSprite; - final String _inactiveAssetPath; - late final Sprite _inactiveSprite; - final SpriteComponent _spriteComponent; - - Future _loadSprites() async { - // TODO(alestiago): I think ideally we would like to do: - // Sprite(path).load so we don't require to store the activeAssetPath and - // the inactive assetPath. - _inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath); - _activeSprite = await gameRef.loadSprite(_activeAssetPath); - } - - /// Activates the [DashNestBumper]. - void activate() { - _spriteComponent - ..sprite = _activeSprite - ..size = _activeSprite.originalSize / 10; - } - - /// Deactivates the [DashNestBumper]. - void deactivate() { - _spriteComponent - ..sprite = _inactiveSprite - ..size = _inactiveSprite.originalSize / 10; - } - - @override - Future onLoad() async { - await super.onLoad(); - await _loadSprites(); - - // TODO(erickzanardo): Look into using onNewState instead. - // Currently doing: onNewState(gameRef.read()) will throw an - // `Exception: build context is not available yet` - deactivate(); - await add(_spriteComponent); - } -} - -/// {@macro dash_nest_bumper} -class BigDashNestBumper extends DashNestBumper { - /// {@macro dash_nest_bumper} - BigDashNestBumper() - : super._( - activeAssetPath: Assets.images.dashBumper.main.active.keyName, - inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName, - spriteComponent: SpriteComponent( - anchor: Anchor.center, - position: Vector2(0, -0.3), - ), - ); - - @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 5.1, - minorRadius: 3.75, - )..rotate(math.pi / 2.1); - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@macro dash_nest_bumper} -class SmallDashNestBumper extends DashNestBumper { - /// {@macro dash_nest_bumper} - SmallDashNestBumper._({ - required String activeAssetPath, - required String inactiveAssetPath, - required SpriteComponent spriteComponent, - }) : super._( - activeAssetPath: activeAssetPath, - inactiveAssetPath: inactiveAssetPath, - spriteComponent: spriteComponent, - ); - - /// {@macro dash_nest_bumper} - SmallDashNestBumper.a() - : this._( - activeAssetPath: Assets.images.dashBumper.a.active.keyName, - inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName, - spriteComponent: SpriteComponent( - anchor: Anchor.center, - position: Vector2(0.35, -1.2), - ), - ); - - /// {@macro dash_nest_bumper} - SmallDashNestBumper.b() - : this._( - activeAssetPath: Assets.images.dashBumper.b.active.keyName, - inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName, - spriteComponent: SpriteComponent( - anchor: Anchor.center, - position: Vector2(0.35, -1.2), - ), - ); - - @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 3, - minorRadius: 2.25, - )..rotate(math.pi / 2); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..839cbd67 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'dash_nest_bumper_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart new file mode 100644 index 00000000..829229e4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class DashNestBumperBallContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart new file mode 100644 index 00000000..8fc6b157 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart @@ -0,0 +1,19 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'dash_nest_bumper_state.dart'; + +class DashNestBumperCubit extends Cubit { + DashNestBumperCubit() : super(DashNestBumperState.inactive); + + /// Event added when the bumper contacts with a ball. + void onBallContacted() { + emit(DashNestBumperState.active); + } + + /// Event added when the bumper should return to its initial configuration. + void onReset() { + emit(DashNestBumperState.inactive); + } +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart new file mode 100644 index 00000000..c169069f --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart @@ -0,0 +1,10 @@ +part of 'dash_nest_bumper_cubit.dart'; + +/// Indicates the [DashNestBumperCubit]'s current state. +enum DashNestBumperState { + /// A lit up bumper. + active, + + /// A dimmed bumper. + inactive, +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart new file mode 100644 index 00000000..82ec0036 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart @@ -0,0 +1,154 @@ +import 'dart:math' as math; + +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/dash_nest_bumper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/dash_nest_bumper_cubit.dart'; + +/// {@template dash_nest_bumper} +/// Bumper with a nest appearance. +/// {@endtemplate} +class DashNestBumper extends BodyComponent with InitialPosition { + /// {@macro dash_nest_bumper} + DashNestBumper._({ + required double majorRadius, + required double minorRadius, + required String activeAssetPath, + required String inactiveAssetPath, + required Vector2 spritePosition, + Iterable? children, + required this.bloc, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + super( + renderBody: false, + children: [ + _DashNestBumperSpriteGroupComponent( + activeAssetPath: activeAssetPath, + inactiveAssetPath: inactiveAssetPath, + position: spritePosition, + current: bloc.state, + ), + DashNestBumperBallContactBehavior(), + ...?children, + ], + ); + + /// {@macro dash_nest_bumper} + DashNestBumper.main({ + Iterable? children, + }) : this._( + majorRadius: 5.1, + minorRadius: 3.75, + activeAssetPath: Assets.images.dash.bumper.main.active.keyName, + inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, + spritePosition: Vector2(0, -0.3), + children: children, + bloc: DashNestBumperCubit(), + ); + + /// {@macro dash_nest_bumper} + DashNestBumper.a({ + Iterable? children, + }) : this._( + majorRadius: 3, + minorRadius: 2.5, + activeAssetPath: Assets.images.dash.bumper.a.active.keyName, + inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, + spritePosition: Vector2(0.35, -1.2), + children: children, + bloc: DashNestBumperCubit(), + ); + + /// {@macro dash_nest_bumper} + DashNestBumper.b({ + Iterable? children, + }) : this._( + majorRadius: 3, + minorRadius: 2.5, + activeAssetPath: Assets.images.dash.bumper.b.active.keyName, + inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, + spritePosition: Vector2(0.35, -1.2), + children: children, + bloc: DashNestBumperCubit(), + ); + + /// Creates an [DashNestBumper] without any children. + /// + /// This can be used for testing [DashNestBumper]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + DashNestBumper.test({required this.bloc}) + : _majorRadius = 3, + _minorRadius = 2.5; + + final double _majorRadius; + final double _minorRadius; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final DashNestBumperCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(math.pi / 1.9); + final fixtureDef = FixtureDef(shape, restitution: 4); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _DashNestBumperSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _DashNestBumperSpriteGroupComponent({ + required String activeAssetPath, + required String inactiveAssetPath, + required Vector2 position, + required DashNestBumperState current, + }) : _activeAssetPath = activeAssetPath, + _inactiveAssetPath = inactiveAssetPath, + super( + anchor: Anchor.center, + position: position, + current: current, + ); + + final String _activeAssetPath; + final String _inactiveAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + DashNestBumperState.active: + Sprite(gameRef.images.fromCache(_activeAssetPath)), + DashNestBumperState.inactive: + Sprite(gameRef.images.fromCache(_inactiveAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index dc5b7a26..0e0e2efa 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -1,26 +1,23 @@ -// ignore_for_file: comment_references, avoid_renaming_method_parameters - import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template dinowalls} /// A [Blueprint] which creates walls for the [ChromeDino]. /// {@endtemplate} -class DinoWalls extends Forge2DBlueprint { +class DinoWalls extends Blueprint { /// {@macro dinowalls} - DinoWalls(); - - @override - void build(_) { - addAll([ - _DinoTopWall(), - _DinoBottomWall(), - ]); - } + DinoWalls() + : super( + components: [ + _DinoTopWall(), + _DinoBottomWall(), + ], + ); } /// {@template dino_top_wall} @@ -28,63 +25,71 @@ class DinoWalls extends Forge2DBlueprint { /// {@endtemplate} class _DinoTopWall extends BodyComponent with InitialPosition { ///{@macro dino_top_wall} - _DinoTopWall() : super(priority: 1); + _DinoTopWall() + : super( + priority: RenderPriority.dinoTopWall, + children: [_DinoTopWallSpriteComponent()], + renderBody: false, + ); List _createFixtureDefs() { - final fixturesDef = []; - final topStraightShape = EdgeShape() ..set( - Vector2(29.5, 35.1), - Vector2(28.4, 35.1), + Vector2(28.65, -35.1), + Vector2(29.5, -35.1), ); final topStraightFixtureDef = FixtureDef(topStraightShape); - fixturesDef.add(topStraightFixtureDef); final topCurveShape = BezierCurveShape( controlPoints: [ topStraightShape.vertex1, - Vector2(17.4, 26.38), - Vector2(25.5, 20.7), + Vector2(18.8, -27), + Vector2(26.6, -21), ], ); - fixturesDef.add(FixtureDef(topCurveShape)); + final topCurveFixtureDef = FixtureDef(topCurveShape); final middleCurveShape = BezierCurveShape( controlPoints: [ topCurveShape.vertices.last, - Vector2(27.8, 20.1), - Vector2(26.8, 19.5), + Vector2(27.8, -20.1), + Vector2(26.8, -19.5), ], ); - fixturesDef.add(FixtureDef(middleCurveShape)); + final middleCurveFixtureDef = FixtureDef(middleCurveShape); final bottomCurveShape = BezierCurveShape( controlPoints: [ middleCurveShape.vertices.last, - Vector2(21.15, 16), - Vector2(25.6, 15.2), + Vector2(23, -15), + Vector2(27, -15), ], ); - fixturesDef.add(FixtureDef(bottomCurveShape)); + final bottomCurveFixtureDef = FixtureDef(bottomCurveShape); final bottomStraightShape = EdgeShape() ..set( bottomCurveShape.vertices.last, - Vector2(31, 14.5), + Vector2(31, -14.5), ); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + topStraightFixtureDef, + topCurveFixtureDef, + middleCurveFixtureDef, + bottomCurveFixtureDef, + bottomStraightFixtureDef, + ]; } @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + final body = world.createBody(bodyDef); _createFixtureDefs().forEach( (fixture) => body.createFixture( @@ -96,26 +101,20 @@ class _DinoTopWall extends BodyComponent with InitialPosition { return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_DinoTopWallSpriteComponent()); - } } class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.dinoLandTop.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.dinoLandTop.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22, -41.8); + position = Vector2(22.8, -38.9); } } @@ -124,97 +123,91 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { /// {@endtemplate} class _DinoBottomWall extends BodyComponent with InitialPosition { ///{@macro dino_top_wall} - _DinoBottomWall() : super(priority: 1); + _DinoBottomWall() + : super( + priority: RenderPriority.dinoBottomWall, + children: [_DinoBottomWallSpriteComponent()], + renderBody: false, + ); List _createFixtureDefs() { - final fixturesDef = []; + const restitution = 1.0; - final topStraightControlPoints = [ - Vector2(32.4, 8.3), - Vector2(25, 7.7), - ]; final topStraightShape = EdgeShape() ..set( - topStraightControlPoints.first, - topStraightControlPoints.last, + Vector2(32.4, -8.8), + Vector2(25, -7.7), ); - final topStraightFixtureDef = FixtureDef(topStraightShape); - fixturesDef.add(topStraightFixtureDef); + final topStraightFixtureDef = FixtureDef( + topStraightShape, + restitution: restitution, + ); - final topLeftCurveControlPoints = [ - topStraightControlPoints.last, - Vector2(21.8, 7), - Vector2(29.5, -13.8), - ]; final topLeftCurveShape = BezierCurveShape( - controlPoints: topLeftCurveControlPoints, + controlPoints: [ + topStraightShape.vertex2, + Vector2(21.8, -7), + Vector2(29.8, 13.8), + ], + ); + final topLeftCurveFixtureDef = FixtureDef( + topLeftCurveShape, + restitution: restitution, ); - fixturesDef.add(FixtureDef(topLeftCurveShape)); - final bottomLeftStraightControlPoints = [ - topLeftCurveControlPoints.last, - Vector2(31.8, -44.1), - ]; final bottomLeftStraightShape = EdgeShape() ..set( - bottomLeftStraightControlPoints.first, - bottomLeftStraightControlPoints.last, + topLeftCurveShape.vertices.last, + Vector2(31.9, 44.1), ); - final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape); - fixturesDef.add(bottomLeftStraightFixtureDef); + final bottomLeftStraightFixtureDef = FixtureDef( + bottomLeftStraightShape, + restitution: restitution, + ); - final bottomStraightControlPoints = [ - bottomLeftStraightControlPoints.last, - Vector2(37.8, -44.1), - ]; final bottomStraightShape = EdgeShape() ..set( - bottomStraightControlPoints.first, - bottomStraightControlPoints.last, + bottomLeftStraightShape.vertex2, + Vector2(37.8, 44.1), ); - final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); - fixturesDef.add(bottomStraightFixtureDef); + final bottomStraightFixtureDef = FixtureDef( + bottomStraightShape, + restitution: restitution, + ); - return fixturesDef; + return [ + topStraightFixtureDef, + topLeftCurveFixtureDef, + bottomLeftStraightFixtureDef, + bottomStraightFixtureDef, + ]; } @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); final body = world.createBody(bodyDef); - _createFixtureDefs().forEach( - (fixture) => body.createFixture( - fixture - ..restitution = 0.1 - ..friction = 0, - ), - ); + _createFixtureDefs().forEach(body.createFixture); return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_DinoBottomWallSpriteComponent()); - } } class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.dinoLandBottom.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.dinoLandBottom.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(23.8, -9.5); + position = Vector2(23.6, -9.5); } } diff --git a/packages/pinball_components/lib/src/components/fire_effect.dart b/packages/pinball_components/lib/src/components/fire_effect.dart index cf8c3707..14639527 100644 --- a/packages/pinball_components/lib/src/components/fire_effect.dart +++ b/packages/pinball_components/lib/src/components/fire_effect.dart @@ -73,7 +73,7 @@ class FireEffect extends ParticleSystemComponent { spreadTween.transform(random.nextDouble()), spreadTween.transform(random.nextDouble()), ); - final finalDirection = Vector2(direction.x, -direction.y) + spread; + final finalDirection = Vector2(direction.x, direction.y) + spread; final speed = finalDirection * (burstPower * 20); return AcceleratedParticle( diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index e57ba773..cb9ce3d4 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -13,7 +13,10 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// {@macro flipper} Flipper({ required this.side, - }); + }) : super( + renderBody: false, + children: [_FlipperSpriteComponent(side: side)], + ); /// The size of the [Flipper]. static final size = Vector2(13.5, 4.3); @@ -32,13 +35,13 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// Applies downward linear velocity to the [Flipper], moving it to its /// resting position. void moveDown() { - body.linearVelocity = Vector2(0, -_speed); + 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); + body.linearVelocity = Vector2(0, -_speed); } /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. @@ -55,7 +58,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { } List _createFixtureDefs() { - final fixturesDef = []; final direction = side.direction; final assetShadow = Flipper.size.x * 0.012 * -direction; @@ -72,7 +74,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { 0, ); final bigCircleFixtureDef = FixtureDef(bigCircleShape); - fixturesDef.add(bigCircleFixtureDef); final smallCircleShape = CircleShape()..radius = size.y * 0.23; smallCircleShape.position.setValues( @@ -82,7 +83,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { 0, ); final smallCircleFixtureDef = FixtureDef(smallCircleShape); - fixturesDef.add(smallCircleFixtureDef); final trapeziumVertices = side.isLeft ? [ @@ -98,29 +98,34 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { Vector2(smallCircleShape.position.x, -smallCircleShape.radius), ]; final trapezium = PolygonShape()..set(trapeziumVertices); - final trapeziumFixtureDef = FixtureDef(trapezium) - ..density = 50.0 // TODO(alestiago): Use a proper density. - ..friction = .1; // TODO(alestiago): Use a proper friction. - fixturesDef.add(trapeziumFixtureDef); + final trapeziumFixtureDef = FixtureDef( + trapezium, + density: 50, // TODO(alestiago): Use a proper density. + friction: .1, // TODO(alestiago): Use a proper friction. + ); - return fixturesDef; + return [ + bigCircleFixtureDef, + smallCircleFixtureDef, + trapeziumFixtureDef, + ]; } @override Future onLoad() async { await super.onLoad(); - renderBody = false; await _anchorToJoint(); - await add(_FlipperSpriteComponent(side: side)); } @override Body createBody() { - final bodyDef = BodyDef() - ..position = initialPosition - ..gravityScale = 0 - ..type = BodyType.dynamic; + final bodyDef = BodyDef( + position: initialPosition, + gravityScale: Vector2.zero(), + type: BodyType.dynamic, + ); + final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); @@ -138,21 +143,24 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { } class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { - _FlipperSpriteComponent({required BoardSide side}) : _side = side; + _FlipperSpriteComponent({required BoardSide side}) + : _side = side, + super(anchor: Anchor.center); final BoardSide _side; @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - (_side.isLeft) - ? Assets.images.flipper.left.keyName - : Assets.images.flipper.right.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + (_side.isLeft) + ? Assets.images.flipper.left.keyName + : Assets.images.flipper.right.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; } } @@ -169,7 +177,7 @@ class _FlipperAnchor extends JointAnchor { initialPosition = Vector2( (Flipper.size.x * flipper.side.direction) / 2 - (1.65 * flipper.side.direction), - 0.15, + -0.15, ); } } diff --git a/packages/pinball_components/lib/src/components/flutter_sign_post.dart b/packages/pinball_components/lib/src/components/flutter_sign_post.dart deleted file mode 100644 index 070fa316..00000000 --- a/packages/pinball_components/lib/src/components/flutter_sign_post.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template flutter_sign_post} -/// A sign, found in the Flutter Forest. -/// {@endtemplate} -class FlutterSignPost extends BodyComponent with InitialPosition { - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_FlutterSignPostSpriteComponent()); - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 0.25; - final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -class _FlutterSignPostSpriteComponent extends SpriteComponent with HasGameRef { - @override - Future onLoad() async { - await super.onLoad(); - - final sprite = await gameRef.loadSprite( - Assets.images.flutterSignPost.keyName, - ); - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.bottomCenter; - position = Vector2(0.65, 0.45); - } -} diff --git a/packages/pinball_components/lib/src/components/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter.dart deleted file mode 100644 index 9e9e2dec..00000000 --- a/packages/pinball_components/lib/src/components/google_letter.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame/effects.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template google_letter} -/// Circular sensor that represents a letter in "GOOGLE" for a given index. -/// {@endtemplate} -class GoogleLetter extends BodyComponent with InitialPosition { - /// {@macro google_letter} - GoogleLetter(int index) - : _sprite = _GoogleLetterSprite( - _GoogleLetterSprite.spritePaths[index], - ); - - final _GoogleLetterSprite _sprite; - - /// Activates this [GoogleLetter]. - // TODO(alestiago): Improve doc comment once activate and deactivate - // are implemented with the actual assets. - Future activate() => _sprite.activate(); - - /// Deactivates this [GoogleLetter]. - Future deactivate() => _sprite.deactivate(); - - @override - Future onLoad() async { - await super.onLoad(); - await add(_sprite); - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 1.85; - final fixtureDef = FixtureDef(shape)..isSensor = true; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -class _GoogleLetterSprite extends SpriteComponent with HasGameRef { - _GoogleLetterSprite(String path) : _path = path; - - static final spritePaths = [ - Assets.images.googleWord.letter1.keyName, - Assets.images.googleWord.letter2.keyName, - Assets.images.googleWord.letter3.keyName, - Assets.images.googleWord.letter4.keyName, - Assets.images.googleWord.letter5.keyName, - Assets.images.googleWord.letter6.keyName, - ]; - - final String _path; - - // TODO(alestiago): Correctly implement activate and deactivate once the - // assets are provided. - Future activate() async { - await add( - _GoogleLetterColorEffect(color: Colors.green), - ); - } - - Future deactivate() async { - await add( - _GoogleLetterColorEffect(color: Colors.red), - ); - } - - @override - Future onLoad() async { - await super.onLoad(); - - final sprite = await gameRef.loadSprite(_path); - this.sprite = sprite; - // TODO(alestiago): Size correctly once the assets are provided. - size = sprite.originalSize / 5; - anchor = Anchor.center; - } -} - -class _GoogleLetterColorEffect extends ColorEffect { - _GoogleLetterColorEffect({ - required Color color, - }) : super( - color, - const Offset(0, 1), - EffectController(duration: 0.25), - ); -} diff --git a/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart new file mode 100644 index 00000000..df54c1f4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'google_letter_ball_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart new file mode 100644 index 00000000..c3f0423e --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class GoogleLetterBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart new file mode 100644 index 00000000..a352e98d --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'google_letter_state.dart'; + +class GoogleLetterCubit extends Cubit { + GoogleLetterCubit() : super(GoogleLetterState.inactive); + + void onBallContacted() { + emit(GoogleLetterState.active); + } + + void onReset() { + emit(GoogleLetterState.inactive); + } +} diff --git a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart new file mode 100644 index 00000000..e1339320 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart @@ -0,0 +1,10 @@ +part of 'google_letter_cubit.dart'; + +/// Indicates the [GoogleLetterCubit]'s current state. +enum GoogleLetterState { + /// A lit up letter. + active, + + /// A dimmed letter. + inactive, +} diff --git a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart new file mode 100644 index 00000000..63207e01 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart @@ -0,0 +1,91 @@ +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/google_letter/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/google_letter_cubit.dart'; + +/// {@template google_letter} +/// Circular sensor that represents a letter in "GOOGLE" for a given index. +/// {@endtemplate} +class GoogleLetter extends BodyComponent with InitialPosition { + /// {@macro google_letter} + GoogleLetter( + int index, + ) : bloc = GoogleLetterCubit(), + super( + children: [ + GoogleLetterBallContactBehavior(), + _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]) + ], + ); + + /// Creates a [GoogleLetter] without any children. + /// + /// This can be used for testing [GoogleLetter]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + GoogleLetter.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final GoogleLetterCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = CircleShape()..radius = 1.85; + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _GoogleLetterSprite extends SpriteComponent + with HasGameRef, ParentIsA { + _GoogleLetterSprite(String path) + : _path = path, + super(anchor: Anchor.center); + + static final spritePaths = [ + Assets.images.googleWord.letter1.keyName, + Assets.images.googleWord.letter2.keyName, + Assets.images.googleWord.letter3.keyName, + Assets.images.googleWord.letter4.keyName, + Assets.images.googleWord.letter5.keyName, + Assets.images.googleWord.letter6.keyName, + ]; + + final String _path; + + @override + Future onLoad() async { + await super.onLoad(); + // TODO(alisonryan2002): Make SpriteGroupComponent. + // parent.bloc.stream.listen(); + + // TODO(alestiago): Used cached assets. + final sprite = await gameRef.loadSprite(_path); + this.sprite = sprite; + // TODO(alestiago): Size correctly once the assets are provided. + size = sprite.originalSize / 5; + } +} diff --git a/packages/pinball_components/lib/src/components/joint_anchor.dart b/packages/pinball_components/lib/src/components/joint_anchor.dart index 7ca75ba0..63875d40 100644 --- a/packages/pinball_components/lib/src/components/joint_anchor.dart +++ b/packages/pinball_components/lib/src/components/joint_anchor.dart @@ -22,7 +22,9 @@ class JointAnchor extends BodyComponent with InitialPosition { @override Body createBody() { - final bodyDef = BodyDef()..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + ); return world.createBody(bodyDef); } } diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index de009595..12cd638d 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -16,7 +16,11 @@ class Kicker extends BodyComponent with InitialPosition { /// {@macro kicker} Kicker({ required BoardSide side, - }) : _side = side; + }) : _side = side, + super( + children: [_KickerSpriteComponent(side: side)], + renderBody: false, + ); /// The size of the [Kicker] body. static final Vector2 size = Vector2(4.4, 15); @@ -34,16 +38,16 @@ class Kicker extends BodyComponent with InitialPosition { const quarterPi = math.pi / 4; final upperCircle = CircleShape()..radius = 1.6; - upperCircle.position.setValues(0, -upperCircle.radius / 2); - final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0; + upperCircle.position.setValues(0, upperCircle.radius / 2); + final upperCircleFixtureDef = FixtureDef(upperCircle); fixturesDefs.add(upperCircleFixtureDef); final lowerCircle = CircleShape()..radius = 1.6; lowerCircle.position.setValues( size.x * -direction, - -size.y - 0.8, + size.y + 0.8, ); - final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0; + final lowerCircleFixtureDef = FixtureDef(lowerCircle); fixturesDefs.add(lowerCircleFixtureDef); final wallFacingEdge = EdgeShape() @@ -53,9 +57,9 @@ class Kicker extends BodyComponent with InitialPosition { upperCircle.radius * direction, 0, ), - Vector2(2.5 * direction, -size.y + 2), + Vector2(2.5 * direction, size.y - 2), ); - final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0; + final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge); fixturesDefs.add(wallFacingLineFixtureDef); final bottomEdge = EdgeShape() @@ -64,10 +68,10 @@ class Kicker extends BodyComponent with InitialPosition { lowerCircle.position + Vector2( lowerCircle.radius * math.cos(quarterPi) * direction, - -lowerCircle.radius * math.sin(quarterPi), + lowerCircle.radius * math.sin(quarterPi), ), ); - final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0; + final bottomLineFixtureDef = FixtureDef(bottomEdge); fixturesDefs.add(bottomLineFixtureDef); final bouncyEdge = EdgeShape() @@ -75,19 +79,20 @@ class Kicker extends BodyComponent with InitialPosition { upperCircle.position + Vector2( upperCircle.radius * math.cos(quarterPi) * -direction, - upperCircle.radius * math.sin(quarterPi), + -upperCircle.radius * math.sin(quarterPi), ), lowerCircle.position + Vector2( lowerCircle.radius * math.cos(quarterPi) * -direction, - lowerCircle.radius * math.sin(quarterPi), + -lowerCircle.radius * math.sin(quarterPi), ), ); - final bouncyFixtureDef = FixtureDef(bouncyEdge) + final bouncyFixtureDef = FixtureDef( + bouncyEdge, // TODO(alestiago): Play with restitution value once game is bundled. - ..restitution = 10.0 - ..friction = 0; + restitution: 10, + ); fixturesDefs.add(bouncyFixtureDef); // TODO(alestiago): Evaluate if there is value on centering the fixtures. @@ -97,7 +102,7 @@ class Kicker extends BodyComponent with InitialPosition { lowerCircle.position + Vector2( lowerCircle.radius * math.cos(quarterPi) * -direction, - -lowerCircle.radius * math.sin(quarterPi), + lowerCircle.radius * math.sin(quarterPi), ), wallFacingEdge.vertex2, ], @@ -111,19 +116,14 @@ class Kicker extends BodyComponent with InitialPosition { @override Body createBody() { - final bodyDef = BodyDef()..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - await add(_KickerSpriteComponent(side: _side)); - } } class _KickerSpriteComponent extends SpriteComponent with HasGameRef { @@ -135,6 +135,7 @@ class _KickerSpriteComponent extends SpriteComponent with HasGameRef { Future onLoad() async { await super.onLoad(); + // TODO(alestiago): Used cached asset. final sprite = await gameRef.loadSprite( (_side.isLeft) ? Assets.images.kicker.left.keyName diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index deaa3941..13f063b6 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -5,100 +5,94 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template launch_ramp} /// A [Blueprint] which creates the [_LaunchRampBase] and /// [_LaunchRampForegroundRailing]. /// {@endtemplate} -class LaunchRamp extends Forge2DBlueprint { - /// Base priority for [Ball] while inside [LaunchRamp]. - static const ballPriorityInsideRamp = 0; - - @override - void build(_) { - addAllContactCallback([ - RampOpeningBallContactCallback<_LaunchRampExit>(), - ]); - - final launchRampBase = _LaunchRampBase()..layer = Layer.launcher; - - final launchRampForegroundRailing = _LaunchRampForegroundRailing() - ..layer = Layer.launcher; - - final launchRampExit = _LaunchRampExit(rotation: math.pi / 2) - ..initialPosition = Vector2(1.8, 34.2) - ..layer = Layer.opening - ..renderBody = false; - - addAll([ - launchRampBase, - launchRampForegroundRailing, - launchRampExit, - ]); - } +class LaunchRamp extends Blueprint { + /// {@macro launch_ramp} + LaunchRamp() + : super( + components: [ + _LaunchRampBase(), + _LaunchRampForegroundRailing(), + _LaunchRampExit()..initialPosition = Vector2(0.6, -34), + _LaunchRampCloseWall()..initialPosition = Vector2(4, -69.5), + ], + ); } /// {@template launch_ramp_base} /// Ramp the [Ball] is launched from at the beginning of each ball life. /// {@endtemplate} -class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { +class _LaunchRampBase extends BodyComponent with Layered { /// {@macro launch_ramp_base} _LaunchRampBase() : super( - priority: LaunchRamp.ballPriorityInsideRamp - 1, + priority: RenderPriority.launchRamp, + renderBody: false, + children: [ + _LaunchRampBackgroundRailingSpriteComponent(), + _LaunchRampBaseSpriteComponent(), + ], ) { layer = Layer.launcher; } + // TODO(ruimiguel): final asset differs slightly from the current shape. We + // need to fix shape with correct vertices, but right now merge them to have + // final assets at game and not be blocked. List _createFixtureDefs() { final fixturesDef = []; final rightStraightShape = EdgeShape() ..set( - Vector2(31.4, 61.4), - Vector2(46.5, -68.4), + Vector2(31.4, -61.4), + Vector2(46.5, 68.4), ); final rightStraightFixtureDef = FixtureDef(rightStraightShape); fixturesDef.add(rightStraightFixtureDef); final leftStraightShape = EdgeShape() ..set( - Vector2(27.8, 61.4), - Vector2(41.5, -68.4), + Vector2(27.8, -61.4), + Vector2(41.5, 68.4), ); final leftStraightFixtureDef = FixtureDef(leftStraightShape); fixturesDef.add(leftStraightFixtureDef); final topCurveShape = ArcShape( - center: Vector2(20.5, 61.1), + center: Vector2(20.5, -61.1), arcRadius: 11, angle: 1.6, - rotation: -1.65, + rotation: 0.1, ); final topCurveFixtureDef = FixtureDef(topCurveShape); fixturesDef.add(topCurveFixtureDef); final bottomCurveShape = ArcShape( - center: Vector2(19.3, 60.3), + center: Vector2(19.3, -60.3), arcRadius: 8.5, angle: 1.48, - rotation: -1.58, + rotation: 0.1, ); final bottomCurveFixtureDef = FixtureDef(bottomCurveShape); fixturesDef.add(bottomCurveFixtureDef); final topStraightShape = EdgeShape() ..set( - Vector2(3.7, 70.1), - Vector2(19.1, 72.1), + Vector2(3.7, -70.1), + Vector2(19.1, -72.1), ); final topStraightFixtureDef = FixtureDef(topStraightShape); fixturesDef.add(topStraightFixtureDef); final bottomStraightShape = EdgeShape() ..set( - Vector2(3.7, 66.9), - Vector2(19.1, 68.8), + Vector2(3.7, -66.9), + Vector2(19.1, -68.8), ); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); fixturesDef.add(bottomStraightFixtureDef); @@ -108,37 +102,41 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - - final body = world.createBody(bodyDef); + final body = world.createBody(BodyDef()); _createFixtureDefs().forEach(body.createFixture); return body; } +} +class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - renderBody = false; - await add(_LaunchRampBaseSpriteComponent()); + final sprite = await gameRef.loadSprite( + Assets.images.launchRamp.ramp.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(25.65, 0.7); } } -class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { +class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent + with HasGameRef { @override Future onLoad() async { await super.onLoad(); final sprite = await gameRef.loadSprite( - Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, ); this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; - position = Vector2(25.65, 0); + position = Vector2(25.6, -1.3); } } @@ -146,40 +144,39 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { /// Foreground railing for the [_LaunchRampBase] to render in front of the /// [Ball]. /// {@endtemplate} -class _LaunchRampForegroundRailing extends BodyComponent - with InitialPosition, Layered { +class _LaunchRampForegroundRailing extends BodyComponent { /// {@macro launch_ramp_foreground_railing} _LaunchRampForegroundRailing() : super( - priority: LaunchRamp.ballPriorityInsideRamp + 1, - ) { - layer = Layer.launcher; - } + priority: RenderPriority.launchRampForegroundRailing, + children: [_LaunchRampForegroundRailingSpriteComponent()], + renderBody: false, + ); List _createFixtureDefs() { final fixturesDef = []; final rightStraightShape = EdgeShape() ..set( - Vector2(27.6, 57.9), - Vector2(30, 35.1), + Vector2(27.6, -57.9), + Vector2(38.1, 42.6), ); final rightStraightFixtureDef = FixtureDef(rightStraightShape); fixturesDef.add(rightStraightFixtureDef); final curveShape = ArcShape( - center: Vector2(20.1, 59.3), + center: Vector2(20.1, -59.3), arcRadius: 7.5, angle: 1.8, - rotation: -1.63, + rotation: -0.13, ); final curveFixtureDef = FixtureDef(curveShape); fixturesDef.add(curveFixtureDef); final topStraightShape = EdgeShape() ..set( - Vector2(3.7, 66.8), - Vector2(19.7, 66.8), + Vector2(3.7, -66.8), + Vector2(19.7, -66.8), ); final topStraightFixtureDef = FixtureDef(topStraightShape); fixturesDef.add(topStraightFixtureDef); @@ -189,23 +186,11 @@ class _LaunchRampForegroundRailing extends BodyComponent @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - - final body = world.createBody(bodyDef); + final body = world.createBody(BodyDef()); _createFixtureDefs().forEach(body.createFixture); return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_LaunchRampForegroundRailingSpriteComponent()); - } } class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent @@ -220,26 +205,45 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; - position = Vector2(22.8, 0); + position = Vector2(22.8, 0.5); + } +} + +class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered { + _LaunchRampCloseWall() : super(renderBody: false) { + layer = Layer.board; + } + + @override + Body createBody() { + final shape = EdgeShape()..set(Vector2.zero(), Vector2(0, 3)); + + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition; + + return world.createBody(bodyDef)..createFixture(fixtureDef); } } /// {@template launch_ramp_exit} -/// [RampOpening] with [Layer.launcher] to filter [Ball]s exiting the +/// [LayerSensor] with [Layer.launcher] to filter [Ball]s exiting the /// [LaunchRamp]. /// {@endtemplate} -class _LaunchRampExit extends RampOpening { +class _LaunchRampExit extends LayerSensor { /// {@macro launch_ramp_exit} - _LaunchRampExit({ - required double rotation, - }) : _rotation = rotation, - super( + _LaunchRampExit() + : super( insideLayer: Layer.launcher, - orientation: RampOrientation.down, - insidePriority: LaunchRamp.ballPriorityInsideRamp, - ); - - final double _rotation; + outsideLayer: Layer.board, + orientation: LayerEntranceOrientation.down, + insidePriority: RenderPriority.ballOnLaunchRamp, + outsidePriority: RenderPriority.ballOnBoard, + ) { + layer = Layer.launcher; + } static final Vector2 _size = Vector2(1.6, 0.1); @@ -249,6 +253,6 @@ class _LaunchRampExit extends RampOpening { _size.x, _size.y, initialPosition, - _rotation, + math.pi / 2, ); } diff --git a/packages/pinball_components/lib/src/components/layer.dart b/packages/pinball_components/lib/src/components/layer.dart index 10477eff..9b20ecf2 100644 --- a/packages/pinball_components/lib/src/components/layer.dart +++ b/packages/pinball_components/lib/src/components/layer.dart @@ -21,8 +21,7 @@ mixin Layered on BodyComponent { set layer(Layer value) { _layer = value; if (!isLoaded) { - // TODO(alestiago): Use loaded.whenComplete once provided. - mounted.whenComplete(_applyMaskBits); + loaded.whenComplete(_applyMaskBits); } else { _applyMaskBits(); } @@ -89,7 +88,7 @@ extension LayerMaskBits on Layer { case Layer.spaceshipEntranceRamp: return 0x0002; case Layer.launcher: - return 0x0005; + return 0x0008; case Layer.spaceship: return 0x000A; case Layer.spaceshipExitRail: diff --git a/packages/pinball_components/lib/src/components/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor.dart new file mode 100644 index 00000000..7a749357 --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor.dart @@ -0,0 +1,103 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template layer_entrance_orientation} +/// Determines if a layer entrance is oriented [up] or [down] on the board. +/// {@endtemplate} +enum LayerEntranceOrientation { + /// Facing up on the Board. + up, + + /// Facing down on the Board. + down, +} + +/// {@template layer_sensor} +/// [BodyComponent] located at the entrance and exit of a [Layer]. +/// +/// By default the base [layer] is set to [Layer.board] and the +/// [outsidePriority] is set to the lowest possible [Layer]. +/// {@endtemplate} +abstract class LayerSensor extends BodyComponent + with InitialPosition, Layered, ContactCallbacks { + /// {@macro layer_sensor} + LayerSensor({ + required Layer insideLayer, + Layer? outsideLayer, + required int insidePriority, + int? outsidePriority, + required this.orientation, + }) : _insideLayer = insideLayer, + _outsideLayer = outsideLayer ?? Layer.board, + _insidePriority = insidePriority, + _outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard, + super(renderBody: false) { + layer = Layer.opening; + } + final Layer _insideLayer; + final Layer _outsideLayer; + final int _insidePriority; + final int _outsidePriority; + + /// Mask bits value for collisions on [Layer]. + Layer get insideLayer => _insideLayer; + + /// Mask bits value for collisions outside of [Layer]. + Layer get outsideLayer => _outsideLayer; + + /// Render priority for the [Ball] on [Layer]. + int get insidePriority => _insidePriority; + + /// Render priority for the [Ball] outside of [Layer]. + int get outsidePriority => _outsidePriority; + + /// The [Shape] of the [LayerSensor]. + Shape get shape; + + /// {@macro layer_entrance_orientation} + // TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for + // collision calculations. + final LayerEntranceOrientation orientation; + + @override + Body createBody() { + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.layer != insideLayer) { + final isBallEnteringOpening = + (orientation == LayerEntranceOrientation.down && + other.body.linearVelocity.y < 0) || + (orientation == LayerEntranceOrientation.up && + other.body.linearVelocity.y > 0); + + if (isBallEnteringOpening) { + other + ..layer = insideLayer + ..priority = insidePriority + ..reorderChildren(); + } + } else { + other + ..layer = outsideLayer + ..priority = outsidePriority + ..reorderChildren(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart new file mode 100644 index 00000000..735a5490 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -0,0 +1,233 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template plunger} +/// [Plunger] serves as a spring, that shoots the ball on the right side of the +/// playfield. +/// +/// [Plunger] ignores gravity so the player controls its downward [pull]. +/// {@endtemplate} +class Plunger extends BodyComponent with InitialPosition, Layered { + /// {@macro plunger} + Plunger({ + required this.compressionDistance, + // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities + // are fixed. + }) : super( + priority: RenderPriority.plunger, + renderBody: false, + ) { + layer = Layer.launcher; + } + + /// Distance the plunger can lower. + final double compressionDistance; + + late final _PlungerSpriteAnimationGroupComponent _spriteComponent; + + 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; + } + + /// Set a constant downward velocity on the [Plunger]. + void pull() { + body.linearVelocity = Vector2(0, 7); + _spriteComponent.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 velocity = (initialPosition.y - body.position.y) * 5; + body.linearVelocity = Vector2(0, velocity); + _spriteComponent.release(); + } + + /// 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(); + + _spriteComponent = _PlungerSpriteAnimationGroupComponent(); + await add(_spriteComponent); + } +} + +/// 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(); + + // TODO(alestiago): Used cached images. + 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; + + // TODO(ruimiguel): we only need plunger pull animation, and release is just + // to reverse it, so we need to divide by 2 while we don't have only half of + // the animation (but amountPerRow and amountPerColumn needs to be correct + // in order of calculate textureSize correctly). + + 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(18.6, 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/ramp_opening.dart b/packages/pinball_components/lib/src/components/ramp_opening.dart deleted file mode 100644 index cb6066f2..00000000 --- a/packages/pinball_components/lib/src/components/ramp_opening.dart +++ /dev/null @@ -1,127 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template ramp_orientation} -/// Determines if a ramp is facing [up] or [down] on the Board. -/// {@endtemplate} -enum RampOrientation { - /// Facing up on the Board. - up, - - /// Facing down on the Board. - down, -} - -/// {@template ramp_opening} -/// [BodyComponent] located at the entrance and exit of a ramp. -/// -/// [RampOpeningBallContactCallback] detects when a [Ball] passes -/// through this opening. -/// -/// By default the base [layer] is set to [Layer.board] and the -/// [outsidePriority] is set to the lowest possible [Layer]. -/// {@endtemplate} -// TODO(ruialonso): Consider renaming the class. -abstract class RampOpening extends BodyComponent with InitialPosition, Layered { - /// {@macro ramp_opening} - RampOpening({ - required Layer insideLayer, - Layer? outsideLayer, - int? insidePriority, - int? outsidePriority, - required this.orientation, - }) : _insideLayer = insideLayer, - _outsideLayer = outsideLayer ?? Layer.board, - _insidePriority = insidePriority ?? 0, - _outsidePriority = outsidePriority ?? 0 { - layer = Layer.opening; - } - final Layer _insideLayer; - final Layer _outsideLayer; - final int _insidePriority; - final int _outsidePriority; - - /// Mask of category bits for collision inside ramp. - Layer get insideLayer => _insideLayer; - - /// Mask of category bits for collision outside ramp. - Layer get outsideLayer => _outsideLayer; - - /// Priority for the [Ball] inside ramp. - int get insidePriority => _insidePriority; - - /// Priority for the [Ball] outside ramp. - int get outsidePriority => _outsidePriority; - - /// The [Shape] of the [RampOpening]. - Shape get shape; - - /// {@macro ramp_orientation} - // TODO(ruimiguel): Try to remove the need of [RampOrientation] for collision - // calculations. - final RampOrientation orientation; - - @override - Body createBody() { - final fixtureDef = FixtureDef(shape)..isSensor = true; - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template ramp_opening_ball_contact_callback} -/// Detects when a [Ball] enters or exits a ramp through a [RampOpening]. -/// -/// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is -/// outside or inside a ramp. -/// {@endtemplate} -class RampOpeningBallContactCallback - extends ContactCallback { - /// [Ball]s currently inside the ramp. - final _ballsInside = {}; - - @override - void begin(Ball ball, Opening opening, Contact _) { - Layer layer; - - if (!_ballsInside.contains(ball)) { - layer = opening.insideLayer; - _ballsInside.add(ball); - ball - ..sendTo(opening.insidePriority) - ..layer = layer; - } else { - _ballsInside.remove(ball); - } - } - - @override - void end(Ball ball, Opening opening, Contact _) { - if (!_ballsInside.contains(ball)) { - ball.layer = opening.outsideLayer; - } else { - // TODO(ruimiguel): change this code. Check what happens with ball that - // slightly touch Opening and goes out again. With InitialPosition change - // now doesn't work position.y comparison - final isBallOutsideOpening = - (opening.orientation == RampOrientation.down && - ball.body.linearVelocity.y < 0) || - (opening.orientation == RampOrientation.up && - ball.body.linearVelocity.y > 0); - - if (isBallOutsideOpening) { - ball - ..sendTo(opening.outsidePriority) - ..layer = opening.outsideLayer; - _ballsInside.remove(ball); - } - } - } -} diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart new file mode 100644 index 00000000..cf523029 --- /dev/null +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -0,0 +1,117 @@ +// ignore_for_file: public_member_api_docs + +import 'package:pinball_components/pinball_components.dart'; + +/// {@template render_priority} +/// Priorities for the component rendering order in the pinball game. +/// {@endtemplate} +// TODO(allisonryan0002): find alternative to section comments. +abstract class RenderPriority { + static const _base = 0; + static const _above = 1; + static const _below = -1; + + // Ball + + /// Render priority for the [Ball] while it's on the board. + static const int ballOnBoard = _base; + + /// Render priority for the [Ball] while it's on the [SpaceshipRamp]. + static const int ballOnSpaceshipRamp = + _above + spaceshipRampBackgroundRailing; + + /// Render priority for the [Ball] while it's on the [Spaceship]. + static const int ballOnSpaceship = _above + spaceshipSaucer; + + /// Render priority for the [Ball] while it's on the [SpaceshipRail]. + static const int ballOnSpaceshipRail = _below + spaceshipSaucer; + + /// Render priority for the [Ball] while it's on the [LaunchRamp]. + static const int ballOnLaunchRamp = _above + launchRamp; + + // Background + + // TODO(allisonryan0002): fix this magic priority. Could bump all priorities + // so there are no negatives. + static const int background = 3 * _below + _base; + + // Boundaries + + static const int bottomBoundary = _above + dinoBottomWall; + + static const int outerBoundary = _above + background; + + static const int outerBottomBoundary = _above + rocket; + + // Bottom Group + + static const int bottomGroup = _above + ballOnBoard; + + // Launcher + + static const int launchRamp = _above + outerBoundary; + + static const int launchRampForegroundRailing = _below + ballOnBoard; + + static const int plunger = _above + launchRamp; + + static const int rocket = _above + bottomBoundary; + + // Dino Land + + static const int dinoTopWall = _above + ballOnBoard; + + static const int dino = _above + dinoTopWall; + + static const int dinoBottomWall = _above + dino; + + static const int slingshot = _above + dinoBottomWall; + + // Flutter Forest + + static const int flutterForest = _above + launchRampForegroundRailing; + + // Sparky Fire Zone + + static const int computerBase = _below + ballOnBoard; + + static const int computerTop = _above + ballOnBoard; + + static const int sparkyAnimatronic = _above + spaceshipRampForegroundRailing; + + static const int sparkyBumper = _above + ballOnBoard; + + static const int turboChargeFlame = _above + ballOnBoard; + + // Android Spaceship + + static const int spaceshipRail = _above + bottomGroup; + + static const int spaceshipRailForeground = _above + spaceshipRail; + + static const int spaceshipSaucer = _above + spaceshipRail; + + static const int spaceshipSaucerWall = _above + spaceshipSaucer; + + static const int androidHead = _above + spaceshipSaucer; + + static const int spaceshipRamp = _above + ballOnBoard; + + static const int spaceshipRampBackgroundRailing = _above + spaceshipRamp; + + static const int spaceshipRampArrow = _above + spaceshipRamp; + + static const int spaceshipRampForegroundRailing = + _above + ballOnSpaceshipRamp; + + static const int spaceshipRampBoardOpening = _below + ballOnBoard; + + static const int alienBumper = _above + ballOnBoard; + + // Score Text + + static const int scoreText = _above + spaceshipRampForegroundRailing; + + // Debug information + static const int debugInfo = _above + scoreText; +} diff --git a/packages/pinball_components/lib/src/components/rocket.dart b/packages/pinball_components/lib/src/components/rocket.dart new file mode 100644 index 00000000..3f9161ca --- /dev/null +++ b/packages/pinball_components/lib/src/components/rocket.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; + +/// {@template rocket_sprite_component} +/// A [SpriteComponent] for the rocket over [Plunger]. +/// {@endtemplate} +class RocketSpriteComponent extends SpriteComponent with HasGameRef { + // TODO(ruimiguel): change this priority to be over launcher ramp and bottom + // wall. + /// {@macro rocket_sprite_component} + RocketSpriteComponent() : super(priority: RenderPriority.rocket); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.plunger.rocket.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + } +} diff --git a/packages/pinball_components/lib/src/components/score_text.dart b/packages/pinball_components/lib/src/components/score_text.dart index 01b26385..a81b4a6f 100644 --- a/packages/pinball_components/lib/src/components/score_text.dart +++ b/packages/pinball_components/lib/src/components/score_text.dart @@ -18,7 +18,7 @@ class ScoreText extends TextComponent { text: text, position: position, anchor: Anchor.center, - priority: 100, + priority: RenderPriority.scoreText, ); late final Effect _effect; diff --git a/packages/pinball_components/lib/src/components/signpost.dart b/packages/pinball_components/lib/src/components/signpost.dart new file mode 100644 index 00000000..13425342 --- /dev/null +++ b/packages/pinball_components/lib/src/components/signpost.dart @@ -0,0 +1,101 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// Represents the [Signpost]'s current [Sprite] state. +@visibleForTesting +enum SignpostSpriteState { + /// Signpost with no active dashes. + inactive, + + /// Signpost with a single sign of active dashes. + active1, + + /// Signpost with two signs of active dashes. + active2, + + /// Signpost with all signs of active dashes. + active3, +} + +extension on SignpostSpriteState { + String get path { + switch (this) { + case SignpostSpriteState.inactive: + return Assets.images.signpost.inactive.keyName; + case SignpostSpriteState.active1: + return Assets.images.signpost.active1.keyName; + case SignpostSpriteState.active2: + return Assets.images.signpost.active2.keyName; + case SignpostSpriteState.active3: + return Assets.images.signpost.active3.keyName; + } + } + + SignpostSpriteState get next { + return SignpostSpriteState + .values[(index + 1) % SignpostSpriteState.values.length]; + } +} + +/// {@template signpost} +/// A sign, found in the Flutter Forest. +/// +/// Lights up a new sign whenever all three [DashNestBumper]s are hit. +/// {@endtemplate} +class Signpost extends BodyComponent with InitialPosition { + /// {@macro signpost} + Signpost({ + Iterable? children, + }) : super( + renderBody: false, + children: [ + _SignpostSpriteComponent(), + ...?children, + ], + ); + + /// Forwards the sprite to the next [SignpostSpriteState]. + /// + /// If the current state is the last one it cycles back to the initial state. + void progress() => firstChild<_SignpostSpriteComponent>()!.progress(); + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.25; + final fixtureDef = FixtureDef(shape); + final bodyDef = BodyDef( + position: initialPosition, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _SignpostSpriteComponent extends SpriteGroupComponent + with HasGameRef { + _SignpostSpriteComponent() + : super( + anchor: Anchor.bottomCenter, + position: Vector2(0.65, 0.45), + ); + + void progress() => current = current?.next; + + @override + Future onLoad() async { + await super.onLoad(); + + final sprites = {}; + this.sprites = sprites; + for (final spriteState in SignpostSpriteState.values) { + sprites[spriteState] = Sprite( + gameRef.images.fromCache(spriteState.path), + ); + } + + current = SignpostSpriteState.inactive; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart index 0ebe13ce..b48bf2f9 100644 --- a/packages/pinball_components/lib/src/components/slingshot.dart +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -1,49 +1,29 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'dart:math' as math; - import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template slingshots} -/// A [Blueprint] which creates the left and right pairs of [Slingshot]s. +/// A [Blueprint] which creates the pair of [Slingshot]s on the right side of +/// the board. /// {@endtemplate} -class Slingshots extends Forge2DBlueprint { - @override - void build(_) { - // TODO(allisonryan0002): use radians values instead of converting degrees. - final leftUpperSlingshot = Slingshot( - length: 5.66, - angle: -1.5 * (math.pi / 180), - spritePath: Assets.images.slingshot.leftUpper.keyName, - )..initialPosition = Vector2(-29, 1.5); - - final leftLowerSlingshot = Slingshot( - length: 3.54, - angle: -29.1 * (math.pi / 180), - spritePath: Assets.images.slingshot.leftLower.keyName, - )..initialPosition = Vector2(-31, -6.2); - - final rightUpperSlingshot = Slingshot( - length: 5.64, - angle: 1 * (math.pi / 180), - spritePath: Assets.images.slingshot.rightUpper.keyName, - )..initialPosition = Vector2(22.3, 1.58); - - final rightLowerSlingshot = Slingshot( - length: 3.46, - angle: 26.8 * (math.pi / 180), - spritePath: Assets.images.slingshot.rightLower.keyName, - )..initialPosition = Vector2(24.7, -6.2); - - addAll([ - leftUpperSlingshot, - leftLowerSlingshot, - rightUpperSlingshot, - rightLowerSlingshot, - ]); - } +class Slingshots extends Blueprint { + /// {@macro slingshots} + Slingshots() + : super( + components: [ + Slingshot( + length: 5.64, + angle: -0.017, + spritePath: Assets.images.slingshot.upper.keyName, + )..initialPosition = Vector2(22.3, -1.58), + Slingshot( + length: 3.46, + angle: -0.468, + spritePath: Assets.images.slingshot.lower.keyName, + )..initialPosition = Vector2(24.7, 6.2), + ], + ); } /// {@template slingshot} @@ -57,82 +37,87 @@ class Slingshot extends BodyComponent with InitialPosition { required String spritePath, }) : _length = length, _angle = angle, - _spritePath = spritePath, - super(priority: 1); + super( + priority: RenderPriority.slingshot, + children: [_SlinghsotSpriteComponent(spritePath, angle: angle)], + renderBody: false, + ); final double _length; final double _angle; - final String _spritePath; - List _createFixtureDefs() { - final fixturesDef = []; const circleRadius = 1.55; final topCircleShape = CircleShape()..radius = circleRadius; - topCircleShape.position.setValues(0, _length / 2); - final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0; - fixturesDef.add(topCircleFixtureDef); + topCircleShape.position.setValues(0, -_length / 2); + final topCircleFixtureDef = FixtureDef(topCircleShape); final bottomCircleShape = CircleShape()..radius = circleRadius; - bottomCircleShape.position.setValues(0, -_length / 2); - final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0; - fixturesDef.add(bottomCircleFixtureDef); + bottomCircleShape.position.setValues(0, _length / 2); + final bottomCircleFixtureDef = FixtureDef(bottomCircleShape); final leftEdgeShape = EdgeShape() ..set( Vector2(circleRadius, _length / 2), Vector2(circleRadius, -_length / 2), ); - final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape) - ..friction = 0 - ..restitution = 5; - fixturesDef.add(leftEdgeShapeFixtureDef); + final leftEdgeShapeFixtureDef = FixtureDef( + leftEdgeShape, + restitution: 5, + ); final rightEdgeShape = EdgeShape() ..set( Vector2(-circleRadius, _length / 2), Vector2(-circleRadius, -_length / 2), ); - final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape) - ..friction = 0 - ..restitution = 5; - fixturesDef.add(rightEdgeShapeFixtureDef); + final rightEdgeShapeFixtureDef = FixtureDef( + rightEdgeShape, + restitution: 5, + ); - return fixturesDef; + return [ + topCircleFixtureDef, + bottomCircleFixtureDef, + leftEdgeShapeFixtureDef, + rightEdgeShapeFixtureDef, + ]; } @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..angle = _angle; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + angle: _angle, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); return body; } +} + +class _SlinghsotSpriteComponent extends SpriteComponent with HasGameRef { + _SlinghsotSpriteComponent( + String path, { + required double angle, + }) : _path = path, + super( + angle: -angle, + anchor: Anchor.center, + ); + + final String _path; @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); - renderBody = false; - } - - Future _loadSprite() async { - final sprite = await gameRef.loadSprite(_spritePath); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - angle: _angle, - ), - ); + final sprite = Sprite(gameRef.images.fromCache(_path)); + this.sprite = sprite; + size = sprite.originalSize / 10; } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 6643a53a..a52df81d 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid_renaming_method_parameters - import 'dart:async'; import 'dart:math'; @@ -7,42 +5,33 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship} /// A [Blueprint] which creates the spaceship feature. /// {@endtemplate} -class Spaceship extends Forge2DBlueprint { +class Spaceship extends Blueprint { /// {@macro spaceship} - Spaceship({required this.position}); + Spaceship({required Vector2 position}) + : super( + components: [ + SpaceshipSaucer()..initialPosition = position, + _SpaceshipEntrance()..initialPosition = position, + AndroidHead()..initialPosition = position, + _SpaceshipHole( + outsideLayer: Layer.spaceshipExitRail, + outsidePriority: RenderPriority.ballOnSpaceshipRail, + )..initialPosition = position - Vector2(5.2, -4.8), + _SpaceshipHole( + outsideLayer: Layer.board, + outsidePriority: RenderPriority.ballOnBoard, + )..initialPosition = position - Vector2(-7.2, -0.8), + SpaceshipWall()..initialPosition = position, + ], + ); /// Total size of the spaceship. static final size = Vector2(25, 19); - - /// The [position] where the elements will be created - final Vector2 position; - - /// Base priority for wall while be on spaceship. - static const ballPriorityWhenOnSpaceship = 4; - - @override - void build(_) { - addAllContactCallback([ - SpaceshipHoleBallContactCallback(), - SpaceshipEntranceBallContactCallback(), - ]); - - addAll([ - SpaceshipSaucer()..initialPosition = position, - SpaceshipEntrance()..initialPosition = position, - AndroidHead()..initialPosition = position, - SpaceshipHole( - outsideLayer: Layer.spaceshipExitRail, - outsidePriority: SpaceshipRail.ballPriorityInsideRail, - )..initialPosition = position - Vector2(5.2, 4.8), - SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), - SpaceshipWall()..initialPosition = position, - ]); - } } /// {@template spaceship_saucer} @@ -51,41 +40,48 @@ class Spaceship extends Forge2DBlueprint { class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_saucer} SpaceshipSaucer() - : super(priority: Spaceship.ballPriorityWhenOnSpaceship - 1) { + : super( + priority: RenderPriority.spaceshipSaucer, + renderBody: false, + children: [ + _SpaceshipSaucerSpriteComponent(), + ], + ) { layer = Layer.spaceship; } @override - Future onLoad() async { - await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.saucer.keyName, + Body createBody() { + final shape = CircleShape()..radius = 3; + final fixtureDef = FixtureDef( + shape, + isSensor: true, ); - - await add( - SpriteComponent( - sprite: sprite, - size: Spaceship.size, - anchor: Anchor.center, - ), + final bodyDef = BodyDef( + position: initialPosition, + userData: this, ); - renderBody = false; + return world.createBody(bodyDef)..createFixture(fixtureDef); } +} - @override - Body createBody() { - final circleShape = CircleShape()..radius = 3; +class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef { + _SpaceshipSaucerSpriteComponent() + : super( + anchor: Anchor.center, + // TODO(alestiago): Refactor to use sprite orignial size instead. + size: Spaceship.size, + ); - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + @override + Future onLoad() async { + await super.onLoad(); - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(circleShape)..isSensor = true, - ); + // TODO(alestiago): Use cached sprite. + sprite = await gameRef.loadSprite( + Assets.images.spaceship.saucer.keyName, + ); } } @@ -95,26 +91,23 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@endtemplate} class AndroidHead extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_bridge} - AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { + AndroidHead() + : super( + priority: RenderPriority.androidHead, + children: [_AndroidHeadSpriteAnimation()], + renderBody: false, + ) { layer = Layer.spaceship; } - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_AndroidHeadSpriteAnimation()); - } - @override Body createBody() { final circleShape = CircleShape()..radius = 2; - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef) ..createFixture( @@ -146,25 +139,18 @@ class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent } } -/// {@template spaceship_entrance} -/// A sensor [BodyComponent] used to detect when the ball enters the -/// the spaceship area in order to modify its filter data so the ball -/// can correctly collide only with the Spaceship -/// {@endtemplate} -class SpaceshipEntrance extends RampOpening { - /// {@macro spaceship_entrance} - SpaceshipEntrance() +class _SpaceshipEntrance extends LayerSensor { + _SpaceshipEntrance() : super( insideLayer: Layer.spaceship, - orientation: RampOrientation.up, - insidePriority: Spaceship.ballPriorityWhenOnSpaceship, + orientation: LayerEntranceOrientation.up, + insidePriority: RenderPriority.ballOnSpaceship, ) { layer = Layer.spaceship; } @override Shape get shape { - renderBody = false; final radius = Spaceship.size.y / 2; return PolygonShape() ..setAsEdge( @@ -180,31 +166,25 @@ class SpaceshipEntrance extends RampOpening { } } -/// {@template spaceship_hole} -/// A sensor [BodyComponent] responsible for sending the [Ball] -/// out from the [Spaceship]. -/// {@endtemplate} -class SpaceshipHole extends RampOpening { - /// {@macro spaceship_hole} - SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1}) +class _SpaceshipHole extends LayerSensor { + _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) : super( insideLayer: Layer.spaceship, outsideLayer: outsideLayer, - orientation: RampOrientation.up, - insidePriority: 4, + orientation: LayerEntranceOrientation.down, + insidePriority: RenderPriority.ballOnSpaceship, outsidePriority: outsidePriority, ) { - renderBody = false; layer = Layer.spaceship; } @override Shape get shape { return ArcShape( - center: Vector2(0, 3.2), + center: Vector2(0, -3.2), arcRadius: 5, angle: 1, - rotation: 60 * pi / 180, + rotation: -2, ); } } @@ -242,55 +222,25 @@ class _SpaceshipWallShape extends ChainShape { /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_wall} - SpaceshipWall() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { + SpaceshipWall() + : super( + priority: RenderPriority.spaceshipSaucerWall, + renderBody: false, + ) { layer = Layer.spaceship; } @override Body createBody() { - renderBody = false; - - final wallShape = _SpaceshipWallShape(); - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..angle = 90 * pi / 172 - ..type = BodyType.static; + final shape = _SpaceshipWallShape(); + final fixtureDef = FixtureDef(shape); - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(wallShape)..restitution = 1, - ); - } -} - -/// [ContactCallback] that handles the contact between the [Ball] -/// and the [SpaceshipEntrance]. -/// -/// It modifies the [Ball] priority and filter data so it can appear on top of -/// the spaceship and also only collide with the spaceship. -class SpaceshipEntranceBallContactCallback - extends ContactCallback { - @override - void begin(SpaceshipEntrance entrance, Ball ball, _) { - ball - ..sendTo(entrance.insidePriority) - ..layer = Layer.spaceship; - } -} + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + angle: -1.7, + ); -/// [ContactCallback] that handles the contact between the [Ball] -/// and a [SpaceshipHole]. -/// -/// It sets the [Ball] priority and filter data so it will outside of the -/// [Spaceship]. -class SpaceshipHoleBallContactCallback - extends ContactCallback { - @override - void begin(SpaceshipHole hole, Ball ball, _) { - ball - ..sendTo(hole.outsidePriority) - ..layer = hole.outsideLayer; + return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index b63e401a..3dfd2c1c 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -1,51 +1,36 @@ -// ignore_for_file: avoid_renaming_method_parameters - import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_rail} /// A [Blueprint] for the spaceship drop tube. /// {@endtemplate} -class SpaceshipRail extends Forge2DBlueprint { +class SpaceshipRail extends Blueprint { /// {@macro spaceship_rail} - SpaceshipRail(); - - /// Base priority for [Ball] while inside [SpaceshipRail]. - static const ballPriorityInsideRail = 2; - - @override - void build(_) { - addAllContactCallback([ - SpaceshipRailExitBallContactCallback(), - ]); - - final railRamp = _SpaceshipRailRamp(); - final railEnd = SpaceshipRailExit(); - final topBase = _SpaceshipRailBase(radius: 0.55) - ..initialPosition = Vector2(-26.15, 18.65); - final bottomBase = _SpaceshipRailBase(radius: 0.8) - ..initialPosition = Vector2(-25.5, -12.9); - final railForeground = _SpaceshipRailForeground(); - - addAll([ - railRamp, - railEnd, - topBase, - bottomBase, - railForeground, - ]); - } + SpaceshipRail() + : super( + components: [ + _SpaceshipRailRamp(), + _SpaceshipRailExit(), + _SpaceshipRailBase(radius: 0.55) + ..initialPosition = Vector2(-26.15, -18.65), + _SpaceshipRailBase(radius: 0.8) + ..initialPosition = Vector2(-25.5, 12.9), + _SpaceshipRailForeground() + ], + ); } -/// Represents the spaceship drop rail from the [Spaceship]. -class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { +class _SpaceshipRailRamp extends BodyComponent with Layered { _SpaceshipRailRamp() : super( - priority: SpaceshipRail.ballPriorityInsideRail - 1, + priority: RenderPriority.spaceshipRail, + renderBody: false, + children: [_SpaceshipRailRampSpriteComponent()], ) { layer = Layer.spaceshipExitRail; } @@ -54,19 +39,19 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { final fixturesDefs = []; final topArcShape = ArcShape( - center: Vector2(-35.5, 30.9), + center: Vector2(-35.5, -30.9), arcRadius: 2.5, angle: math.pi, - rotation: 2.9, + rotation: 0.2, ); final topArcFixtureDef = FixtureDef(topArcShape); fixturesDefs.add(topArcFixtureDef); final topLeftCurveShape = BezierCurveShape( controlPoints: [ - Vector2(-37.9, 30.4), - Vector2(-38, 23.9), - Vector2(-30.93, 18.2), + Vector2(-37.9, -30.4), + Vector2(-38, -23.9), + Vector2(-30.93, -18.2), ], ); final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape); @@ -75,8 +60,8 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { final middleLeftCurveShape = BezierCurveShape( controlPoints: [ topLeftCurveShape.vertices.last, - Vector2(-22.6, 10.3), - Vector2(-30, 0.2), + Vector2(-22.6, -10.3), + Vector2(-30, -0.2), ], ); final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape); @@ -85,8 +70,8 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { final bottomLeftCurveShape = BezierCurveShape( controlPoints: [ middleLeftCurveShape.vertices.last, - Vector2(-36, -8.6), - Vector2(-32.04, -18.3), + Vector2(-36, 8.6), + Vector2(-32.04, 18.3), ], ); final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape); @@ -94,8 +79,8 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { final topRightStraightShape = EdgeShape() ..set( - Vector2(-33, 31.3), - Vector2(-27.2, 21.3), + Vector2(-33, -31.3), + Vector2(-27.2, -21.3), ); final topRightStraightFixtureDef = FixtureDef(topRightStraightShape); fixturesDefs.add(topRightStraightFixtureDef); @@ -103,8 +88,8 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { final middleRightCurveShape = BezierCurveShape( controlPoints: [ topRightStraightShape.vertex1, - Vector2(-16.5, 11.4), - Vector2(-25.29, -1.7), + Vector2(-16.5, -11.4), + Vector2(-25.29, 1.7), ], ); final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape); @@ -113,8 +98,8 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { final bottomRightCurveShape = BezierCurveShape( controlPoints: [ middleRightCurveShape.vertices.last, - Vector2(-29.91, -8.5), - Vector2(-26.8, -15.7), + Vector2(-29.91, 8.5), + Vector2(-26.8, 15.7), ], ); final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape); @@ -125,23 +110,10 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - - final body = world.createBody(bodyDef); + final body = world.createBody(BodyDef()); _createFixtureDefs().forEach(body.createFixture); - return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_SpaceshipRailRampSpriteComponent()); - } } class _SpaceshipRailRampSpriteComponent extends SpriteComponent @@ -162,7 +134,7 @@ class _SpaceshipRailRampSpriteComponent extends SpriteComponent class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { _SpaceshipRailForeground() - : super(priority: SpaceshipRail.ballPriorityInsideRail + 1); + : super(priority: RenderPriority.spaceshipRailForeground); @override Future onLoad() async { @@ -179,69 +151,40 @@ class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { } /// Represents the ground bases of the [_SpaceshipRailRamp]. -class _SpaceshipRailBase extends BodyComponent with InitialPosition, Layered { - _SpaceshipRailBase({required this.radius}) - : super( - priority: SpaceshipRail.ballPriorityInsideRail + 1, - ) { - renderBody = false; - layer = Layer.board; - } +class _SpaceshipRailBase extends BodyComponent with InitialPosition { + _SpaceshipRailBase({required this.radius}) : super(renderBody: false); final double radius; @override Body createBody() { final shape = CircleShape()..radius = radius; - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; + final bodyDef = BodyDef( + position: initialPosition, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } } -/// {@template spaceship_rail_exit} -/// A sensor [BodyComponent] responsible for sending the [Ball] -/// back to the board. -/// {@endtemplate} -class SpaceshipRailExit extends RampOpening { - /// {@macro spaceship_rail_exit} - SpaceshipRailExit() +class _SpaceshipRailExit extends LayerSensor { + _SpaceshipRailExit() : super( - orientation: RampOrientation.down, + orientation: LayerEntranceOrientation.down, insideLayer: Layer.spaceshipExitRail, - insidePriority: SpaceshipRail.ballPriorityInsideRail, + insidePriority: RenderPriority.ballOnSpaceshipRail, ) { - renderBody = false; layer = Layer.spaceshipExitRail; } @override Shape get shape { return ArcShape( - center: Vector2(-29, -19), + center: Vector2(-29, 19), arcRadius: 2.5, angle: math.pi * 0.4, - rotation: 0.26, + rotation: -1.4, ); } } - -/// [ContactCallback] that handles the contact between the [Ball] -/// and a [SpaceshipRailExit]. -/// -/// It resets the [Ball] priority and filter data so it will "be back" on the -/// board. -class SpaceshipRailExitBallContactCallback - extends ContactCallback { - @override - void begin(SpaceshipRailExit exitRail, Ball ball, _) { - ball - ..sendTo(exitRail.outsidePriority) - ..layer = exitRail.outsideLayer; - } -} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 452d101e..c9a1d574 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -1,65 +1,108 @@ -// ignore_for_file: avoid_renaming_method_parameters, comment_references - import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_ramp} -/// A [Blueprint] which creates the [_SpaceshipRampBackground]. +/// A [Blueprint] which creates the ramp leading into the [Spaceship]. /// {@endtemplate} -class SpaceshipRamp extends Forge2DBlueprint { +class SpaceshipRamp extends Blueprint { /// {@macro spaceship_ramp} - SpaceshipRamp(); + SpaceshipRamp() + : super( + components: [ + _SpaceshipRampOpening( + outsidePriority: RenderPriority.ballOnBoard, + rotation: math.pi, + ) + ..initialPosition = Vector2(1.7, -19.8) + ..layer = Layer.opening, + _SpaceshipRampOpening( + outsideLayer: Layer.spaceship, + outsidePriority: RenderPriority.ballOnSpaceship, + rotation: math.pi, + ) + ..initialPosition = Vector2(-13.7, -18.6) + ..layer = Layer.spaceshipEntranceRamp, + _SpaceshipRampBackground(), + _SpaceshipRampBoardOpeningSpriteComponent() + ..position = Vector2(3.4, -39.5), + _SpaceshipRampForegroundRailing(), + _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), + _SpaceshipRampBackgroundRailingSpriteComponent(), + _SpaceshipRampArrowSpriteComponent(), + ], + ); + + /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. + /// + /// If the current state is the last one it cycles back to the initial state. + void progress() => components + .whereType<_SpaceshipRampArrowSpriteComponent>() + .first + .progress(); +} - /// Base priority for wall while be in the ramp. - static const int ballPriorityInsideRamp = 4; +/// Indicates the state of the arrow on the [SpaceshipRamp]. +@visibleForTesting +enum SpaceshipRampArrowSpriteState { + /// Arrow with no dashes lit up. + inactive, - @override - void build(_) { - addAllContactCallback([ - RampOpeningBallContactCallback<_SpaceshipRampOpening>(), - ]); - - final rightOpening = _SpaceshipRampOpening( - // TODO(ruimiguel): set Board priority when defined. - outsidePriority: 1, - rotation: math.pi, - ) - ..initialPosition = Vector2(1.7, 19) - ..layer = Layer.opening; - final leftOpening = _SpaceshipRampOpening( - outsideLayer: Layer.spaceship, - outsidePriority: Spaceship.ballPriorityWhenOnSpaceship, - rotation: math.pi, - ) - ..initialPosition = Vector2(-13.7, 19) - ..layer = Layer.spaceshipEntranceRamp; - - final spaceshipRamp = _SpaceshipRampBackground(); - - final spaceshipRampForegroundRailing = _SpaceshipRampForegroundRailing(); - - final baseRight = _SpaceshipRampBase()..initialPosition = Vector2(1.7, 20); - - addAll([ - rightOpening, - leftOpening, - baseRight, - spaceshipRamp, - spaceshipRampForegroundRailing, - ]); + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, +} + +extension on SpaceshipRampArrowSpriteState { + String get path { + switch (this) { + case SpaceshipRampArrowSpriteState.inactive: + return Assets.images.spaceship.ramp.arrow.inactive.keyName; + case SpaceshipRampArrowSpriteState.active1: + return Assets.images.spaceship.ramp.arrow.active1.keyName; + case SpaceshipRampArrowSpriteState.active2: + return Assets.images.spaceship.ramp.arrow.active2.keyName; + case SpaceshipRampArrowSpriteState.active3: + return Assets.images.spaceship.ramp.arrow.active3.keyName; + case SpaceshipRampArrowSpriteState.active4: + return Assets.images.spaceship.ramp.arrow.active4.keyName; + case SpaceshipRampArrowSpriteState.active5: + return Assets.images.spaceship.ramp.arrow.active5.keyName; + } + } + + SpaceshipRampArrowSpriteState get next { + return SpaceshipRampArrowSpriteState + .values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; } } -/// Represents the upper left blue ramp of the [Board] with its background -/// railing. class _SpaceshipRampBackground extends BodyComponent with InitialPosition, Layered { _SpaceshipRampBackground() - : super(priority: SpaceshipRamp.ballPriorityInsideRamp - 1) { + : super( + priority: RenderPriority.spaceshipRamp, + renderBody: false, + children: [ + _SpaceshipRampBackgroundRampSpriteComponent(), + ], + ) { layer = Layer.spaceshipEntranceRamp; } @@ -67,192 +110,238 @@ class _SpaceshipRampBackground extends BodyComponent static const width = 5.0; List _createFixtureDefs() { - final fixturesDef = []; - final outerLeftCurveShape = BezierCurveShape( controlPoints: [ - Vector2(-30.95, 38), - Vector2(-32.5, 71.25), - Vector2(-14.2, 71.25), + Vector2(-30.75, -37.3), + Vector2(-32.5, -71.25), + Vector2(-14.2, -71.25), ], ); - - final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape); - fixturesDef.add(outerLeftCurveFixtureDef); - final outerRightCurveShape = BezierCurveShape( controlPoints: [ outerLeftCurveShape.vertices.last, - Vector2(4.7, 71.25), - Vector2(6.3, 40), + Vector2(2.5, -71.9), + Vector2(6.1, -44.9), ], ); + final boardOpeningEdgeShape = EdgeShape() + ..set( + outerRightCurveShape.vertices.last, + Vector2(7.3, -41.1), + ); - final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape); - fixturesDef.add(outerRightCurveFixtureDef); - - return fixturesDef; + return [ + FixtureDef(outerRightCurveShape), + FixtureDef(outerLeftCurveShape), + FixtureDef(boardOpeningEdgeShape), + ]; } @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - + final bodyDef = BodyDef(position: initialPosition); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); return body; } +} + +class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent + with HasGameRef { + _SpaceshipRampBackgroundRailingSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-11.7, -54.3), + priority: RenderPriority.spaceshipRampBackgroundRailing, + ); @override Future onLoad() async { await super.onLoad(); - renderBody = false; - - await add(_SpaceshipRampBackgroundRailingSpriteComponent()); - await add(_SpaceshipRampBackgroundRampSpriteComponent()); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.spaceship.ramp.railingBackground.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; } } -class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent +class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent with HasGameRef { + _SpaceshipRampBackgroundRampSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-10.7, -53.6), + ); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.ramp.railingBackground.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.spaceship.ramp.main.keyName, + ), ); this.sprite = sprite; - size = Vector2(38.3, 35.1); - anchor = Anchor.center; - position = Vector2(-12.2, -54.5); + size = sprite.originalSize / 10; } } -class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent +/// {@template spaceship_ramp_arrow_sprite_component} +/// An arrow inside [SpaceshipRamp]. +/// +/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. +/// {@endtemplate} +class _SpaceshipRampArrowSpriteComponent + extends SpriteGroupComponent + with HasGameRef { + /// {@macro spaceship_ramp_arrow_sprite_component} + _SpaceshipRampArrowSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-3.9, -56.5), + priority: RenderPriority.spaceshipRampArrow, + ); + + /// Changes arrow image to the next [Sprite]. + void progress() => current = current?.next; + + @override + Future onLoad() async { + await super.onLoad(); + final sprites = {}; + this.sprites = sprites; + for (final spriteState in SpaceshipRampArrowSpriteState.values) { + sprites[spriteState] = Sprite( + gameRef.images.fromCache(spriteState.path), + ); + } + + current = SpaceshipRampArrowSpriteState.inactive; + size = sprites[current]!.originalSize / 10; + } +} + +class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent with HasGameRef { + _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.ramp.main.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.spaceship.ramp.boardOpening.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-12.2, -53.5); } } -/// Represents the foreground of the railing upper left blue ramp. class _SpaceshipRampForegroundRailing extends BodyComponent with InitialPosition, Layered { _SpaceshipRampForegroundRailing() - : super(priority: SpaceshipRamp.ballPriorityInsideRamp + 1) { + : super( + priority: RenderPriority.spaceshipRampForegroundRailing, + renderBody: false, + children: [_SpaceshipRampForegroundRailingSpriteComponent()], + ) { layer = Layer.spaceshipEntranceRamp; } List _createFixtureDefs() { - final fixturesDef = []; - final innerLeftCurveShape = BezierCurveShape( controlPoints: [ - Vector2(-24.5, 38), - Vector2(-26.3, 64), - Vector2(-13.8, 64.5), + Vector2(-24.5, -38), + Vector2(-26.3, -64), + Vector2(-13.8, -64.5), ], ); - - final innerLeftCurveFixtureDef = FixtureDef(innerLeftCurveShape); - fixturesDef.add(innerLeftCurveFixtureDef); - final innerRightCurveShape = BezierCurveShape( controlPoints: [ innerLeftCurveShape.vertices.last, - Vector2(-1, 64.5), - Vector2(0.1, 39.5), + Vector2(-2.5, -66.2), + Vector2(0, -44.5), ], ); + final boardOpeningEdgeShape = EdgeShape() + ..set( + innerRightCurveShape.vertices.last, + Vector2(-0.85, -40.8), + ); - final innerRightCurveFixtureDef = FixtureDef(innerRightCurveShape); - fixturesDef.add(innerRightCurveFixtureDef); - - return fixturesDef; + return [ + FixtureDef(innerLeftCurveShape), + FixtureDef(innerRightCurveShape), + FixtureDef(boardOpeningEdgeShape), + ]; } @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - + final bodyDef = BodyDef(position: initialPosition); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_SpaceshipRampForegroundRailingSpriteComponent()); - } } class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent with HasGameRef { + _SpaceshipRampForegroundRailingSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-12.3, -52.5), + ); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.ramp.railingForeground.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.spaceship.ramp.railingForeground.keyName, + ), ); this.sprite = sprite; - size = Vector2(26.1, 28.3); - anchor = Anchor.center; - position = Vector2(-12.2, -52.5); + size = sprite.originalSize / 10; } } -/// Represents the ground right base of the [SpaceshipRamp]. class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered { - _SpaceshipRampBase() { - renderBody = false; + _SpaceshipRampBase() : super(renderBody: false) { layer = Layer.board; } @override Body createBody() { - const baseWidth = 6; + const baseWidth = 9; final baseShape = BezierCurveShape( controlPoints: [ Vector2(initialPosition.x - baseWidth / 2, initialPosition.y), Vector2(initialPosition.x - baseWidth / 2, initialPosition.y) + - Vector2(2, 2), + Vector2(2, -5), Vector2(initialPosition.x + baseWidth / 2, initialPosition.y) + - Vector2(-2, 2), + Vector2(-2, -5), Vector2(initialPosition.x + baseWidth / 2, initialPosition.y) ], ); final fixtureDef = FixtureDef(baseShape); - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef(position: initialPosition); return world.createBody(bodyDef)..createFixture(fixtureDef); } } /// {@template spaceship_ramp_opening} -/// [RampOpening] with [Layer.spaceshipEntranceRamp] to filter [Ball] collisions +/// [LayerSensor] with [Layer.spaceshipEntranceRamp] to filter [Ball] collisions /// inside [_SpaceshipRampBackground]. /// {@endtemplate} -class _SpaceshipRampOpening extends RampOpening { +class _SpaceshipRampOpening extends LayerSensor { /// {@macro spaceship_ramp_opening} _SpaceshipRampOpening({ Layer? outsideLayer, @@ -262,16 +351,14 @@ class _SpaceshipRampOpening extends RampOpening { super( insideLayer: Layer.spaceshipEntranceRamp, outsideLayer: outsideLayer, - orientation: RampOrientation.down, - insidePriority: SpaceshipRamp.ballPriorityInsideRamp, + orientation: LayerEntranceOrientation.down, + insidePriority: RenderPriority.ballOnSpaceshipRamp, outsidePriority: outsidePriority, - ) { - renderBody = false; - } + ); final double _rotation; - static final Vector2 _size = Vector2(_SpaceshipRampBackground.width / 4, .1); + static final Vector2 _size = Vector2(_SpaceshipRampBackground.width / 3, .1); @override Shape get shape { diff --git a/packages/pinball_components/lib/src/components/sparky_animatronic.dart b/packages/pinball_components/lib/src/components/sparky_animatronic.dart new file mode 100644 index 00000000..714a5700 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_animatronic.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template sparky_animatronic} +/// Animated Sparky that sits on top of the [SparkyComputer]. +/// {@endtemplate} +class SparkyAnimatronic extends SpriteAnimationComponent with HasGameRef { + /// {@macro sparky_animatronic} + SparkyAnimatronic() + : super( + anchor: Anchor.center, + playing: false, + priority: RenderPriority.sparkyAnimatronic, + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.sparky.animatronic.keyName, + ); + + const amountPerRow = 9; + const amountPerColumn = 7; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: (amountPerRow * amountPerColumn) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + loop: false, + ), + )..onComplete = () { + animation?.reset(); + playing = false; + }; + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper.dart deleted file mode 100644 index c4798624..00000000 --- a/packages/pinball_components/lib/src/components/sparky_bumper.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:math' as math; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template sparky_bumper} -/// Bumper for Sparky area. -/// {@endtemplate} -// TODO(ruimiguel): refactor later to unify with DashBumpers. -class SparkyBumper extends BodyComponent with InitialPosition { - /// {@macro sparky_bumper} - SparkyBumper._({ - required double majorRadius, - required double minorRadius, - required String activeAssetPath, - required String inactiveAssetPath, - required SpriteComponent spriteComponent, - }) : _majorRadius = majorRadius, - _minorRadius = minorRadius, - _activeAssetPath = activeAssetPath, - _inactiveAssetPath = inactiveAssetPath, - _spriteComponent = spriteComponent; - - /// {@macro sparky_bumper} - SparkyBumper.a() - : this._( - majorRadius: 2.9, - minorRadius: 2.1, - activeAssetPath: Assets.images.sparky.bumper.a.active.keyName, - inactiveAssetPath: Assets.images.sparky.bumper.a.inactive.keyName, - spriteComponent: SpriteComponent( - anchor: Anchor.center, - position: Vector2(0, -0.25), - ), - ); - - /// {@macro sparky_bumper} - SparkyBumper.b() - : this._( - majorRadius: 2.85, - minorRadius: 2, - activeAssetPath: Assets.images.sparky.bumper.b.active.keyName, - inactiveAssetPath: Assets.images.sparky.bumper.b.inactive.keyName, - spriteComponent: SpriteComponent( - anchor: Anchor.center, - position: Vector2(0, -0.35), - ), - ); - - /// {@macro sparky_bumper} - SparkyBumper.c() - : this._( - majorRadius: 3, - minorRadius: 2.2, - activeAssetPath: Assets.images.sparky.bumper.c.active.keyName, - inactiveAssetPath: Assets.images.sparky.bumper.c.inactive.keyName, - spriteComponent: SpriteComponent( - anchor: Anchor.center, - position: Vector2(0, -0.4), - ), - ); - - final double _majorRadius; - final double _minorRadius; - final String _activeAssetPath; - late final Sprite _activeSprite; - final String _inactiveAssetPath; - late final Sprite _inactiveSprite; - final SpriteComponent _spriteComponent; - - @override - Future onLoad() async { - await super.onLoad(); - await _loadSprites(); - - // TODO(erickzanardo): Look into using onNewState instead. - // Currently doing: onNewState(gameRef.read()) will throw an - // `Exception: build context is not available yet` - deactivate(); - await add(_spriteComponent); - } - - @override - Body createBody() { - renderBody = false; - - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: _majorRadius, - minorRadius: _minorRadius, - )..rotate(math.pi / 1.9); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - Future _loadSprites() async { - // TODO(alestiago): I think ideally we would like to do: - // Sprite(path).load so we don't require to store the activeAssetPath and - // the inactive assetPath. - _inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath); - _activeSprite = await gameRef.loadSprite(_activeAssetPath); - } - - /// Activates the [DashNestBumper]. - void activate() { - _spriteComponent - ..sprite = _activeSprite - ..size = _activeSprite.originalSize / 10; - } - - /// Deactivates the [DashNestBumper]. - void deactivate() { - _spriteComponent - ..sprite = _inactiveSprite - ..size = _inactiveSprite.originalSize / 10; - } -} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..faaa510b --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'sparky_bumper_ball_contact_behavior.dart'; +export 'sparky_bumper_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior.dart new file mode 100644 index 00000000..57db300c --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class SparkyBumperBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart new file mode 100644 index 00000000..81cfa5e1 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template sparky_bumper_blinking_behavior} +/// Makes a [SparkyBumper] blink back to [SparkyBumperState.active] when +/// [SparkyBumperState.inactive]. +/// {@endtemplate} +class SparkyBumperBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro sparky_bumper_sprite_behavior} + SparkyBumperBlinkingBehavior() : super(period: 0.05); + + void _onNewState(SparkyBumperState state) { + switch (state) { + case SparkyBumperState.active: + break; + case SparkyBumperState.inactive: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart new file mode 100644 index 00000000..bbb9b63b --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'sparky_bumper_state.dart'; + +class SparkyBumperCubit extends Cubit { + SparkyBumperCubit() : super(SparkyBumperState.active); + + void onBallContacted() { + emit(SparkyBumperState.inactive); + } + + void onBlinked() { + emit(SparkyBumperState.active); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart new file mode 100644 index 00000000..35cc5ffa --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart @@ -0,0 +1,10 @@ +part of 'sparky_bumper_cubit.dart'; + +/// Indicates the [SparkyBumperCubit]'s current state. +enum SparkyBumperState { + /// A lit up bumper. + active, + + /// A dimmed bumper. + inactive, +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart new file mode 100644 index 00000000..d6434375 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart @@ -0,0 +1,164 @@ +import 'dart:math' as math; + +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/sparky_bumper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/sparky_bumper_cubit.dart'; + +/// {@template sparky_bumper} +/// Bumper for Sparky area. +/// {@endtemplate} +class SparkyBumper extends BodyComponent with InitialPosition { + /// {@macro sparky_bumper} + SparkyBumper._({ + required double majorRadius, + required double minorRadius, + required String onAssetPath, + required String offAssetPath, + required Vector2 spritePosition, + required this.bloc, + Iterable? children, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + super( + priority: RenderPriority.sparkyBumper, + renderBody: false, + children: [ + SparkyBumperBallContactBehavior(), + SparkyBumperBlinkingBehavior(), + _SparkyBumperSpriteGroupComponent( + onAssetPath: onAssetPath, + offAssetPath: offAssetPath, + position: spritePosition, + state: bloc.state, + ), + ...?children, + ], + ); + + /// {@macro sparky_bumper} + SparkyBumper.a({ + Iterable? children, + }) : this._( + majorRadius: 2.9, + minorRadius: 2.1, + onAssetPath: Assets.images.sparky.bumper.a.active.keyName, + offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName, + spritePosition: Vector2(0, -0.25), + bloc: SparkyBumperCubit(), + children: children, + ); + + /// {@macro sparky_bumper} + SparkyBumper.b({ + Iterable? children, + }) : this._( + majorRadius: 2.85, + minorRadius: 2, + onAssetPath: Assets.images.sparky.bumper.b.active.keyName, + offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName, + spritePosition: Vector2(0, -0.35), + bloc: SparkyBumperCubit(), + children: children, + ); + + /// {@macro sparky_bumper} + SparkyBumper.c({ + Iterable? children, + }) : this._( + majorRadius: 3, + minorRadius: 2.2, + onAssetPath: Assets.images.sparky.bumper.c.active.keyName, + offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName, + spritePosition: Vector2(0, -0.4), + bloc: SparkyBumperCubit(), + children: children, + ); + + /// Creates an [SparkyBumper] without any children. + /// + /// This can be used for testing [SparkyBumper]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + SparkyBumper.test({ + required this.bloc, + }) : _majorRadius = 3, + _minorRadius = 2.2; + + final double _majorRadius; + final double _minorRadius; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SparkyBumperCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(math.pi / 2.1); + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _SparkyBumperSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _SparkyBumperSpriteGroupComponent({ + required String onAssetPath, + required String offAssetPath, + required Vector2 position, + required SparkyBumperState state, + }) : _onAssetPath = onAssetPath, + _offAssetPath = offAssetPath, + super( + anchor: Anchor.center, + position: position, + current: state, + ); + + final String _onAssetPath; + final String _offAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + SparkyBumperState.active: Sprite( + gameRef.images.fromCache(_onAssetPath), + ), + SparkyBumperState.inactive: Sprite( + gameRef.images.fromCache(_offAssetPath), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer.dart index 6933a9ca..7f0cef8c 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer.dart @@ -3,76 +3,62 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_computer} -/// A [Blueprint] which creates the [_ComputerBase] and -/// [_ComputerTopSpriteComponent]. +/// A computer owned by Sparky. /// {@endtemplate} -class SparkyComputer extends Forge2DBlueprint { - @override - void build(_) { - final computerBase = _ComputerBase(); - final computerTop = _ComputerTopSpriteComponent(); - - addAll([ - computerBase, - computerTop, - ]); - } +class SparkyComputer extends Blueprint { + /// {@macro sparky_computer} + SparkyComputer() + : super( + components: [ + _ComputerBase(), + _ComputerTopSpriteComponent(), + ], + ); } class _ComputerBase extends BodyComponent with InitialPosition { - _ComputerBase(); + _ComputerBase() + : super( + priority: RenderPriority.computerBase, + renderBody: false, + children: [_ComputerBaseSpriteComponent()], + ); List _createFixtureDefs() { - final fixturesDef = []; - final leftEdge = EdgeShape() ..set( - Vector2(-14.9, 46), - Vector2(-15.3, 49.6), + Vector2(-14.9, -46), + Vector2(-15.3, -49.6), ); - final leftEdgeFixtureDef = FixtureDef(leftEdge); - fixturesDef.add(leftEdgeFixtureDef); - final topEdge = EdgeShape() ..set( - Vector2(-15.3, 49.6), - Vector2(-10.7, 50.6), + Vector2(-15.3, -49.6), + Vector2(-10.7, -50.6), ); - final topEdgeFixtureDef = FixtureDef(topEdge); - fixturesDef.add(topEdgeFixtureDef); - final rightEdge = EdgeShape() ..set( - Vector2(-10.7, 50.6), - Vector2(-9, 47.2), + Vector2(-10.7, -50.6), + Vector2(-9, -47.2), ); - final rightEdgeFixtureDef = FixtureDef(rightEdge); - fixturesDef.add(rightEdgeFixtureDef); - return fixturesDef; + return [ + FixtureDef(leftEdge), + FixtureDef(topEdge), + FixtureDef(rightEdge), + ]; } @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; - + final bodyDef = BodyDef(position: initialPosition); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); return body; } - - @override - Future onLoad() async { - await super.onLoad(); - renderBody = false; - - await add(_ComputerBaseSpriteComponent()); - } } class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { @@ -99,7 +85,7 @@ class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef { : super( anchor: Anchor.center, position: Vector2(-12.45, -49.75), - priority: 1, + priority: RenderPriority.computerTop, ); @override diff --git a/packages/pinball_components/lib/src/extensions/extensions.dart b/packages/pinball_components/lib/src/extensions/extensions.dart new file mode 100644 index 00000000..4be86fd3 --- /dev/null +++ b/packages/pinball_components/lib/src/extensions/extensions.dart @@ -0,0 +1 @@ +export 'score.dart'; diff --git a/packages/pinball_components/lib/src/extensions/score.dart b/packages/pinball_components/lib/src/extensions/score.dart new file mode 100644 index 00000000..bd60d27e --- /dev/null +++ b/packages/pinball_components/lib/src/extensions/score.dart @@ -0,0 +1,11 @@ +import 'package:intl/intl.dart'; + +final _numberFormat = NumberFormat('#,###'); + +/// Adds score related extensions to int +extension ScoreX on int { + /// Formats this number as a score value + String formatScore() { + return _numberFormat.format(this); + } +} diff --git a/packages/pinball_components/lib/src/flame/blueprint.dart b/packages/pinball_components/lib/src/flame/blueprint.dart deleted file mode 100644 index 5c2df683..00000000 --- a/packages/pinball_components/lib/src/flame/blueprint.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; - -const _attachedErrorMessage = "Can't add to attached Blueprints"; - -// TODO(erickzanardo): Keeping this inside our code base -// so we can experiment with the idea, but this is a -// potential upstream change on Flame. - -/// A [Blueprint] is a virtual way of grouping [Component]s -/// that are related, but they need to be added directly on -/// the [FlameGame] level. -// TODO(alestiago): refactor with feat/make-blueprint-extend-component. -abstract class Blueprint extends Component { - final List _components = []; - final List _blueprints = []; - - bool _isAttached = false; - - /// Called before the the [Component]s managed - /// by this blueprint is added to the [FlameGame] - void build(T gameRef); - - /// Attach the [Component]s built on [build] to the [game] - /// instance - @mustCallSuper - Future attach(T game) async { - build(game); - await Future.wait([ - game.addAll(_components), - ..._blueprints.map(game.addFromBlueprint).toList(), - ]); - _isAttached = true; - } - - /// Adds a single [Component] to this blueprint. - @override - Future add(Component component) async { - assert(!_isAttached, _attachedErrorMessage); - _components.add(component); - } - - /// Adds a list of [Blueprint]s to this blueprint. - void addAllBlueprints(List blueprints) { - assert(!_isAttached, _attachedErrorMessage); - _blueprints.addAll(blueprints); - } - - /// Adds a single [Blueprint] to this blueprint. - void addBlueprint(Blueprint blueprint) { - assert(!_isAttached, _attachedErrorMessage); - _blueprints.add(blueprint); - } - - /// Returns a copy of the components built by this blueprint - List get components => List.unmodifiable(_components); - - /// Returns a copy of the children blueprints - List get blueprints => List.unmodifiable(_blueprints); -} - -/// A [Blueprint] that provides additional -/// structures specific to flame_forge2d -abstract class Forge2DBlueprint extends Blueprint { - final List _callbacks = []; - - /// Adds a single [ContactCallback] to this blueprint - void addContactCallback(ContactCallback callback) { - assert(!_isAttached, _attachedErrorMessage); - _callbacks.add(callback); - } - - /// Adds a collection of [ContactCallback]s to this blueprint - void addAllContactCallback(List callbacks) { - assert(!_isAttached, _attachedErrorMessage); - _callbacks.addAll(callbacks); - } - - @override - Future attach(Forge2DGame game) async { - await super.attach(game); - - for (final callback in _callbacks) { - game.addContactCallback(callback); - } - } - - /// Returns a copy of the callbacks built by this blueprint - List get callbacks => List.unmodifiable(_callbacks); -} - -/// Adds helper methods regardin [Blueprint]s to [FlameGame] -extension FlameGameBlueprint on FlameGame { - /// Shortcut to attach a [Blueprint] instance to this game - /// equivalent to `MyBluepinrt().attach(game)` - Future addFromBlueprint(Blueprint blueprint) async { - await blueprint.attach(this); - } -} diff --git a/packages/pinball_components/lib/src/flame/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart deleted file mode 100644 index 9af8dba6..00000000 --- a/packages/pinball_components/lib/src/flame/flame.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'blueprint.dart'; -export 'priority.dart'; diff --git a/packages/pinball_components/lib/src/flame/priority.dart b/packages/pinball_components/lib/src/flame/priority.dart deleted file mode 100644 index f4dccabf..00000000 --- a/packages/pinball_components/lib/src/flame/priority.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:math' as math; -import 'package:flame/components.dart'; - -/// Helper methods to change the [priority] of a [Component]. -extension ComponentPriorityX on Component { - static const _lowestPriority = 0; - - /// Changes the priority to a specific one. - void sendTo(int destinationPriority) { - if (priority != destinationPriority) { - priority = math.max(destinationPriority, _lowestPriority); - reorderChildren(); - } - } - - /// Changes the priority to the lowest possible. - void sendToBack() { - if (priority != _lowestPriority) { - priority = _lowestPriority; - reorderChildren(); - } - } - - /// Decreases the priority to be lower than another [Component]. - void showBehindOf(Component other) { - if (priority >= other.priority) { - priority = math.max(other.priority - 1, _lowestPriority); - reorderChildren(); - } - } - - /// Increases the priority to be higher than another [Component]. - void showInFrontOf(Component other) { - if (priority <= other.priority) { - priority = other.priority + 1; - reorderChildren(); - } - } -} diff --git a/packages/pinball_components/lib/src/pinball_components.dart b/packages/pinball_components/lib/src/pinball_components.dart index bd8f99de..e50f9875 100644 --- a/packages/pinball_components/lib/src/pinball_components.dart +++ b/packages/pinball_components/lib/src/pinball_components.dart @@ -1,2 +1,2 @@ export 'components/components.dart'; -export 'flame/flame.dart'; +export 'extensions/extensions.dart'; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 72263d1d..27bf0aec 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -7,15 +7,25 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: - flame: ^1.1.0 - flame_forge2d: ^0.10.0 + bloc: ^8.0.3 + flame: ^1.1.1 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter geometry: path: ../geometry - + intl: ^0.17.0 + pinball_flame: + path: ../pinball_flame + pinball_theme: + path: ../pinball_theme dev_dependencies: + bloc_test: ^9.0.3 flame_test: ^1.3.0 flutter_test: sdk: flutter @@ -36,26 +46,34 @@ flutter: assets: - assets/images/ + - assets/images/ball/ - assets/images/baseboard/ - assets/images/boundary/ - assets/images/dino/ - assets/images/flipper/ - assets/images/launch_ramp/ - - assets/images/dash_bumper/a/ - - assets/images/dash_bumper/b/ - - assets/images/dash_bumper/main/ + - assets/images/dash/ + - assets/images/dash/bumper/a/ + - assets/images/dash/bumper/b/ + - assets/images/dash/bumper/main/ - assets/images/spaceship/ - assets/images/spaceship/rail/ - assets/images/spaceship/ramp/ + - assets/images/spaceship/ramp/arrow/ - assets/images/chrome_dino/ - assets/images/kicker/ + - assets/images/plunger/ - assets/images/slingshot/ + - assets/images/alien_bumper/a/ + - assets/images/alien_bumper/b/ + - assets/images/sparky/ - assets/images/sparky/computer/ - assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/c/ - assets/images/backboard/ - assets/images/google_word/ + - assets/images/signpost/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/common/add_game.dart b/packages/pinball_components/sandbox/lib/common/add_game.dart new file mode 100644 index 00000000..5b6388d3 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/common/add_game.dart @@ -0,0 +1,36 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; + +const _path = + 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/'; + +extension StoryAddGame on Story { + void addGame({ + required String title, + required String description, + required Game Function(DashbookContext) gameBuilder, + }) { + final _chapter = Chapter( + title, + (DashbookContext context) { + final game = gameBuilder(context); + if (game is Traceable) { + game.trace = context.boolProperty('Trace', true); + } + + return GameWidget(game: game); + }, + this, + codeLink: '$_path${name.toPath()}/${title.toPath()}', + info: description, + ); + chapters.add(_chapter); + } +} + +extension on String { + String toPath() { + return replaceAll(' ', '_')..toLowerCase(); + } +} diff --git a/packages/pinball_components/sandbox/lib/common/common.dart b/packages/pinball_components/sandbox/lib/common/common.dart index bb232e24..53010f5e 100644 --- a/packages/pinball_components/sandbox/lib/common/common.dart +++ b/packages/pinball_components/sandbox/lib/common/common.dart @@ -1,3 +1,3 @@ +export 'add_game.dart'; export 'games.dart'; -export 'methods.dart'; export 'trace.dart'; diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 4aae07cb..89d16450 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -5,13 +5,25 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -abstract class BasicGame extends Forge2DGame { - BasicGame() { +abstract class AssetsGame extends Forge2DGame { + AssetsGame({ + List? imagesFileNames, + }) : _imagesFileNames = imagesFileNames { images.prefix = ''; } + + final List? _imagesFileNames; + + @override + Future onLoad() async { + await super.onLoad(); + if (_imagesFileNames != null) { + await images.loadAll(_imagesFileNames!); + } + } } -abstract class LineGame extends BasicGame with PanDetector { +abstract class LineGame extends AssetsGame with PanDetector { Vector2? _lineEnd; @override diff --git a/packages/pinball_components/sandbox/lib/common/methods.dart b/packages/pinball_components/sandbox/lib/common/methods.dart deleted file mode 100644 index 35198922..00000000 --- a/packages/pinball_components/sandbox/lib/common/methods.dart +++ /dev/null @@ -1,3 +0,0 @@ -String buildSourceLink(String path) { - return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path'; -} diff --git a/packages/pinball_components/sandbox/lib/common/trace.dart b/packages/pinball_components/sandbox/lib/common/trace.dart index 44760a7c..6129a779 100644 --- a/packages/pinball_components/sandbox/lib/common/trace.dart +++ b/packages/pinball_components/sandbox/lib/common/trace.dart @@ -12,8 +12,6 @@ extension BodyTrace on BodyComponent { unawaited( mounted.whenComplete(() { - children.whereType().first.setOpacity(0.5); - descendants().whereType().forEach((anchor) { final fixtureDef = FixtureDef(CircleShape()..radius = 0.5); anchor.body.createFixture(fixtureDef); @@ -32,9 +30,13 @@ mixin Traceable on Forge2DGame { }) async { if (trace) { await ready(); - children + descendants() .whereType() .forEach((bodyComponent) => bodyComponent.trace()); + + descendants() + .whereType() + .forEach((sprite) => sprite.setOpacity(0.5)); } } } diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 193a0d8f..e5f7f177 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -16,20 +16,20 @@ void main() { addLayerStories(dashbook); addEffectsStories(dashbook); addFlipperStories(dashbook); - addSpaceshipStories(dashbook); addBaseboardStories(dashbook); addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); addKickerStories(dashbook); + addPlungerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); - addZoomStories(dashbook); + addAlienZoneStories(dashbook); addBoundariesStories(dashbook); addGoogleWordStories(dashbook); - addSpaceshipRampStories(dashbook); - addSpaceshipRailStories(dashbook); addLaunchRampStories(dashbook); addScoreTextStories(dashbook); + addBackboardStories(dashbook); + addDinoWallStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart new file mode 100644 index 00000000..4832a468 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AlienBumperAGame extends BallGame { + AlienBumperAGame() + : super( + color: const Color(0xFF0000FF), + imagesFileNames: [ + Assets.images.alienBumper.a.active.keyName, + Assets.images.alienBumper.a.inactive.keyName, + ], + ); + + static const description = ''' + Shows how a AlienBumperA is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await add( + AlienBumper.a()..priority = 1, + ); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart new file mode 100644 index 00000000..abb206ca --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AlienBumperBGame extends BallGame { + AlienBumperBGame() + : super( + color: const Color(0xFF0000FF), + imagesFileNames: [ + Assets.images.alienBumper.b.active.keyName, + Assets.images.alienBumper.b.inactive.keyName, + ], + ); + + static const description = ''' + Shows how a AlienBumperB is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await add( + AlienBumper.b()..priority = 1, + ); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart similarity index 66% rename from packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart rename to packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart index 97124c3f..ad897dd4 100644 --- a/packages/pinball_components/sandbox/lib/stories/spaceship/basic_spaceship_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart @@ -3,13 +3,14 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/common/common.dart'; -class BasicSpaceshipGame extends BasicGame with TapDetector { - static const info = ''' +class SpaceshipGame extends AssetsGame with TapDetector { + static const description = ''' Shows how a Spaceship works. - Tap anywhere on the screen to spawn a Ball into the game. + - Tap anywhere on the screen to spawn a Ball into the game. '''; @override @@ -17,10 +18,10 @@ class BasicSpaceshipGame extends BasicGame with TapDetector { await super.onLoad(); camera.followVector2(Vector2.zero()); - - unawaited( - addFromBlueprint(Spaceship(position: Vector2.zero())), + await addFromBlueprint( + Spaceship(position: Vector2.zero()), ); + await ready(); } @override diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart similarity index 72% rename from packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart rename to packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart index cef04304..2a13fb5e 100644 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart @@ -3,17 +3,18 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SpaceshipRailGame extends BasicBallGame { +class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( color: Colors.blue, - ballPriority: SpaceshipRail.ballPriorityInsideRail, + ballPriority: RenderPriority.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, ); - static const info = ''' + static const description = ''' Shows how SpaceshipRail are rendered. - Activate the "trace" parameter to overlay the body. @@ -25,10 +26,8 @@ class SpaceshipRailGame extends BasicBallGame { await super.onLoad(); camera.followVector2(Vector2(-30, -10)); - - final spaceshipRail = SpaceshipRail(); - unawaited(addFromBlueprint(spaceshipRail)); - + await addFromBlueprint(SpaceshipRail()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart new file mode 100644 index 00000000..1817f40a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SpaceshipRampGame extends BallGame with KeyboardEvents { + SpaceshipRampGame() + : super( + color: Colors.blue, + ballPriority: RenderPriority.ballOnSpaceshipRamp, + ballLayer: Layer.spaceshipEntranceRamp, + imagesFileNames: [ + Assets.images.spaceship.ramp.railingBackground.keyName, + Assets.images.spaceship.ramp.main.keyName, + Assets.images.spaceship.ramp.boardOpening.keyName, + Assets.images.spaceship.ramp.railingForeground.keyName, + Assets.images.spaceship.ramp.arrow.inactive.keyName, + Assets.images.spaceship.ramp.arrow.active1.keyName, + Assets.images.spaceship.ramp.arrow.active2.keyName, + Assets.images.spaceship.ramp.arrow.active3.keyName, + Assets.images.spaceship.ramp.arrow.active4.keyName, + Assets.images.spaceship.ramp.arrow.active5.keyName, + ], + ); + + static const description = ''' + Shows how SpaceshipRamp is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. + - Press space to progress arrow sprites. +'''; + + late final SpaceshipRamp _spaceshipRamp; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2(-12, -50)); + await addFromBlueprint( + _spaceshipRamp = SpaceshipRamp(), + ); + await traceAllBodies(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space) { + _spaceshipRamp.progress(); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart new file mode 100644 index 00000000..b4e7c1b6 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart @@ -0,0 +1,36 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart'; +import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart'; +import 'package:sandbox/stories/alien_zone/spaceship_game.dart'; +import 'package:sandbox/stories/alien_zone/spaceship_rail_game.dart'; +import 'package:sandbox/stories/alien_zone/spaceship_ramp_game.dart'; + +void addAlienZoneStories(Dashbook dashbook) { + dashbook.storiesOf('Alien Zone') + ..addGame( + title: 'Alien Bumper A', + description: AlienBumperAGame.description, + gameBuilder: (_) => AlienBumperAGame(), + ) + ..addGame( + title: 'Alien Bumper B', + description: AlienBumperBGame.description, + gameBuilder: (_) => AlienBumperBGame(), + ) + ..addGame( + title: 'Spaceship', + description: SpaceshipGame.description, + gameBuilder: (_) => SpaceshipGame(), + ) + ..addGame( + title: 'Spaceship Rail', + description: SpaceshipRailGame.description, + gameBuilder: (_) => SpaceshipRailGame(), + ) + ..addGame( + title: 'Spaceship Ramp', + description: SpaceshipRampGame.description, + gameBuilder: (_) => SpaceshipRampGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart new file mode 100644 index 00000000..639a4b57 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart @@ -0,0 +1,54 @@ +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_theme/pinball_theme.dart'; +import 'package:sandbox/common/common.dart'; + +class BackboardGameOverGame extends AssetsGame + with HasKeyboardHandlerComponents { + BackboardGameOverGame(this.score, this.character) + : super( + imagesFileNames: characterIconPaths.values.toList(), + ); + + static const description = ''' + Shows how the Backboard in game over mode is rendered. + + - Select a character to update the character icon. + '''; + + static final characterIconPaths = { + 'Dash': Assets.images.dash.leaderboardIcon.keyName, + 'Sparky': Assets.images.sparky.leaderboardIcon.keyName, + 'Android': Assets.images.android.leaderboardIcon.keyName, + 'Dino': Assets.images.dino.leaderboardIcon.keyName, + }; + + final int score; + + final String character; + + @override + Future onLoad() async { + camera + ..followVector2(Vector2.zero()) + ..zoom = 5; + + await add( + Backboard.gameOver( + position: Vector2(0, 20), + score: score, + characterIconPath: characterIconPaths[character]!, + onSubmit: (initials) { + add( + ScoreText( + text: 'User $initials made $score', + position: Vector2(0, 50), + color: Colors.pink, + ), + ); + }, + ), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart new file mode 100644 index 00000000..6da9206c --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart @@ -0,0 +1,25 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BackboardWaitingGame extends AssetsGame { + BackboardWaitingGame() + : super( + imagesFileNames: [], + ); + + static const description = ''' + Shows how the Backboard in waiting mode is rendered. + '''; + + @override + Future onLoad() async { + camera + ..followVector2(Vector2.zero()) + ..zoom = 5; + + await add( + Backboard.waiting(position: Vector2(0, 20)), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart new file mode 100644 index 00000000..b8c85d10 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart @@ -0,0 +1,25 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/backboard/backboard_game_over_game.dart'; +import 'package:sandbox/stories/backboard/backboard_waiting_game.dart'; + +void addBackboardStories(Dashbook dashbook) { + dashbook.storiesOf('Backboard') + ..addGame( + title: 'Waiting', + description: BackboardWaitingGame.description, + gameBuilder: (_) => BackboardWaitingGame(), + ) + ..addGame( + title: 'Game over', + description: BackboardGameOverGame.description, + gameBuilder: (context) => BackboardGameOverGame( + context.numberProperty('Score', 9000000000).toInt(), + context.listProperty( + 'Character', + BackboardGameOverGame.characterIconPaths.keys.first, + BackboardGameOverGame.characterIconPaths.keys.toList(), + ), + ), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart index 3b8fe149..7f07de97 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart @@ -4,10 +4,10 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; class BallBoosterGame extends LineGame { - static const info = ''' + static const description = ''' Shows how a Ball with a boost works. - Drag to launch a boosted Ball. + - Drag to launch a boosted Ball. '''; @override diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart index 4fbeae1b..e57a0322 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart @@ -3,14 +3,20 @@ import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BasicBallGame extends BasicGame with TapDetector, Traceable { - BasicBallGame({ - required this.color, +class BallGame extends AssetsGame with TapDetector, Traceable { + BallGame({ + this.color = Colors.blue, this.ballPriority = 0, this.ballLayer = Layer.all, - }); + List? imagesFileNames, + }) : super( + imagesFileNames: [ + Assets.images.ball.ball.keyName, + if (imagesFileNames != null) ...imagesFileNames, + ], + ); - static const info = ''' + static const description = ''' Shows how a Ball works. - Tap anywhere on the screen to spawn a ball into the game. diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index 64892d22..eb472282 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart @@ -1,5 +1,4 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/ball_booster_game.dart'; @@ -7,22 +6,16 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; void addBallStories(Dashbook dashbook) { dashbook.storiesOf('Ball') - ..add( - 'Basic', - (context) => GameWidget( - game: BasicBallGame( - color: context.colorProperty('color', Colors.blue), - )..trace = context.boolProperty('Trace', true), + ..addGame( + title: 'Colored', + description: BallGame.description, + gameBuilder: (context) => BallGame( + color: context.colorProperty('color', Colors.blue), ), - codeLink: buildSourceLink('ball/basic.dart'), - info: BasicBallGame.info, ) - ..add( - 'Booster', - (context) => GameWidget( - game: BallBoosterGame(), - ), - codeLink: buildSourceLink('ball/ball_booster.dart'), - info: BallBoosterGame.info, + ..addGame( + title: 'Booster', + description: BallBoosterGame.description, + gameBuilder: (context) => BallBoosterGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart new file mode 100644 index 00000000..4e86f732 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart @@ -0,0 +1,37 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class BaseboardGame extends BallGame { + BaseboardGame() + : super( + imagesFileNames: [ + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + ], + ); + + static const description = ''' + Shows how the Baseboards are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + await addAll([ + Baseboard(side: BoardSide.left) + ..initialPosition = center - Vector2(25, 0) + ..priority = 1, + Baseboard(side: BoardSide.right) + ..initialPosition = center + Vector2(25, 0) + ..priority = 1, + ]); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/basic_baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/basic_baseboard_game.dart deleted file mode 100644 index 0650fa13..00000000 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/basic_baseboard_game.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; - -class BasicBaseboardGame extends BasicGame { - static const info = 'Shows how a Baseboard works.'; - - @override - Future onLoad() async { - await super.onLoad(); - - final center = screenToWorld(camera.viewport.canvasSize! / 2); - - final leftBaseboard = Baseboard(side: BoardSide.left) - ..initialPosition = center - Vector2(25, 0); - final rightBaseboard = Baseboard(side: BoardSide.right) - ..initialPosition = center + Vector2(25, 0); - - await addAll([ - leftBaseboard, - rightBaseboard, - ]); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart index b3982af4..b07e3a73 100644 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/baseboard/basic_baseboard_game.dart'; +import 'package:sandbox/stories/baseboard/baseboard_game.dart'; void addBaseboardStories(Dashbook dashbook) { - dashbook.storiesOf('Baseboard').add( - 'Basic', - (context) => GameWidget( - game: BasicBaseboardGame(), - ), - codeLink: buildSourceLink('baseboard/basic.dart'), - info: BasicBaseboardGame.info, + dashbook.storiesOf('Baseboard').addGame( + title: 'Traced', + description: BaseboardGame.description, + gameBuilder: (_) => BaseboardGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart index a98fb7b9..cf78750d 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart @@ -1,12 +1,19 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BoundariesGame extends BasicBallGame with Traceable { - BoundariesGame() : super(color: const Color(0xFFFF0000)); +class BoundariesGame extends BallGame { + BoundariesGame() + : super( + imagesFileNames: [ + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.boundary.bottom.keyName, + ], + ); - static const info = ''' + static const description = ''' Shows how Boundaries are rendered. - Activate the "trace" parameter to overlay the body. @@ -17,13 +24,11 @@ class BoundariesGame extends BasicBallGame with Traceable { Future onLoad() async { await super.onLoad(); - await addFromBlueprint(Boundaries()); - await ready(); - camera ..followVector2(Vector2.zero()) ..zoom = 6; - + await addFromBlueprint(Boundaries()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart b/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart index bebb0df7..02bb87b4 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/boundaries/boundaries_game.dart'; void addBoundariesStories(Dashbook dashbook) { - dashbook.storiesOf('Boundaries').add( - 'Basic', - (context) => GameWidget( - game: BoundariesGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('boundaries_game/basic.dart'), - info: BoundariesGame.info, + dashbook.storiesOf('Boundaries').addGame( + title: 'Traced', + description: BoundariesGame.description, + gameBuilder: (_) => BoundariesGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart index 94bf6e44..2e6831e3 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart @@ -2,7 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; class ChromeDinoGame extends Forge2DGame { - static const info = 'Shows how a ChromeDino is rendered.'; + static const description = 'Shows how a ChromeDino is rendered.'; @override Future onLoad() async { diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart index fb7c2ee1..391cdca7 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart'; void addChromeDinoStories(Dashbook dashbook) { - dashbook.storiesOf('Chrome Dino').add( - 'Basic', - (context) => GameWidget( - game: ChromeDinoGame(), - ), - codeLink: buildSourceLink('chrome_dino/basic.dart'), - info: ChromeDinoGame.info, + dashbook.storiesOf('Chrome Dino').addGame( + title: 'Trace', + description: ChromeDinoGame.description, + gameBuilder: (_) => ChromeDinoGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart new file mode 100644 index 00000000..b491d64b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class DinoWallGame extends BallGame { + DinoWallGame() : super(); + + static const description = ''' + Shows how DinoWalls are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + await images.loadAll([ + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, + ]); + + await addFromBlueprint(DinoWalls()); + camera.followVector2(Vector2.zero()); + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart new file mode 100644 index 00000000..e24d26cc --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/dino_wall/dino_wall_game.dart'; + +void addDinoWallStories(Dashbook dashbook) { + dashbook.storiesOf('DinoWall').addGame( + title: 'Traced', + description: DinoWallGame.description, + gameBuilder: (_) => DinoWallGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart b/packages/pinball_components/sandbox/lib/stories/effects/camera_zoom_game.dart similarity index 71% rename from packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart rename to packages/pinball_components/sandbox/lib/stories/effects/camera_zoom_game.dart index 276dd39c..11f2b776 100644 --- a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/camera_zoom_game.dart @@ -3,17 +3,18 @@ import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BasicCameraZoomGame extends BasicGame with TapDetector { - static const info = ''' - Simple game to demonstrate how the CameraZoom can be used. - Tap to zoom in/out +class CameraZoomGame extends AssetsGame with TapDetector { + static const description = ''' + Shows how CameraZoom can be used. + + - Tap to zoom in/out. '''; bool zoomApplied = false; @override Future onLoad() async { - final sprite = await loadSprite(Assets.images.flutterSignPost.keyName); + final sprite = await loadSprite(Assets.images.signpost.inactive.keyName); await add( SpriteComponent( diff --git a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart index ecc22910..3ca8ec1a 100644 --- a/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/fire_effect_game.dart @@ -3,10 +3,10 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; class FireEffectGame extends LineGame { - static const info = ''' + static const description = ''' Shows how the FireEffect renders. - Drag a line to define the trail direction. + - Drag a line to define the trail direction. '''; @override diff --git a/packages/pinball_components/sandbox/lib/stories/effects/stories.dart b/packages/pinball_components/sandbox/lib/stories/effects/stories.dart index 37ba434e..9b022987 100644 --- a/packages/pinball_components/sandbox/lib/stories/effects/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/effects/stories.dart @@ -1,13 +1,18 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/effects/camera_zoom_game.dart'; import 'package:sandbox/stories/effects/fire_effect_game.dart'; void addEffectsStories(Dashbook dashbook) { - dashbook.storiesOf('Effects').add( - 'Fire Effect', - (context) => GameWidget(game: FireEffectGame()), - codeLink: buildSourceLink('effects/fire_effect.dart'), - info: FireEffectGame.info, - ); + dashbook.storiesOf('Effects') + ..addGame( + title: 'Fire', + description: FireEffectGame.description, + gameBuilder: (_) => FireEffectGame(), + ) + ..addGame( + title: 'CameraZoom', + description: CameraZoomGame.description, + gameBuilder: (_) => CameraZoomGame(), + ); } diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart index 5a9e1787..789fa8b4 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart @@ -2,14 +2,19 @@ import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class FlipperGame extends BasicBallGame with KeyboardEvents, Traceable { - FlipperGame() : super(color: Colors.blue); +class FlipperGame extends BallGame with KeyboardEvents { + FlipperGame() + : super( + imagesFileNames: [ + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ], + ); - static const info = ''' + static const description = ''' Shows how Flippers are rendered. - Activate the "trace" parameter to overlay the body. @@ -36,15 +41,11 @@ class FlipperGame extends BasicBallGame with KeyboardEvents, Traceable { await super.onLoad(); final center = screenToWorld(camera.viewport.canvasSize! / 2); - - leftFlipper = Flipper(side: BoardSide.left) - ..initialPosition = center - Vector2(Flipper.size.x, 0); - rightFlipper = Flipper(side: BoardSide.right) - ..initialPosition = center + Vector2(Flipper.size.x, 0); - await addAll([ - leftFlipper, - rightFlipper, + leftFlipper = Flipper(side: BoardSide.left) + ..initialPosition = center - Vector2(Flipper.size.x, 0), + rightFlipper = Flipper(side: BoardSide.right) + ..initialPosition = center + Vector2(Flipper.size.x, 0), ]); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart index f8aa0075..2ef2a4b6 100644 --- a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/flipper/flipper_game.dart'; void addFlipperStories(Dashbook dashbook) { - dashbook.storiesOf('Flipper').add( - 'Basic', - (context) => GameWidget( - game: FlipperGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flipper/basic.dart'), - info: FlipperGame.info, + dashbook.storiesOf('Flipper').addGame( + title: 'Traced', + description: FlipperGame.description, + gameBuilder: (_) => FlipperGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart index c1407819..3580a175 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart @@ -1,15 +1,19 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BigDashNestBumperGame extends BasicBallGame with Traceable { - BigDashNestBumperGame() : super(color: const Color(0xFF0000FF)); +class BigDashNestBumperGame extends BallGame { + BigDashNestBumperGame() + : super( + imagesFileNames: [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + ], + ); - static const info = ''' + static const description = ''' Shows how a BigDashNestBumper is rendered. - Activate the "trace" parameter to overlay the body. @@ -18,9 +22,11 @@ class BigDashNestBumperGame extends BasicBallGame with Traceable { @override Future onLoad() async { await super.onLoad(); + camera.followVector2(Vector2.zero()); - await add(BigDashNestBumper()..priority = 1); - await traceAllBodies(); + await add( + DashNestBumper.main()..priority = 1, + ); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart deleted file mode 100644 index f3ba7bda..00000000 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/ball/basic_ball_game.dart'; - -class FlutterSignPostGame extends BasicBallGame with Traceable { - FlutterSignPostGame() : super(color: const Color(0xFF0000FF)); - - static const info = ''' - Shows how a FlutterSignPost is rendered. - - - Activate the "trace" parameter to overlay the body. -'''; - - @override - Future onLoad() async { - await super.onLoad(); - camera.followVector2(Vector2.zero()); - await add(FlutterSignPost()..priority = 1); - await traceAllBodies(); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart new file mode 100644 index 00000000..349dd811 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SignpostGame extends BallGame { + SignpostGame() + : super( + imagesFileNames: [ + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + ], + ); + + static const description = ''' + Shows how a Signpost is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap to progress the sprite. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await add(Signpost()); + await traceAllBodies(); + } + + @override + void onTap() { + super.onTap(); + firstChild()!.progress(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart index a8499581..071f6aa1 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart @@ -1,15 +1,19 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SmallDashNestBumperAGame extends BasicBallGame with Traceable { - SmallDashNestBumperAGame() : super(color: const Color(0xFF0000FF)); +class SmallDashNestBumperAGame extends BallGame { + SmallDashNestBumperAGame() + : super( + imagesFileNames: [ + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + ], + ); - static const info = ''' + static const description = ''' Shows how a SmallDashNestBumper ("a") is rendered. - Activate the "trace" parameter to overlay the body. @@ -18,9 +22,9 @@ class SmallDashNestBumperAGame extends BasicBallGame with Traceable { @override Future onLoad() async { await super.onLoad(); + camera.followVector2(Vector2.zero()); - await add(SmallDashNestBumper.a()..priority = 1); - await traceAllBodies(); + await add(DashNestBumper.a()..priority = 1); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart index 91b2a383..a47b9962 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart @@ -1,15 +1,19 @@ import 'dart:async'; -import 'dart:ui'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SmallDashNestBumperBGame extends BasicBallGame with Traceable { - SmallDashNestBumperBGame() : super(color: const Color(0xFF0000FF)); +class SmallDashNestBumperBGame extends BallGame { + SmallDashNestBumperBGame() + : super( + imagesFileNames: [ + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + ], + ); - static const info = ''' + static const description = ''' Shows how a SmallDashNestBumper ("b") is rendered. - Activate the "trace" parameter to overlay the body. @@ -18,9 +22,9 @@ class SmallDashNestBumperBGame extends BasicBallGame with Traceable { @override Future onLoad() async { await super.onLoad(); + camera.followVector2(Vector2.zero()); - await add(SmallDashNestBumper.b()..priority = 1); - await traceAllBodies(); + await add(DashNestBumper.b()..priority = 1); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart index a625d174..ef9c1ffb 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -1,47 +1,30 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart'; -import 'package:sandbox/stories/flutter_forest/flutter_sign_post_game.dart'; +import 'package:sandbox/stories/flutter_forest/signpost_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; void addDashNestBumperStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') - ..add( - 'Flutter Sign Post', - (context) => GameWidget( - game: FlutterSignPostGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/flutter_sign_post.dart'), - info: FlutterSignPostGame.info, + ..addGame( + title: 'Signpost', + description: SignpostGame.description, + gameBuilder: (_) => SignpostGame(), ) - ..add( - 'Big Dash Nest Bumper', - (context) => GameWidget( - game: BigDashNestBumperGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/big_dash_nest_bumper.dart'), - info: BigDashNestBumperGame.info, + ..addGame( + title: 'Big Dash Nest Bumper', + description: BigDashNestBumperGame.description, + gameBuilder: (_) => BigDashNestBumperGame(), ) - ..add( - 'Small Dash Nest Bumper A', - (context) => GameWidget( - game: SmallDashNestBumperAGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_a.dart'), - info: SmallDashNestBumperAGame.info, + ..addGame( + title: 'Small Dash Nest Bumper A', + description: SmallDashNestBumperAGame.description, + gameBuilder: (_) => SmallDashNestBumperAGame(), ) - ..add( - 'Small Dash Nest Bumper B', - (context) => GameWidget( - game: SmallDashNestBumperBGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_b.dart'), - info: SmallDashNestBumperBGame.info, + ..addGame( + title: 'Small Dash Nest Bumper B', + description: SmallDashNestBumperBGame.description, + gameBuilder: (_) => SmallDashNestBumperBGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart index ad6e556b..be90fdb9 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class GoogleLetterGame extends BasicBallGame { +class GoogleLetterGame extends BallGame { GoogleLetterGame() : super(color: const Color(0xFF009900)); - static const info = ''' + static const description = ''' Shows how a GoogleLetter is rendered. - Tap anywhere on the screen to spawn a ball into the game. @@ -17,7 +17,6 @@ class GoogleLetterGame extends BasicBallGame { @override Future onLoad() async { await super.onLoad(); - addContactCallback(_BallGoogleLetterContactCallback()); camera.followVector2(Vector2.zero()); await add(GoogleLetter(0)); @@ -25,12 +24,3 @@ class GoogleLetterGame extends BasicBallGame { await traceAllBodies(); } } - -class _BallGoogleLetterContactCallback - extends ContactCallback { - @override - void begin(Ball a, GoogleLetter b, Contact contact) { - super.begin(a, b, contact); - b.activate(); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart b/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart index 290bf9dd..858a9a30 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/google_word/google_letter_game.dart'; void addGoogleWordStories(Dashbook dashbook) { - dashbook.storiesOf('Google Word').add( - 'Letter', - (context) => GameWidget( - game: GoogleLetterGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('google_word/letter.dart'), - info: GoogleLetterGame.info, + dashbook.storiesOf('Google Word').addGame( + title: 'Letter 0', + description: GoogleLetterGame.description, + gameBuilder: (_) => GoogleLetterGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart index 1b29c3f9..9b7d96cc 100644 --- a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart @@ -1,12 +1,9 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class KickerGame extends BasicBallGame with Traceable { - KickerGame() : super(color: const Color(0xFFFF0000)); - - static const info = ''' +class KickerGame extends BallGame { + static const description = ''' Shows how Kickers are rendered. - Activate the "trace" parameter to overlay the body. @@ -18,14 +15,14 @@ class KickerGame extends BasicBallGame with Traceable { await super.onLoad(); final center = screenToWorld(camera.viewport.canvasSize! / 2); - - final leftKicker = Kicker(side: BoardSide.left) - ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y); - await add(leftKicker); - - final rightKicker = Kicker(side: BoardSide.right) - ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y); - await add(rightKicker); + await addAll( + [ + Kicker(side: BoardSide.left) + ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + Kicker(side: BoardSide.right) + ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y), + ], + ); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart index 77d6ff29..cfebb7e4 100644 --- a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/kicker/kicker_game.dart'; void addKickerStories(Dashbook dashbook) { - dashbook.storiesOf('Kickers').add( - 'Basic', - (context) => GameWidget( - game: KickerGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('kicker_game/basic.dart'), - info: KickerGame.info, + dashbook.storiesOf('Kickers').addGame( + title: 'Traced', + description: KickerGame.description, + gameBuilder: (_) => KickerGame(), ); } 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 5258de86..1be94133 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 @@ -3,17 +3,18 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class LaunchRampGame extends BasicBallGame { +class LaunchRampGame extends BallGame { LaunchRampGame() : super( color: Colors.blue, - ballPriority: LaunchRamp.ballPriorityInsideRamp, + ballPriority: RenderPriority.ballOnLaunchRamp, ballLayer: Layer.launcher, ); - static const info = ''' + static const description = ''' Shows how LaunchRamp are rendered. - Activate the "trace" parameter to overlay the body. @@ -27,10 +28,8 @@ class LaunchRampGame extends BasicBallGame { camera ..followVector2(Vector2(0, 0)) ..zoom = 7.5; - - final launchRamp = LaunchRamp(); - unawaited(addFromBlueprint(launchRamp)); - + await addFromBlueprint(LaunchRamp()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart index 083a4584..e02ec093 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/launch_ramp/launch_ramp_game.dart'; void addLaunchRampStories(Dashbook dashbook) { - dashbook.storiesOf('LaunchRamp').add( - 'Basic', - (context) => GameWidget( - game: LaunchRampGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('launch_ramp/basic.dart'), - info: LaunchRampGame.info, + dashbook.storiesOf('LaunchRamp').addGame( + title: 'Traced', + description: LaunchRampGame.description, + gameBuilder: (_) => LaunchRampGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart b/packages/pinball_components/sandbox/lib/stories/layer/layer_game.dart similarity index 59% rename from packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart rename to packages/pinball_components/sandbox/lib/stories/layer/layer_game.dart index a6361094..40dcc824 100644 --- a/packages/pinball_components/sandbox/lib/stories/layer/basic_layer_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/layer/layer_game.dart @@ -2,36 +2,35 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BasicLayerGame extends BasicGame with TapDetector { - BasicLayerGame({required this.color}); - - static const info = ''' +class LayerGame extends BallGame with TapDetector { + static const description = ''' Shows how Layers work when a Ball hits other components. - Tap anywhere on the screen to spawn a Ball into the game. + - Tap anywhere on the screen to spawn a Ball into the game. '''; - final Color color; - @override Future onLoad() async { - await add(BigSquare()..initialPosition = Vector2(30, -40)); - await add(SmallSquare()..initialPosition = Vector2(50, -40)); - await add(UnlayeredSquare()..initialPosition = Vector2(60, -40)); - } - - @override - void onTapUp(TapUpInfo info) { - add( - Ball(baseColor: color)..initialPosition = info.eventPosition.game, + await addAll( + [ + _BigSquare()..initialPosition = Vector2(30, -40), + _SmallSquare()..initialPosition = Vector2(50, -40), + _UnlayeredSquare()..initialPosition = Vector2(60, -40), + ], ); } } -class BigSquare extends BodyComponent with InitialPosition, Layered { - BigSquare() { +class _BigSquare extends BodyComponent with InitialPosition, Layered { + _BigSquare() + : super( + children: [ + _UnlayeredSquare()..initialPosition = Vector2.all(4), + _SmallSquare()..initialPosition = Vector2.all(-4), + ], + ) { paint = Paint() ..color = const Color.fromARGB(255, 8, 218, 241) ..style = PaintingStyle.stroke; @@ -42,27 +41,13 @@ class BigSquare extends BodyComponent with InitialPosition, Layered { Body createBody() { final shape = PolygonShape()..setAsBoxXY(16, 16); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - return world.createBody(bodyDef)..createFixture(fixtureDef); } - - @override - Future onLoad() async { - await super.onLoad(); - - await addAll( - [ - UnlayeredSquare()..initialPosition = Vector2.all(4), - SmallSquare()..initialPosition = Vector2.all(-4), - ], - ); - } } -class SmallSquare extends BodyComponent with InitialPosition, Layered { - SmallSquare() { +class _SmallSquare extends BodyComponent with InitialPosition, Layered { + _SmallSquare() { paint = Paint() ..color = const Color.fromARGB(255, 27, 241, 8) ..style = PaintingStyle.stroke; @@ -73,15 +58,13 @@ class SmallSquare extends BodyComponent with InitialPosition, Layered { Body createBody() { final shape = PolygonShape()..setAsBoxXY(2, 2); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - return world.createBody(bodyDef)..createFixture(fixtureDef); } } -class UnlayeredSquare extends BodyComponent with InitialPosition { - UnlayeredSquare() { +class _UnlayeredSquare extends BodyComponent with InitialPosition { + _UnlayeredSquare() { paint = Paint() ..color = const Color.fromARGB(255, 241, 8, 8) ..style = PaintingStyle.stroke; @@ -91,9 +74,7 @@ class UnlayeredSquare extends BodyComponent with InitialPosition { Body createBody() { final shape = PolygonShape()..setAsBoxXY(3, 3); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; - return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/packages/pinball_components/sandbox/lib/stories/layer/stories.dart b/packages/pinball_components/sandbox/lib/stories/layer/stories.dart index 12ac028b..64d21b87 100644 --- a/packages/pinball_components/sandbox/lib/stories/layer/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/layer/stories.dart @@ -1,18 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/layer/basic_layer_game.dart'; +import 'package:sandbox/stories/layer/layer_game.dart'; void addLayerStories(Dashbook dashbook) { - dashbook.storiesOf('Layer').add( - 'Layer', - (context) => GameWidget( - game: BasicLayerGame( - color: context.colorProperty('color', Colors.blue), - ), - ), - codeLink: buildSourceLink('layer/basic.dart'), - info: BasicLayerGame.info, + dashbook.storiesOf('Layer').addGame( + title: 'Example', + description: LayerGame.description, + gameBuilder: (_) => LayerGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart new file mode 100644 index 00000000..29eded8c --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -0,0 +1,53 @@ +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class PlungerGame extends BallGame with KeyboardEvents, Traceable { + PlungerGame() : super(color: const Color(0xFFFF0000)); + + static const description = ''' + Shows how Plunger is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + static const _downKeys = [ + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.space, + ]; + + late Plunger plunger; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + await add( + plunger = Plunger(compressionDistance: 29) + ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + ); + await traceAllBodies(); + } + + @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; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart b/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart new file mode 100644 index 00000000..af54435a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/plunger/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/plunger/plunger_game.dart'; + +void addPlungerStories(Dashbook dashbook) { + dashbook.storiesOf('Plunger').addGame( + title: 'Traced', + description: PlungerGame.description, + gameBuilder: (_) => PlungerGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart b/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart similarity index 88% rename from packages/pinball_components/sandbox/lib/stories/score_text/basic.dart rename to packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart index 49b83863..aa776405 100644 --- a/packages/pinball_components/sandbox/lib/stories/score_text/basic.dart +++ b/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class ScoreTextBasicGame extends BasicGame with TapDetector { - static const info = ''' +class ScoreTextGame extends AssetsGame with TapDetector { + static const description = ''' Simple game to show how score text works, - Tap anywhere on the screen to spawn an text on the given location. diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart index 85caef1b..c4899a27 100644 --- a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/score_text/basic.dart'; +import 'package:sandbox/stories/score_text/score_text_game.dart'; void addScoreTextStories(Dashbook dashbook) { - dashbook.storiesOf('ScoreText').add( - 'Basic', - (context) => GameWidget( - game: ScoreTextBasicGame(), - ), - codeLink: buildSourceLink('score_text/basic.dart'), - info: ScoreTextBasicGame.info, + dashbook.storiesOf('ScoreText').addGame( + title: 'Basic', + description: ScoreTextGame.description, + gameBuilder: (_) => ScoreTextGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart index 8d54f391..28858088 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart @@ -1,12 +1,18 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SlingshotGame extends BasicBallGame with Traceable { - SlingshotGame() : super(color: const Color(0xFFFF0000)); +class SlingshotGame extends BallGame { + SlingshotGame() + : super( + imagesFileNames: [ + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ], + ); - static const info = ''' + static const description = ''' Shows how Slingshots are rendered. - Activate the "trace" parameter to overlay the body. @@ -16,8 +22,10 @@ class SlingshotGame extends BasicBallGame with Traceable { @override Future onLoad() async { await super.onLoad(); - await addFromBlueprint(Slingshots()); + camera.followVector2(Vector2.zero()); + await addFromBlueprint(Slingshots()); + await ready(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart index 70dfa021..e4c04a0f 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/slingshot/slingshot_game.dart'; void addSlingshotStories(Dashbook dashbook) { - dashbook.storiesOf('Slingshots').add( - 'Basic', - (context) => GameWidget( - game: SlingshotGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('slingshot_game/basic.dart'), - info: SlingshotGame.info, + dashbook.storiesOf('Slingshots').addGame( + title: 'Traced', + description: SlingshotGame.description, + gameBuilder: (_) => SlingshotGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship/stories.dart deleted file mode 100644 index ac7720a0..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship/stories.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/spaceship/basic_spaceship_game.dart'; - -void addSpaceshipStories(Dashbook dashbook) { - dashbook.storiesOf('Spaceship').add( - 'Basic', - (context) => GameWidget( - game: BasicSpaceshipGame(), - ), - codeLink: buildSourceLink('spaceship/basic.dart'), - info: BasicSpaceshipGame.info, - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart deleted file mode 100644 index e69ed1db..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_rail/stories.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/spaceship_rail/spaceship_rail_game.dart'; - -void addSpaceshipRailStories(Dashbook dashbook) { - dashbook.storiesOf('SpaceshipRail').add( - 'Basic', - (context) => GameWidget( - game: SpaceshipRailGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('spaceship_rail/basic.dart'), - info: SpaceshipRailGame.info, - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart deleted file mode 100644 index e3850da3..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/spaceship_ramp_game.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/stories/ball/basic_ball_game.dart'; - -class SpaceshipRampGame extends BasicBallGame { - SpaceshipRampGame() - : super( - color: Colors.blue, - ballPriority: SpaceshipRamp.ballPriorityInsideRamp, - ballLayer: Layer.spaceshipEntranceRamp, - ); - - static const info = ''' - Shows how SpaceshipRamp are rendered. - - - Activate the "trace" parameter to overlay the body. - - Tap anywhere on the screen to spawn a ball into the game. -'''; - - @override - Future onLoad() async { - await super.onLoad(); - - camera.followVector2(Vector2(-10, -20)); - - final spaceshipRamp = SpaceshipRamp(); - unawaited(addFromBlueprint(spaceshipRamp)); - - await traceAllBodies(); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart b/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart deleted file mode 100644 index f0aeadff..00000000 --- a/packages/pinball_components/sandbox/lib/stories/spaceship_ramp/stories.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/spaceship_ramp/spaceship_ramp_game.dart'; - -void addSpaceshipRampStories(Dashbook dashbook) { - dashbook.storiesOf('SpaceshipRamp').add( - 'Basic', - (context) => GameWidget( - game: SpaceshipRampGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('spaceship_ramp/basic.dart'), - info: SpaceshipRampGame.info, - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart index a57beb8d..9bebde9e 100644 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart @@ -2,28 +2,34 @@ import 'dart:async'; import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SparkyBumperGame extends BasicBallGame with Traceable { - SparkyBumperGame() : super(color: const Color(0xFF0000FF)); - - static const info = ''' +class SparkyBumperGame extends BallGame { + static const description = ''' Shows how a SparkyBumper is rendered. - Activate the "trace" parameter to overlay the body. + - Activate the "trace" parameter to overlay the body. '''; @override Future onLoad() async { await super.onLoad(); + await images.loadAll([ + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, + ]); + final center = screenToWorld(camera.viewport.canvasSize! / 2); final sparkyBumperA = SparkyBumper.a() - ..initialPosition = Vector2(center.x - 20, center.y - 20) + ..initialPosition = Vector2(center.x - 20, center.y + 20) ..priority = 1; final sparkyBumperB = SparkyBumper.b() - ..initialPosition = Vector2(center.x - 10, center.y + 10) + ..initialPosition = Vector2(center.x - 10, center.y - 10) ..priority = 1; final sparkyBumperC = SparkyBumper.c() ..initialPosition = Vector2(center.x + 20, center.y) diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart index 1a5f8801..418636db 100644 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart @@ -1,15 +1,11 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart'; void addSparkyBumperStories(Dashbook dashbook) { - dashbook.storiesOf('Sparky Bumpers').add( - 'Basic', - (context) => GameWidget( - game: SparkyBumperGame()..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('sparky_bumper/basic.dart'), - info: SparkyBumperGame.info, + dashbook.storiesOf('Sparky Bumpers').addGame( + title: 'Traced', + description: SparkyBumperGame.description, + gameBuilder: (_) => SparkyBumperGame(), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 0a795ec7..d8103b4d 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,17 +1,17 @@ +export 'alien_zone/stories.dart'; +export 'backboard/stories.dart'; export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; +export 'dino_wall/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'plunger/stories.dart'; export 'score_text/stories.dart'; export 'slingshot/stories.dart'; -export 'spaceship/stories.dart'; -export 'spaceship_rail/stories.dart'; -export 'spaceship_ramp/stories.dart'; export 'sparky_bumper/stories.dart'; -export 'zoom/stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart b/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart deleted file mode 100644 index 653d5491..00000000 --- a/packages/pinball_components/sandbox/lib/stories/zoom/stories.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/zoom/basic_zoom_game.dart'; - -void addZoomStories(Dashbook dashbook) { - dashbook.storiesOf('CameraZoom').add( - 'Basic', - (context) => GameWidget( - game: BasicCameraZoomGame(), - ), - codeLink: buildSourceLink('zoom/basic_zoom_game.dart'), - info: BasicCameraZoomGame.info, - ); -} diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index b0de4903..8d61da32 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -64,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" fake_async: dependency: transitive description: @@ -91,14 +98,16 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" flame_forge2d: dependency: "direct main" description: - name: flame_forge2d - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.0" + path: "packages/flame_forge2d" + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + url: "https://github.com/flame-engine/flame/" + source: git + version: "0.11.0" flutter: dependency: "direct main" description: flutter @@ -134,7 +143,7 @@ packages: name: forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.10.0" + version: "0.11.0" freezed_annotation: dependency: transitive description: @@ -149,13 +158,20 @@ packages: relative: true source: path version: "1.0.0+1" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: transitive description: @@ -183,7 +199,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -204,7 +220,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider_linux: dependency: transitive description: @@ -233,6 +249,20 @@ packages: relative: true source: path version: "1.0.0+1" + pinball_flame: + dependency: "direct main" + description: + path: "../../pinball_flame" + relative: true + source: path + version: "1.0.0+1" + pinball_theme: + dependency: "direct main" + description: + path: "../../pinball_theme" + relative: true + source: path + version: "1.0.0+1" platform: dependency: transitive description: @@ -321,7 +351,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -356,7 +386,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" typed_data: dependency: transitive description: @@ -426,7 +456,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" very_good_analysis: dependency: "direct dev" description: diff --git a/packages/pinball_components/sandbox/pubspec.yaml b/packages/pinball_components/sandbox/pubspec.yaml index 94c0479b..d663cb04 100644 --- a/packages/pinball_components/sandbox/pubspec.yaml +++ b/packages/pinball_components/sandbox/pubspec.yaml @@ -8,12 +8,20 @@ environment: dependencies: dashbook: ^0.1.7 - flame: ^1.1.0 - flame_forge2d: ^0.10.0 + flame: ^1.1.1 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter pinball_components: path: ../ + pinball_flame: + path: ../../pinball_flame + pinball_theme: + path: ../../pinball_theme dev_dependencies: flutter_test: diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 21d5d01a..d69a6131 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -13,15 +13,14 @@ class MockBall extends Mock implements Ball {} class MockGame extends Mock implements Forge2DGame {} -class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} +class MockContact extends Mock implements Contact {} -class MockSpaceshipHole extends Mock implements SpaceshipHole {} +class MockComponent extends Mock implements Component {} -class MockSpaceshipRailExit extends Mock implements SpaceshipRailExit {} +class MockAlienBumperCubit extends Mock implements AlienBumperCubit {} -class MockContact extends Mock implements Contact {} +class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} -class MockContactCallback extends Mock - implements ContactCallback {} +class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} -class MockComponent extends Mock implements Component {} +class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} diff --git a/packages/pinball_components/test/helpers/test_game.dart b/packages/pinball_components/test/helpers/test_game.dart index a1219868..1f8b9ee6 100644 --- a/packages/pinball_components/test/helpers/test_game.dart +++ b/packages/pinball_components/test/helpers/test_game.dart @@ -1,7 +1,22 @@ +import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class TestGame extends Forge2DGame { - TestGame() { + TestGame([List? assets]) : _assets = assets { images.prefix = ''; } + + final List? _assets; + + @override + Future onLoad() async { + if (_assets != null) { + await images.loadAll(_assets!); + } + await super.onLoad(); + } +} + +class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents { + KeyboardTestGame([List? assets]) : super(assets); } diff --git a/packages/pinball_components/test/src/components/alien_bumper/alien_bumper_test.dart b/packages/pinball_components/test/src/components/alien_bumper/alien_bumper_test.dart new file mode 100644 index 00000000..be34d4f8 --- /dev/null +++ b/packages/pinball_components/test/src/components/alien_bumper/alien_bumper_test.dart @@ -0,0 +1,78 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.alienBumper.a.active.keyName, + Assets.images.alienBumper.a.inactive.keyName, + Assets.images.alienBumper.b.active.keyName, + Assets.images.alienBumper.b.inactive.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('AlienBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final alienBumper = AlienBumper.a(); + await game.ensureAdd(alienBumper); + expect(game.contains(alienBumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final alienBumper = AlienBumper.b(); + await game.ensureAdd(alienBumper); + expect(game.contains(alienBumper), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockAlienBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AlienBumperState.active, + ); + when(bloc.close).thenAnswer((_) async {}); + final alienBumper = AlienBumper.test(bloc: bloc); + + await game.ensureAdd(alienBumper); + game.remove(alienBumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final alienBumper = AlienBumper.a( + children: [component], + ); + await game.ensureAdd(alienBumper); + expect(alienBumper.children, contains(component)); + }); + + flameTester.test('an AlienBumperBallContactBehavior', (game) async { + final alienBumper = AlienBumper.a(); + await game.ensureAdd(alienBumper); + expect( + alienBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/alien_bumper/behaviors/alien_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/alien_bumper/behaviors/alien_bumper_ball_contact_behavior_test.dart new file mode 100644 index 00000000..93904958 --- /dev/null +++ b/packages/pinball_components/test/src/components/alien_bumper/behaviors/alien_bumper_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'AlienBumperBallContactBehavior', + () { + test('can be instantiated', () { + expect( + AlienBumperBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = AlienBumperBallContactBehavior(); + final bloc = MockAlienBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AlienBumperState.active, + ); + + final alienBumper = AlienBumper.test(bloc: bloc); + await alienBumper.add(behavior); + await game.ensureAdd(alienBumper); + + behavior.beginContact(MockBall(), MockContact()); + + verify(alienBumper.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/alien_bumper/behaviors/alien_bumper_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/alien_bumper/behaviors/alien_bumper_blinking_behavior_test.dart new file mode 100644 index 00000000..e8e5a571 --- /dev/null +++ b/packages/pinball_components/test/src/components/alien_bumper/behaviors/alien_bumper_blinking_behavior_test.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'AlienBumperBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlinked after 0.05 seconds when inactive', + setUp: (game, tester) async { + final behavior = AlienBumperBlinkingBehavior(); + final bloc = MockAlienBumperCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: AlienBumperState.active, + ); + + final alienBumper = AlienBumper.test(bloc: bloc); + await alienBumper.add(behavior); + await game.ensureAdd(alienBumper); + + streamController.add(AlienBumperState.inactive); + await tester.pump(); + game.update(0.05); + + await streamController.close(); + verify(bloc.onBlinked).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/alien_bumper/cubit/alien_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/alien_bumper/cubit/alien_bumper_cubit_test.dart new file mode 100644 index 00000000..140249ea --- /dev/null +++ b/packages/pinball_components/test/src/components/alien_bumper/cubit/alien_bumper_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'AlienBumperCubit', + () { + blocTest( + 'onBallContacted emits inactive', + build: AlienBumperCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [AlienBumperState.inactive], + ); + + blocTest( + 'onBlinked emits active', + build: AlienBumperCubit.new, + act: (bloc) => bloc.onBlinked(), + expect: () => [AlienBumperState.active], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/backboard_test.dart b/packages/pinball_components/test/src/components/backboard_test.dart index 2d95cc47..aee2481a 100644 --- a/packages/pinball_components/test/src/components/backboard_test.dart +++ b/packages/pinball_components/test/src/components/backboard_test.dart @@ -1,15 +1,19 @@ -// ignore_for_file: unawaited_futures +// ignore_for_file: unawaited_futures, cascade_invocations import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_theme/pinball_theme.dart'; import '../../helpers/helpers.dart'; void main() { group('Backboard', () { - final tester = FlameTester(TestGame.new); + final characterIconPath = Assets.images.dash.leaderboardIcon.keyName; + final tester = FlameTester(() => KeyboardTestGame([characterIconPath])); group('on waitingMode', () { tester.testGameWidget( @@ -17,7 +21,8 @@ void main() { setUp: (game, tester) async { game.camera.zoom = 2; game.camera.followVector2(Vector2.zero()); - await game.ensureAdd(Backboard(position: Vector2(0, 15))); + await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15))); + await tester.pump(); }, verify: (game, tester) async { await expectLater( @@ -34,20 +39,147 @@ void main() { setUp: (game, tester) async { game.camera.zoom = 2; game.camera.followVector2(Vector2.zero()); - final backboard = Backboard(position: Vector2(0, 15)); + final backboard = Backboard.gameOver( + position: Vector2(0, 15), + score: 1000, + characterIconPath: characterIconPath, + onSubmit: (_) {}, + ); + await game.ensureAdd(backboard); + }, + verify: (game, tester) async { + final prompts = + game.descendants().whereType().length; + expect(prompts, equals(3)); + + final score = game.descendants().firstWhere( + (component) => + component is TextComponent && component.text == '1,000', + ); + expect(score, isNotNull); + }, + ); + + tester.testGameWidget( + 'can change the initials', + setUp: (game, tester) async { + final backboard = Backboard.gameOver( + position: Vector2(0, 15), + score: 1000, + characterIconPath: characterIconPath, + onSubmit: (_) {}, + ); await game.ensureAdd(backboard); - await backboard.gameOverMode(); - await game.ready(); + // Focus is already on the first letter + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + + // Move to the next an press up again + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + + // One more time + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + + // Back to the previous and increase one more + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); }, verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/backboard/game_over.png'), + final backboard = game + .descendants() + .firstWhere((component) => component is BackboardGameOver) + as BackboardGameOver; + + expect(backboard.initials, equals('BCB')); + }, + ); + + String? submitedInitials; + tester.testGameWidget( + 'submits the initials', + setUp: (game, tester) async { + final backboard = Backboard.gameOver( + position: Vector2(0, 15), + score: 1000, + characterIconPath: characterIconPath, + onSubmit: (value) { + submitedInitials = value; + }, ); + await game.ensureAdd(backboard); + + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pump(); + }, + verify: (game, tester) async { + expect(submitedInitials, equals('AAA')); }, ); }); }); + + group('BackboardLetterPrompt', () { + final tester = FlameTester(KeyboardTestGame.new); + + tester.testGameWidget( + 'cycles the char up and down when it has focus', + setUp: (game, tester) async { + await game.ensureAdd( + BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()), + ); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); + }, + verify: (game, tester) async { + final prompt = game.firstChild(); + expect(prompt?.char, equals('C')); + }, + ); + + tester.testGameWidget( + "does nothing when it doesn't have focus", + setUp: (game, tester) async { + await game.ensureAdd( + BackboardLetterPrompt(position: Vector2.zero()), + ); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + }, + verify: (game, tester) async { + final prompt = game.firstChild(); + expect(prompt?.char, equals('A')); + }, + ); + + tester.testGameWidget( + 'blinks the prompt when it has the focus', + setUp: (game, tester) async { + await game.ensureAdd( + BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true), + ); + }, + verify: (game, tester) async { + final underscore = game.descendants().whereType().first; + expect(underscore.paint.color, Colors.white); + + game.update(2); + expect(underscore.paint.color, Colors.transparent); + }, + ); + }); } diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball_test.dart index 4fb8b5ff..26a03886 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; @@ -48,7 +49,7 @@ void main() { final ball = Ball(baseColor: Colors.blue); await game.ensureAdd(ball); - ball.body.gravityScale = 0; + ball.body.gravityScale = Vector2.zero(); ball.body.linearVelocity.setValues(10, 10); game.update(1); expect(ball.body.position, isNot(equals(ball.initialPosition))); @@ -153,7 +154,7 @@ void main() { ball.stop(); ball.resume(); - ball.body.gravityScale = 0; + ball.body.gravityScale = Vector2.zero(); ball.body.linearVelocity.setValues(10, 10); game.update(1); expect(ball.body.position, isNot(equals(ball.initialPosition))); @@ -169,20 +170,41 @@ void main() { expect(ball.body.linearVelocity, equals(Vector2.zero())); - ball.boost(Vector2.all(10)); + await ball.boost(Vector2.all(10)); expect(ball.body.linearVelocity.x, greaterThan(0)); expect(ball.body.linearVelocity.y, greaterThan(0)); }); - flameTester.test('adds fire effect components to the game', (game) async { + flameTester.test('adds TurboChargeSpriteAnimation', (game) async { final ball = Ball(baseColor: Colors.blue); await game.ensureAdd(ball); - ball.boost(Vector2.all(10)); + await ball.boost(Vector2.all(10)); game.update(0); - await game.ready(); - expect(game.children.whereType().length, greaterThan(0)); + expect( + ball.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('removes TurboChargeSpriteAnimation after it finishes', + (game) async { + final ball = Ball(baseColor: Colors.blue); + await game.ensureAdd(ball); + + await ball.boost(Vector2.all(10)); + game.update(0); + + final turboChargeSpriteAnimation = + ball.children.whereType().single; + + expect(ball.contains(turboChargeSpriteAnimation), isTrue); + + game.update(turboChargeSpriteAnimation.animation!.totalDuration()); + game.update(0.1); + + expect(ball.contains(turboChargeSpriteAnimation), isFalse); }); }); }); diff --git a/packages/pinball_components/test/src/components/baseboard_test.dart b/packages/pinball_components/test/src/components/baseboard_test.dart index b1ce58e2..101e3e21 100644 --- a/packages/pinball_components/test/src/components/baseboard_test.dart +++ b/packages/pinball_components/test/src/components/baseboard_test.dart @@ -9,10 +9,35 @@ import '../../helpers/helpers.dart'; void main() { group('Baseboard', () { - // TODO(allisonryan0002): Add golden tests. - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final leftBaseboard = Baseboard( + side: BoardSide.left, + )..initialPosition = Vector2(-20, 0); + final rightBaseboard = Baseboard( + side: BoardSide.right, + )..initialPosition = Vector2(20, 0); + + await game.ensureAddAll([leftBaseboard, rightBaseboard]); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/baseboard.png'), + ); + }, + ); flameTester.test( 'loads correctly', @@ -57,8 +82,8 @@ void main() { ); await game.ensureAddAll([leftBaseboard, rightBaseboard]); - expect(leftBaseboard.body.angle, isNegative); - expect(rightBaseboard.body.angle, isPositive); + expect(leftBaseboard.body.angle, isPositive); + expect(rightBaseboard.body.angle, isNegative); }, ); }); diff --git a/packages/pinball_components/test/src/components/board_dimensions_test.dart b/packages/pinball_components/test/src/components/board_dimensions_test.dart index afd4a2d8..2529cac1 100644 --- a/packages/pinball_components/test/src/components/board_dimensions_test.dart +++ b/packages/pinball_components/test/src/components/board_dimensions_test.dart @@ -19,9 +19,5 @@ void main() { test('has perspectiveShrinkFactor', () { expect(BoardDimensions.perspectiveShrinkFactor, equals(0.63)); }); - - test('has shrinkAdjustedHeight', () { - expect(BoardDimensions.shrinkAdjustedHeight, isNotNull); - }); }); } diff --git a/packages/pinball_components/test/src/components/boundaries_test.dart b/packages/pinball_components/test/src/components/boundaries_test.dart index 2c6fe1da..4e2fb497 100644 --- a/packages/pinball_components/test/src/components/boundaries_test.dart +++ b/packages/pinball_components/test/src/components/boundaries_test.dart @@ -4,28 +4,37 @@ 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 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('Boundaries', () { - final tester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); - tester.testGameWidget( + final assets = [ + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.boundary.bottom.keyName, + ]; + final flameTester = FlameTester(TestGame.new); + + flameTester.testGameWidget( 'render correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); await game.addFromBlueprint(Boundaries()); await game.ready(); + game.camera.followVector2(Vector2.zero()); - game.camera.zoom = 3.9; + game.camera.zoom = 3.2; + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/boundaries.png'), + ); }, - // TODO(allisonryan0002): enable test when workflows are fixed. - // verify: (game, tester) async { - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/boundaries.png'), - // ); - // }, ); }); } diff --git a/packages/pinball_components/test/src/components/camera_zoom_test.dart b/packages/pinball_components/test/src/components/camera_zoom_test.dart index 00f43847..a7f64eca 100644 --- a/packages/pinball_components/test/src/components/camera_zoom_test.dart +++ b/packages/pinball_components/test/src/components/camera_zoom_test.dart @@ -17,7 +17,7 @@ void main() { game.camera.followVector2(Vector2.zero()); game.camera.zoom = 10; final sprite = await game.loadSprite( - Assets.images.flutterSignPost.keyName, + Assets.images.signpost.inactive.keyName, ); await game.add( diff --git a/packages/pinball_components/test/src/components/dash_animatronic_test.dart b/packages/pinball_components/test/src/components/dash_animatronic_test.dart new file mode 100644 index 00000000..d0707223 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_animatronic_test.dart @@ -0,0 +1,71 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/extensions.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.dash.animatronic.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + group('DashAnimatronic', () { + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.load(asset); + await game.ensureAdd(DashAnimatronic()..playing = true); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + final animationDuration = + game.firstChild()!.animation!.totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/dash_animatronic/start.png'), + ); + + game.update(animationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/dash_animatronic/middle.png'), + ); + + game.update(animationDuration * 0.75); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/dash_animatronic/end.png'), + ); + }, + ); + flameTester.test( + 'loads correctly', + (game) async { + final dashAnimatronic = DashAnimatronic(); + await game.ensureAdd(dashAnimatronic); + + expect(game.contains(dashAnimatronic), isTrue); + }, + ); + + flameTester.test( + 'stops animating after animation completes', + (game) async { + final dashAnimatronic = DashAnimatronic(); + await game.ensureAdd(dashAnimatronic); + + dashAnimatronic.playing = true; + game.update(4); + + expect(dashAnimatronic.playing, isFalse); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart new file mode 100644 index 00000000..bf7513bd --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'DashNestBumperBallContactBehavior', + () { + test('can be instantiated', () { + expect( + DashNestBumperBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = DashNestBumperBallContactBehavior(); + final bloc = MockDashNestBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: DashNestBumperState.active, + ); + + final dashNestBumper = DashNestBumper.test(bloc: bloc); + await dashNestBumper.add(behavior); + await game.ensureAdd(dashNestBumper); + + behavior.beginContact(MockBall(), MockContact()); + + verify(dashNestBumper.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart new file mode 100644 index 00000000..7e26bbf3 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'DashNestBumperCubit', + () { + blocTest( + 'onBallContacted emits active', + build: DashNestBumperCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [DashNestBumperState.active], + ); + + blocTest( + 'onReset emits inactive', + build: DashNestBumperCubit.new, + act: (bloc) => bloc.onReset(), + expect: () => [DashNestBumperState.inactive], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart new file mode 100644 index 00000000..67764951 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart @@ -0,0 +1,88 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DashNestBumper', () { + final assets = [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('"main" loads correctly', (game) async { + final bumper = DashNestBumper.main(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"a" loads correctly', (game) async { + final bumper = DashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = DashNestBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockDashNestBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: DashNestBumperState.inactive, + ); + when(bloc.close).thenAnswer((_) async {}); + final dashNestBumper = DashNestBumper.test(bloc: bloc); + + await game.ensureAdd(dashNestBumper); + game.remove(dashNestBumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('adds new children', (game) async { + final component = Component(); + final dashNestBumper = DashNestBumper.a( + children: [component], + ); + await game.ensureAdd(dashNestBumper); + expect(dashNestBumper.children, contains(component)); + }); + + flameTester.test('a DashNestBumperBallContactBehavior', (game) async { + final dashNestBumper = DashNestBumper.a(); + await game.ensureAdd(dashNestBumper); + expect( + dashNestBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart deleted file mode 100644 index 2c6bb00c..00000000 --- a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -// ignore_for_file: cascade_invocations - -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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); - - group('BigDashNestBumper', () { - flameTester.test('loads correctly', (game) async { - final bumper = BigDashNestBumper(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('activate returns normally', (game) async { - final bumper = BigDashNestBumper(); - await game.ensureAdd(bumper); - - expect(bumper.activate, returnsNormally); - }); - - flameTester.test('deactivate returns normally', (game) async { - final bumper = BigDashNestBumper(); - await game.ensureAdd(bumper); - - expect(bumper.deactivate, returnsNormally); - }); - - flameTester.test('changes sprite', (game) async { - final bumper = BigDashNestBumper(); - await game.ensureAdd(bumper); - - final spriteComponent = bumper.firstChild()!; - - final deactivatedSprite = spriteComponent.sprite; - bumper.activate(); - expect( - spriteComponent.sprite, - isNot(equals(deactivatedSprite)), - ); - - final activatedSprite = spriteComponent.sprite; - bumper.deactivate(); - expect( - spriteComponent.sprite, - isNot(equals(activatedSprite)), - ); - - expect( - activatedSprite, - isNot(equals(deactivatedSprite)), - ); - }); - }); - - group('SmallDashNestBumper', () { - flameTester.test('"a" loads correctly', (game) async { - final bumper = SmallDashNestBumper.a(); - await game.ensureAdd(bumper); - - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"b" loads correctly', (game) async { - final bumper = SmallDashNestBumper.b(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('activate returns normally', (game) async { - final bumper = SmallDashNestBumper.a(); - await game.ensureAdd(bumper); - - expect(bumper.activate, returnsNormally); - }); - - flameTester.test('deactivate returns normally', (game) async { - final bumper = SmallDashNestBumper.a(); - await game.ensureAdd(bumper); - - expect(bumper.deactivate, returnsNormally); - }); - - flameTester.test('changes sprite', (game) async { - final bumper = SmallDashNestBumper.a(); - await game.ensureAdd(bumper); - - final spriteComponent = bumper.firstChild()!; - - final deactivatedSprite = spriteComponent.sprite; - bumper.activate(); - expect( - spriteComponent.sprite, - isNot(equals(deactivatedSprite)), - ); - - final activatedSprite = spriteComponent.sprite; - bumper.deactivate(); - expect( - spriteComponent.sprite, - isNot(equals(activatedSprite)), - ); - - expect( - activatedSprite, - isNot(equals(deactivatedSprite)), - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index bb85bc8e..7ed97248 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -1,15 +1,41 @@ // 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 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('DinoWalls', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.addFromBlueprint(DinoWalls()); + await game.ready(); + + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 6.5; + + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/dino-walls.png'), + ); + }, + ); flameTester.test( 'loads correctly', diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper_test.dart index efd4d2b0..c34d0d1c 100644 --- a/packages/pinball_components/test/src/components/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper_test.dart @@ -10,12 +10,38 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group('Flipper', () { - // TODO(alestiago): Add golden tests. // TODO(alestiago): Consider testing always both left and right Flipper. + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final leftFlipper = Flipper( + side: BoardSide.left, + )..initialPosition = Vector2(-10, 0); + final rightFlipper = Flipper( + side: BoardSide.right, + )..initialPosition = Vector2(10, 0); + + await game.ensureAddAll([leftFlipper, rightFlipper]); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/flipper.png'), + ); + }, + ); + flameTester.test( 'loads correctly', (game) async { @@ -55,7 +81,7 @@ void main() { final flipper = Flipper(side: BoardSide.left); await game.ensureAdd(flipper); - expect(flipper.body.gravityScale, isZero); + expect(flipper.body.gravityScale, equals(Vector2.zero())); }, ); @@ -113,7 +139,7 @@ void main() { expect(flipper.body.linearVelocity, equals(Vector2.zero())); flipper.moveDown(); - expect(flipper.body.linearVelocity.y, lessThan(0)); + expect(flipper.body.linearVelocity.y, isPositive); }, ); @@ -126,7 +152,7 @@ void main() { expect(flipper.body.linearVelocity, equals(Vector2.zero())); flipper.moveUp(); - expect(flipper.body.linearVelocity.y, greaterThan(0)); + expect(flipper.body.linearVelocity.y, isNegative); }, ); }); diff --git a/packages/pinball_components/test/src/components/flutter_sign_post_test.dart b/packages/pinball_components/test/src/components/flutter_sign_post_test.dart deleted file mode 100644 index 98815af7..00000000 --- a/packages/pinball_components/test/src/components/flutter_sign_post_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -// ignore_for_file: cascade_invocations - -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('FlutterSignPost', () { - flameTester.test( - 'loads correctly', - (game) async { - final flutterSignPost = FlutterSignPost(); - await game.ready(); - await game.ensureAdd(flutterSignPost); - - expect(game.contains(flutterSignPost), isTrue); - }, - ); - }); -} diff --git a/packages/pinball_components/test/src/components/golden/backboard/game_over.png b/packages/pinball_components/test/src/components/golden/backboard/game_over.png deleted file mode 100644 index 04a8e3ad..00000000 Binary files a/packages/pinball_components/test/src/components/golden/backboard/game_over.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/golden/backboard/waiting.png b/packages/pinball_components/test/src/components/golden/backboard/waiting.png index 25e24a6b..00164289 100644 Binary files a/packages/pinball_components/test/src/components/golden/backboard/waiting.png and b/packages/pinball_components/test/src/components/golden/backboard/waiting.png differ diff --git a/packages/pinball_components/test/src/components/golden/baseboard.png b/packages/pinball_components/test/src/components/golden/baseboard.png new file mode 100644 index 00000000..01141551 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/baseboard.png differ diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index f184e3eb..2612679a 100644 Binary files a/packages/pinball_components/test/src/components/golden/boundaries.png and b/packages/pinball_components/test/src/components/golden/boundaries.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png index be784ada..1d3daa81 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png index 3809f0d0..f0312ae5 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png index a6215d65..5fd65077 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png differ diff --git a/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png b/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png new file mode 100644 index 00000000..c8218fe1 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dash_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/dash_animatronic/middle.png b/packages/pinball_components/test/src/components/golden/dash_animatronic/middle.png new file mode 100644 index 00000000..9e79695a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dash_animatronic/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/dash_animatronic/start.png b/packages/pinball_components/test/src/components/golden/dash_animatronic/start.png new file mode 100644 index 00000000..3e5e91f5 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dash_animatronic/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png b/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png deleted file mode 100644 index 14ae9c0d..00000000 Binary files a/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/golden/dino-top-wall.png b/packages/pinball_components/test/src/components/golden/dino-top-wall.png deleted file mode 100644 index 0d434d69..00000000 Binary files a/packages/pinball_components/test/src/components/golden/dino-top-wall.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png new file mode 100644 index 00000000..5956b43b Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/pinball_components/test/src/components/golden/flipper.png b/packages/pinball_components/test/src/components/golden/flipper.png new file mode 100644 index 00000000..07fe81ed Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/flipper.png differ diff --git a/packages/pinball_components/test/src/components/golden/launch-ramp.png b/packages/pinball_components/test/src/components/golden/launch-ramp.png index e872c533..52ab2510 100644 Binary files a/packages/pinball_components/test/src/components/golden/launch-ramp.png and b/packages/pinball_components/test/src/components/golden/launch-ramp.png differ diff --git a/packages/pinball_components/test/src/components/golden/plunger/pull.png b/packages/pinball_components/test/src/components/golden/plunger/pull.png new file mode 100644 index 00000000..0ec27a4e Binary files /dev/null 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 new file mode 100644 index 00000000..61f7a4d9 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/plunger/release.png differ diff --git a/packages/pinball_components/test/src/components/golden/rocket.png b/packages/pinball_components/test/src/components/golden/rocket.png new file mode 100644 index 00000000..9511f3d5 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/rocket.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active1.png b/packages/pinball_components/test/src/components/golden/signpost/active1.png new file mode 100644 index 00000000..f11af5a8 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/active1.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active2.png b/packages/pinball_components/test/src/components/golden/signpost/active2.png new file mode 100644 index 00000000..6ddf8786 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/active2.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active3.png b/packages/pinball_components/test/src/components/golden/signpost/active3.png new file mode 100644 index 00000000..5e9b0005 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/active3.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/inactive.png b/packages/pinball_components/test/src/components/golden/signpost/inactive.png new file mode 100644 index 00000000..7ed00fba Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/inactive.png differ diff --git a/packages/pinball_components/test/src/components/golden/slingshots.png b/packages/pinball_components/test/src/components/golden/slingshots.png index 2e4ada7b..d0b01f76 100644 Binary files a/packages/pinball_components/test/src/components/golden/slingshots.png and b/packages/pinball_components/test/src/components/golden/slingshots.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship-rail.png b/packages/pinball_components/test/src/components/golden/spaceship-rail.png new file mode 100644 index 00000000..d81f7dba Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship-rail.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship.png b/packages/pinball_components/test/src/components/golden/spaceship.png index da665718..d43db8c7 100644 Binary files a/packages/pinball_components/test/src/components/golden/spaceship.png and b/packages/pinball_components/test/src/components/golden/spaceship.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship_ramp/active1.png b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active1.png new file mode 100644 index 00000000..1342e4a9 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active1.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship_ramp/active2.png b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active2.png new file mode 100644 index 00000000..daf85a54 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active2.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship_ramp/active3.png b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active3.png new file mode 100644 index 00000000..3d9f1998 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active3.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship_ramp/active4.png b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active4.png new file mode 100644 index 00000000..aa7eac8f Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active4.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship_ramp/active5.png b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active5.png new file mode 100644 index 00000000..597b1b66 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship_ramp/active5.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship_ramp/inactive.png b/packages/pinball_components/test/src/components/golden/spaceship_ramp/inactive.png new file mode 100644 index 00000000..edd38070 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship_ramp/inactive.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky-computer.png b/packages/pinball_components/test/src/components/golden/sparky-computer.png index 2f7ff65b..165a79da 100644 Binary files a/packages/pinball_components/test/src/components/golden/sparky-computer.png and b/packages/pinball_components/test/src/components/golden/sparky-computer.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky_animatronic/end.png b/packages/pinball_components/test/src/components/golden/sparky_animatronic/end.png new file mode 100644 index 00000000..5e963f14 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/sparky_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky_animatronic/middle.png b/packages/pinball_components/test/src/components/golden/sparky_animatronic/middle.png new file mode 100644 index 00000000..2665c5cb Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/sparky_animatronic/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky_animatronic/start.png b/packages/pinball_components/test/src/components/golden/sparky_animatronic/start.png new file mode 100644 index 00000000..ea3e6344 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/sparky_animatronic/start.png differ diff --git a/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart new file mode 100644 index 00000000..bf261460 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'GoogleLetterBallContactBehavior', + () { + test('can be instantiated', () { + expect( + GoogleLetterBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = GoogleLetterBallContactBehavior(); + final bloc = MockGoogleLetterCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: GoogleLetterState.active, + ); + + final googleLetter = GoogleLetter.test(bloc: bloc); + await googleLetter.add(behavior); + await game.ensureAdd(googleLetter); + + behavior.beginContact(MockBall(), MockContact()); + + verify(googleLetter.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart b/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart new file mode 100644 index 00000000..390aa192 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'GoogleLetterCubit', + () { + blocTest( + 'onBallContacted emits active', + build: GoogleLetterCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [GoogleLetterState.active], + ); + + blocTest( + 'onReset emits inactive', + build: GoogleLetterCubit.new, + act: (bloc) => bloc.onReset(), + expect: () => [GoogleLetterState.inactive], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart similarity index 62% rename from packages/pinball_components/test/src/components/google_letter_test.dart rename to packages/pinball_components/test/src/components/google_letter/google_letter_test.dart index cdfd3c4a..624168b9 100644 --- a/packages/pinball_components/test/src/components/google_letter_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart @@ -1,11 +1,13 @@ // ignore_for_file: cascade_invocations -import 'package:flame/effects.dart'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -83,44 +85,35 @@ void main() { expect(() => GoogleLetter(6), throwsA(isA())); }); - group('activate', () { - flameTester.test('returns normally', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await expectLater(googleLetter.activate, returnsNormally); - }); - - flameTester.test('adds an Effect', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await googleLetter.activate(); - await game.ready(); - - expect( - googleLetter.descendants().whereType().length, - equals(1), - ); - }); + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockGoogleLetterCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: GoogleLetterState.active, + ); + when(bloc.close).thenAnswer((_) async {}); + final googleLetter = GoogleLetter.test(bloc: bloc); + + await game.ensureAdd(googleLetter); + game.remove(googleLetter); + await game.ready(); + + verify(bloc.close).called(1); }); - group('deactivate', () { - flameTester.test('returns normally', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await expectLater(googleLetter.deactivate, returnsNormally); - }); - - flameTester.test('adds an Effect', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await googleLetter.deactivate(); - await game.ready(); - - expect( - googleLetter.descendants().whereType().length, - equals(1), - ); - }); + flameTester.test('adds a GoogleLetterBallContactBehavior', (game) async { + final googleLetter = GoogleLetter(0); + await game.ensureAdd(googleLetter); + expect( + googleLetter.children + .whereType() + .single, + isNotNull, + ); }); }); } diff --git a/packages/pinball_components/test/src/components/kicker_test.dart b/packages/pinball_components/test/src/components/kicker_test.dart index 55802703..8c48a1fb 100644 --- a/packages/pinball_components/test/src/components/kicker_test.dart +++ b/packages/pinball_components/test/src/components/kicker_test.dart @@ -21,17 +21,15 @@ void main() { side: BoardSide.right, )..initialPosition = Vector2(20, 0); - await game.addAll([leftKicker, rightKicker]); - await game.ready(); + await game.ensureAddAll([leftKicker, rightKicker]); game.camera.followVector2(Vector2.zero()); }, - // TODO(ruimiguel): enable test when workflows are fixed. - //verify: (game, tester) async { - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/kickers.png'), - // ); - //}, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/kickers.png'), + ); + }, ); flameTester.test( diff --git a/packages/pinball_components/test/src/components/launch_ramp_test.dart b/packages/pinball_components/test/src/components/launch_ramp_test.dart index c7c7b1b2..2defc168 100644 --- a/packages/pinball_components/test/src/components/launch_ramp_test.dart +++ b/packages/pinball_components/test/src/components/launch_ramp_test.dart @@ -4,6 +4,7 @@ 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 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -19,13 +20,12 @@ void main() { game.camera.followVector2(Vector2.zero()); game.camera.zoom = 4.1; }, - // TODO(allisonryan0002): enable test when workflows are fixed. - // verify: (game, tester) async { - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/launch-ramp.png'), - // ); - // }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/launch-ramp.png'), + ); + }, ); }); } diff --git a/packages/pinball_components/test/src/components/layer_sensor_test.dart b/packages/pinball_components/test/src/components/layer_sensor_test.dart new file mode 100644 index 00000000..2d1b21be --- /dev/null +++ b/packages/pinball_components/test/src/components/layer_sensor_test.dart @@ -0,0 +1,174 @@ +// 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_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +class TestLayerSensor extends LayerSensor { + TestLayerSensor({ + required LayerEntranceOrientation orientation, + required int insidePriority, + required Layer insideLayer, + }) : super( + insideLayer: insideLayer, + insidePriority: insidePriority, + orientation: orientation, + ); + + @override + Shape get shape => PolygonShape()..setAsBoxXY(1, 1); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + const insidePriority = 1; + + group('LayerSensor', () { + flameTester.test( + 'loads correctly', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insidePriority: insidePriority, + insideLayer: Layer.spaceshipEntranceRamp, + ); + await game.ensureAdd(layerSensor); + + expect(game.contains(layerSensor), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'is static', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insidePriority: insidePriority, + insideLayer: Layer.spaceshipEntranceRamp, + ); + await game.ensureAdd(layerSensor); + + expect(layerSensor.body.bodyType, equals(BodyType.static)); + }, + ); + + group('first fixture', () { + const pathwayLayer = Layer.spaceshipEntranceRamp; + const openingLayer = Layer.opening; + + flameTester.test( + 'exists', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insidePriority: insidePriority, + insideLayer: pathwayLayer, + )..layer = openingLayer; + await game.ensureAdd(layerSensor); + + expect(layerSensor.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'shape is a polygon', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insidePriority: insidePriority, + insideLayer: pathwayLayer, + )..layer = openingLayer; + await game.ensureAdd(layerSensor); + + final fixture = layerSensor.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.polygon)); + }, + ); + + flameTester.test( + 'is sensor', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insidePriority: insidePriority, + insideLayer: pathwayLayer, + )..layer = openingLayer; + await game.ensureAdd(layerSensor); + + final fixture = layerSensor.body.fixtures[0]; + expect(fixture.isSensor, isTrue); + }, + ); + }); + }); + }); + + group('beginContact', () { + late Ball ball; + late Body body; + + setUp(() { + ball = MockBall(); + body = MockBody(); + + when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); + when(() => ball.layer).thenReturn(Layer.board); + }); + + flameTester.test( + 'changes ball layer and priority ' + 'when a ball enters and exits a downward oriented LayerSensor', + (game) async { + final sensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insidePriority: insidePriority, + insideLayer: Layer.spaceshipEntranceRamp, + )..initialPosition = Vector2(0, 10); + + when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); + + sensor.beginContact(ball, MockContact()); + verify(() => ball.layer = sensor.insideLayer).called(1); + verify(() => ball.priority = sensor.insidePriority).called(1); + verify(ball.reorderChildren).called(1); + + when(() => ball.layer).thenReturn(sensor.insideLayer); + + sensor.beginContact(ball, MockContact()); + verify(() => ball.layer = Layer.board); + verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); + verify(ball.reorderChildren).called(1); + }); + + flameTester.test( + 'changes ball layer and priority ' + 'when a ball enters and exits an upward oriented LayerSensor', + (game) async { + final sensor = TestLayerSensor( + orientation: LayerEntranceOrientation.up, + insidePriority: insidePriority, + insideLayer: Layer.spaceshipEntranceRamp, + )..initialPosition = Vector2(0, 10); + + when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); + + sensor.beginContact(ball, MockContact()); + verify(() => ball.layer = sensor.insideLayer).called(1); + verify(() => ball.priority = sensor.insidePriority).called(1); + verify(ball.reorderChildren).called(1); + + when(() => ball.layer).thenReturn(sensor.insideLayer); + + sensor.beginContact(ball, MockContact()); + verify(() => ball.layer = Layer.board); + verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); + verify(ball.reorderChildren).called(1); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/layer_test.dart b/packages/pinball_components/test/src/components/layer_test.dart index f2372c45..d47702ea 100644 --- a/packages/pinball_components/test/src/components/layer_test.dart +++ b/packages/pinball_components/test/src/components/layer_test.dart @@ -57,8 +57,6 @@ void main() { const expectedLayer = Layer.spaceshipEntranceRamp; final component = TestLayeredBodyComponent()..layer = expectedLayer; await game.ensureAdd(component); - // TODO(alestiago): modify once component.loaded is available. - await component.mounted; _expectLayerOnFixtures( fixtures: component.body.fixtures, @@ -79,8 +77,6 @@ void main() { component.layer = expectedLayer; await game.ensureAdd(component); - // TODO(alestiago): modify once component.loaded is available. - await component.mounted; _expectLayerOnFixtures( fixtures: component.body.fixtures, diff --git a/test/game/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart similarity index 57% rename from test/game/components/plunger_test.dart rename to packages/pinball_components/test/src/components/plunger_test.dart index 65789ae0..eafc15d5 100644 --- a/test/game/components/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger_test.dart @@ -1,12 +1,9 @@ // ignore_for_file: cascade_invocations -import 'dart:collection'; - 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:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; @@ -20,22 +17,29 @@ void main() { flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.add( - Plunger( - compressionDistance: compressionDistance, - ), - ); - await game.ready(); + await game.ensureAdd(Plunger(compressionDistance: compressionDistance)); + game.camera.followVector2(Vector2.zero()); game.camera.zoom = 4.1; }, - // TODO(ruimiguel): enable test when workflows are fixed. - // verify: (game, tester) async { - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/plunger.png'), - // ); - // }, + 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( @@ -72,7 +76,7 @@ void main() { ); await game.ensureAdd(plunger); - expect(plunger.body.gravityScale, isZero); + expect(plunger.body.gravityScale, equals(Vector2.zero())); }, ); }); @@ -117,13 +121,7 @@ void main() { ); }); - group('onKeyEvent', () { - final keys = UnmodifiableListView([ - LogicalKeyboardKey.space, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.keyS, - ]); - + group('pull', () { late Plunger plunger; setUp(() { @@ -132,56 +130,61 @@ void main() { ); }); - testRawKeyUpEvents(keys, (event) { - final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) - ? event.logicalKey.keyLabel - : 'Space'; - flameTester.test( - 'moves upwards when $keyLabel is released ' - 'and plunger is below its starting position', - (game) async { - await game.ensureAdd(plunger); - plunger.body.setTransform(Vector2(0, -1), 0); - plunger.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); + 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; - testRawKeyUpEvents(keys, (event) { - final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) - ? event.logicalKey.keyLabel - : 'Space'; - flameTester.test( - 'does not move when $keyLabel is released ' - 'and plunger is in its starting position', - (game) async { - await game.ensureAdd(plunger); - plunger.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, + setUp(() { + plunger = Plunger( + compressionDistance: compressionDistance, ); }); - testRawKeyDownEvents(keys, (event) { - final keyLabel = (event.logicalKey != LogicalKeyboardKey.space) - ? event.logicalKey.keyLabel - : 'Space'; - flameTester.test( - 'moves downwards when $keyLabel is pressed', - (game) async { - await game.ensureAdd(plunger); - plunger.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isNegative); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); + 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); + }, + ); }); }); @@ -201,7 +204,7 @@ void main() { expect( plungerAnchor.body.position.y, - equals(plunger.body.position.y - compressionDistance), + equals(plunger.body.position.y + compressionDistance), ); }, ); @@ -210,11 +213,13 @@ void main() { group('PlungerAnchorPrismaticJointDef', () { const compressionDistance = 10.0; late Plunger plunger; + late PlungerAnchor anchor; setUp(() { plunger = Plunger( compressionDistance: compressionDistance, ); + anchor = PlungerAnchor(plunger: plunger); }); group('initializes with', () { @@ -222,7 +227,6 @@ void main() { 'plunger body as bodyA', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -238,7 +242,6 @@ void main() { 'anchor body as bodyB', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -255,7 +258,6 @@ void main() { 'limits enabled', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -272,7 +274,6 @@ void main() { 'lower translation limit as negative infinity', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -289,7 +290,6 @@ void main() { 'connected body collison enabled', (game) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); final jointDef = PlungerAnchorPrismaticJointDef( @@ -303,53 +303,47 @@ void main() { ); }); - testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { - late final anchor = PlungerAnchor(plunger: plunger); - flameTester.testGameWidget( - 'plunger cannot go below anchor', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); + 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)); + // 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)); + 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); - }, - ); - }); + await tester.pump(const Duration(seconds: 1)); + }, + verify: (game, tester) async { + expect(plunger.body.position.y < anchor.body.position.y, isTrue); + }, + ); - testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { - flameTester.testGameWidget( - 'plunger cannot excessively exceed starting position', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); - await game.ensureAdd(anchor); + 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)); + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(PrismaticJoint(jointDef)); - plunger.body.setTransform(Vector2(0, -1), 0); + 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); - }, - ); - }); + 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/ramp_opening_test.dart b/packages/pinball_components/test/src/components/ramp_opening_test.dart deleted file mode 100644 index 46e56e3d..00000000 --- a/packages/pinball_components/test/src/components/ramp_opening_test.dart +++ /dev/null @@ -1,239 +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:mocktail/mocktail.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -class TestRampOpening extends RampOpening { - TestRampOpening({ - required RampOrientation orientation, - required Layer pathwayLayer, - }) : super( - insideLayer: pathwayLayer, - orientation: orientation, - ); - - @override - Shape get shape => PolygonShape() - ..set([ - Vector2(0, 0), - Vector2(0, 1), - Vector2(1, 1), - Vector2(1, 0), - ]); -} - -class TestRampOpeningBallContactCallback - extends RampOpeningBallContactCallback { - TestRampOpeningBallContactCallback() : super(); -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); - - group('RampOpening', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); - - flameTester.test( - 'loads correctly', - (game) async { - final ramp = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: Layer.spaceshipEntranceRamp, - ); - await game.ready(); - await game.ensureAdd(ramp); - - expect(game.contains(ramp), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'is static', - (game) async { - final ramp = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: Layer.spaceshipEntranceRamp, - ); - await game.ensureAdd(ramp); - - expect(ramp.body.bodyType, equals(BodyType.static)); - }, - ); - - group('first fixture', () { - const pathwayLayer = Layer.spaceshipEntranceRamp; - const openingLayer = Layer.opening; - - flameTester.test( - 'exists', - (game) async { - final ramp = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: pathwayLayer, - )..layer = openingLayer; - await game.ensureAdd(ramp); - - expect(ramp.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'shape is a polygon', - (game) async { - final ramp = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: pathwayLayer, - )..layer = openingLayer; - await game.ensureAdd(ramp); - - final fixture = ramp.body.fixtures[0]; - expect(fixture.shape.shapeType, equals(ShapeType.polygon)); - }, - ); - - flameTester.test( - 'is sensor', - (game) async { - final ramp = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: pathwayLayer, - )..layer = openingLayer; - await game.ensureAdd(ramp); - - final fixture = ramp.body.fixtures[0]; - expect(fixture.isSensor, isTrue); - }, - ); - }); - }); - }); - - group('RampOpeningBallContactCallback', () { - flameTester.test( - 'changes ball layer ' - 'when a ball enters upwards into a downward ramp opening', - (game) async { - final ball = MockBall(); - final body = MockBody(); - final area = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: Layer.spaceshipEntranceRamp, - ); - final callback = TestRampOpeningBallContactCallback(); - - when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); - when(() => body.position).thenReturn(Vector2.zero()); - when(() => ball.layer).thenReturn(Layer.board); - - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.insideLayer).called(1); - }); - - flameTester.test( - 'changes ball layer ' - 'when a ball enters downwards into a upward ramp opening', - (game) async { - final ball = MockBall(); - final body = MockBody(); - final area = TestRampOpening( - orientation: RampOrientation.up, - pathwayLayer: Layer.spaceshipEntranceRamp, - ); - final callback = TestRampOpeningBallContactCallback(); - - when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); - when(() => body.position).thenReturn(Vector2.zero()); - when(() => ball.layer).thenReturn(Layer.board); - - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.insideLayer).called(1); - }); - - flameTester.test( - 'changes ball layer ' - 'when a ball exits from a downward oriented ramp', (game) async { - final ball = MockBall(); - final body = MockBody(); - final area = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: Layer.spaceshipEntranceRamp, - )..initialPosition = Vector2(0, 10); - final callback = TestRampOpeningBallContactCallback(); - - when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); - when(() => body.position).thenReturn(Vector2.zero()); - when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - when(() => ball.layer).thenReturn(Layer.board); - - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.insideLayer).called(1); - - callback.end(ball, area, MockContact()); - verify(() => ball.layer = Layer.board); - }); - - flameTester.test( - 'changes ball layer ' - 'when a ball exits from a upward oriented ramp', (game) async { - final ball = MockBall(); - final body = MockBody(); - final area = TestRampOpening( - orientation: RampOrientation.up, - pathwayLayer: Layer.spaceshipEntranceRamp, - )..initialPosition = Vector2(0, 10); - final callback = TestRampOpeningBallContactCallback(); - - when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); - when(() => body.position).thenReturn(Vector2.zero()); - when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - when(() => ball.layer).thenReturn(Layer.board); - - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.insideLayer).called(1); - - callback.end(ball, area, MockContact()); - verify(() => ball.layer = Layer.board); - }); - - flameTester.test( - 'change ball layer from pathwayLayer to Layer.board ' - 'when a ball enters and exits from ramp', (game) async { - final ball = MockBall(); - final body = MockBody(); - final area = TestRampOpening( - orientation: RampOrientation.down, - pathwayLayer: Layer.spaceshipEntranceRamp, - )..initialPosition = Vector2(0, 10); - final callback = TestRampOpeningBallContactCallback(); - - when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); - when(() => body.position).thenReturn(Vector2.zero()); - when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - when(() => ball.layer).thenReturn(Layer.board); - - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.insideLayer).called(1); - - callback.end(ball, area, MockContact()); - verifyNever(() => ball.layer = Layer.board); - - callback.begin(ball, area, MockContact()); - verifyNever(() => ball.layer = area.insideLayer); - - callback.end(ball, area, MockContact()); - verify(() => ball.layer = Layer.board); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/rocket_test.dart b/packages/pinball_components/test/src/components/rocket_test.dart new file mode 100644 index 00000000..87cfe515 --- /dev/null +++ b/packages/pinball_components/test/src/components/rocket_test.dart @@ -0,0 +1,28 @@ +// ignore_for_file: cascade_invocations + +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 '../../helpers/helpers.dart'; + +void main() { + group('RocketSpriteComponent', () { + final tester = FlameTester(TestGame.new); + + tester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + game.camera.followVector2(Vector2.zero()); + await game.ensureAdd(RocketSpriteComponent()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/rocket.png'), + ); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/signpost_test.dart b/packages/pinball_components/test/src/components/signpost_test.dart new file mode 100644 index 00000000..23aa6bd0 --- /dev/null +++ b/packages/pinball_components/test/src/components/signpost_test.dart @@ -0,0 +1,164 @@ +// ignore_for_file: cascade_invocations + +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 '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('Signpost', () { + flameTester.test( + 'loads correctly', + (game) async { + final signpost = Signpost(); + await game.ready(); + await game.ensureAdd(signpost); + + expect(game.contains(signpost), isTrue); + }, + ); + + group('renders correctly', () { + flameTester.testGameWidget( + 'inactive sprite', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final signpost = Signpost(); + await game.ensureAdd(signpost); + await tester.pump(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.inactive, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/inactive.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active1 sprite', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final signpost = Signpost(); + await game.ensureAdd(signpost); + signpost.progress(); + await tester.pump(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.active1, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/active1.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active2 sprite', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final signpost = Signpost(); + await game.ensureAdd(signpost); + signpost + ..progress() + ..progress(); + await tester.pump(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.active2, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/active2.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active3 sprite', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final signpost = Signpost(); + await game.ensureAdd(signpost); + signpost + ..progress() + ..progress() + ..progress(); + await tester.pump(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.active3, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/active3.png'), + ); + }, + ); + }); + + flameTester.test( + 'progress correctly cycles through all sprites', + (game) async { + final signpost = Signpost(); + await game.ready(); + await game.ensureAdd(signpost); + + final spriteComponent = signpost.firstChild()!; + + expect(spriteComponent.current, SignpostSpriteState.inactive); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.active1); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.active2); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.active3); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.inactive); + }, + ); + + flameTester.test('adds new children', (game) async { + final component = Component(); + final signpost = Signpost( + children: [component], + ); + await game.ensureAdd(signpost); + expect(signpost.children, contains(component)); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/slingshot_test.dart b/packages/pinball_components/test/src/components/slingshot_test.dart index 6f015e13..69296f78 100644 --- a/packages/pinball_components/test/src/components/slingshot_test.dart +++ b/packages/pinball_components/test/src/components/slingshot_test.dart @@ -4,30 +4,35 @@ 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 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('Slingshot', () { - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); const length = 2.0; const angle = 0.0; - final spritePath = Assets.images.slingshot.leftUpper.keyName; flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); await game.addFromBlueprint(Slingshots()); - await game.ready(); game.camera.followVector2(Vector2.zero()); + await game.ready(); + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/slingshots.png'), + ); }, - // TODO(allisonryan0002): enable test when workflows are fixed. - // verify: (game, tester) async { - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/slingshots.png'), - // ); - // }, ); flameTester.test( @@ -36,7 +41,7 @@ void main() { final slingshot = Slingshot( length: length, angle: angle, - spritePath: spritePath, + spritePath: assets.first, ); await game.ensureAdd(slingshot); @@ -50,7 +55,7 @@ void main() { final slingshot = Slingshot( length: length, angle: angle, - spritePath: spritePath, + spritePath: assets.first, ); await game.ensureAdd(slingshot); @@ -64,7 +69,7 @@ void main() { final slingshot = Slingshot( length: length, angle: angle, - spritePath: spritePath, + spritePath: assets.first, ); await game.ensureAdd(slingshot); @@ -82,7 +87,7 @@ void main() { final slingshot = Slingshot( length: length, angle: angle, - spritePath: spritePath, + spritePath: assets.first, ); await game.ensureAdd(slingshot); diff --git a/packages/pinball_components/test/src/components/spaceship_rail_test.dart b/packages/pinball_components/test/src/components/spaceship_rail_test.dart index 6c5410b1..d3242ff6 100644 --- a/packages/pinball_components/test/src/components/spaceship_rail_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_rail_test.dart @@ -3,8 +3,8 @@ 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_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -13,6 +13,23 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.addFromBlueprint(SpaceshipRail()); + await game.ready(); + + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 8; + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/spaceship-rail.png'), + ); + }, + ); + flameTester.test( 'loads correctly', (game) async { @@ -26,57 +43,4 @@ void main() { }, ); }); - - // TODO(alestiago): Make ContactCallback private and use `beginContact` - // instead. - group('SpaceshipRailExitBallContactCallback', () { - late Forge2DGame game; - late SpaceshipRailExit railExit; - late Ball ball; - late Body body; - late Fixture fixture; - late Filter filterData; - - setUp(() { - game = MockGame(); - - railExit = MockSpaceshipRailExit(); - - ball = MockBall(); - body = MockBody(); - when(() => ball.gameRef).thenReturn(game); - when(() => ball.body).thenReturn(body); - - fixture = MockFixture(); - filterData = MockFilter(); - when(() => body.fixtures).thenReturn([fixture]); - when(() => fixture.filterData).thenReturn(filterData); - }); - - setUp(() { - when(() => ball.priority).thenReturn(1); - when(() => railExit.outsideLayer).thenReturn(Layer.board); - when(() => railExit.outsidePriority).thenReturn(0); - }); - - test('changes the ball priority on contact', () { - SpaceshipRailExitBallContactCallback().begin( - railExit, - ball, - MockContact(), - ); - - verify(() => ball.sendTo(railExit.outsidePriority)).called(1); - }); - - test('changes the ball layer on contact', () { - SpaceshipRailExitBallContactCallback().begin( - railExit, - ball, - MockContact(), - ); - - verify(() => ball.layer = railExit.outsideLayer).called(1); - }); - }); } diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart index 4d124596..a65ba18b 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart @@ -1,27 +1,227 @@ // ignore_for_file: cascade_invocations +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_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { - group('SpaceshipRamp', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.spaceship.ramp.boardOpening.keyName, + Assets.images.spaceship.ramp.railingForeground.keyName, + Assets.images.spaceship.ramp.railingBackground.keyName, + Assets.images.spaceship.ramp.main.keyName, + Assets.images.spaceship.ramp.arrow.inactive.keyName, + Assets.images.spaceship.ramp.arrow.active1.keyName, + Assets.images.spaceship.ramp.arrow.active2.keyName, + Assets.images.spaceship.ramp.arrow.active3.keyName, + Assets.images.spaceship.ramp.arrow.active4.keyName, + Assets.images.spaceship.ramp.arrow.active5.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + group('SpaceshipRamp', () { flameTester.test( 'loads correctly', (game) async { - final spaceshipEntranceRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipEntranceRamp); + final spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); await game.ready(); - for (final element in spaceshipEntranceRamp.components) { - expect(game.contains(element), isTrue); + for (final component in spaceshipRamp.components) { + expect(game.contains(component), isTrue); } }, ); + + 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 spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); + await tester.pump(); + + expect( + spaceshipRamp.components + .whereType() + .first + .current, + 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 spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); + spaceshipRamp.progress(); + await tester.pump(); + + expect( + spaceshipRamp.components + .whereType() + .first + .current, + 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 spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); + spaceshipRamp + ..progress() + ..progress(); + await tester.pump(); + + expect( + spaceshipRamp.components + .whereType() + .first + .current, + SpaceshipRampArrowSpriteState.active2, + ); + + game.camera.followVector2(centerForSpaceshipRamp); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}active2.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active3 sprite', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); + spaceshipRamp + ..progress() + ..progress() + ..progress(); + await tester.pump(); + + expect( + spaceshipRamp.components + .whereType() + .first + .current, + SpaceshipRampArrowSpriteState.active3, + ); + + game.camera.followVector2(centerForSpaceshipRamp); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}active3.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active4 sprite', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); + spaceshipRamp + ..progress() + ..progress() + ..progress() + ..progress(); + await tester.pump(); + + expect( + spaceshipRamp.components + .whereType() + .first + .current, + SpaceshipRampArrowSpriteState.active4, + ); + + 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 spaceshipRamp = SpaceshipRamp(); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); + spaceshipRamp + ..progress() + ..progress() + ..progress() + ..progress() + ..progress(); + await tester.pump(); + + expect( + spaceshipRamp.components + .whereType() + .first + .current, + SpaceshipRampArrowSpriteState.active5, + ); + + game.camera.followVector2(centerForSpaceshipRamp); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}active5.png'), + ); + }, + ); + }); }); } diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart index 4d980c69..c9a90746 100644 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -5,6 +5,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_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -14,8 +15,6 @@ void main() { late Fixture fixture; late Body body; late Ball ball; - late SpaceshipEntrance entrance; - late SpaceshipHole hole; late Forge2DGame game; setUp(() { @@ -32,9 +31,6 @@ void main() { ball = MockBall(); when(() => ball.gameRef).thenReturn(game); when(() => ball.body).thenReturn(body); - - entrance = MockSpaceshipEntrance(); - hole = MockSpaceshipHole(); }); group('Spaceship', () { @@ -43,49 +39,18 @@ void main() { tester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(Spaceship(position: Vector2(30, -30))); + final position = Vector2(30, -30); + await game.addFromBlueprint(Spaceship(position: position)); + game.camera.followVector2(position); await game.ready(); - await tester.pump(); }, verify: (game, tester) async { - // FIXME(erickzanardo): Failing pipeline. - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/spaceship.png'), - // ); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/spaceship.png'), + ); }, ); }); - - group('SpaceshipEntranceBallContactCallback', () { - test('changes the ball priority on contact', () { - when(() => ball.priority).thenReturn(2); - when(() => entrance.insidePriority).thenReturn(3); - - SpaceshipEntranceBallContactCallback().begin( - entrance, - ball, - MockContact(), - ); - - verify(() => ball.sendTo(entrance.insidePriority)).called(1); - }); - }); - - group('SpaceshipHoleBallContactCallback', () { - test('changes the ball priority on contact', () { - when(() => ball.priority).thenReturn(2); - when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.outsidePriority).thenReturn(1); - - SpaceshipHoleBallContactCallback().begin( - hole, - ball, - MockContact(), - ); - - verify(() => ball.sendTo(hole.outsidePriority)).called(1); - }); - }); }); } diff --git a/packages/pinball_components/test/src/components/sparky_animatronic_test.dart b/packages/pinball_components/test/src/components/sparky_animatronic_test.dart new file mode 100644 index 00000000..66c2e0a6 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_animatronic_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/extensions.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(); + + group('SparkyAnimatronic', () { + final asset = Assets.images.sparky.animatronic.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.load(asset); + await game.ensureAdd(SparkyAnimatronic()..playing = true); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + final animationDuration = + game.firstChild()!.animation!.totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky_animatronic/start.png'), + ); + + game.update(animationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky_animatronic/middle.png'), + ); + + game.update(animationDuration * 0.75); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky_animatronic/end.png'), + ); + }, + ); + + flameTester.test( + 'loads correctly', + (game) async { + final sparkyAnimatronic = SparkyAnimatronic(); + await game.ensureAdd(sparkyAnimatronic); + + expect(game.contains(sparkyAnimatronic), isTrue); + }, + ); + + flameTester.test( + 'stops animating after animation completes', + (game) async { + final sparkyAnimatronic = SparkyAnimatronic(); + await game.ensureAdd(sparkyAnimatronic); + + sparkyAnimatronic.playing = true; + final animationDuration = + game.firstChild()!.animation!.totalDuration(); + game.update(animationDuration); + + expect(sparkyAnimatronic.playing, isFalse); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart new file mode 100644 index 00000000..88bd8145 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SparkyBumperBallContactBehavior', + () { + test('can be instantiated', () { + expect( + SparkyBumperBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = SparkyBumperBallContactBehavior(); + final bloc = MockSparkyBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyBumperState.active, + ); + + final sparkyBumper = SparkyBumper.test(bloc: bloc); + await sparkyBumper.add(behavior); + await game.ensureAdd(sparkyBumper); + + behavior.beginContact(MockBall(), MockContact()); + + verify(sparkyBumper.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart new file mode 100644 index 00000000..0d938820 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SparkyBumperBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlinked after 0.05 seconds when inactive', + setUp: (game, tester) async { + final behavior = SparkyBumperBlinkingBehavior(); + final bloc = MockSparkyBumperCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SparkyBumperState.active, + ); + + final sparkyBumper = SparkyBumper.test(bloc: bloc); + await sparkyBumper.add(behavior); + await game.ensureAdd(sparkyBumper); + + streamController.add(SparkyBumperState.inactive); + await tester.pump(); + game.update(0.05); + + await streamController.close(); + verify(bloc.onBlinked).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart new file mode 100644 index 00000000..4192f806 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'SparkyBumperCubit', + () { + blocTest( + 'onBallContacted emits inactive', + build: SparkyBumperCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [SparkyBumperState.inactive], + ); + + blocTest( + 'onBlinked emits active', + build: SparkyBumperCubit.new, + act: (bloc) => bloc.onBlinked(), + expect: () => [SparkyBumperState.active], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart new file mode 100644 index 00000000..225b5922 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('SparkyBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final sparkyBumper = SparkyBumper.a(); + await game.ensureAdd(sparkyBumper); + expect(game.contains(sparkyBumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final sparkyBumper = SparkyBumper.b(); + await game.ensureAdd(sparkyBumper); + expect(game.contains(sparkyBumper), isTrue); + }); + + flameTester.test('"c" loads correctly', (game) async { + final sparkyBumper = SparkyBumper.c(); + await game.ensureAdd(sparkyBumper); + expect(game.contains(sparkyBumper), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockSparkyBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyBumperState.active, + ); + when(bloc.close).thenAnswer((_) async {}); + final sparkyBumper = SparkyBumper.test(bloc: bloc); + + await game.ensureAdd(sparkyBumper); + game.remove(sparkyBumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final sparkyBumper = SparkyBumper.a( + children: [component], + ); + await game.ensureAdd(sparkyBumper); + expect(sparkyBumper.children, contains(component)); + }); + + flameTester.test('a SparkyBumperBallContactBehavior', (game) async { + final sparkyBumper = SparkyBumper.a(); + await game.ensureAdd(sparkyBumper); + expect( + sparkyBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper_test.dart deleted file mode 100644 index 470c254b..00000000 --- a/packages/pinball_components/test/src/components/sparky_bumper_test.dart +++ /dev/null @@ -1,74 +0,0 @@ -// ignore_for_file: cascade_invocations - -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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); - - group('SparkyBumper', () { - flameTester.test('"a" loads correctly', (game) async { - final bumper = SparkyBumper.a(); - await game.ensureAdd(bumper); - - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"b" loads correctly', (game) async { - final bumper = SparkyBumper.b(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"c" loads correctly', (game) async { - final bumper = SparkyBumper.c(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('activate returns normally', (game) async { - final bumper = SparkyBumper.a(); - await game.ensureAdd(bumper); - - expect(bumper.activate, returnsNormally); - }); - - flameTester.test('deactivate returns normally', (game) async { - final bumper = SparkyBumper.a(); - await game.ensureAdd(bumper); - - expect(bumper.deactivate, returnsNormally); - }); - - flameTester.test('changes sprite', (game) async { - final bumper = SparkyBumper.a(); - await game.ensureAdd(bumper); - - final spriteComponent = bumper.firstChild()!; - - final deactivatedSprite = spriteComponent.sprite; - bumper.activate(); - expect( - spriteComponent.sprite, - isNot(equals(deactivatedSprite)), - ); - - final activatedSprite = spriteComponent.sprite; - bumper.deactivate(); - expect( - spriteComponent.sprite, - isNot(equals(activatedSprite)), - ); - - expect( - activatedSprite, - isNot(equals(deactivatedSprite)), - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/sparky_computer_test.dart b/packages/pinball_components/test/src/components/sparky_computer_test.dart index 7e761b97..6b19481e 100644 --- a/packages/pinball_components/test/src/components/sparky_computer_test.dart +++ b/packages/pinball_components/test/src/components/sparky_computer_test.dart @@ -4,6 +4,7 @@ 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 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -16,15 +17,15 @@ void main() { setUp: (game, tester) async { await game.addFromBlueprint(SparkyComputer()); await game.ready(); + game.camera.followVector2(Vector2(-15, -50)); }, - // TODO(allisonryan0002): enable test when workflows are fixed. - // verify: (game, tester) async { - // await expectLater( - // find.byGame(), - // matchesGoldenFile('golden/sparky-computer.png'), - // ); - // }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/sparky-computer.png'), + ); + }, ); }); } diff --git a/packages/pinball_components/test/src/flame/blueprint_test.dart b/packages/pinball_components/test/src/flame/blueprint_test.dart deleted file mode 100644 index a9629422..00000000 --- a/packages/pinball_components/test/src/flame/blueprint_test.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -class MyBlueprint extends Blueprint { - @override - void build(_) { - add(Component()); - addAll([Component(), Component()]); - } -} - -class MyOtherBlueprint extends Blueprint { - @override - void build(_) { - add(Component()); - } -} - -class YetMyOtherBlueprint extends Blueprint { - @override - void build(_) { - add(Component()); - } -} - -class MyComposedBlueprint extends Blueprint { - @override - void build(_) { - addBlueprint(MyBlueprint()); - addAllBlueprints([MyOtherBlueprint(), YetMyOtherBlueprint()]); - } -} - -class MyForge2dBlueprint extends Forge2DBlueprint { - @override - void build(_) { - addContactCallback(MockContactCallback()); - addAllContactCallback([MockContactCallback(), MockContactCallback()]); - } -} - -void main() { - group('Blueprint', () { - setUpAll(() { - registerFallbackValue(MyBlueprint()); - registerFallbackValue(Component()); - }); - - test('components can be added to it', () { - final blueprint = MyBlueprint()..build(MockGame()); - - expect(blueprint.components.length, equals(3)); - }); - - test('blueprints can be added to it', () { - final blueprint = MyComposedBlueprint()..build(MockGame()); - - expect(blueprint.blueprints.length, equals(3)); - }); - - test('adds the components to a game on attach', () { - final mockGame = MockGame(); - when(() => mockGame.addAll(any())).thenAnswer((_) async {}); - MyBlueprint().attach(mockGame); - - verify(() => mockGame.addAll(any())).called(1); - }); - - test('adds components from a child Blueprint the to a game on attach', () { - final mockGame = MockGame(); - when(() => mockGame.addAll(any())).thenAnswer((_) async {}); - MyComposedBlueprint().attach(mockGame); - - verify(() => mockGame.addAll(any())).called(4); - }); - - test( - 'throws assertion error when adding to an already attached blueprint', - () async { - final mockGame = MockGame(); - when(() => mockGame.addAll(any())).thenAnswer((_) async {}); - final blueprint = MyBlueprint(); - await blueprint.attach(mockGame); - - expect(() => blueprint.add(Component()), throwsAssertionError); - expect(() => blueprint.addAll([Component()]), throwsAssertionError); - }, - ); - }); - - group('Forge2DBlueprint', () { - setUpAll(() { - registerFallbackValue(SpaceshipHoleBallContactCallback()); - }); - - test('callbacks can be added to it', () { - final blueprint = MyForge2dBlueprint()..build(MockGame()); - - expect(blueprint.callbacks.length, equals(3)); - }); - - test('adds the callbacks to a game on attach', () async { - final mockGame = MockGame(); - when(() => mockGame.addAll(any())).thenAnswer((_) async {}); - when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); - await MyForge2dBlueprint().attach(mockGame); - - verify(() => mockGame.addContactCallback(any())).called(3); - }); - - test( - 'throws assertion error when adding to an already attached blueprint', - () async { - final mockGame = MockGame(); - when(() => mockGame.addAll(any())).thenAnswer((_) async {}); - when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); - final blueprint = MyForge2dBlueprint(); - await blueprint.attach(mockGame); - - expect( - () => blueprint.addContactCallback(MockContactCallback()), - throwsAssertionError, - ); - expect( - () => blueprint.addAllContactCallback([MockContactCallback()]), - throwsAssertionError, - ); - }, - ); - }); -} diff --git a/packages/pinball_components/test/src/flame/priority_test.dart b/packages/pinball_components/test/src/flame/priority_test.dart deleted file mode 100644 index 231c7744..00000000 --- a/packages/pinball_components/test/src/flame/priority_test.dart +++ /dev/null @@ -1,221 +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:mocktail/mocktail.dart'; -import 'package:pinball_components/src/flame/priority.dart'; - -import '../../helpers/helpers.dart'; - -class TestBodyComponent extends BodyComponent { - @override - Body createBody() { - final fixtureDef = FixtureDef(CircleShape()); - return world.createBody(BodyDef())..createFixture(fixtureDef); - } -} - -void main() { - final flameTester = FlameTester(Forge2DGame.new); - - group('ComponentPriorityX', () { - group('sendTo', () { - flameTester.test( - 'changes the priority correctly to other level', - (game) async { - const newPriority = 5; - final component = TestBodyComponent()..priority = 4; - - component.sendTo(newPriority); - - expect(component.priority, equals(newPriority)); - }, - ); - - flameTester.test( - 'calls reorderChildren if the new priority is different', - (game) async { - const newPriority = 5; - final component = MockComponent(); - when(() => component.priority).thenReturn(4); - - component.sendTo(newPriority); - - verify(component.reorderChildren).called(1); - }, - ); - - flameTester.test( - "doesn't call reorderChildren if the priority is the same", - (game) async { - const newPriority = 5; - final component = MockComponent(); - when(() => component.priority).thenReturn(newPriority); - - component.sendTo(newPriority); - - verifyNever(component.reorderChildren); - }, - ); - }); - - group('sendToBack', () { - flameTester.test( - 'changes the priority correctly to board level', - (game) async { - final component = TestBodyComponent()..priority = 4; - - component.sendToBack(); - - expect(component.priority, equals(0)); - }, - ); - - flameTester.test( - 'calls reorderChildren if the priority is greater than lowest level', - (game) async { - final component = MockComponent(); - when(() => component.priority).thenReturn(4); - - component.sendToBack(); - - verify(component.reorderChildren).called(1); - }, - ); - - flameTester.test( - "doesn't call reorderChildren if the priority is the lowest level", - (game) async { - final component = MockComponent(); - when(() => component.priority).thenReturn(0); - - component.sendToBack(); - - verifyNever(component.reorderChildren); - }, - ); - }); - - group('showBehindOf', () { - flameTester.test( - 'changes the priority if it is greater than other component', - (game) async { - const startPriority = 2; - final component = TestBodyComponent()..priority = startPriority; - final otherComponent = TestBodyComponent() - ..priority = startPriority - 1; - - component.showBehindOf(otherComponent); - - expect(component.priority, equals(otherComponent.priority - 1)); - }, - ); - - flameTester.test( - "doesn't change the priority if it is lower than other component", - (game) async { - const startPriority = 2; - final component = TestBodyComponent()..priority = startPriority; - final otherComponent = TestBodyComponent() - ..priority = startPriority + 1; - - component.showBehindOf(otherComponent); - - expect(component.priority, equals(startPriority)); - }, - ); - - flameTester.test( - 'calls reorderChildren if the priority is greater than other component', - (game) async { - const startPriority = 2; - final component = MockComponent(); - final otherComponent = MockComponent(); - when(() => component.priority).thenReturn(startPriority); - when(() => otherComponent.priority).thenReturn(startPriority - 1); - - component.showBehindOf(otherComponent); - - verify(component.reorderChildren).called(1); - }, - ); - - flameTester.test( - "doesn't call reorderChildren if the priority is lower than other " - 'component', - (game) async { - const startPriority = 2; - final component = MockComponent(); - final otherComponent = MockComponent(); - when(() => component.priority).thenReturn(startPriority); - when(() => otherComponent.priority).thenReturn(startPriority + 1); - - component.showBehindOf(otherComponent); - - verifyNever(component.reorderChildren); - }, - ); - }); - - group('showInFrontOf', () { - flameTester.test( - 'changes the priority if it is lower than other component', - (game) async { - const startPriority = 2; - final component = TestBodyComponent()..priority = startPriority; - final otherComponent = TestBodyComponent() - ..priority = startPriority + 1; - - component.showInFrontOf(otherComponent); - - expect(component.priority, equals(otherComponent.priority + 1)); - }, - ); - - flameTester.test( - "doesn't change the priority if it is greater than other component", - (game) async { - const startPriority = 2; - final component = TestBodyComponent()..priority = startPriority; - final otherComponent = TestBodyComponent() - ..priority = startPriority - 1; - - component.showInFrontOf(otherComponent); - - expect(component.priority, equals(startPriority)); - }, - ); - - flameTester.test( - 'calls reorderChildren if the priority is lower than other component', - (game) async { - const startPriority = 2; - final component = MockComponent(); - final otherComponent = MockComponent(); - when(() => component.priority).thenReturn(startPriority); - when(() => otherComponent.priority).thenReturn(startPriority + 1); - - component.showInFrontOf(otherComponent); - - verify(component.reorderChildren).called(1); - }, - ); - - flameTester.test( - "doesn't call reorderChildren if the priority is greater than other " - 'component', - (game) async { - const startPriority = 2; - final component = MockComponent(); - final otherComponent = MockComponent(); - when(() => component.priority).thenReturn(startPriority); - when(() => otherComponent.priority).thenReturn(startPriority - 1); - - component.showInFrontOf(otherComponent); - - verifyNever(component.reorderChildren); - }, - ); - }); - }); -} diff --git a/packages/pinball_flame/.gitignore b/packages/pinball_flame/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_flame/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/pinball_flame/README.md b/packages/pinball_flame/README.md new file mode 100644 index 00000000..ecf0f4d4 --- /dev/null +++ b/packages/pinball_flame/README.md @@ -0,0 +1,11 @@ +# pinball_flame + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Set of out-of-the-way solutions for common Pinball game problems. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/pinball_flame/analysis_options.yaml b/packages/pinball_flame/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/pinball_flame/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart new file mode 100644 index 00000000..7eb4c3a9 --- /dev/null +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -0,0 +1,8 @@ +library pinball_flame; + +export 'src/blueprint.dart'; +export 'src/component_controller.dart'; +export 'src/contact_behavior.dart'; +export 'src/keyboard_input_controller.dart'; +export 'src/parent_is_a.dart'; +export 'src/sprite_animation.dart'; diff --git a/packages/pinball_flame/lib/src/blueprint.dart b/packages/pinball_flame/lib/src/blueprint.dart new file mode 100644 index 00000000..c7bd5a5e --- /dev/null +++ b/packages/pinball_flame/lib/src/blueprint.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +// TODO(erickzanardo): Keeping this inside our code base so we can experiment +// with the idea, but this is a potential upstream change on Flame. + +/// {@template blueprint} +/// A [Blueprint] is a virtual way of grouping [Component]s that are related. +/// {@endtemplate blueprint} +class Blueprint { + /// {@macro blueprint} + Blueprint({ + Iterable? components, + Iterable? blueprints, + }) { + if (components != null) _components.addAll(components); + if (blueprints != null) { + _blueprints.addAll(blueprints); + for (final blueprint in blueprints) { + _components.addAll(blueprint.components); + } + } + } + + final List _components = []; + + final List _blueprints = []; + + Future _addToParent(Component parent) async { + await parent.addAll(_components); + } + + /// Returns a copy of the components built by this blueprint. + List get components => List.unmodifiable(_components); + + /// Returns a copy of the blueprints built by this blueprint. + List get blueprints => List.unmodifiable(_blueprints); +} + +/// Adds helper methods regarding [Blueprint]s to [FlameGame]. +extension FlameGameBlueprint on Component { + /// Shortcut to add a [Blueprint]s components to its parent. + Future addFromBlueprint(Blueprint blueprint) async { + await blueprint._addToParent(this); + } +} diff --git a/lib/flame/component_controller.dart b/packages/pinball_flame/lib/src/component_controller.dart similarity index 91% rename from lib/flame/component_controller.dart rename to packages/pinball_flame/lib/src/component_controller.dart index b9568348..6afc1c40 100644 --- a/lib/flame/component_controller.dart +++ b/packages/pinball_flame/lib/src/component_controller.dart @@ -1,12 +1,9 @@ import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/foundation.dart'; /// {@template component_controller} /// A [ComponentController] is a [Component] in charge of handling the logic /// associated with another [Component]. -/// -/// [ComponentController]s usually implement [BlocComponent]. /// {@endtemplate} abstract class ComponentController extends Component { /// {@macro component_controller} diff --git a/packages/pinball_flame/lib/src/contact_behavior.dart b/packages/pinball_flame/lib/src/contact_behavior.dart new file mode 100644 index 00000000..79112398 --- /dev/null +++ b/packages/pinball_flame/lib/src/contact_behavior.dart @@ -0,0 +1,95 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Appends a new [ContactCallbacks] to the parent. +/// +/// This is a convenience class for adding a [ContactCallbacks] to the parent. +/// In constract with just adding a [ContactCallbacks] to the parent's body +/// userData, this class respects the previous [ContactCallbacks] in the +/// parent's body userData, if any. Hence, it avoids overriding any previous +/// [ContactCallbacks] in the parent. +/// +/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup], +/// and resetting the parent's userData accordingly. +// TODO(alestiago): Make use of generics to infer the type of the contact. +// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267 +// TODO(alestiago): Consider if there is a need to support adjusting a fixture's +// userData. +class ContactBehavior extends Component + with ContactCallbacks, ParentIsA { + @override + @mustCallSuper + Future onLoad() async { + final userData = parent.body.userData; + if (userData is _ContactCallbacksGroup) { + userData.addContactCallbacks(this); + } else if (userData is ContactCallbacks) { + final contactCallbacksGroup = _ContactCallbacksGroup() + ..addContactCallbacks(userData) + ..addContactCallbacks(this); + parent.body.userData = contactCallbacksGroup; + } else { + parent.body.userData = this; + } + } +} + +class _ContactCallbacksGroup implements ContactCallbacks { + final List _contactCallbacks = []; + + @override + @mustCallSuper + void beginContact(Object other, Contact contact) { + onBeginContact?.call(other, contact); + for (final callback in _contactCallbacks) { + callback.beginContact(other, contact); + } + } + + @override + @mustCallSuper + void endContact(Object other, Contact contact) { + onEndContact?.call(other, contact); + for (final callback in _contactCallbacks) { + callback.endContact(other, contact); + } + } + + @override + @mustCallSuper + void preSolve(Object other, Contact contact, Manifold oldManifold) { + onPreSolve?.call(other, contact, oldManifold); + for (final callback in _contactCallbacks) { + callback.preSolve(other, contact, oldManifold); + } + } + + @override + @mustCallSuper + void postSolve(Object other, Contact contact, ContactImpulse impulse) { + onPostSolve?.call(other, contact, impulse); + for (final callback in _contactCallbacks) { + callback.postSolve(other, contact, impulse); + } + } + + void addContactCallbacks(ContactCallbacks callback) { + _contactCallbacks.add(callback); + } + + @override + void Function(Object other, Contact contact)? onBeginContact; + + @override + void Function(Object other, Contact contact)? onEndContact; + + @override + void Function(Object other, Contact contact, ContactImpulse impulse)? + onPostSolve; + + @override + void Function(Object other, Contact contact, Manifold oldManifold)? + onPreSolve; +} diff --git a/packages/pinball_flame/lib/src/keyboard_input_controller.dart b/packages/pinball_flame/lib/src/keyboard_input_controller.dart new file mode 100644 index 00000000..8249e599 --- /dev/null +++ b/packages/pinball_flame/lib/src/keyboard_input_controller.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; + +/// The signature for a key handle function +typedef KeyHandlerCallback = bool Function(); + +/// {@template keyboard_input_controller} +/// A [Component] that receives keyboard input and executes registered methods. +/// {@endtemplate} +class KeyboardInputController extends Component with KeyboardHandler { + /// {@macro keyboard_input_controller} + KeyboardInputController({ + Map keyUp = const {}, + Map keyDown = const {}, + }) : _keyUp = keyUp, + _keyDown = keyDown; + + final Map _keyUp; + final Map _keyDown; + + @override + bool onKeyEvent(RawKeyEvent event, Set keysPressed) { + final isUp = event is RawKeyUpEvent; + + final handlers = isUp ? _keyUp : _keyDown; + final handler = handlers[event.logicalKey]; + + if (handler != null) { + return handler(); + } + + return true; + } +} diff --git a/packages/pinball_flame/lib/src/parent_is_a.dart b/packages/pinball_flame/lib/src/parent_is_a.dart new file mode 100644 index 00000000..19159c89 --- /dev/null +++ b/packages/pinball_flame/lib/src/parent_is_a.dart @@ -0,0 +1,15 @@ +import 'package:flame/components.dart'; + +// TODO(alestiago): Remove once the following is merged: +// https://github.com/flame-engine/flame/pull/1566 + +/// A mixin that ensures a parent is of the given type [T]. +mixin ParentIsA on Component { + @override + T get parent => super.parent! as T; + + @override + Future? addToParent(covariant T parent) { + return super.addToParent(parent); + } +} diff --git a/packages/pinball_flame/lib/src/sprite_animation.dart b/packages/pinball_flame/lib/src/sprite_animation.dart new file mode 100644 index 00000000..2990fb14 --- /dev/null +++ b/packages/pinball_flame/lib/src/sprite_animation.dart @@ -0,0 +1,102 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/image_composition.dart'; +import 'package:flutter/material.dart' hide Animation; + +/// {@template flame.widgets.sprite_animation_widget} +/// A [StatelessWidget] that renders a [SpriteAnimation]. +/// {@endtemplate} +// TODO(arturplaczek): Remove when this PR will be merged. +// https://github.com/flame-engine/flame/pull/1552 +class SpriteAnimationWidget extends StatelessWidget { + /// {@macro flame.widgets.sprite_animation_widget} + const SpriteAnimationWidget({ + required this.controller, + this.anchor = Anchor.topLeft, + Key? key, + }) : super(key: key); + + /// The positioning [Anchor]. + final Anchor anchor; + + final SpriteAnimationController controller; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (_, __) { + return CustomPaint( + painter: SpritePainter( + controller.animation.getSprite(), + anchor, + ), + ); + }, + ); + } +} + +class SpriteAnimationController extends AnimationController { + SpriteAnimationController({ + required TickerProvider vsync, + required this.animation, + }) : super(vsync: vsync) { + duration = Duration(seconds: animation.totalDuration().ceil()); + } + + final SpriteAnimation animation; + + double? _lastUpdated; + + @override + void notifyListeners() { + super.notifyListeners(); + + final now = DateTime.now().millisecond.toDouble(); + final dt = max(0, (now - (_lastUpdated ?? 0)) / 1000); + animation.update(dt); + _lastUpdated = now; + } +} + +class SpritePainter extends CustomPainter { + SpritePainter( + this._sprite, + this._anchor, { + double angle = 0, + }) : _angle = angle; + + final Sprite _sprite; + final Anchor _anchor; + final double _angle; + + @override + bool shouldRepaint(SpritePainter oldDelegate) { + return oldDelegate._sprite != _sprite || + oldDelegate._anchor != _anchor || + oldDelegate._angle != _angle; + } + + @override + void paint(Canvas canvas, Size size) { + final boxSize = size.toVector2(); + final rate = boxSize.clone()..divide(_sprite.srcSize); + final minRate = min(rate.x, rate.y); + final paintSize = _sprite.srcSize * minRate; + final anchorPosition = _anchor.toVector2(); + final boxAnchorPosition = boxSize.clone()..multiply(anchorPosition); + final spriteAnchorPosition = anchorPosition..multiply(paintSize); + + canvas + ..translateVector(boxAnchorPosition..sub(spriteAnchorPosition)) + ..renderRotated( + _angle, + spriteAnchorPosition, + (canvas) => _sprite.render(canvas, size: paintSize), + ); + } +} diff --git a/packages/pinball_flame/pubspec.yaml b/packages/pinball_flame/pubspec.yaml new file mode 100644 index 00000000..89caf5bb --- /dev/null +++ b/packages/pinball_flame/pubspec.yaml @@ -0,0 +1,24 @@ +name: pinball_flame +description: Set of out-of-the-way solutions for common Pinball game problems. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flame: ^1.1.1 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + flutter: + sdk: flutter + +dev_dependencies: + flame_test: ^1.3.0 + flutter_test: + sdk: flutter + mocktail: ^0.3.0 + very_good_analysis: ^2.4.0 diff --git a/packages/pinball_flame/test/helpers/helpers.dart b/packages/pinball_flame/test/helpers/helpers.dart new file mode 100644 index 00000000..efe914f6 --- /dev/null +++ b/packages/pinball_flame/test/helpers/helpers.dart @@ -0,0 +1 @@ +export 'mocks.dart'; diff --git a/packages/pinball_flame/test/helpers/mocks.dart b/packages/pinball_flame/test/helpers/mocks.dart new file mode 100644 index 00000000..1c5042ff --- /dev/null +++ b/packages/pinball_flame/test/helpers/mocks.dart @@ -0,0 +1,7 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockForge2DGame extends Mock implements Forge2DGame {} + +class MockComponent extends Mock implements Component {} diff --git a/packages/pinball_flame/test/src/blueprint_test.dart b/packages/pinball_flame/test/src/blueprint_test.dart new file mode 100644 index 00000000..402d5059 --- /dev/null +++ b/packages/pinball_flame/test/src/blueprint_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Blueprint', () { + final flameTester = FlameTester(FlameGame.new); + + test('correctly sets and gets components', () { + final component1 = Component(); + final component2 = Component(); + final blueprint = Blueprint( + components: [ + component1, + component2, + ], + ); + + expect(blueprint.components.length, 2); + expect(blueprint.components, contains(component1)); + expect(blueprint.components, contains(component2)); + }); + + test('correctly sets and gets blueprints', () { + final blueprint2 = Blueprint( + components: [Component()], + ); + final blueprint1 = Blueprint( + components: [Component()], + blueprints: [blueprint2], + ); + + expect(blueprint1.blueprints, contains(blueprint2)); + }); + + flameTester.test('adds the components to parent on attach', (game) async { + final blueprint = Blueprint( + components: [ + Component(), + Component(), + ], + ); + await game.addFromBlueprint(blueprint); + await game.ready(); + + for (final component in blueprint.components) { + expect(game.children.contains(component), isTrue); + } + }); + + flameTester.test('adds components from a child Blueprint', (game) async { + final childBlueprint = Blueprint( + components: [ + Component(), + Component(), + ], + ); + final parentBlueprint = Blueprint( + components: [ + Component(), + Component(), + ], + blueprints: [ + childBlueprint, + ], + ); + + await game.addFromBlueprint(parentBlueprint); + await game.ready(); + + for (final component in childBlueprint.components) { + expect(game.children, contains(component)); + expect(parentBlueprint.components, contains(component)); + } + for (final component in parentBlueprint.components) { + expect(game.children, contains(component)); + } + }); + }); +} diff --git a/test/flame/component_controller_test.dart b/packages/pinball_flame/test/src/component_controller_test.dart similarity index 97% rename from test/flame/component_controller_test.dart rename to packages/pinball_flame/test/src/component_controller_test.dart index e1973274..0e08be92 100644 --- a/test/flame/component_controller_test.dart +++ b/packages/pinball_flame/test/src/component_controller_test.dart @@ -4,7 +4,7 @@ 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/flame.dart'; +import 'package:pinball_flame/pinball_flame.dart'; class TestComponentController extends ComponentController { TestComponentController(Component component) : super(component); diff --git a/packages/pinball_flame/test/src/contact_behavior_test.dart b/packages/pinball_flame/test/src/contact_behavior_test.dart new file mode 100644 index 00000000..630156ed --- /dev/null +++ b/packages/pinball_flame/test/src/contact_behavior_test.dart @@ -0,0 +1,153 @@ +// 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_flame/pinball_flame.dart'; + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody(BodyDef()); +} + +class _TestContactBehavior extends ContactBehavior { + int beginContactCallsCount = 0; + @override + void beginContact(Object other, Contact contact) { + beginContactCallsCount++; + super.beginContact(other, contact); + } + + int endContactCallsCount = 0; + @override + void endContact(Object other, Contact contact) { + endContactCallsCount++; + super.endContact(other, contact); + } + + int preSolveContactCallsCount = 0; + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + preSolveContactCallsCount++; + super.preSolve(other, contact, oldManifold); + } + + int postSolveContactCallsCount = 0; + @override + void postSolve(Object other, Contact contact, ContactImpulse impulse) { + postSolveContactCallsCount++; + super.postSolve(other, contact, impulse); + } +} + +class _MockContactCallbacks extends Mock implements ContactCallbacks {} + +class _MockContact extends Mock implements Contact {} + +class _MockManifold extends Mock implements Manifold {} + +class _MockContactImpulse extends Mock implements ContactImpulse {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(Forge2DGame.new); + + group('ContactBehavior', () { + late Object other; + late Contact contact; + late Manifold manifold; + late ContactImpulse contactImpulse; + + setUp(() { + other = Object(); + contact = _MockContact(); + manifold = _MockManifold(); + contactImpulse = _MockContactImpulse(); + }); + + flameTester.test( + 'should add a new ContactCallbacks to the parent', + (game) async { + final parent = _TestBodyComponent(); + final contactBehavior = ContactBehavior(); + await parent.add(contactBehavior); + await game.ensureAdd(parent); + + expect(parent.body.userData, contactBehavior); + }, + ); + + flameTester.test( + "should respect the previous ContactCallbacks in the parent's userData", + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + final contactCallbacks1 = _MockContactCallbacks(); + parent.body.userData = contactCallbacks1; + + final contactBehavior = ContactBehavior(); + await parent.ensureAdd(contactBehavior); + + final contactCallbacks = parent.body.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + verify( + () => contactCallbacks1.beginContact(other, contact), + ).called(1); + + contactCallbacks.endContact(other, contact); + verify( + () => contactCallbacks1.endContact(other, contact), + ).called(1); + + contactCallbacks.preSolve(other, contact, manifold); + verify( + () => contactCallbacks1.preSolve(other, contact, manifold), + ).called(1); + + contactCallbacks.postSolve(other, contact, contactImpulse); + verify( + () => contactCallbacks1.postSolve(other, contact, contactImpulse), + ).called(1); + }, + ); + + flameTester.test('can group multiple ContactBehaviors and keep listening', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final contactBehavior1 = _TestContactBehavior(); + final contactBehavior2 = _TestContactBehavior(); + final contactBehavior3 = _TestContactBehavior(); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = parent.body.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }); + }); +} diff --git a/packages/pinball_flame/test/src/keyboard_input_controller_test.dart b/packages/pinball_flame/test/src/keyboard_input_controller_test.dart new file mode 100644 index 00000000..99a0006b --- /dev/null +++ b/packages/pinball_flame/test/src/keyboard_input_controller_test.dart @@ -0,0 +1,78 @@ +// ignore_for_file: cascade_invocations, one_member_abstracts + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +abstract class _KeyCallStub { + bool onCall(); +} + +class KeyCallStub extends Mock implements _KeyCallStub {} + +class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { + final event = MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn(key); + return event; +} + +void main() { + group('KeyboardInputController', () { + test('calls registered handlers', () { + final stub = KeyCallStub(); + when(stub.onCall).thenReturn(true); + + final input = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: stub.onCall, + }, + ); + + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}); + verify(stub.onCall).called(1); + }); + + test( + 'returns false the handler return value', + () { + final stub = KeyCallStub(); + when(stub.onCall).thenReturn(false); + + final input = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: stub.onCall, + }, + ); + + expect( + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}), + isFalse, + ); + }, + ); + + test( + 'returns true (allowing event to bubble) when no handler is registered', + () { + final stub = KeyCallStub(); + when(stub.onCall).thenReturn(true); + + final input = KeyboardInputController(); + + expect( + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}), + isTrue, + ); + }, + ); + }); +} diff --git a/packages/pinball_flame/test/src/sprite_animation_test.dart b/packages/pinball_flame/test/src/sprite_animation_test.dart new file mode 100644 index 00000000..e3b287de --- /dev/null +++ b/packages/pinball_flame/test/src/sprite_animation_test.dart @@ -0,0 +1,74 @@ +import 'package:flame/components.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class MockSpriteAnimationController extends Mock + implements SpriteAnimationController {} + +class MockSpriteAnimation extends Mock implements SpriteAnimation {} + +class MockSprite extends Mock implements Sprite {} + +// TODO(arturplaczek): Remove when this PR will be merged. +// https://github.com/flame-engine/flame/pull/1552 + +void main() { + group('PinballSpriteAnimationWidget', () { + late SpriteAnimationController controller; + late SpriteAnimation animation; + late Sprite sprite; + + setUp(() { + controller = MockSpriteAnimationController(); + animation = MockSpriteAnimation(); + sprite = MockSprite(); + + when(() => controller.animation).thenAnswer((_) => animation); + + when(() => animation.totalDuration()).thenAnswer((_) => 1); + when(() => animation.getSprite()).thenAnswer((_) => sprite); + when(() => sprite.srcSize).thenAnswer((_) => Vector2(1, 1)); + when(() => sprite.srcSize).thenAnswer((_) => Vector2(1, 1)); + }); + + testWidgets('renders correctly', (tester) async { + await tester.pumpWidget( + SpriteAnimationWidget( + controller: controller, + ), + ); + + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + test('SpriteAnimationController is updating animations', () { + SpriteAnimationController( + vsync: const TestVSync(), + animation: animation, + ).notifyListeners(); + + verify(() => animation.update(any())).called(1); + }); + + testWidgets('SpritePainter shouldRepaint returns true when Sprite changed', + (tester) async { + final spritePainter = SpritePainter( + sprite, + Anchor.center, + angle: 45, + ); + + final anotherPainter = SpritePainter( + sprite, + Anchor.center, + angle: 30, + ); + + expect(spritePainter.shouldRepaint(anotherPainter), isTrue); + }); + }); +} diff --git a/packages/pinball_theme/assets/images/android.png b/packages/pinball_theme/assets/images/android.png deleted file mode 100644 index 23f677a5..00000000 Binary files a/packages/pinball_theme/assets/images/android.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/android/animation.png b/packages/pinball_theme/assets/images/android/animation.png new file mode 100644 index 00000000..fc7465be Binary files /dev/null and b/packages/pinball_theme/assets/images/android/animation.png differ diff --git a/packages/pinball_theme/assets/images/android/background.png b/packages/pinball_theme/assets/images/android/background.png new file mode 100644 index 00000000..f751dcdc Binary files /dev/null and b/packages/pinball_theme/assets/images/android/background.png differ diff --git a/packages/pinball_theme/assets/images/android/icon.png b/packages/pinball_theme/assets/images/android/icon.png new file mode 100644 index 00000000..ff365ffe Binary files /dev/null and b/packages/pinball_theme/assets/images/android/icon.png differ diff --git a/packages/pinball_theme/assets/images/android/leaderboard_icon.png b/packages/pinball_theme/assets/images/android/leaderboard_icon.png new file mode 100644 index 00000000..238e29ef Binary files /dev/null and b/packages/pinball_theme/assets/images/android/leaderboard_icon.png differ diff --git a/packages/pinball_theme/assets/images/dash.png b/packages/pinball_theme/assets/images/dash.png deleted file mode 100644 index 43c074a3..00000000 Binary files a/packages/pinball_theme/assets/images/dash.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/dash/animation.png b/packages/pinball_theme/assets/images/dash/animation.png new file mode 100644 index 00000000..e812415f Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/animation.png differ diff --git a/packages/pinball_theme/assets/images/dash/background.png b/packages/pinball_theme/assets/images/dash/background.png new file mode 100644 index 00000000..a36601c9 Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/background.png differ diff --git a/packages/pinball_theme/assets/images/dash/icon.png b/packages/pinball_theme/assets/images/dash/icon.png new file mode 100644 index 00000000..45bba327 Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/icon.png differ diff --git a/packages/pinball_theme/assets/images/dash/leaderboard_icon.png b/packages/pinball_theme/assets/images/dash/leaderboard_icon.png new file mode 100644 index 00000000..5c172d47 Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/leaderboard_icon.png differ diff --git a/packages/pinball_theme/assets/images/dino.png b/packages/pinball_theme/assets/images/dino.png deleted file mode 100644 index 9e5dbf86..00000000 Binary files a/packages/pinball_theme/assets/images/dino.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/dino/animation.png b/packages/pinball_theme/assets/images/dino/animation.png new file mode 100644 index 00000000..c75b16f9 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/animation.png differ diff --git a/packages/pinball_theme/assets/images/dino/background.png b/packages/pinball_theme/assets/images/dino/background.png new file mode 100644 index 00000000..e42f1705 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/background.png differ diff --git a/packages/pinball_theme/assets/images/dino/icon.png b/packages/pinball_theme/assets/images/dino/icon.png new file mode 100644 index 00000000..0114060e Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/icon.png differ diff --git a/packages/pinball_theme/assets/images/dino/leaderboard_icon.png b/packages/pinball_theme/assets/images/dino/leaderboard_icon.png new file mode 100644 index 00000000..b1033371 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/leaderboard_icon.png differ diff --git a/packages/pinball_theme/assets/images/pinball_button.png b/packages/pinball_theme/assets/images/pinball_button.png new file mode 100644 index 00000000..62373b85 Binary files /dev/null and b/packages/pinball_theme/assets/images/pinball_button.png differ diff --git a/packages/pinball_theme/assets/images/select_character_background.png b/packages/pinball_theme/assets/images/select_character_background.png new file mode 100644 index 00000000..69120148 Binary files /dev/null and b/packages/pinball_theme/assets/images/select_character_background.png differ diff --git a/packages/pinball_theme/assets/images/sparky.png b/packages/pinball_theme/assets/images/sparky.png deleted file mode 100644 index 8e484f26..00000000 Binary files a/packages/pinball_theme/assets/images/sparky.png and /dev/null differ diff --git a/packages/pinball_theme/assets/images/sparky/animation.png b/packages/pinball_theme/assets/images/sparky/animation.png new file mode 100644 index 00000000..1aff4772 Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/animation.png differ diff --git a/packages/pinball_theme/assets/images/sparky/background.png b/packages/pinball_theme/assets/images/sparky/background.png new file mode 100644 index 00000000..5044376c Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/background.png differ diff --git a/packages/pinball_theme/assets/images/sparky/icon.png b/packages/pinball_theme/assets/images/sparky/icon.png new file mode 100644 index 00000000..4e484438 Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/icon.png differ diff --git a/packages/pinball_theme/assets/images/sparky/leaderboard_icon.png b/packages/pinball_theme/assets/images/sparky/leaderboard_icon.png new file mode 100644 index 00000000..76001516 Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/leaderboard_icon.png differ diff --git a/packages/pinball_theme/lib/pinball_theme.dart b/packages/pinball_theme/lib/pinball_theme.dart index 139a70dc..c8f9b53e 100644 --- a/packages/pinball_theme/lib/pinball_theme.dart +++ b/packages/pinball_theme/lib/pinball_theme.dart @@ -1,5 +1,4 @@ library pinball_theme; export 'src/generated/generated.dart'; -export 'src/pinball_theme.dart'; export 'src/themes/themes.dart'; diff --git a/packages/pinball_theme/lib/src/generated/assets.gen.dart b/packages/pinball_theme/lib/src/generated/assets.gen.dart index 9dc5c029..3feeecce 100644 --- a/packages/pinball_theme/lib/src/generated/assets.gen.dart +++ b/packages/pinball_theme/lib/src/generated/assets.gen.dart @@ -3,15 +3,120 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); - AssetGenImage get android => const AssetGenImage('assets/images/android.png'); - AssetGenImage get dash => const AssetGenImage('assets/images/dash.png'); - AssetGenImage get dino => const AssetGenImage('assets/images/dino.png'); - AssetGenImage get sparky => const AssetGenImage('assets/images/sparky.png'); + $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen(); + $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); + $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); + + /// File path: assets/images/pinball_button.png + AssetGenImage get pinballButton => + const AssetGenImage('assets/images/pinball_button.png'); + + /// File path: assets/images/select_character_background.png + AssetGenImage get selectCharacterBackground => + const AssetGenImage('assets/images/select_character_background.png'); + + $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); +} + +class $AssetsImagesAndroidGen { + const $AssetsImagesAndroidGen(); + + /// File path: assets/images/android/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/android/animation.png'); + + /// File path: assets/images/android/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/android/background.png'); + + /// File path: assets/images/android/character.png + AssetGenImage get character => + const AssetGenImage('assets/images/android/character.png'); + + /// File path: assets/images/android/icon.png + AssetGenImage get icon => + const AssetGenImage('assets/images/android/icon.png'); + + /// File path: assets/images/android/leaderboard_icon.png + AssetGenImage get leaderboardIcon => + const AssetGenImage('assets/images/android/leaderboard_icon.png'); +} + +class $AssetsImagesDashGen { + const $AssetsImagesDashGen(); + + /// File path: assets/images/dash/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/dash/animation.png'); + + /// File path: assets/images/dash/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/dash/background.png'); + + /// File path: assets/images/dash/character.png + AssetGenImage get character => + const AssetGenImage('assets/images/dash/character.png'); + + /// File path: assets/images/dash/icon.png + AssetGenImage get icon => const AssetGenImage('assets/images/dash/icon.png'); + + /// File path: assets/images/dash/leaderboard_icon.png + AssetGenImage get leaderboardIcon => + const AssetGenImage('assets/images/dash/leaderboard_icon.png'); +} + +class $AssetsImagesDinoGen { + const $AssetsImagesDinoGen(); + + /// File path: assets/images/dino/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/dino/animation.png'); + + /// File path: assets/images/dino/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/dino/background.png'); + + /// File path: assets/images/dino/character.png + AssetGenImage get character => + const AssetGenImage('assets/images/dino/character.png'); + + /// File path: assets/images/dino/icon.png + AssetGenImage get icon => const AssetGenImage('assets/images/dino/icon.png'); + + /// File path: assets/images/dino/leaderboard_icon.png + AssetGenImage get leaderboardIcon => + const AssetGenImage('assets/images/dino/leaderboard_icon.png'); +} + +class $AssetsImagesSparkyGen { + const $AssetsImagesSparkyGen(); + + /// File path: assets/images/sparky/animation.png + AssetGenImage get animation => + const AssetGenImage('assets/images/sparky/animation.png'); + + /// File path: assets/images/sparky/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/sparky/background.png'); + + /// File path: assets/images/sparky/character.png + AssetGenImage get character => + const AssetGenImage('assets/images/sparky/character.png'); + + /// File path: assets/images/sparky/icon.png + AssetGenImage get icon => + const AssetGenImage('assets/images/sparky/icon.png'); + + /// File path: assets/images/sparky/leaderboard_icon.png + AssetGenImage get leaderboardIcon => + const AssetGenImage('assets/images/sparky/leaderboard_icon.png'); } class Assets { diff --git a/packages/pinball_theme/lib/src/pinball_theme.dart b/packages/pinball_theme/lib/src/pinball_theme.dart deleted file mode 100644 index a766a129..00000000 --- a/packages/pinball_theme/lib/src/pinball_theme.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -/// {@template pinball_theme} -/// Defines all theme assets and attributes. -/// -/// Game components should have a getter specified here to load their -/// corresponding assets for the game. -/// {@endtemplate} -class PinballTheme extends Equatable { - /// {@macro pinball_theme} - const PinballTheme({ - required CharacterTheme characterTheme, - }) : _characterTheme = characterTheme; - - final CharacterTheme _characterTheme; - - /// [CharacterTheme] for the chosen character. - CharacterTheme get characterTheme => _characterTheme; - - @override - List get props => [_characterTheme]; -} diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index f6605f52..8989c717 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -8,9 +8,21 @@ class AndroidTheme extends CharacterTheme { /// {@macro android_theme} const AndroidTheme(); + @override + String get name => 'Android'; + @override Color get ballColor => Colors.green; @override - AssetGenImage get characterAsset => Assets.images.android; + AssetGenImage get background => Assets.images.android.background; + + @override + AssetGenImage get icon => Assets.images.android.icon; + + @override + AssetGenImage get leaderboardIcon => Assets.images.android.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.android.animation; } diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 9478f954..072c917f 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -12,12 +12,31 @@ abstract class CharacterTheme extends Equatable { /// {@macro character_theme} const CharacterTheme(); + /// Name of character. + String get name; + /// Ball color for this theme. Color get ballColor; - /// Asset for the theme character. - AssetGenImage get characterAsset; + /// Asset for the background. + AssetGenImage get background; + + /// Icon asset. + AssetGenImage get icon; + + /// Icon asset for the leaderboard. + AssetGenImage get leaderboardIcon; + + /// Asset for the the idle character animation. + AssetGenImage get animation; @override - List get props => [ballColor]; + List get props => [ + name, + ballColor, + background, + icon, + leaderboardIcon, + animation, + ]; } diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index 1b5b357e..7584c8ed 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -8,9 +8,21 @@ class DashTheme extends CharacterTheme { /// {@macro dash_theme} const DashTheme(); + @override + String get name => 'Dash'; + @override Color get ballColor => Colors.blue; @override - AssetGenImage get characterAsset => Assets.images.dash; + AssetGenImage get background => Assets.images.dash.background; + + @override + AssetGenImage get icon => Assets.images.dash.icon; + + @override + AssetGenImage get leaderboardIcon => Assets.images.dash.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.dash.animation; } diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 564cbea0..3baf466c 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -8,9 +8,21 @@ class DinoTheme extends CharacterTheme { /// {@macro dino_theme} const DinoTheme(); + @override + String get name => 'Dino'; + @override Color get ballColor => Colors.grey; @override - AssetGenImage get characterAsset => Assets.images.dino; + AssetGenImage get background => Assets.images.dino.background; + + @override + AssetGenImage get icon => Assets.images.dino.icon; + + @override + AssetGenImage get leaderboardIcon => Assets.images.dino.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.dino.animation; } diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index b4181a8c..7884a22f 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -12,5 +12,17 @@ class SparkyTheme extends CharacterTheme { Color get ballColor => Colors.orange; @override - AssetGenImage get characterAsset => Assets.images.sparky; + String get name => 'Sparky'; + + @override + AssetGenImage get background => Assets.images.sparky.background; + + @override + AssetGenImage get icon => Assets.images.sparky.icon; + + @override + AssetGenImage get leaderboardIcon => Assets.images.sparky.leaderboardIcon; + + @override + AssetGenImage get animation => Assets.images.sparky.animation; } diff --git a/packages/pinball_theme/pubspec.yaml b/packages/pinball_theme/pubspec.yaml index 7d745422..57522939 100644 --- a/packages/pinball_theme/pubspec.yaml +++ b/packages/pinball_theme/pubspec.yaml @@ -17,13 +17,18 @@ dev_dependencies: very_good_analysis: ^2.4.0 flutter: - uses-material-design: true generate: true assets: - assets/images/ + - assets/images/android/ + - assets/images/dash/ + - assets/images/dino/ + - assets/images/sparky/ flutter_gen: assets: package_parameter_enabled: true output: lib/src/generated/ - line_length: 80 \ No newline at end of file + line_length: 80 + integrations: + flutter_svg: true diff --git a/packages/pinball_theme/test/src/pinball_theme_test.dart b/packages/pinball_theme/test/src/pinball_theme_test.dart deleted file mode 100644 index 899eec64..00000000 --- a/packages/pinball_theme/test/src/pinball_theme_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('PinballTheme', () { - const characterTheme = SparkyTheme(); - - test('can be instantiated', () { - expect(PinballTheme(characterTheme: characterTheme), isNotNull); - }); - - test('supports value equality', () { - expect( - PinballTheme(characterTheme: characterTheme), - equals(PinballTheme(characterTheme: characterTheme)), - ); - }); - - test('characterTheme is correct', () { - expect( - PinballTheme(characterTheme: characterTheme).characterTheme, - equals(characterTheme), - ); - }); - }); -} diff --git a/packages/pinball_theme/test/src/themes/android_theme_test.dart b/packages/pinball_theme/test/src/themes/android_theme_test.dart index 24186c35..27a06922 100644 --- a/packages/pinball_theme/test/src/themes/android_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/android_theme_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -13,13 +12,5 @@ void main() { test('supports value equality', () { expect(AndroidTheme(), equals(AndroidTheme())); }); - - test('ballColor is correct', () { - expect(AndroidTheme().ballColor, equals(Colors.green)); - }); - - test('characterAsset is correct', () { - expect(AndroidTheme().characterAsset, equals(Assets.images.android)); - }); }); } diff --git a/packages/pinball_theme/test/src/themes/dash_theme_test.dart b/packages/pinball_theme/test/src/themes/dash_theme_test.dart index 2fb429e0..16605fb3 100644 --- a/packages/pinball_theme/test/src/themes/dash_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/dash_theme_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -13,13 +12,5 @@ void main() { test('supports value equality', () { expect(DashTheme(), equals(DashTheme())); }); - - test('ballColor is correct', () { - expect(DashTheme().ballColor, equals(Colors.blue)); - }); - - test('characterAsset is correct', () { - expect(DashTheme().characterAsset, equals(Assets.images.dash)); - }); }); } diff --git a/packages/pinball_theme/test/src/themes/dino_theme_test.dart b/packages/pinball_theme/test/src/themes/dino_theme_test.dart index 673cccf6..45ea0813 100644 --- a/packages/pinball_theme/test/src/themes/dino_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/dino_theme_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -13,13 +12,5 @@ void main() { test('supports value equality', () { expect(DinoTheme(), equals(DinoTheme())); }); - - test('ballColor is correct', () { - expect(DinoTheme().ballColor, equals(Colors.grey)); - }); - - test('characterAsset is correct', () { - expect(DinoTheme().characterAsset, equals(Assets.images.dino)); - }); }); } diff --git a/packages/pinball_theme/test/src/themes/sparky_theme_test.dart b/packages/pinball_theme/test/src/themes/sparky_theme_test.dart index d0d96566..ce276938 100644 --- a/packages/pinball_theme/test/src/themes/sparky_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/sparky_theme_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -13,13 +12,5 @@ void main() { test('supports value equality', () { expect(SparkyTheme(), equals(SparkyTheme())); }); - - test('ballColor is correct', () { - expect(SparkyTheme().ballColor, equals(Colors.orange)); - }); - - test('characterAsset is correct', () { - expect(SparkyTheme().characterAsset, equals(Assets.images.sparky)); - }); }); } diff --git a/packages/share_repository/.gitignore b/packages/share_repository/.gitignore new file mode 100644 index 00000000..526da158 --- /dev/null +++ b/packages/share_repository/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/packages/share_repository/README.md b/packages/share_repository/README.md new file mode 100644 index 00000000..0473707b --- /dev/null +++ b/packages/share_repository/README.md @@ -0,0 +1,11 @@ +# share_repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Repository to facilitate sharing scores. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/share_repository/analysis_options.yaml b/packages/share_repository/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/share_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/share_repository/lib/share_repository.dart b/packages/share_repository/lib/share_repository.dart new file mode 100644 index 00000000..0a68aff4 --- /dev/null +++ b/packages/share_repository/lib/share_repository.dart @@ -0,0 +1,4 @@ +library share_repository; + +export 'src/models/models.dart'; +export 'src/share_repository.dart'; diff --git a/packages/share_repository/lib/src/models/models.dart b/packages/share_repository/lib/src/models/models.dart new file mode 100644 index 00000000..26819946 --- /dev/null +++ b/packages/share_repository/lib/src/models/models.dart @@ -0,0 +1 @@ +export 'share_platform.dart'; diff --git a/packages/share_repository/lib/src/models/share_platform.dart b/packages/share_repository/lib/src/models/share_platform.dart new file mode 100644 index 00000000..054a4f15 --- /dev/null +++ b/packages/share_repository/lib/src/models/share_platform.dart @@ -0,0 +1,8 @@ +/// The platform that is being used to share a score. +enum SharePlatform { + /// Twitter platform. + twitter, + + /// Facebook platform. + facebook, +} diff --git a/packages/share_repository/lib/src/share_repository.dart b/packages/share_repository/lib/src/share_repository.dart new file mode 100644 index 00000000..6e6679c2 --- /dev/null +++ b/packages/share_repository/lib/src/share_repository.dart @@ -0,0 +1,30 @@ +import 'package:share_repository/share_repository.dart'; + +/// {@template share_repository} +/// Repository to facilitate sharing scores. +/// {@endtemplate} +class ShareRepository { + /// {@macro share_repository} + const ShareRepository({ + required String appUrl, + }) : _appUrl = appUrl; + + final String _appUrl; + + /// Returns a url to share the [value] on the given [platform]. + /// + /// The returned url can be opened using the [url_launcher](https://pub.dev/packages/url_launcher) package. + String shareText({ + required String value, + required SharePlatform platform, + }) { + final encodedUrl = Uri.encodeComponent(_appUrl); + final encodedShareText = Uri.encodeComponent(value); + switch (platform) { + case SharePlatform.twitter: + return 'https://twitter.com/intent/tweet?url=$encodedUrl&text=$encodedShareText'; + case SharePlatform.facebook: + return 'https://www.facebook.com/sharer.php?u=$encodedUrl"e=$encodedShareText'; + } + } +} diff --git a/packages/share_repository/pubspec.yaml b/packages/share_repository/pubspec.yaml new file mode 100644 index 00000000..dc3d4e86 --- /dev/null +++ b/packages/share_repository/pubspec.yaml @@ -0,0 +1,13 @@ +name: share_repository +description: Repository to facilitate sharing scores. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dev_dependencies: + coverage: ^1.1.0 + mocktail: ^0.2.0 + test: ^1.19.2 + very_good_analysis: ^2.4.0 diff --git a/packages/share_repository/test/src/share_repository_test.dart b/packages/share_repository/test/src/share_repository_test.dart new file mode 100644 index 00000000..bdb2c517 --- /dev/null +++ b/packages/share_repository/test/src/share_repository_test.dart @@ -0,0 +1,41 @@ +// ignore_for_file: prefer_const_constructors +import 'package:share_repository/share_repository.dart'; +import 'package:test/test.dart'; + +void main() { + group('ShareRepository', () { + const appUrl = 'https://fakeurl.com/'; + late ShareRepository shareRepository; + + setUp(() { + shareRepository = ShareRepository(appUrl: appUrl); + }); + + test('can be instantiated', () { + expect(ShareRepository(appUrl: appUrl), isNotNull); + }); + + group('shareText', () { + const value = 'hello world!'; + test('returns the correct share url for twitter', () async { + const shareTextUrl = + 'https://twitter.com/intent/tweet?url=https%3A%2F%2Ffakeurl.com%2F&text=hello%20world!'; + final shareTextResult = shareRepository.shareText( + value: value, + platform: SharePlatform.twitter, + ); + expect(shareTextResult, equals(shareTextUrl)); + }); + + test('returns the correct share url for facebook', () async { + const shareTextUrl = + 'https://www.facebook.com/sharer.php?u=https%3A%2F%2Ffakeurl.com%2F"e=hello%20world!'; + final shareTextResult = shareRepository.shareText( + value: value, + platform: SharePlatform.facebook, + ); + expect(shareTextResult, equals(shareTextUrl)); + }); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index fc1e96a6..4a851209 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "31.0.0" + version: "39.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.8.0" + version: "4.0.0" args: dependency: transitive description: @@ -71,13 +71,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" clock: dependency: transitive description: @@ -196,7 +189,7 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" flame_audio: dependency: transitive description: @@ -214,10 +207,12 @@ packages: flame_forge2d: dependency: "direct main" description: - name: flame_forge2d - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.0" + path: "packages/flame_forge2d" + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + url: "https://github.com/flame-engine/flame/" + source: git + version: "0.11.0" flame_test: dependency: "direct dev" description: @@ -258,7 +253,7 @@ packages: name: forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.10.0" + version: "0.11.0" frontend_server_client: dependency: transitive description: @@ -321,7 +316,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: transitive description: @@ -356,7 +351,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -419,7 +414,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider: dependency: transitive description: @@ -483,6 +478,13 @@ packages: relative: true source: path version: "1.0.0+1" + pinball_flame: + dependency: "direct main" + description: + path: "packages/pinball_flame" + relative: true + source: path + version: "1.0.0+1" pinball_theme: dependency: "direct main" description: @@ -585,7 +587,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -627,21 +629,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.19.5" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.13" typed_data: dependency: transitive description: @@ -662,7 +664,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" very_good_analysis: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index d497e561..48c570c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,9 +10,13 @@ dependencies: bloc: ^8.0.2 cloud_firestore: ^3.1.10 equatable: ^2.0.3 - flame: ^1.1.0 + flame: ^1.1.1 flame_bloc: ^1.2.0 - flame_forge2d: ^0.10.0 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter flutter_bloc: ^8.0.1 @@ -27,6 +31,8 @@ dependencies: path: packages/pinball_audio pinball_components: path: packages/pinball_components + pinball_flame: + path: packages/pinball_flame pinball_theme: path: packages/pinball_theme @@ -45,6 +51,8 @@ flutter: assets: - assets/images/components/ + - assets/images/bonus_animation/ + - assets/images/score/ flutter_gen: line_length: 80 diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 01b5fea6..9fc79b5d 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -7,8 +7,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/app/app.dart'; -import 'package:pinball/landing/landing.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import '../../helpers/mocks.dart'; @@ -21,16 +22,18 @@ void main() { setUp(() { leaderboardRepository = MockLeaderboardRepository(); pinballAudio = MockPinballAudio(); + + when(pinballAudio.load).thenAnswer((_) => Future.value()); }); - testWidgets('renders LandingPage', (tester) async { + testWidgets('renders PinballGamePage', (tester) async { await tester.pumpWidget( App( leaderboardRepository: leaderboardRepository, pinballAudio: pinballAudio, ), ); - expect(find.byType(LandingPage), findsOneWidget); + expect(find.byType(PinballGamePage), findsOneWidget); }); }); } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index fb543814..37e14f73 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,8 +21,6 @@ void main() { const GameState( score: 0, balls: 2, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), ], @@ -41,15 +39,11 @@ void main() { const GameState( score: 2, balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 5, balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), ], @@ -69,158 +63,46 @@ void main() { const GameState( score: 0, balls: 2, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 1, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), ], ); }); - group('BonusLetterActivated', () { - blocTest( - 'adds the letter to the state', - build: GameBloc.new, - act: (bloc) => bloc - ..add(const BonusLetterActivated(0)) - ..add(const BonusLetterActivated(1)) - ..add(const BonusLetterActivated(2)), - expect: () => const [ - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0, 1], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0, 1, 2], - activatedDashNests: {}, - bonusHistory: [], - ), - ], - ); - - blocTest( - 'adds the bonus when the bonusWord is completed', - build: GameBloc.new, - act: (bloc) => bloc - ..add(const BonusLetterActivated(0)) - ..add(const BonusLetterActivated(1)) - ..add(const BonusLetterActivated(2)) - ..add(const BonusLetterActivated(3)) - ..add(const BonusLetterActivated(4)) - ..add(const BonusLetterActivated(5)), - expect: () => const [ - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0, 1], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0, 1, 2], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0, 1, 2, 3], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [0, 1, 2, 3, 4], - activatedDashNests: {}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.word], - ), - GameState( - score: GameBloc.bonusWordScore, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.word], - ), - ], - ); - }); - - group('DashNestActivated', () { - blocTest( - 'adds the bonus when all nests are activated', - build: GameBloc.new, - act: (bloc) => bloc - ..add(const DashNestActivated('0')) - ..add(const DashNestActivated('1')) - ..add(const DashNestActivated('2')), - expect: () => const [ - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {'0'}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {'0', '1'}, - bonusHistory: [], - ), - GameState( - score: 0, - balls: 4, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.dashNest], - ), - ], - ); - }); + group( + 'BonusActivated', + () { + blocTest( + 'adds bonus to history', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const BonusActivated(GameBonus.googleWord)) + ..add(const BonusActivated(GameBonus.dashNest)), + expect: () => const [ + GameState( + score: 0, + balls: 3, + bonusHistory: [GameBonus.googleWord], + ), + GameState( + score: 0, + balls: 3, + bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], + ), + ], + ); + }, + ); group('SparkyTurboChargeActivated', () { blocTest( @@ -231,8 +113,6 @@ void main() { GameState( score: 0, balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [GameBonus.sparkyTurboCharge], ), ], diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index 68530aae..d7d587bd 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -41,61 +41,34 @@ void main() { }); }); - group('BonusLetterActivated', () { + group('BonusActivated', () { test('can be instantiated', () { - expect(const BonusLetterActivated(0), isNotNull); + expect(const BonusActivated(GameBonus.dashNest), isNotNull); }); test('supports value equality', () { expect( - BonusLetterActivated(0), - equals(BonusLetterActivated(0)), + BonusActivated(GameBonus.googleWord), + equals(const BonusActivated(GameBonus.googleWord)), ); expect( - BonusLetterActivated(0), - isNot(equals(BonusLetterActivated(1))), + const BonusActivated(GameBonus.googleWord), + isNot(equals(const BonusActivated(GameBonus.dashNest))), ); }); - - test( - 'throws assertion error if index is bigger than the word length', - () { - expect( - () => BonusLetterActivated(8), - throwsAssertionError, - ); - }, - ); }); + }); - group('DashNestActivated', () { - test('can be instantiated', () { - expect(const DashNestActivated('0'), isNotNull); - }); - - test('supports value equality', () { - expect( - DashNestActivated('0'), - equals(DashNestActivated('0')), - ); - expect( - DashNestActivated('0'), - isNot(equals(DashNestActivated('1'))), - ); - }); + group('SparkyTurboChargeActivated', () { + test('can be instantiated', () { + expect(const SparkyTurboChargeActivated(), isNotNull); }); - group('SparkyTurboChargeActivated', () { - test('can be instantiated', () { - expect(const SparkyTurboChargeActivated(), isNotNull); - }); - - test('supports value equality', () { - expect( - SparkyTurboChargeActivated(), - equals(SparkyTurboChargeActivated()), - ); - }); + test('supports value equality', () { + expect( + SparkyTurboChargeActivated(), + equals(SparkyTurboChargeActivated()), + ); }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index ed80d192..8170346f 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -10,16 +10,12 @@ void main() { GameState( score: 0, balls: 0, - activatedBonusLetters: const [], - activatedDashNests: const {}, bonusHistory: const [], ), equals( const GameState( score: 0, balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), ), @@ -32,8 +28,6 @@ void main() { const GameState( score: 0, balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ), isNotNull, @@ -49,8 +43,6 @@ void main() { () => GameState( balls: -1, score: 0, - activatedBonusLetters: const [], - activatedDashNests: const {}, bonusHistory: const [], ), throwsAssertionError, @@ -66,8 +58,6 @@ void main() { () => GameState( balls: 0, score: -1, - activatedBonusLetters: const [], - activatedDashNests: const {}, bonusHistory: const [], ), throwsAssertionError, @@ -82,8 +72,6 @@ void main() { const gameState = GameState( balls: 0, score: 0, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isGameOver, isTrue); @@ -95,44 +83,12 @@ void main() { const gameState = GameState( balls: 1, score: 0, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); }); }); - group('isLetterActivated', () { - test( - 'is true when the letter is activated', - () { - const gameState = GameState( - balls: 3, - score: 0, - activatedBonusLetters: [1], - activatedDashNests: {}, - bonusHistory: [], - ); - expect(gameState.isLetterActivated(1), isTrue); - }, - ); - - test( - 'is false when the letter is not activated', - () { - const gameState = GameState( - balls: 3, - score: 0, - activatedBonusLetters: [1], - activatedDashNests: {}, - bonusHistory: [], - ); - expect(gameState.isLetterActivated(0), isFalse); - }, - ); - }); - group('copyWith', () { test( 'throws AssertionError ' @@ -141,8 +97,6 @@ void main() { const gameState = GameState( balls: 0, score: 2, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ); expect( @@ -159,8 +113,6 @@ void main() { const gameState = GameState( balls: 0, score: 2, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ); expect( @@ -177,16 +129,12 @@ void main() { const gameState = GameState( score: 2, balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, - activatedBonusLetters: const [0], - activatedDashNests: const {'1'}, - bonusHistory: const [GameBonus.word], + bonusHistory: const [GameBonus.googleWord], ); expect(gameState, isNot(equals(otherGameState))); @@ -194,8 +142,6 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, - activatedBonusLetters: otherGameState.activatedBonusLetters, - activatedDashNests: otherGameState.activatedDashNests, bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), diff --git a/test/game/components/alien_zone_test.dart b/test/game/components/alien_zone_test.dart new file mode 100644 index 00000000..7feaded8 --- /dev/null +++ b/test/game/components/alien_zone_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.alienBumper.a.active.keyName, + Assets.images.alienBumper.a.inactive.keyName, + Assets.images.alienBumper.b.active.keyName, + Assets.images.alienBumper.b.inactive.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('AlienZone', () { + flameTester.test( + 'loads correctly', + (game) async { + await game.addFromBlueprint(AlienZone()); + await game.ready(); + }, + ); + + group('loads', () { + flameTester.test( + 'two AlienBumper', + (game) async { + final alienZone = AlienZone(); + await game.addFromBlueprint(alienZone); + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(2), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 9f2a5260..a73d7a50 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -9,7 +9,26 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final assets = [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + Assets.images.dash.animatronic.keyName, + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); group('Board', () { flameTester.test( diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart deleted file mode 100644 index f01fced9..00000000 --- a/test/game/components/bonus_word_test.dart +++ /dev/null @@ -1,376 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/effects.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_bloc/flutter_bloc.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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); - - group('BonusWord', () { - flameTester.test( - 'loads the letters correctly', - (game) async { - final bonusWord = BonusWord( - position: Vector2.zero(), - ); - await game.ensureAdd(bonusWord); - - final letters = bonusWord.descendants().whereType(); - expect(letters.length, equals(GameBloc.bonusWord.length)); - }, - ); - - group('listenWhen', () { - final previousState = MockGameState(); - final currentState = MockGameState(); - - test( - 'returns true when there is a new word bonus awarded', - () { - when(() => previousState.bonusHistory).thenReturn([]); - when(() => currentState.bonusHistory).thenReturn([GameBonus.word]); - - expect( - BonusWord(position: Vector2.zero()).listenWhen( - previousState, - currentState, - ), - isTrue, - ); - }, - ); - - test( - 'returns false when there is no new word bonus awarded', - () { - when(() => previousState.bonusHistory).thenReturn([GameBonus.word]); - when(() => currentState.bonusHistory).thenReturn([GameBonus.word]); - - expect( - BonusWord(position: Vector2.zero()).listenWhen( - previousState, - currentState, - ), - isFalse, - ); - }, - ); - }); - - group('onNewState', () { - final state = MockGameState(); - flameTester.test( - 'adds sequence effect to the letters when the player receives a bonus', - (game) async { - when(() => state.bonusHistory).thenReturn([GameBonus.word]); - - final bonusWord = BonusWord(position: Vector2.zero()); - await game.ensureAdd(bonusWord); - await game.ready(); - - bonusWord.onNewState(state); - game.update(0); // Run one frame so the effects are added - - final letters = bonusWord.children.whereType(); - expect(letters.length, equals(GameBloc.bonusWord.length)); - - for (final letter in letters) { - expect( - letter.children.whereType().length, - equals(1), - ); - } - }, - ); - - flameTester.test( - 'plays the google bonus sound', - (game) async { - when(() => state.bonusHistory).thenReturn([GameBonus.word]); - - final bonusWord = BonusWord(position: Vector2.zero()); - await game.ensureAdd(bonusWord); - await game.ready(); - - bonusWord.onNewState(state); - - verify(bonusWord.gameRef.audio.googleBonus).called(1); - }, - ); - - flameTester.test( - 'adds a color effect to reset the color when the sequence is finished', - (game) async { - when(() => state.bonusHistory).thenReturn([GameBonus.word]); - - final bonusWord = BonusWord(position: Vector2.zero()); - await game.ensureAdd(bonusWord); - await game.ready(); - - bonusWord.onNewState(state); - // Run the amount of time necessary for the animation to finish - game.update(3); - game.update(0); // Run one additional frame so the effects are added - - final letters = bonusWord.children.whereType(); - expect(letters.length, equals(GameBloc.bonusWord.length)); - - for (final letter in letters) { - expect( - letter.children.whereType().length, - equals(1), - ); - } - }, - ); - }); - }); - - group('BonusLetter', () { - final flameTester = FlameTester(EmptyPinballGameTest.new); - - flameTester.test( - 'loads correctly', - (game) async { - final bonusLetter = BonusLetter( - letter: 'G', - index: 0, - ); - await game.ensureAdd(bonusLetter); - await game.ready(); - - expect(game.contains(bonusLetter), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'is static', - (game) async { - final bonusLetter = BonusLetter( - letter: 'G', - index: 0, - ); - await game.ensureAdd(bonusLetter); - - expect(bonusLetter.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final bonusLetter = BonusLetter( - letter: 'G', - index: 0, - ); - await game.ensureAdd(bonusLetter); - - expect(bonusLetter.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'is sensor', - (game) async { - final bonusLetter = BonusLetter( - letter: 'G', - index: 0, - ); - await game.ensureAdd(bonusLetter); - - final fixture = bonusLetter.body.fixtures[0]; - expect(fixture.isSensor, isTrue); - }, - ); - - flameTester.test( - 'shape is circular', - (game) async { - final bonusLetter = BonusLetter( - letter: 'G', - index: 0, - ); - await game.ensureAdd(bonusLetter); - - final fixture = bonusLetter.body.fixtures[0]; - expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(1.85)); - }, - ); - }); - - group('bonus letter activation', () { - late GameBloc gameBloc; - late PinballAudio pinballAudio; - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, - blocBuilder: () => gameBloc, - repositories: () => [ - RepositoryProvider.value(value: pinballAudio), - ], - ); - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - - pinballAudio = MockPinballAudio(); - when(pinballAudio.googleBonus).thenAnswer((_) {}); - }); - - flameBlocTester.testGameWidget( - 'adds BonusLetterActivated to GameBloc when not activated', - setUp: (game, tester) async { - final bonusWord = BonusWord( - position: Vector2.zero(), - ); - await game.ensureAdd(bonusWord); - - final bonusLetters = - game.descendants().whereType().toList(); - for (var index = 0; index < bonusLetters.length; index++) { - final bonusLetter = bonusLetters[index]; - bonusLetter.activate(); - await game.ready(); - - verify(() => gameBloc.add(BonusLetterActivated(index))).called(1); - } - }, - ); - - flameBlocTester.testGameWidget( - "doesn't add BonusLetterActivated to GameBloc when already activated", - setUp: (game, tester) async { - const state = GameState( - score: 0, - balls: 2, - activatedBonusLetters: [0], - activatedDashNests: {}, - bonusHistory: [], - ); - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); - - final bonusLetter = BonusLetter(letter: '', index: 0); - await game.add(bonusLetter); - await game.ready(); - - bonusLetter.activate(); - await game.ready(); - }, - verify: (game, tester) async { - verifyNever(() => gameBloc.add(const BonusLetterActivated(0))); - }, - ); - - flameBlocTester.testGameWidget( - 'adds a ColorEffect', - setUp: (game, tester) async { - const state = GameState( - score: 0, - balls: 2, - activatedBonusLetters: [0], - activatedDashNests: {}, - bonusHistory: [], - ); - - final bonusLetter = BonusLetter(letter: '', index: 0); - await game.add(bonusLetter); - await game.ready(); - - bonusLetter.activate(); - - bonusLetter.onNewState(state); - await tester.pump(); - }, - verify: (game, tester) async { - // TODO(aleastiago): Look into making `testGameWidget` pass the - // subject. - final bonusLetter = game.descendants().whereType().last; - expect( - bonusLetter.children.whereType().length, - equals(1), - ); - }, - ); - - flameBlocTester.testGameWidget( - 'listens when there is a change on the letter status', - setUp: (game, tester) async { - final bonusWord = BonusWord( - position: Vector2.zero(), - ); - await game.ensureAdd(bonusWord); - - final bonusLetters = - game.descendants().whereType().toList(); - for (var index = 0; index < bonusLetters.length; index++) { - final bonusLetter = bonusLetters[index]; - bonusLetter.activate(); - await game.ready(); - - final state = GameState( - score: 0, - balls: 2, - activatedBonusLetters: [index], - activatedDashNests: const {}, - bonusHistory: const [], - ); - - expect( - bonusLetter.listenWhen(const GameState.initial(), state), - isTrue, - ); - } - }, - ); - }); - - group('BonusLetterBallContactCallback', () { - test('calls ball.activate', () { - final ball = MockBall(); - final bonusLetter = MockBonusLetter(); - final contactCallback = BonusLetterBallContactCallback(); - - when(() => bonusLetter.isEnabled).thenReturn(true); - - contactCallback.begin(ball, bonusLetter, MockContact()); - - verify(bonusLetter.activate).called(1); - }); - - test("doesn't call ball.activate when letter is disabled", () { - final ball = MockBall(); - final bonusLetter = MockBonusLetter(); - final contactCallback = BonusLetterBallContactCallback(); - - when(() => bonusLetter.isEnabled).thenReturn(false); - - contactCallback.begin(ball, bonusLetter, MockContact()); - - verifyNever(bonusLetter.activate); - }); - }); - }); -} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 41a1cdca..e615d508 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -41,7 +41,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); @@ -94,6 +94,7 @@ void main() { final controller = WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); + when(() => ball.boost(any())).thenAnswer((_) async {}); await controller.turboCharge(); @@ -109,6 +110,7 @@ void main() { final controller = WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); + when(() => ball.boost(any())).thenAnswer((_) async {}); await controller.turboCharge(); @@ -124,6 +126,7 @@ void main() { final controller = WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); + when(() => ball.boost(any())).thenAnswer((_) async {}); await controller.turboCharge(); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 03c51830..2f970254 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,7 +11,28 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final assets = [ + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () { + final bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + assets: assets, + ); group('FlipperController', () { group('onKeyEvent', () { @@ -42,46 +64,45 @@ void main() { await game.add(flipper); controller.onKeyEvent(event, {}); - expect(flipper.body.linearVelocity.y, isPositive); + expect(flipper.body.linearVelocity.y, isNegative); expect(flipper.body.linearVelocity.x, isZero); }, ); }); - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - await game.ready(); - await game.add(flipper); + testRawKeyDownEvents(leftKeys, (event) { + flameBlocTester.testGameWidget( + 'does nothing when is game over', + setUp: (game, tester) async { + await game.ensureAdd(flipper); controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); + }, + verify: (game, tester) async { + expect(flipper.body.linearVelocity.y, isZero); expect(flipper.body.linearVelocity.x, isZero); }, ); }); - testRawKeyUpEvents(rightKeys, (event) { + testRawKeyUpEvents(leftKeys, (event) { flameTester.test( - 'does nothing ' + 'moves downwards ' 'when ${event.logicalKey.keyLabel} is released', (game) async { await game.ready(); await game.add(flipper); controller.onKeyEvent(event, {}); - expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.y, isPositive); expect(flipper.body.linearVelocity.x, isZero); }, ); }); - testRawKeyDownEvents(rightKeys, (event) { + testRawKeyUpEvents(rightKeys, (event) { flameTester.test( 'does nothing ' - 'when ${event.logicalKey.keyLabel} is pressed', + 'when ${event.logicalKey.keyLabel} is released', (game) async { await game.ready(); await game.add(flipper); @@ -113,7 +134,7 @@ void main() { await game.add(flipper); controller.onKeyEvent(event, {}); - expect(flipper.body.linearVelocity.y, isPositive); + expect(flipper.body.linearVelocity.y, isNegative); expect(flipper.body.linearVelocity.x, isZero); }, ); @@ -128,31 +149,30 @@ void main() { await game.add(flipper); controller.onKeyEvent(event, {}); - expect(flipper.body.linearVelocity.y, isNegative); + expect(flipper.body.linearVelocity.y, isPositive); expect(flipper.body.linearVelocity.x, isZero); }, ); }); - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - await game.ready(); - await game.add(flipper); + testRawKeyDownEvents(rightKeys, (event) { + flameBlocTester.testGameWidget( + 'does nothing when is game over', + setUp: (game, tester) async { + await game.ensureAdd(flipper); controller.onKeyEvent(event, {}); - + }, + verify: (game, tester) async { expect(flipper.body.linearVelocity.y, isZero); expect(flipper.body.linearVelocity.x, isZero); }, ); }); - testRawKeyDownEvents(leftKeys, (event) { + testRawKeyUpEvents(leftKeys, (event) { flameTester.test( 'does nothing ' - 'when ${event.logicalKey.keyLabel} is pressed', + 'when ${event.logicalKey.keyLabel} is released', (game) async { await game.ready(); await game.add(flipper); diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart new file mode 100644 index 00000000..eee2bcb0 --- /dev/null +++ b/test/game/components/controlled_plunger_test.dart @@ -0,0 +1,107 @@ +import 'dart:collection'; + +import 'package:bloc_test/bloc_test.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:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(EmptyPinballTestGame.new); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () { + final bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); + + group('PlungerController', () { + group('onKeyEvent', () { + final downKeys = UnmodifiableListView([ + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.space, + LogicalKeyboardKey.keyS, + ]); + + late Plunger plunger; + late PlungerController controller; + + setUp(() { + plunger = Plunger(compressionDistance: 10); + controller = PlungerController(plunger); + plunger.add(controller); + }); + + testRawKeyDownEvents(downKeys, (event) { + flameTester.test( + 'moves down ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ensureAdd(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.ensureAdd(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.ensureAdd(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 { + await game.ensureAdd(plunger); + controller.onKeyEvent(event, {}); + }, + verify: (game, tester) async { + expect(plunger.body.linearVelocity.y, isZero); + expect(plunger.body.linearVelocity.x, isZero); + }, + ); + }); + }); + }); +} diff --git a/test/game/components/controlled_sparky_computer_test.dart b/test/game/components/controlled_sparky_computer_test.dart deleted file mode 100644 index a3e13486..00000000 --- a/test/game/components/controlled_sparky_computer_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('SparkyComputerController', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); - - late ControlledSparkyComputer controlledSparkyComputer; - - setUp(() { - controlledSparkyComputer = ControlledSparkyComputer(); - }); - - test('can be instantiated', () { - expect( - SparkyComputerController(controlledSparkyComputer), - isA(), - ); - }); - - flameTester.testGameWidget( - 'SparkyTurboChargeSensorBallContactCallback turbo charges the ball', - setUp: (game, tester) async { - final contackCallback = SparkyTurboChargeSensorBallContactCallback(); - final sparkyTurboChargeSensor = MockSparkyTurboChargeSensor(); - final ball = MockControlledBall(); - final controller = MockBallController(); - - when(() => ball.controller).thenReturn(controller); - when(controller.turboCharge).thenAnswer((_) async {}); - - contackCallback.begin(sparkyTurboChargeSensor, ball, MockContact()); - - verify(() => ball.controller.turboCharge()).called(1); - }, - ); - }); -} diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart new file mode 100644 index 00000000..c1834516 --- /dev/null +++ b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart @@ -0,0 +1,84 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + group('FlutterForestBonusBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.dashNest to the game when all bumpers are active', + setUp: (game, tester) async { + final behavior = FlutterForestBonusBehavior(); + final parent = FlutterForest.test(); + final bumpers = [ + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + ]; + await parent.addAll(bumpers); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final bumper in bumpers) { + bumper.bloc.onBallContacted(); + } + await tester.pump(); + + verify( + () => gameBloc.add(const BonusActivated(GameBonus.dashNest)), + ).called(1); + }, + ); + + flameBlocTester.testGameWidget( + 'adds a new ball to the game when all bumpers are active', + setUp: (game, tester) async { + final behavior = FlutterForestBonusBehavior(); + final parent = FlutterForest.test(); + final bumpers = [ + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + ]; + await parent.addAll(bumpers); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final bumper in bumpers) { + bumper.bloc.onBallContacted(); + } + await game.ready(); + + expect( + game.descendants().whereType().single, + isNotNull, + ); + }, + ); + }); +} diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart new file mode 100644 index 00000000..4f32e0f4 --- /dev/null +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -0,0 +1,80 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + Assets.images.dash.animatronic.keyName, + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('FlutterForest', () { + flameTester.test( + 'loads correctly', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + expect(game.contains(flutterForest), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'a Signpost', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a DashAnimatronic', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.firstChild(), + isNotNull, + ); + }, + ); + + flameTester.test( + 'three DashNestBumper', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(3), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart deleted file mode 100644 index d85fe54f..00000000 --- a/test/game/components/flutter_forest_test.dart +++ /dev/null @@ -1,299 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); - - group('FlutterForest', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect(game.contains(flutterForest), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'a FlutterSignPost', - (game) async { - await game.ready(); - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect( - flutterForest.descendants().whereType().length, - equals(1), - ); - }, - ); - - flameTester.test( - 'a BigDashNestBumper', - (game) async { - await game.ready(); - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect( - flutterForest.descendants().whereType().length, - equals(1), - ); - }, - ); - - flameTester.test( - 'two SmallDashNestBumper', - (game) async { - await game.ready(); - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect( - flutterForest.descendants().whereType().length, - equals(2), - ); - }, - ); - }); - - group('controller', () { - group('listenWhen', () { - final gameBloc = MockGameBloc(); - final flameBlocTester = FlameBlocTester( - gameBuilder: TestGame.new, - blocBuilder: () => gameBloc, - ); - - flameBlocTester.testGameWidget( - 'listens when a Bonus.dashNest and a bonusBall is added', - verify: (game, tester) async { - final flutterForest = FlutterForest(); - - const state = GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.dashNest], - ); - - expect( - flutterForest.controller - .listenWhen(const GameState.initial(), state), - isTrue, - ); - }, - ); - }); - }); - - flameTester.test( - 'onNewState adds a new ball', - (game) async { - final flutterForest = FlutterForest(); - await game.ready(); - await game.ensureAdd(flutterForest); - - final previousBalls = game.descendants().whereType().length; - flutterForest.controller.onNewState(MockGameState()); - await game.ready(); - - expect( - game.descendants().whereType().length, - greaterThan(previousBalls), - ); - }, - ); - - group('bumpers', () { - late Ball ball; - late GameBloc gameBloc; - - setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, - blocBuilder: () => gameBloc, - ); - - flameBlocTester.testGameWidget( - 'add DashNestActivated event', - setUp: (game, tester) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - await game.ensureAdd(ball); - - final bumpers = - flutterForest.descendants().whereType(); - - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - final controller = bumper.firstChild()!; - verify( - () => gameBloc.add(DashNestActivated(controller.id)), - ).called(1); - } - }, - ); - - flameBlocTester.testGameWidget( - 'add Scored event', - setUp: (game, tester) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - await game.ensureAdd(ball); - game.addContactCallback(BallScorePointsCallback(game)); - - final bumpers = flutterForest.descendants().whereType(); - - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - verify( - () => gameBloc.add( - Scored(points: bumper.points), - ), - ).called(1); - } - }, - ); - }); - }); - - group('DashNestBumperController', () { - late DashNestBumper dashNestBumper; - - setUp(() { - dashNestBumper = MockDashNestBumper(); - }); - - group( - 'listensWhen', - () { - late GameState previousState; - late GameState newState; - - setUp( - () { - previousState = MockGameState(); - newState = MockGameState(); - }, - ); - - test('listens when the id is added to activatedDashNests', () { - const id = ''; - final controller = DashNestBumperController( - dashNestBumper, - id: id, - ); - - when(() => previousState.activatedDashNests).thenReturn({}); - when(() => newState.activatedDashNests).thenReturn({id}); - - expect(controller.listenWhen(previousState, newState), isTrue); - }); - - test('listens when the id is removed from activatedDashNests', () { - const id = ''; - final controller = DashNestBumperController( - dashNestBumper, - id: id, - ); - - when(() => previousState.activatedDashNests).thenReturn({id}); - when(() => newState.activatedDashNests).thenReturn({}); - - expect(controller.listenWhen(previousState, newState), isTrue); - }); - - test("doesn't listen when the id is never in activatedDashNests", () { - final controller = DashNestBumperController( - dashNestBumper, - id: '', - ); - - when(() => previousState.activatedDashNests).thenReturn({}); - when(() => newState.activatedDashNests).thenReturn({}); - - expect(controller.listenWhen(previousState, newState), isFalse); - }); - - test("doesn't listen when the id still in activatedDashNests", () { - const id = ''; - final controller = DashNestBumperController( - dashNestBumper, - id: id, - ); - - when(() => previousState.activatedDashNests).thenReturn({id}); - when(() => newState.activatedDashNests).thenReturn({id}); - - expect(controller.listenWhen(previousState, newState), isFalse); - }); - }, - ); - - group( - 'onNewState', - () { - late GameState state; - - setUp(() { - state = MockGameState(); - }); - - test( - 'activates the bumper when id in activatedDashNests', - () { - const id = ''; - final controller = DashNestBumperController( - dashNestBumper, - id: id, - ); - - when(() => state.activatedDashNests).thenReturn({id}); - controller.onNewState(state); - - verify(() => dashNestBumper.activate()).called(1); - }, - ); - - test( - 'deactivates the bumper when id not in activatedDashNests', - () { - final controller = DashNestBumperController( - dashNestBumper, - id: '', - ); - - when(() => state.activatedDashNests).thenReturn({}); - controller.onNewState(state); - - verify(() => dashNestBumper.deactivate()).called(1); - }, - ); - }, - ); - }); -} diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index dc1d9ab8..3de04b90 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart'; import '../../helpers/helpers.dart'; @@ -15,9 +16,7 @@ void main() { final state = GameState( score: 10, balls: 0, - activatedBonusLetters: const [], bonusHistory: const [], - activatedDashNests: const {}, ); final previous = GameState.initial(); @@ -42,7 +41,13 @@ void main() { gameFlowController = GameFlowController(game); overlays = MockActiveOverlaysNotifier(); - when(backboard.gameOverMode).thenAnswer((_) async {}); + when( + () => backboard.gameOverMode( + score: any(named: 'score'), + characterIconPath: any(named: 'characterIconPath'), + onSubmit: any(named: 'onSubmit'), + ), + ).thenAnswer((_) async {}); when(backboard.waitingMode).thenAnswer((_) async {}); when(cameraController.focusOnBackboard).thenAnswer((_) async {}); when(cameraController.focusOnGame).thenAnswer((_) async {}); @@ -52,6 +57,7 @@ void main() { when(game.firstChild).thenReturn(backboard); when(game.firstChild).thenReturn(cameraController); when(() => game.overlays).thenReturn(overlays); + when(() => game.characterTheme).thenReturn(DashTheme()); }); test( @@ -61,13 +67,17 @@ void main() { GameState( score: 10, balls: 0, - activatedBonusLetters: const [], bonusHistory: const [], - activatedDashNests: const {}, ), ); - verify(backboard.gameOverMode).called(1); + verify( + () => backboard.gameOverMode( + score: 0, + characterIconPath: any(named: 'characterIconPath'), + onSubmit: any(named: 'onSubmit'), + ), + ).called(1); verify(cameraController.focusOnBackboard).called(1); }, ); diff --git a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart new file mode 100644 index 00000000..deca61ee --- /dev/null +++ b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart @@ -0,0 +1,61 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('GoogleWordBonusBehaviors', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.googleWord to the game when all letters are activated', + setUp: (game, tester) async { + final behavior = GoogleWordBonusBehavior(); + final parent = GoogleWord.test(); + final letters = [ + GoogleLetter(0), + GoogleLetter(1), + GoogleLetter(2), + GoogleLetter(3), + GoogleLetter(4), + GoogleLetter(5), + ]; + await parent.addAll(letters); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final letter in letters) { + letter.bloc.onBallContacted(); + } + await tester.pump(); + + verify( + () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), + ).called(1); + }, + ); + }); +} diff --git a/test/game/components/google_word/google_word_test.dart b/test/game/components/google_word/google_word_test.dart new file mode 100644 index 00000000..2d7d04e5 --- /dev/null +++ b/test/game/components/google_word/google_word_test.dart @@ -0,0 +1,26 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(EmptyPinballTestGame.new); + + group('GoogleWord', () { + flameTester.test( + 'loads the letters correctly', + (game) async { + const word = 'Google'; + final googleWord = GoogleWord(position: Vector2.zero()); + await game.ensureAdd(googleWord); + + final letters = googleWord.children.whereType(); + expect(letters.length, equals(word.length)); + }, + ); + }); +} diff --git a/test/game/components/score_effect_controller_test.dart b/test/game/components/score_effect_controller_test.dart deleted file mode 100644 index 241f040b..00000000 --- a/test/game/components/score_effect_controller_test.dart +++ /dev/null @@ -1,115 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('ScoreEffectController', () { - late ScoreEffectController controller; - late PinballGame game; - - setUpAll(() { - registerFallbackValue(Component()); - }); - - setUp(() { - game = MockPinballGame(); - when(() => game.add(any())).thenAnswer((_) async {}); - - controller = ScoreEffectController(game); - }); - - group('listenWhen', () { - test('returns true when the user has earned points', () { - const previous = GameState.initial(); - const current = GameState( - score: 10, - balls: 3, - activatedBonusLetters: [], - bonusHistory: [], - activatedDashNests: {}, - ); - expect(controller.listenWhen(previous, current), isTrue); - }); - - test( - 'returns true when the user has earned points and there was no ' - 'previous state', - () { - const current = GameState( - score: 10, - balls: 3, - activatedBonusLetters: [], - bonusHistory: [], - activatedDashNests: {}, - ); - expect(controller.listenWhen(null, current), isTrue); - }, - ); - - test( - 'returns false when no points were earned', - () { - const current = GameState.initial(); - const previous = GameState.initial(); - expect(controller.listenWhen(previous, current), isFalse); - }, - ); - }); - - group('onNewState', () { - test( - 'adds a ScoreText with the correct score for the ' - 'first time', - () { - const state = GameState( - score: 10, - balls: 3, - activatedBonusLetters: [], - bonusHistory: [], - activatedDashNests: {}, - ); - - controller.onNewState(state); - - final effect = - verify(() => game.add(captureAny())).captured.first as ScoreText; - - expect(effect.text, equals('10')); - }, - ); - - test('adds a ScoreTextEffect with the correct score', () { - controller.onNewState( - const GameState( - score: 10, - balls: 3, - activatedBonusLetters: [], - bonusHistory: [], - activatedDashNests: {}, - ), - ); - - controller.onNewState( - const GameState( - score: 14, - balls: 3, - activatedBonusLetters: [], - bonusHistory: [], - activatedDashNests: {}, - ), - ); - - final effect = - verify(() => game.add(captureAny())).captured.last as ScoreText; - - expect(effect.text, equals('4')); - }); - }); - }); -} diff --git a/test/game/components/score_points_test.dart b/test/game/components/score_points_test.dart deleted file mode 100644 index 8317f20c..00000000 --- a/test/game/components/score_points_test.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.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 '../../helpers/helpers.dart'; - -class FakeScorePoints extends BodyComponent with ScorePoints { - @override - Body createBody() { - throw UnimplementedError(); - } - - @override - int get points => 2; -} - -void main() { - group('BallScorePointsCallback', () { - late PinballGame game; - late GameBloc bloc; - late PinballAudio audio; - late Ball ball; - late FakeScorePoints fakeScorePoints; - - setUp(() { - game = MockPinballGame(); - bloc = MockGameBloc(); - ball = MockBall(); - audio = MockPinballAudio(); - fakeScorePoints = FakeScorePoints(); - }); - - setUpAll(() { - registerFallbackValue(FakeGameEvent()); - }); - - group('begin', () { - test( - 'emits Scored event with points', - () { - when(game.read).thenReturn(bloc); - when(() => game.audio).thenReturn(audio); - - BallScorePointsCallback(game).begin( - ball, - fakeScorePoints, - FakeContact(), - ); - - verify( - () => bloc.add( - Scored(points: fakeScorePoints.points), - ), - ).called(1); - }, - ); - - test( - 'plays a Score sound', - () { - when(game.read).thenReturn(bloc); - when(() => game.audio).thenReturn(audio); - - BallScorePointsCallback(game).begin( - ball, - fakeScorePoints, - FakeContact(), - ); - - verify(audio.score).called(1); - }, - ); - }); - }); -} diff --git a/test/game/components/scoring_behavior_test.dart b/test/game/components/scoring_behavior_test.dart new file mode 100644 index 00000000..d5e706b0 --- /dev/null +++ b/test/game/components/scoring_behavior_test.dart @@ -0,0 +1,111 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody(BodyDef()); +} + +void main() { + group('ScoringBehavior', () { + group('beginContact', () { + late GameBloc bloc; + late PinballAudio audio; + late Ball ball; + late BodyComponent parent; + + setUp(() { + audio = MockPinballAudio(); + + ball = MockBall(); + final ballBody = MockBody(); + when(() => ball.body).thenReturn(ballBody); + when(() => ballBody.position).thenReturn(Vector2.all(4)); + + parent = _TestBodyComponent(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: () => EmptyPinballTestGame( + audio: audio, + ), + blocBuilder: () { + bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); + + flameBlocTester.testGameWidget( + 'emits Scored event with points', + setUp: (game, tester) async { + const points = 20; + final scoringBehavior = ScoringBehavior(points: points); + await parent.add(scoringBehavior); + await game.ensureAdd(parent); + + scoringBehavior.beginContact(ball, MockContact()); + + verify( + () => bloc.add( + const Scored(points: points), + ), + ).called(1); + }, + ); + + flameBlocTester.testGameWidget( + 'plays score sound', + setUp: (game, tester) async { + const points = 20; + final scoringBehavior = ScoringBehavior(points: points); + await parent.add(scoringBehavior); + await game.ensureAdd(parent); + + scoringBehavior.beginContact(ball, MockContact()); + + verify(audio.score).called(1); + }, + ); + + flameBlocTester.testGameWidget( + "adds a ScoreText component at Ball's position with points", + setUp: (game, tester) async { + const points = 20; + final scoringBehavior = ScoringBehavior(points: points); + await parent.add(scoringBehavior); + await game.ensureAdd(parent); + + scoringBehavior.beginContact(ball, MockContact()); + await game.ready(); + + final scoreText = game.descendants().whereType(); + expect(scoreText.length, equals(1)); + expect( + scoreText.first.text, + equals(points.toString()), + ); + expect( + scoreText.first.position, + equals(ball.body.position), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart index da8d8404..9b254617 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_fire_zone_test.dart @@ -1,115 +1,110 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - -import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final assets = [ + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.animatronic.keyName, + ]; + + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); group('SparkyFireZone', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); - - expect(game.contains(sparkyFireZone), isTrue); - }, - ); + flameTester.test('loads correctly', (game) async { + await game.addFromBlueprint(SparkyFireZone()); + await game.ready(); + }); group('loads', () { flameTester.test( - 'three SparkyBumper', + 'a SparkyComputer', + (game) async { + expect( + SparkyFireZone().blueprints.whereType().single, + isNotNull, + ); + }, + ); + + flameTester.test( + 'a SparkyAnimatronic', (game) async { + final sparkyFireZone = SparkyFireZone(); + await game.addFromBlueprint(sparkyFireZone); await game.ready(); + + expect( + game.descendants().whereType().single, + isNotNull, + ); + }, + ); + + flameTester.test( + 'three SparkyBumper', + (game) async { final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); + await game.addFromBlueprint(sparkyFireZone); + await game.ready(); expect( - sparkyFireZone.descendants().whereType().length, + game.descendants().whereType().length, equals(3), ); }, ); }); + }); - group('bumpers', () { - late ControlledSparkyBumper controlledSparkyBumper; - late Ball ball; - late GameBloc gameBloc; - - setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, - blocBuilder: () => gameBloc, - ); + group('SparkyComputerSensor', () { + flameTester.test('calls turboCharge', (game) async { + final sensor = SparkyComputerSensor(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + when(controller.turboCharge).thenAnswer((_) async {}); - flameTester.testGameWidget( - 'activate when deactivated bumper is hit', - setUp: (game, tester) async { - controlledSparkyBumper = ControlledSparkyBumper.a(); - await game.ensureAdd(controlledSparkyBumper); + await game.ensureAddAll([ + sensor, + SparkyAnimatronic(), + ]); - controlledSparkyBumper.controller.hit(); - }, - verify: (game, tester) async { - expect(controlledSparkyBumper.controller.isActivated, isTrue); - }, - ); + sensor.beginContact(ball, MockContact()); - flameTester.testGameWidget( - 'deactivate when activated bumper is hit', - setUp: (game, tester) async { - controlledSparkyBumper = ControlledSparkyBumper.a(); - await game.ensureAdd(controlledSparkyBumper); - - controlledSparkyBumper.controller.hit(); - controlledSparkyBumper.controller.hit(); - }, - verify: (game, tester) async { - expect(controlledSparkyBumper.controller.isActivated, isFalse); - }, - ); + verify(() => ball.controller.turboCharge()).called(1); + }); - flameBlocTester.testGameWidget( - 'add Scored event', - setUp: (game, tester) async { - final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); - await game.ensureAdd(ball); - game.addContactCallback(BallScorePointsCallback(game)); - - final bumpers = sparkyFireZone.descendants().whereType(); - - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - verify( - () => gameBloc.add( - Scored(points: bumper.points), - ), - ).called(1); - } - }, - ); + flameTester.test('plays SparkyAnimatronic', (game) async { + final sensor = SparkyComputerSensor(); + final sparkyAnimatronic = SparkyAnimatronic(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + when(controller.turboCharge).thenAnswer((_) async {}); + await game.ensureAddAll([ + sensor, + sparkyAnimatronic, + ]); + + expect(sparkyAnimatronic.playing, isFalse); + sensor.beginContact(ball, MockContact()); + expect(sparkyAnimatronic.playing, isTrue); }); }); } diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index f8e7483c..2905ab9a 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -9,7 +9,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); group('Wall', () { flameTester.test( @@ -110,17 +110,18 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); flameBlocTester.testGameWidget( 'when ball is launch', setUp: (game, tester) async { - final ball = ControlledBall.launch(theme: game.theme); + final ball = ControlledBall.launch( + characterTheme: game.characterTheme, + ); final wall = BottomWall(); await game.ensureAddAll([ball, wall]); - game.addContactCallback(BottomWallBallContactCallback()); beginContact(game, ball, wall); await game.ready(); @@ -132,10 +133,11 @@ void main() { flameBlocTester.testGameWidget( 'when ball is bonus', setUp: (game, tester) async { - final ball = ControlledBall.bonus(theme: game.theme); + final ball = ControlledBall.bonus( + characterTheme: game.characterTheme, + ); final wall = BottomWall(); await game.ensureAddAll([ball, wall]); - game.addContactCallback(BottomWallBallContactCallback()); beginContact(game, ball, wall); await game.ready(); @@ -150,7 +152,6 @@ void main() { final ball = ControlledBall.debug(); final wall = BottomWall(); await game.ensureAddAll([ball, wall]); - game.addContactCallback(BottomWallBallContactCallback()); beginContact(game, ball, wall); await game.ready(); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2dfd5d76..b265c0aa 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -12,25 +12,62 @@ import '../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.new); - final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); + final assets = [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + Assets.images.dash.animatronic.keyName, + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + Assets.images.alienBumper.a.active.keyName, + Assets.images.alienBumper.a.inactive.keyName, + Assets.images.alienBumper.b.active.keyName, + Assets.images.alienBumper.b.inactive.keyName, + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.spaceship.ramp.boardOpening.keyName, + Assets.images.spaceship.ramp.railingForeground.keyName, + Assets.images.spaceship.ramp.railingBackground.keyName, + Assets.images.spaceship.ramp.main.keyName, + Assets.images.spaceship.ramp.arrow.inactive.keyName, + Assets.images.spaceship.ramp.arrow.active1.keyName, + Assets.images.spaceship.ramp.arrow.active2.keyName, + Assets.images.spaceship.ramp.arrow.active3.keyName, + Assets.images.spaceship.ramp.arrow.active4.keyName, + Assets.images.spaceship.ramp.arrow.active5.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.boundary.bottom.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, + ]; + final flameTester = FlameTester( + () => PinballTestGame(assets: assets), + ); + final debugModeFlameTester = FlameTester( + () => DebugPinballTestGame(assets: assets), + ); group('PinballGame', () { - // TODO(alestiago): test if [PinballGame] registers - // [BallScorePointsCallback] once the following issue is resolved: - // https://github.com/flame-engine/flame/issues/1416 group('components', () { - flameTester.test( - 'has three Walls', - (game) async { - await game.ready(); - final walls = game.children.where( - (component) => component is Wall && component is! BottomWall, - ); - expect(walls.length, 3); - }, - ); - + // TODO(alestiago): tests that Blueprints get added once the Blueprint + // class is removed. flameTester.test( 'has only one BottomWall', (game) async { @@ -62,14 +99,6 @@ void main() { ); }); - flameTester.test( - 'one SparkyFireZone', - (game) async { - await game.ready(); - expect(game.children.whereType().length, equals(1)); - }, - ); - group('controller', () { // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () { @@ -80,8 +109,9 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + // assets: assets, ); flameBlocTester.testGameWidget( @@ -198,8 +228,9 @@ void main() { final debugModeFlameBlocTester = FlameBlocTester( - gameBuilder: DebugPinballGameTest.new, + gameBuilder: DebugPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); debugModeFlameBlocTester.testGameWidget( diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart deleted file mode 100644 index 953b89eb..00000000 --- a/test/game/view/game_hud_test.dart +++ /dev/null @@ -1,85 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; -import '../../helpers/helpers.dart'; - -void main() { - group('GameHud', () { - late GameBloc gameBloc; - const initialState = GameState( - score: 10, - balls: 2, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ); - - void _mockState(GameState state) { - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); - } - - Future _pumpHud(WidgetTester tester) async { - await tester.pumpApp( - GameHud(), - gameBloc: gameBloc, - ); - } - - setUp(() { - gameBloc = MockGameBloc(); - _mockState(initialState); - }); - - testWidgets( - 'renders the current score', - (tester) async { - await _pumpHud(tester); - expect(find.text(initialState.score.toString()), findsOneWidget); - }, - ); - - testWidgets( - 'renders the current ball number', - (tester) async { - await _pumpHud(tester); - expect( - find.byType(CircleAvatar), - findsNWidgets(initialState.balls), - ); - }, - ); - - testWidgets('updates the score', (tester) async { - await _pumpHud(tester); - expect(find.text(initialState.score.toString()), findsOneWidget); - - _mockState(initialState.copyWith(score: 20)); - - await tester.pump(); - expect(find.text('20'), findsOneWidget); - }); - - testWidgets('updates the ball number', (tester) async { - await _pumpHud(tester); - expect( - find.byType(CircleAvatar), - findsNWidgets(initialState.balls), - ); - - _mockState(initialState.copyWith(balls: 1)); - - await tester.pump(); - expect( - find.byType(CircleAvatar), - findsNWidgets(1), - ); - }); - }); -} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 7a1419fb..f8b62d05 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -5,40 +5,48 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import '../../helpers/helpers.dart'; void main() { - const theme = PinballTheme(characterTheme: DashTheme()); - final game = PinballGameTest(); + final game = PinballTestGame(); group('PinballGamePage', () { - testWidgets('renders PinballGameView', (tester) async { - final gameBloc = MockGameBloc(); + late CharacterThemeCubit characterThemeCubit; + late GameBloc gameBloc; + + setUp(() async { + await Future.wait(game.preLoadAssets()); + characterThemeCubit = MockCharacterThemeCubit(); + gameBloc = MockGameBloc(); + + whenListen( + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), + ); + whenListen( gameBloc, Stream.value(const GameState.initial()), initialState: const GameState.initial(), ); + }); + testWidgets('renders PinballGameView', (tester) async { await tester.pumpApp( - PinballGamePage(theme: theme, game: game), - gameBloc: gameBloc, + PinballGamePage(), + characterThemeCubit: characterThemeCubit, ); + expect(find.byType(PinballGameView), findsOneWidget); }); testWidgets( 'renders the loading indicator while the assets load', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - final assetsManagerCubit = MockAssetsManagerCubit(); final initialAssetsState = AssetsManagerState( loadables: [Future.value()], @@ -51,27 +59,59 @@ void main() { ); await tester.pumpApp( - PinballGamePage(theme: theme, game: game), - gameBloc: gameBloc, + PinballGameView( + game: game, + ), assetsManagerCubit: assetsManagerCubit, + characterThemeCubit: characterThemeCubit, ); - expect(find.text('0.0'), findsOneWidget); - final loadedAssetsState = AssetsManagerState( - loadables: [Future.value()], - loaded: [Future.value()], - ); - whenListen( - assetsManagerCubit, - Stream.value(loadedAssetsState), - initialState: loadedAssetsState, + expect( + find.byWidgetPredicate( + (widget) => + widget is LinearProgressIndicator && widget.value == 0.0, + ), + findsOneWidget, ); - - await tester.pump(); - expect(find.byType(PinballGameView), findsOneWidget); }, ); + testWidgets( + 'renders PinballGameLoadedView after resources have been loaded', + (tester) async { + final assetsManagerCubit = MockAssetsManagerCubit(); + final startGameBloc = MockStartGameBloc(); + + final loadedAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: [Future.value()], + ); + whenListen( + assetsManagerCubit, + Stream.value(loadedAssetsState), + initialState: loadedAssetsState, + ); + whenListen( + startGameBloc, + Stream.value(StartGameState.initial()), + initialState: StartGameState.initial(), + ); + + await tester.pumpApp( + PinballGameView( + game: game, + ), + assetsManagerCubit: assetsManagerCubit, + characterThemeCubit: characterThemeCubit, + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + + await tester.pump(); + + expect(find.byType(PinballGameLoadedView), findsOneWidget); + }); + group('route', () { Future pumpRoute({ required WidgetTester tester, @@ -85,7 +125,6 @@ void main() { onPressed: () { Navigator.of(context).push( PinballGamePage.route( - theme: theme, isDebugMode: isDebugMode, ), ); @@ -95,6 +134,7 @@ void main() { }, ), ), + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('Tap me')); @@ -128,23 +168,61 @@ void main() { }); group('PinballGameView', () { - testWidgets('renders game and a hud', (tester) async { - final gameBloc = MockGameBloc(); + final gameBloc = MockGameBloc(); + final startGameBloc = MockStartGameBloc(); + + setUp(() async { + await Future.wait(game.preLoadAssets()); + whenListen( gameBloc, Stream.value(const GameState.initial()), initialState: const GameState.initial(), ); + whenListen( + startGameBloc, + Stream.value(StartGameState.initial()), + initialState: StartGameState.initial(), + ); + }); + + testWidgets('renders game', (tester) async { await tester.pumpApp( PinballGameView(game: game), gameBloc: gameBloc, + startGameBloc: startGameBloc, ); expect( find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); + // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc + // status + // expect( + // find.byType(GameHud), + // findsNothing, + // ); + }); + + testWidgets('renders a hud on play state', (tester) async { + final startGameState = StartGameState.initial().copyWith( + status: StartGameStatus.play, + ); + + whenListen( + startGameBloc, + Stream.value(startGameState), + initialState: startGameState, + ); + + await tester.pumpApp( + PinballGameView(game: game), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + expect( find.byType(GameHud), findsOneWidget, diff --git a/test/game/view/widgets/bonus_animation_test.dart b/test/game/view/widgets/bonus_animation_test.dart new file mode 100644 index 00000000..11e249c7 --- /dev/null +++ b/test/game/view/widgets/bonus_animation_test.dart @@ -0,0 +1,143 @@ +// ignore_for_file: invalid_use_of_protected_member + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flame/assets.dart'; +import 'package:flame/flame.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/game/view/widgets/bonus_animation.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../helpers/helpers.dart'; + +class MockImages extends Mock implements Images {} + +class MockImage extends Mock implements ui.Image {} + +class MockCallback extends Mock { + void call(); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + // TODO(arturplaczek): need to find for a better solution for loading image + // or use original images from BonusAnimation.loadAssets() + final image = await decodeImageFromList(Uint8List.fromList(fakeImage)); + final images = MockImages(); + when(() => images.fromCache(any())).thenReturn(image); + when(() => images.load(any())).thenAnswer((_) => Future.value(image)); + Flame.images = images; + }); + + group('loads SpriteAnimationWidget correctly for', () { + testWidgets('dashNest', (tester) async { + await tester.pumpApp( + BonusAnimation.dashNest(), + ); + await tester.pump(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + testWidgets('dinoChomp', (tester) async { + await tester.pumpApp( + BonusAnimation.dinoChomp(), + ); + await tester.pump(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + testWidgets('sparkyTurboCharge', (tester) async { + await tester.pumpApp( + BonusAnimation.sparkyTurboCharge(), + ); + await tester.pump(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + testWidgets('googleWord', (tester) async { + await tester.pumpApp( + BonusAnimation.googleWord(), + ); + await tester.pump(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + testWidgets('androidSpaceship', (tester) async { + await tester.pumpApp( + BonusAnimation.androidSpaceship(), + ); + await tester.pump(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + }); + + // TODO(arturplaczek): refactor this test when there is a new version of the + // flame with an onComplete callback in SpriteAnimationWidget + // https://github.com/flame-engine/flame/issues/1543 + testWidgets('called onCompleted callback at the end of animation ', + (tester) async { + final callback = MockCallback(); + + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BonusAnimation.dashNest( + onCompleted: callback.call, + ), + ), + ), + ); + + await tester.pump(); + + await Future.delayed(const Duration(seconds: 4)); + + await tester.pump(); + + verify(callback.call).called(1); + }); + }); + + testWidgets('called onCompleted once when animation changed', (tester) async { + final callback = MockCallback(); + final secondAnimation = BonusAnimation.sparkyTurboCharge( + onCompleted: callback.call, + ); + + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BonusAnimation.dashNest( + onCompleted: callback.call, + ), + ), + ), + ); + + await tester.pump(); + + tester + .state(find.byType(BonusAnimation)) + .didUpdateWidget(secondAnimation); + + await Future.delayed(const Duration(seconds: 4)); + + await tester.pump(); + + verify(callback.call).called(1); + }); + }); +} diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart new file mode 100644 index 00000000..fe8bd092 --- /dev/null +++ b/test/game/view/widgets/game_hud_test.dart @@ -0,0 +1,154 @@ +// ignore_for_file: prefer_const_constructors + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/assets.dart'; +import 'package:flame/flame.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; + +import '../../../helpers/helpers.dart'; + +class MockImages extends Mock implements Images {} + +class MockImage extends Mock implements ui.Image {} + +void main() { + group('GameHud', () { + late GameBloc gameBloc; + + const initialState = GameState( + score: 1000, + balls: 2, + bonusHistory: [], + ); + + setUp(() async { + gameBloc = MockGameBloc(); + + // TODO(arturplaczek): need to find for a better solution for loading + // image or use original images from BonusAnimation.loadAssets() + final image = await decodeImageFromList(Uint8List.fromList(fakeImage)); + final images = MockImages(); + when(() => images.fromCache(any())).thenReturn(image); + when(() => images.load(any())).thenAnswer((_) => Future.value(image)); + Flame.images = images; + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + // We cannot use pumpApp when we are testing animation because + // animation tests needs to be run and check in tester.runAsync + Future _pumpAppWithWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: BlocProvider.value( + value: gameBloc, + child: GameHud(), + ), + ), + ), + ); + } + + group('renders ScoreView widget', () { + testWidgets( + 'with the score', + (tester) async { + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.text(initialState.score.formatScore()), findsOneWidget); + }, + ); + + testWidgets( + 'on game over', + (tester) async { + final state = initialState.copyWith( + bonusHistory: [GameBonus.dashNest], + balls: 0, + ); + + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.byType(ScoreView), findsOneWidget); + expect(find.byType(BonusAnimation), findsNothing); + }, + ); + }); + + for (final gameBonus in GameBonus.values) { + testWidgets('renders BonusAnimation for $gameBonus', (tester) async { + await tester.runAsync(() async { + final state = initialState.copyWith( + bonusHistory: [gameBonus], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + + await _pumpAppWithWidget(tester); + await tester.pump(); + + expect(find.byType(BonusAnimation), findsOneWidget); + }); + }); + } + + testWidgets( + 'goes back to ScoreView after the animation', + (tester) async { + await tester.runAsync(() async { + final state = initialState.copyWith( + bonusHistory: [GameBonus.dashNest], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + + await _pumpAppWithWidget(tester); + await tester.pump(); + // TODO(arturplaczek): remove magic number once this is merged: + // https://github.com/flame-engine/flame/pull/1564 + await Future.delayed(const Duration(seconds: 4)); + + await expectLater(find.byType(ScoreView), findsOneWidget); + }); + }, + ); + }); +} diff --git a/test/game/view/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart similarity index 70% rename from test/game/view/play_button_overlay_test.dart rename to test/game/view/widgets/play_button_overlay_test.dart index 020998d4..0345978d 100644 --- a/test/game/view/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,8 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { group('PlayButtonOverlay', () { @@ -31,5 +32,15 @@ void main() { verify(gameFlowController.start).called(1); }); + + testWidgets('displays CharacterSelectionDialog when tapped', + (tester) async { + await tester.pumpApp(PlayButtonOverlay(game: game)); + + await tester.tap(find.text('Play')); + await tester.pump(); + + expect(find.byType(CharacterSelectionDialog), findsOneWidget); + }); }); } diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart new file mode 100644 index 00000000..8281ce83 --- /dev/null +++ b/test/game/view/widgets/round_count_display_test.dart @@ -0,0 +1,132 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/app_colors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('RoundCountDisplay renders', () { + late GameBloc gameBloc; + const initialState = GameState( + score: 0, + balls: 3, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + testWidgets('three active round indicator', (tester) async { + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.byType(RoundIndicator), findsNWidgets(3)); + }); + + testWidgets('two active round indicator', (tester) async { + final state = initialState.copyWith( + balls: 2, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && widget.isActive, + ), + findsNWidgets(2), + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && !widget.isActive, + ), + findsOneWidget, + ); + }); + + testWidgets('one active round indicator', (tester) async { + final state = initialState.copyWith( + balls: 1, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && widget.isActive, + ), + findsOneWidget, + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && !widget.isActive, + ), + findsNWidgets(2), + ); + }); + }); + + testWidgets('active round indicator is displaying with proper color', + (tester) async { + await tester.pumpApp( + const RoundIndicator(isActive: true), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is Container && widget.color == AppColors.orange, + ), + findsOneWidget, + ); + }); + + testWidgets('inactive round indicator is displaying with proper color', + (tester) async { + await tester.pumpApp( + const RoundIndicator(isActive: false), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is Container && + widget.color == AppColors.orange.withAlpha(128), + ), + findsOneWidget, + ); + }); +} diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart new file mode 100644 index 00000000..0d3af694 --- /dev/null +++ b/test/game/view/widgets/score_view_test.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + late GameBloc gameBloc; + late StreamController stateController; + const score = 123456789; + const initialState = GameState( + score: score, + balls: 1, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + stateController = StreamController()..add(initialState); + + whenListen( + gameBloc, + stateController.stream, + initialState: initialState, + ); + }); + + group('ScoreView', () { + testWidgets('renders score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(score.formatScore()), findsOneWidget); + }); + + testWidgets('renders game over', (tester) async { + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + stateController.add( + initialState.copyWith( + balls: 0, + ), + ); + + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(l10n.gameOver), findsOneWidget); + }); + + testWidgets('updates the score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + + expect(find.text(score.formatScore()), findsOneWidget); + + final newState = initialState.copyWith( + score: 987654321, + ); + + stateController.add(newState); + + await tester.pump(); + + expect(find.text(newState.score.formatScore()), findsOneWidget); + }); + }); +} diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index d0eea644..2c23e3fe 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -1,16 +1,23 @@ -import 'package:flame/src/game/flame_game.dart'; +import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; class FlameBlocTester> extends FlameTester { FlameBlocTester({ required GameCreateFunction gameBuilder, required B Function() blocBuilder, + // TODO(allisonryan0002): find alternative for testGameWidget. Loading + // assets in onLoad fails because the game loads after + List? assets, List Function()? repositories, }) : super( gameBuilder, pumpWidget: (gameWidget, tester) async { + if (assets != null) { + await Future.wait(assets.map(gameWidget.game.images.load)); + } await tester.pumpWidget( BlocProvider.value( value: blocBuilder(), diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart deleted file mode 100644 index 8e054fe0..00000000 --- a/test/helpers/extensions.dart +++ /dev/null @@ -1,31 +0,0 @@ -// ignore_for_file: must_call_super - -import 'package:pinball/game/game.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import 'helpers.dart'; - -class PinballGameTest extends PinballGame { - PinballGameTest() - : super( - audio: MockPinballAudio(), - theme: const PinballTheme( - characterTheme: DashTheme(), - ), - ); -} - -class DebugPinballGameTest extends DebugPinballGame { - DebugPinballGameTest() - : super( - audio: MockPinballAudio(), - theme: const PinballTheme( - characterTheme: DashTheme(), - ), - ); -} - -class EmptyPinballGameTest extends PinballGameTest { - @override - Future onLoad() async {} -} diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart index 706733a1..d782ede4 100644 --- a/test/helpers/fakes.dart +++ b/test/helpers/fakes.dart @@ -5,3 +5,70 @@ import 'package:pinball/game/game.dart'; class FakeContact extends Fake implements Contact {} class FakeGameEvent extends Fake implements GameEvent {} + +const fakeImage = [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, +]; diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 4b6c29f1..8732035a 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -5,11 +5,10 @@ // https://verygood.ventures // license that can be found in the LICENSE file or at export 'builders.dart'; -export 'extensions.dart'; export 'fakes.dart'; export 'forge2d.dart'; export 'key_testers.dart'; export 'mocks.dart'; export 'navigator.dart'; export 'pump_app.dart'; -export 'test_game.dart'; +export 'test_games.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index df6728cc..da6f5124 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -8,7 +8,8 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -28,19 +29,13 @@ class MockBallController extends Mock implements BallController {} class MockContact extends Mock implements Contact {} -class MockContactCallback extends Mock - implements ContactCallback {} - -class MockRampOpening extends Mock implements RampOpening {} - -class MockRampOpeningBallContactCallback extends Mock - implements RampOpeningBallContactCallback {} - class MockGameBloc extends Mock implements GameBloc {} +class MockStartGameBloc extends Mock implements StartGameBloc {} + class MockGameState extends Mock implements GameState {} -class MockThemeCubit extends Mock implements ThemeCubit {} +class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} @@ -64,8 +59,6 @@ class MockTapUpInfo extends Mock implements TapUpInfo {} class MockEventPosition extends Mock implements EventPosition {} -class MockBonusLetter extends Mock implements BonusLetter {} - class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} @@ -76,8 +69,7 @@ class MockDashNestBumper extends Mock implements DashNestBumper {} class MockPinballAudio extends Mock implements PinballAudio {} -class MockSparkyTurboChargeSensor extends Mock - implements SparkyTurboChargeSensor {} +class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {} class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} @@ -89,3 +81,7 @@ class MockActiveOverlaysNotifier extends Mock implements ActiveOverlaysNotifier {} class MockGameFlowController extends Mock implements GameFlowController {} + +class MockAlienBumper extends Mock implements AlienBumper {} + +class MockSparkyBumper extends Mock implements SparkyBumper {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 92e2c042..2c112426 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -14,7 +14,8 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'helpers.dart'; @@ -51,8 +52,9 @@ extension PumpApp on WidgetTester { Widget widget, { MockNavigator? navigator, GameBloc? gameBloc, + StartGameBloc? startGameBloc, AssetsManagerCubit? assetsManagerCubit, - ThemeCubit? themeCubit, + CharacterThemeCubit? characterThemeCubit, LeaderboardRepository? leaderboardRepository, PinballAudio? pinballAudio, }) { @@ -70,11 +72,14 @@ extension PumpApp on WidgetTester { child: MultiBlocProvider( providers: [ BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), + value: characterThemeCubit ?? MockCharacterThemeCubit(), ), BlocProvider.value( value: gameBloc ?? MockGameBloc(), ), + BlocProvider.value( + value: startGameBloc ?? MockStartGameBloc(), + ), BlocProvider.value( value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(), ), diff --git a/test/helpers/test_game.dart b/test/helpers/test_game.dart deleted file mode 100644 index 3c6ff42f..00000000 --- a/test/helpers/test_game.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; - -class TestGame extends Forge2DGame with FlameBloc { - TestGame() { - images.prefix = ''; - } -} diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart new file mode 100644 index 00000000..baa466b8 --- /dev/null +++ b/test/helpers/test_games.dart @@ -0,0 +1,79 @@ +// ignore_for_file: must_call_super + +import 'dart:async'; + +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +import 'helpers.dart'; + +class TestGame extends Forge2DGame with FlameBloc { + TestGame() { + images.prefix = ''; + } +} + +class PinballTestGame extends PinballGame { + PinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + }) : _assets = assets, + super( + audio: audio ?? MockPinballAudio(), + characterTheme: theme ?? const DashTheme(), + ); + final List? _assets; + + @override + Future onLoad() async { + if (_assets != null) { + await images.loadAll(_assets!); + } + await super.onLoad(); + } +} + +class DebugPinballTestGame extends DebugPinballGame { + DebugPinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + }) : _assets = assets, + super( + audio: audio ?? MockPinballAudio(), + characterTheme: theme ?? const DashTheme(), + ); + + final List? _assets; + + @override + Future onLoad() async { + if (_assets != null) { + await images.loadAll(_assets!); + } + await super.onLoad(); + } +} + +class EmptyPinballTestGame extends PinballTestGame { + EmptyPinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + }) : super( + assets: assets, + audio: audio, + theme: theme, + ); + + @override + Future onLoad() async { + if (_assets != null) { + await images.loadAll(_assets!); + } + } +} diff --git a/test/landing/view/landing_page_test.dart b/test/landing/view/landing_page_test.dart deleted file mode 100644 index 369f8cab..00000000 --- a/test/landing/view/landing_page_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/landing/landing.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LandingPage', () { - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - await tester.pumpApp(LandingPage()); - - expect(find.byType(TextButton), findsNWidgets(2)); - expect(find.text(l10n.play), findsOneWidget); - expect(find.text(l10n.howToPlay), findsOneWidget); - }); - - testWidgets('tapping on play button navigates to CharacterSelectionPage', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - - await tester.pumpApp( - LandingPage(), - navigator: navigator, - ); - - await tester.tap(find.widgetWithText(TextButton, l10n.play)); - - verify(() => navigator.push(any())).called(1); - }); - - testWidgets('tapping on how to play button displays dialog with controls', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - await tester.pumpApp(LandingPage()); - - await tester.tap(find.widgetWithText(TextButton, l10n.howToPlay)); - await tester.pump(); - - expect(find.byType(Dialog), findsOneWidget); - }); - }); - - group('KeyIndicator', () { - testWidgets('fromKeyName renders correctly', (tester) async { - const keyName = 'A'; - - await tester.pumpApp( - KeyIndicator.fromKeyName(keyName: keyName), - ); - - expect(find.text(keyName), findsOneWidget); - }); - - testWidgets('fromIcon renders correctly', (tester) async { - const keyIcon = Icons.keyboard_arrow_down; - - await tester.pumpApp( - KeyIndicator.fromIcon(keyIcon: keyIcon), - ); - - expect(find.byIcon(keyIcon), findsOneWidget); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart index a40a1cdb..1b5d41d9 100644 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ b/test/leaderboard/bloc/leaderboard_state_test.dart @@ -30,7 +30,7 @@ void main() { rank: '1', playerInitials: 'ABC', score: 1500, - character: DashTheme().characterAsset, + character: DashTheme().leaderboardIcon, ); test( diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart index 4221d727..daacb4a7 100644 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ b/test/leaderboard/view/leaderboard_page_test.dart @@ -121,7 +121,7 @@ void main() { rank: '1', playerInitials: 'ABC', score: 10000, - character: DashTheme().characterAsset, + character: DashTheme().leaderboardIcon, ), ], ), diff --git a/test/select_character/cubit/character_theme_cubit_test.dart b/test/select_character/cubit/character_theme_cubit_test.dart new file mode 100644 index 00000000..967eb1e1 --- /dev/null +++ b/test/select_character/cubit/character_theme_cubit_test.dart @@ -0,0 +1,25 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('CharacterThemeCubit', () { + test('initial state has Dash character theme', () { + final characterThemeCubit = CharacterThemeCubit(); + expect( + characterThemeCubit.state.characterTheme, + equals(const DashTheme()), + ); + }); + + blocTest( + 'charcterSelected emits selected character theme', + build: CharacterThemeCubit.new, + act: (bloc) => bloc.characterSelected(const SparkyTheme()), + expect: () => [ + const CharacterThemeState(SparkyTheme()), + ], + ); + }); +} diff --git a/test/theme/cubit/theme_state_test.dart b/test/select_character/cubit/character_theme_state_test.dart similarity index 54% rename from test/theme/cubit/theme_state_test.dart rename to test/select_character/cubit/character_theme_state_test.dart index 49a2a387..c0d584e2 100644 --- a/test/theme/cubit/theme_state_test.dart +++ b/test/select_character/cubit/character_theme_state_test.dart @@ -1,18 +1,18 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; void main() { group('ThemeState', () { test('can be instantiated', () { - expect(const ThemeState.initial(), isNotNull); + expect(const CharacterThemeState.initial(), isNotNull); }); test('supports value equality', () { expect( - ThemeState.initial(), - equals(const ThemeState.initial()), + CharacterThemeState.initial(), + equals(const CharacterThemeState.initial()), ); }); }); diff --git a/test/theme/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart similarity index 67% rename from test/theme/view/character_selection_page_test.dart rename to test/select_character/view/character_selection_page_test.dart index eeac690f..0dda92d7 100644 --- a/test/theme/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -4,28 +4,29 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import '../../helpers/helpers.dart'; void main() { - late ThemeCubit themeCubit; + late CharacterThemeCubit characterThemeCubit; setUp(() { - themeCubit = MockThemeCubit(); + characterThemeCubit = MockCharacterThemeCubit(); whenListen( - themeCubit, - const Stream.empty(), - initialState: const ThemeState.initial(), + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), ); }); group('CharacterSelectionPage', () { testWidgets('renders CharacterSelectionView', (tester) async { await tester.pumpApp( - CharacterSelectionPage(), - themeCubit: themeCubit, + CharacterSelectionDialog(), + characterThemeCubit: characterThemeCubit, ); expect(find.byType(CharacterSelectionView), findsOneWidget); }); @@ -38,20 +39,20 @@ void main() { return ElevatedButton( onPressed: () { Navigator.of(context) - .push(CharacterSelectionPage.route()); + .push(CharacterSelectionDialog.route()); }, child: Text('Tap me'), ); }, ), ), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('Tap me')); await tester.pumpAndSettle(); - expect(find.byType(CharacterSelectionPage), findsOneWidget); + expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); @@ -60,7 +61,7 @@ void main() { const titleText = 'Choose your character!'; await tester.pumpApp( CharacterSelectionView(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.text(titleText), findsOneWidget); @@ -74,35 +75,33 @@ void main() { await tester.pumpApp( CharacterSelectionView(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.byKey(sparkyButtonKey)); - verify(() => themeCubit.characterSelected(SparkyTheme())).called(1); + verify(() => characterThemeCubit.characterSelected(SparkyTheme())) + .called(1); }); - testWidgets('navigates to PinballGamePage when start is tapped', + testWidgets('displays how to play dialog when start is tapped', (tester) async { - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - await tester.pumpApp( CharacterSelectionView(), - themeCubit: themeCubit, - navigator: navigator, + characterThemeCubit: characterThemeCubit, ); await tester.ensureVisible(find.byType(TextButton)); await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); - verify(() => navigator.push(any())).called(1); + expect(find.byType(HowToPlayDialog), findsOneWidget); }); }); testWidgets('CharacterImageButton renders correctly', (tester) async { await tester.pumpApp( CharacterImageButton(DashTheme()), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.byType(Image), findsOneWidget); diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart new file mode 100644 index 00000000..ec1b3ced --- /dev/null +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -0,0 +1,62 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/start_game/bloc/start_game_bloc.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + late PinballGame pinballGame; + + setUp(() { + pinballGame = MockPinballGame(); + + when( + () => pinballGame.gameFlowController, + ).thenReturn( + MockGameFlowController(), + ); + }); + + group('StartGameBloc', () { + blocTest( + 'on PlayTapped changes status to selectCharacter', + build: () => StartGameBloc( + game: pinballGame, + ), + act: (bloc) => bloc.add(const PlayTapped()), + expect: () => [ + const StartGameState( + status: StartGameStatus.selectCharacter, + ) + ], + ); + + blocTest( + 'on CharacterSelected changes status to howToPlay', + build: () => StartGameBloc( + game: pinballGame, + ), + act: (bloc) => bloc.add(const CharacterSelected()), + expect: () => [ + const StartGameState( + status: StartGameStatus.howToPlay, + ) + ], + ); + + blocTest( + 'on HowToPlayFinished changes status to play', + build: () => StartGameBloc( + game: pinballGame, + ), + act: (bloc) => bloc.add(const HowToPlayFinished()), + expect: () => [ + const StartGameState( + status: StartGameStatus.play, + ) + ], + ); + }); +} diff --git a/test/start_game/bloc/start_game_event_test.dart b/test/start_game/bloc/start_game_event_test.dart new file mode 100644 index 00000000..cf481d9f --- /dev/null +++ b/test/start_game/bloc/start_game_event_test.dart @@ -0,0 +1,29 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/start_game/bloc/start_game_bloc.dart'; + +void main() { + group('StartGameEvent', () { + test('PlayTapped supports value equality', () { + expect( + PlayTapped(), + equals(PlayTapped()), + ); + }); + + test('CharacterSelected supports value equality', () { + expect( + CharacterSelected(), + equals(CharacterSelected()), + ); + }); + + test('HowToPlayFinished supports value equality', () { + expect( + HowToPlayFinished(), + equals(HowToPlayFinished()), + ); + }); + }); +} diff --git a/test/start_game/bloc/start_game_state_test.dart b/test/start_game/bloc/start_game_state_test.dart new file mode 100644 index 00000000..7ede696d --- /dev/null +++ b/test/start_game/bloc/start_game_state_test.dart @@ -0,0 +1,43 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/start_game/bloc/start_game_bloc.dart'; + +void main() { + group('StartGameState', () { + final testState = StartGameState( + status: StartGameStatus.selectCharacter, + ); + + test('initial state has correct values', () { + final state = StartGameState( + status: StartGameStatus.initial, + ); + + expect(state, StartGameState.initial()); + }); + + test('supports value equality', () { + final secondState = StartGameState( + status: StartGameStatus.selectCharacter, + ); + + expect(testState, secondState); + }); + + test('supports copyWith', () { + final secondState = testState.copyWith(); + + expect(testState, secondState); + }); + + test('has correct props', () { + expect( + testState.props, + equals([ + StartGameStatus.selectCharacter, + ]), + ); + }); + }); +} diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart new file mode 100644 index 00000000..082f102e --- /dev/null +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/start_game/start_game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('HowToPlayDialog', () { + testWidgets('displays dialog', (tester) async { + await tester.pumpApp(HowToPlayDialog()); + + expect(find.byType(Dialog), findsOneWidget); + }); + }); + + group('KeyIndicator', () { + testWidgets('fromKeyName renders correctly', (tester) async { + const keyName = 'A'; + + await tester.pumpApp( + KeyIndicator.fromKeyName(keyName: keyName), + ); + + expect(find.text(keyName), findsOneWidget); + }); + + testWidgets('fromIcon renders correctly', (tester) async { + const keyIcon = Icons.keyboard_arrow_down; + + await tester.pumpApp( + KeyIndicator.fromIcon(keyIcon: keyIcon), + ); + + expect(find.byIcon(keyIcon), findsOneWidget); + }); + }); +} diff --git a/test/theme/cubit/theme_cubit_test.dart b/test/theme/cubit/theme_cubit_test.dart deleted file mode 100644 index 1f2d24e0..00000000 --- a/test/theme/cubit/theme_cubit_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('ThemeCubit', () { - test('initial state has Dash character theme', () { - final themeCubit = ThemeCubit(); - expect(themeCubit.state.theme.characterTheme, equals(const DashTheme())); - }); - - blocTest( - 'charcterSelected emits selected character theme', - build: ThemeCubit.new, - act: (bloc) => bloc.characterSelected(const SparkyTheme()), - expect: () => [ - const ThemeState(PinballTheme(characterTheme: SparkyTheme())), - ], - ); - }); -} diff --git a/web/index.html b/web/index.html index ff6e451b..37e17170 100644 --- a/web/index.html +++ b/web/index.html @@ -19,18 +19,42 @@ - + + + + + + + + + + + + + + + + + + + + - + - Pinball + I/O Pinball Machine - Flutter