diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index ce1a78b4..ba604f17 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -11,14 +11,11 @@ class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { on(_onBallLost); on(_onScored); - on(_onBonusLetterActivated); + on(_onBonusActivated); on(_onDashNestActivated); on(_onSparkyTurboChargeActivated); } - static const bonusWord = 'GOOGLE'; - static const bonusWordScore = 10000; - void _onBallLost(BallLost event, Emitter emit) { emit(state.copyWith(balls: state.balls - 1)); } @@ -29,29 +26,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 _onBonusActivated(BonusActivated event, Emitter emit) { + emit( + state.copyWith( + bonusHistory: [...state.bonusHistory, event.bonus], + ), + ); } void _onDashNestActivated(DashNestActivated event, Emitter emit) { diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index ee5315ad..392cc50f 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -33,17 +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]; + List get props => [bonus]; } class DashNestActivated extends GameEvent { diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 0d9485e9..aa1144c0 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -4,9 +4,8 @@ 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, @@ -23,7 +22,6 @@ 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"), @@ -32,7 +30,6 @@ class GameState extends Equatable { const GameState.initial() : score = 0, balls = 3, - activatedBonusLetters = const [], activatedDashNests = const {}, bonusHistory = const []; @@ -44,9 +41,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; @@ -57,14 +51,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, }) { @@ -76,8 +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,7 +74,6 @@ class GameState extends Equatable { List get props => [ score, balls, - activatedBonusLetters, activatedDashNests, bonusHistory, ]; diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart deleted file mode 100644 index f3b9743c..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/components.dart b/lib/game/components/components.dart index 058bfe20..e05f9f00 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,6 +1,5 @@ export 'alien_zone.dart'; export 'board.dart'; -export 'bonus_word.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; @@ -8,6 +7,7 @@ export 'controlled_plunger.dart'; export 'controlled_sparky_computer.dart'; export 'flutter_forest.dart'; export 'game_flow_controller.dart'; +export 'google_word.dart'; export 'launcher.dart'; export 'score_effect_controller.dart'; export 'score_points.dart'; diff --git a/lib/game/components/controlled_flipper.dart b/lib/game/components/controlled_flipper.dart index 9b73b6d3..f1991fe1 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/lib/game/components/controlled_flipper.dart @@ -1,6 +1,8 @@ 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'; /// {@template controlled_flipper} @@ -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 index 167f129e..cec71876 100644 --- a/lib/game/components/controlled_plunger.dart +++ b/lib/game/components/controlled_plunger.dart @@ -1,6 +1,8 @@ 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'; /// {@template controlled_plunger} @@ -18,7 +20,7 @@ class ControlledPlunger extends Plunger with Controls { /// A [ComponentController] that controls a [Plunger]s movement. /// {@endtemplate} class PlungerController extends ComponentController - with KeyboardHandler { + with KeyboardHandler, BlocComponent { /// {@macro plunger_controller} PlungerController(Plunger plunger) : super(plunger); @@ -36,6 +38,7 @@ class PlungerController 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/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index b0f6f514..01ee0c32 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -28,7 +28,11 @@ 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, + ); component.firstChild()?.focusOnBackboard(); } diff --git a/lib/game/components/google_word.dart b/lib/game/components/google_word.dart new file mode 100644 index 00000000..754a0fff --- /dev/null +++ b/lib/game/components/google_word.dart @@ -0,0 +1,83 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template google_word} +/// Loads all [GoogleLetter]s to compose a [GoogleWord]. +/// {@endtemplate} +class GoogleWord extends Component + with HasGameRef, Controls<_GoogleWordController> { + /// {@macro google_word} + GoogleWord({ + required Vector2 position, + }) : _position = position { + controller = _GoogleWordController(this); + } + + final Vector2 _position; + + @override + Future onLoad() async { + await super.onLoad(); + gameRef.addContactCallback(_GoogleLetterBallContactCallback()); + + final offsets = [ + Vector2(-12.92, 1.82), + Vector2(-8.33, -0.65), + Vector2(-2.88, -1.75), + Vector2(2.88, -1.75), + Vector2(8.33, -0.65), + Vector2(12.92, 1.82), + ]; + + final letters = []; + for (var index = 0; index < offsets.length; index++) { + letters.add( + GoogleLetter(index)..initialPosition = _position + offsets[index], + ); + } + + await addAll(letters); + } +} + +class _GoogleWordController extends ComponentController + with HasGameRef { + _GoogleWordController(GoogleWord googleWord) : super(googleWord); + + final _activatedLetters = {}; + + void activate(GoogleLetter googleLetter) { + if (!_activatedLetters.add(googleLetter)) return; + + googleLetter.activate(); + + final activatedBonus = _activatedLetters.length == 6; + if (activatedBonus) { + gameRef.audio.googleBonus(); + gameRef.read().add(const BonusActivated(GameBonus.googleWord)); + component.children.whereType().forEach( + (letter) => letter.deactivate(), + ); + _activatedLetters.clear(); + } + } +} + +/// Activates a [GoogleLetter] when it contacts with a [Ball]. +class _GoogleLetterBallContactCallback + extends ContactCallback { + @override + void begin(GoogleLetter googleLetter, _, __) { + final parent = googleLetter.parent; + if (parent is GoogleWord) { + parent.controller.activate(googleLetter); + } + } +} diff --git a/lib/game/components/score_effect_controller.dart b/lib/game/components/score_effect_controller.dart index 7fafd4b5..f9fe9349 100644 --- a/lib/game/components/score_effect_controller.dart +++ b/lib/game/components/score_effect_controller.dart @@ -37,7 +37,7 @@ class ScoreEffectController extends ComponentController text: newScore.toString(), position: Vector2( _noise(), - _noise() + (-BoardDimensions.bounds.topCenter.dy + 10), + _noise() + (BoardDimensions.bounds.topCenter.dy + 10), ), ), ); diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 4d06bd13..e7c7a343 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -58,6 +58,13 @@ extension PinballGameAssetsX on PinballGame { 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(Assets.images.components.background.path), ]; } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 9c59f2f4..8cff11c5 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -41,7 +41,7 @@ class PinballGame extends Forge2DGame // 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(); unawaited(addFromBlueprint(Boundaries())); @@ -72,7 +72,6 @@ class PinballGame extends Forge2DGame void _addContactCallbacks() { addContactCallback(BallScorePointsCallback(this)); addContactCallback(BottomWallBallContactCallback()); - addContactCallback(BonusLetterBallContactCallback()); } Future _addGameBoundaries() async { @@ -82,7 +81,7 @@ class PinballGame extends Forge2DGame Future _addBonusWord() async { await add( - BonusWord( + GoogleWord( position: Vector2( BoardDimensions.bounds.center.dx - 4.1, BoardDimensions.bounds.center.dy + 1.8, 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/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 35a998a4..f45543b0 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -57,6 +57,8 @@ class $AssetsImagesBackboardGen { /// File path: assets/images/backboard/backboard_scores.png AssetGenImage get backboardScores => const AssetGenImage('assets/images/backboard/backboard_scores.png'); + AssetGenImage get display => + const AssetGenImage('assets/images/backboard/display.png'); } class $AssetsImagesBaseboardGen { @@ -307,11 +309,8 @@ class $AssetsImagesSparkyBumperGen { class $AssetsImagesSparkyComputerGen { const $AssetsImagesSparkyComputerGen(); - /// File path: assets/images/sparky/computer/base.png AssetGenImage get base => const AssetGenImage('assets/images/sparky/computer/base.png'); - - /// File path: assets/images/sparky/computer/top.png AssetGenImage get top => const AssetGenImage('assets/images/sparky/computer/top.png'); } @@ -355,11 +354,8 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); - /// File path: assets/images/sparky/bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky/bumper/a/active.png'); - - /// File path: assets/images/sparky/bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); } @@ -367,11 +363,8 @@ class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperBGen { const $AssetsImagesSparkyBumperBGen(); - /// File path: assets/images/sparky/bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky/bumper/b/active.png'); - - /// File path: assets/images/sparky/bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); } @@ -379,11 +372,8 @@ class $AssetsImagesSparkyBumperBGen { class $AssetsImagesSparkyBumperCGen { const $AssetsImagesSparkyBumperCGen(); - /// File path: assets/images/sparky/bumper/c/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky/bumper/c/active.png'); - - /// File path: assets/images/sparky/bumper/c/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); } diff --git a/packages/pinball_components/lib/src/components/alien_bumper.dart b/packages/pinball_components/lib/src/components/alien_bumper.dart index 74c2f3bd..b78c6adc 100644 --- a/packages/pinball_components/lib/src/components/alien_bumper.dart +++ b/packages/pinball_components/lib/src/components/alien_bumper.dart @@ -73,13 +73,14 @@ class AlienBumper extends BodyComponent with InitialPosition { majorRadius: _majorRadius, minorRadius: _minorRadius, )..rotate(1.29); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } 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..c5c4ac17 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard.dart @@ -0,0 +1,75 @@ +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 int score, + required BackboardOnSubmit onSubmit, + }) { + return Backboard(position: position) + ..gameOverMode( + score: score, + onSubmit: onSubmit, + ); + } + + /// [TextPaint] used on the [Backboard] + static final textPaint = TextPaint( + style: 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, + BackboardOnSubmit? onSubmit, + }) async { + children.removeWhere((_) => true); + await add( + BackboardGameOver( + score: score, + 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..05f89217 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.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, + BackboardOnSubmit? onSubmit, + }) : _score = score, + _onSubmit = onSubmit; + + final int _score; + final BackboardOnSubmit? _onSubmit; + + @override + Future onLoad() async { + final backgroundSprite = await gameRef.loadSprite( + Assets.images.backboard.backboardGameOver.keyName, + ); + + unawaited( + add( + SpriteComponent( + sprite: backgroundSprite, + size: backgroundSprite.originalSize / 10, + anchor: Anchor.bottomCenter, + ), + ), + ); + + final displaySprite = await gameRef.loadSprite( + Assets.images.backboard.display.keyName, + ); + + unawaited( + add( + SpriteComponent( + sprite: displaySprite, + size: displaySprite.originalSize / 10, + anchor: Anchor.bottomCenter, + position: Vector2(0, -11.5), + ), + ), + ); + + unawaited( + add( + TextComponent( + text: _score.formatScore(), + position: Vector2(-22, -46.5), + anchor: Anchor.center, + textRenderer: Backboard.textPaint, + ), + ), + ); + + for (var i = 0; i < 3; i++) { + unawaited( + add( + BackboardLetterPrompt( + position: Vector2( + 20 + (6 * i).toDouble(), + -46.5, + ), + hasFocus: i == 0, + ), + ), + ); + } + + unawaited( + 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; + } +} 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..8f404d53 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart @@ -0,0 +1,106 @@ +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'; + +/// {@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( + 4, + 1.2, + ), + anchor: Anchor.center, + position: Vector2(0, 4), + ); + + unawaited(add(_underscore)); + + _input = TextComponent( + text: 'A', + textRenderer: Backboard.textPaint, + anchor: Anchor.center, + ); + unawaited(add(_input)); + + _underscoreBlinker = TimerComponent( + period: 0.6, + repeat: true, + autoStart: _hasFocus, + onTick: () { + _underscore.paint.color = (_underscore.paint.color == Colors.white) + ? Colors.transparent + : Colors.white; + }, + ); + + unawaited(add(_underscoreBlinker)); + + unawaited( + 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 3852fe48..86ae269f 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'dart:ui'; import 'package:flame/components.dart'; @@ -48,11 +49,15 @@ class Ball extends BodyComponent @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); } @@ -92,7 +97,8 @@ class Ball extends BodyComponent unawaited(gameRef.add(effect)); } - _rescale(); + _rescaleSize(); + _setPositionalGravity(); } /// Applies a boost on this [Ball]. @@ -101,19 +107,36 @@ class Ball extends BodyComponent _boostTimer = _boostDuration; } - void _rescale() { + void _rescaleSize() { final boardHeight = BoardDimensions.bounds.height; - const maxShrinkAmount = BoardDimensions.perspectiveShrinkFactor; + const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; - final adjustedYPosition = -body.position.y + (boardHeight / 2); + final standardizedYPosition = body.position.y + (boardHeight / 2); - final scaleFactor = ((boardHeight - adjustedYPosition) / - BoardDimensions.shrinkAdjustedHeight) + - maxShrinkAmount; + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; _spriteComponent.scale = Vector2.all(scaleFactor); } + + 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 positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) * + maxXGravityPercentage) * + defaultGravity; + + final positionalYForce = math.sqrt( + math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2), + ); + + body.gravityOverride = Vector2(positionalXForce, positionalYForce); + } } class _BallSpriteComponent extends SpriteComponent with HasGameRef { diff --git a/packages/pinball_components/lib/src/components/baseboard.dart b/packages/pinball_components/lib/src/components/baseboard.dart index df602f65..03826d6c 100644 --- a/packages/pinball_components/lib/src/components/baseboard.dart +++ b/packages/pinball_components/lib/src/components/baseboard.dart @@ -89,10 +89,10 @@ class Baseboard extends BodyComponent with InitialPosition { @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); diff --git a/packages/pinball_components/lib/src/components/board_dimensions.dart b/packages/pinball_components/lib/src/components/board_dimensions.dart index 83e3e29f..3d547996 100644 --- a/packages/pinball_components/lib/src/components/board_dimensions.dart +++ b/packages/pinball_components/lib/src/components/board_dimensions.dart @@ -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/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart index bea6d6da..7846f140 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino.dart @@ -56,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 @@ -95,10 +97,11 @@ class ChromeDino extends BodyComponent with InitialPosition { @override Body createBody() { - final bodyDef = BodyDef() - ..gravityScale = Vector2.zero() - ..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); @@ -113,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 e719783f..7b4e1ddd 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,5 @@ export 'alien_bumper.dart'; -export 'backboard.dart'; +export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; export 'board_dimensions.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart index f14fff89..b796b9d1 100644 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart @@ -80,10 +80,10 @@ class BigDashNestBumper extends DashNestBumper { minorRadius: 3.75, )..rotate(math.pi / 1.9); final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } @@ -131,13 +131,14 @@ class SmallDashNestBumper extends DashNestBumper { majorRadius: 3, minorRadius: 2.25, )..rotate(math.pi / 2); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 287c429b..97da2da0 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -81,10 +81,11 @@ class _DinoTopWall extends BodyComponent with InitialPosition { @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( @@ -128,6 +129,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { List _createFixtureDefs() { final fixturesDef = []; + const restitution = 1.0; final topStraightControlPoints = [ Vector2(32.4, -8.3), @@ -138,7 +140,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { topStraightControlPoints.first, topStraightControlPoints.last, ); - final topStraightFixtureDef = FixtureDef(topStraightShape); + final topStraightFixtureDef = FixtureDef( + topStraightShape, + restitution: restitution, + ); fixturesDef.add(topStraightFixtureDef); final topLeftCurveControlPoints = [ @@ -149,7 +154,11 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { final topLeftCurveShape = BezierCurveShape( controlPoints: topLeftCurveControlPoints, ); - fixturesDef.add(FixtureDef(topLeftCurveShape)); + final topLeftCurveFixtureDef = FixtureDef( + topLeftCurveShape, + restitution: restitution, + ); + fixturesDef.add(topLeftCurveFixtureDef); final bottomLeftStraightControlPoints = [ topLeftCurveControlPoints.last, @@ -160,7 +169,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { bottomLeftStraightControlPoints.first, bottomLeftStraightControlPoints.last, ); - final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape); + final bottomLeftStraightFixtureDef = FixtureDef( + bottomLeftStraightShape, + restitution: restitution, + ); fixturesDef.add(bottomLeftStraightFixtureDef); final bottomStraightControlPoints = [ @@ -172,7 +184,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { bottomStraightControlPoints.first, bottomStraightControlPoints.last, ); - final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); + final bottomStraightFixtureDef = FixtureDef( + bottomStraightShape, + restitution: restitution, + ); fixturesDef.add(bottomStraightFixtureDef); return fixturesDef; @@ -180,19 +195,13 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { @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; } diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index ebe468b3..f825070a 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -99,9 +99,11 @@ 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. + final trapeziumFixtureDef = FixtureDef( + trapezium, + density: 50, // TODO(alestiago): Use a proper density. + friction: .1, // TODO(alestiago): Use a proper friction. + ); fixturesDef.add(trapeziumFixtureDef); return fixturesDef; @@ -118,10 +120,12 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { @override Body createBody() { - final bodyDef = BodyDef() - ..position = initialPosition - ..gravityScale = Vector2.zero() - ..type = BodyType.dynamic; + final bodyDef = BodyDef( + position: initialPosition, + gravityScale: Vector2.zero(), + type: BodyType.dynamic, + ); + final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); diff --git a/packages/pinball_components/lib/src/components/flutter_sign_post.dart b/packages/pinball_components/lib/src/components/flutter_sign_post.dart index 47e721a5..210e9bdf 100644 --- a/packages/pinball_components/lib/src/components/flutter_sign_post.dart +++ b/packages/pinball_components/lib/src/components/flutter_sign_post.dart @@ -21,7 +21,9 @@ class FlutterSignPost extends BodyComponent with InitialPosition { Body createBody() { final shape = CircleShape()..radius = 0.25; final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef()..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/lib/src/components/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter.dart index 9e9e2dec..1ed31435 100644 --- a/packages/pinball_components/lib/src/components/google_letter.dart +++ b/packages/pinball_components/lib/src/components/google_letter.dart @@ -33,12 +33,14 @@ class GoogleLetter extends BodyComponent with InitialPosition { @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; + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/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 f70de757..a932b441 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -35,7 +35,7 @@ class Kicker extends BodyComponent with InitialPosition { final upperCircle = CircleShape()..radius = 1.6; upperCircle.position.setValues(0, upperCircle.radius / 2); - final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0; + final upperCircleFixtureDef = FixtureDef(upperCircle); fixturesDefs.add(upperCircleFixtureDef); final lowerCircle = CircleShape()..radius = 1.6; @@ -43,7 +43,7 @@ class Kicker extends BodyComponent with InitialPosition { size.x * -direction, size.y + 0.8, ); - final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0; + final lowerCircleFixtureDef = FixtureDef(lowerCircle); fixturesDefs.add(lowerCircleFixtureDef); final wallFacingEdge = EdgeShape() @@ -55,7 +55,7 @@ class Kicker extends BodyComponent with InitialPosition { ), Vector2(2.5 * direction, size.y - 2), ); - final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0; + final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge); fixturesDefs.add(wallFacingLineFixtureDef); final bottomEdge = EdgeShape() @@ -67,7 +67,7 @@ class Kicker extends BodyComponent with InitialPosition { lowerCircle.radius * math.sin(quarterPi), ), ); - final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0; + final bottomLineFixtureDef = FixtureDef(bottomEdge); fixturesDefs.add(bottomLineFixtureDef); final bouncyEdge = EdgeShape() @@ -84,10 +84,11 @@ class Kicker extends BodyComponent with InitialPosition { ), ); - 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. @@ -111,7 +112,9 @@ 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); diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 12ba9edf..4043d3c8 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -103,9 +103,10 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index 363e703e..15e93733 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -33,11 +33,12 @@ class Plunger extends BodyComponent with InitialPosition, Layered { final fixtureDef = FixtureDef(shape)..density = 80; - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this - ..type = BodyType.dynamic - ..gravityScale = Vector2.zero(); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + type: BodyType.dynamic, + gravityScale: Vector2.zero(), + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/lib/src/components/ramp_opening.dart b/packages/pinball_components/lib/src/components/ramp_opening.dart index 829ad362..2434b31a 100644 --- a/packages/pinball_components/lib/src/components/ramp_opening.dart +++ b/packages/pinball_components/lib/src/components/ramp_opening.dart @@ -65,12 +65,14 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final fixtureDef = FixtureDef(shape)..isSensor = true; - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart index 8e76bd71..99333136 100644 --- a/packages/pinball_components/lib/src/components/slingshot.dart +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -56,12 +56,12 @@ class Slingshot extends BodyComponent with InitialPosition { final topCircleShape = CircleShape()..radius = circleRadius; topCircleShape.position.setValues(0, -_length / 2); - final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0; + final topCircleFixtureDef = FixtureDef(topCircleShape); fixturesDef.add(topCircleFixtureDef); final bottomCircleShape = CircleShape()..radius = circleRadius; bottomCircleShape.position.setValues(0, _length / 2); - final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0; + final bottomCircleFixtureDef = FixtureDef(bottomCircleShape); fixturesDef.add(bottomCircleFixtureDef); final leftEdgeShape = EdgeShape() @@ -69,9 +69,11 @@ class Slingshot extends BodyComponent with InitialPosition { Vector2(circleRadius, _length / 2), Vector2(circleRadius, -_length / 2), ); - final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape) - ..friction = 0 - ..restitution = 5; + final leftEdgeShapeFixtureDef = FixtureDef( + leftEdgeShape, + restitution: 5, + ); + fixturesDef.add(leftEdgeShapeFixtureDef); final rightEdgeShape = EdgeShape() @@ -79,9 +81,10 @@ class Slingshot extends BodyComponent with InitialPosition { Vector2(-circleRadius, _length / 2), Vector2(-circleRadius, -_length / 2), ); - final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape) - ..friction = 0 - ..restitution = 5; + final rightEdgeShapeFixtureDef = FixtureDef( + rightEdgeShape, + restitution: 5, + ); fixturesDef.add(rightEdgeShapeFixtureDef); return fixturesDef; @@ -89,10 +92,11 @@ class Slingshot extends BodyComponent with InitialPosition { @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); diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 46b2c506..28e29ae3 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -71,17 +71,17 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final circleShape = CircleShape()..radius = 3; - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + final shape = CircleShape()..radius = 3; + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(circleShape)..isSensor = true, - ); + return world.createBody(bodyDef)..createFixture(fixtureDef); } } @@ -107,10 +107,10 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered { 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( @@ -246,18 +246,16 @@ class SpaceshipWall extends BodyComponent with InitialPosition, Layered { Body createBody() { renderBody = false; - final wallShape = _SpaceshipWallShape(); + final shape = _SpaceshipWallShape(); + final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..angle = -1.7 - ..type = BodyType.static; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + angle: -1.7, + ); - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(wallShape)..restitution = 1, - ); + 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 60a96d80..c88fe738 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -119,9 +119,10 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); @@ -183,12 +184,11 @@ class _SpaceshipRailBase extends BodyComponent with InitialPosition { @override Body createBody() { final shape = CircleShape()..radius = radius; - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 99f985f7..480c2549 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -75,7 +75,6 @@ class _SpaceshipRampBackground extends BodyComponent Vector2(-14.2, -71.25), ], ); - final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape); fixturesDef.add(outerLeftCurveFixtureDef); @@ -86,7 +85,6 @@ class _SpaceshipRampBackground extends BodyComponent Vector2(6.1, -44.9), ], ); - final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape); fixturesDef.add(outerRightCurveFixtureDef); @@ -103,9 +101,10 @@ class _SpaceshipRampBackground extends BodyComponent @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); @@ -213,9 +212,10 @@ class _SpaceshipRampForegroundRailing extends BodyComponent @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); @@ -267,10 +267,10 @@ class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered { ], ); final fixtureDef = FixtureDef(baseShape); - - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); return world.createBody(bodyDef)..createFixture(fixtureDef); } diff --git a/packages/pinball_components/lib/src/components/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper.dart index 31a5dac0..e4d9ebef 100644 --- a/packages/pinball_components/lib/src/components/sparky_bumper.dart +++ b/packages/pinball_components/lib/src/components/sparky_bumper.dart @@ -91,10 +91,10 @@ class SparkyBumper extends BodyComponent with InitialPosition { majorRadius: _majorRadius, minorRadius: _minorRadius, )..rotate(math.pi / 2.1); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); final bodyDef = BodyDef() ..position = initialPosition ..userData = this; diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer.dart index d3aa2b05..1cf4ee83 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer.dart @@ -56,9 +56,10 @@ class _ComputerBase extends BodyComponent with InitialPosition { @override Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition; + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); final body = world.createBody(bodyDef); _createFixtureDefs().forEach(body.createFixture); 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/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart index 9af8dba6..9b766995 100644 --- a/packages/pinball_components/lib/src/flame/flame.dart +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -1,2 +1,3 @@ export 'blueprint.dart'; +export 'keyboard_input_controller.dart'; export 'priority.dart'; diff --git a/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart b/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart new file mode 100644 index 00000000..8249e599 --- /dev/null +++ b/packages/pinball_components/lib/src/flame/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_components/lib/src/pinball_components.dart b/packages/pinball_components/lib/src/pinball_components.dart index bd8f99de..50dee227 100644 --- a/packages/pinball_components/lib/src/pinball_components.dart +++ b/packages/pinball_components/lib/src/pinball_components.dart @@ -1,2 +1,3 @@ export 'components/components.dart'; +export 'extensions/extensions.dart'; export 'flame/flame.dart'; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index cf2a22a2..d27084f1 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: sdk: flutter geometry: path: ../geometry + intl: ^0.17.0 dev_dependencies: diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 4aae07cb..4fb158bd 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -11,6 +11,9 @@ abstract class BasicGame extends Forge2DGame { } } +abstract class BasicKeyboardGame extends BasicGame + with HasKeyboardHandlerComponents {} + abstract class LineGame extends BasicGame with PanDetector { Vector2? _lineEnd; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 5cf36b3d..73ae296c 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -32,6 +32,7 @@ void main() { addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreTextStories(dashbook); + addBackboardStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart b/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart new file mode 100644 index 00000000..a513276f --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BackboardGameOverGame extends BasicKeyboardGame { + BackboardGameOverGame(this.score); + + static const info = ''' + Simple example showing the waiting mode of the backboard. + '''; + + final int score; + + @override + Future onLoad() async { + camera + ..followVector2(Vector2.zero()) + ..zoom = 5; + + await add( + Backboard.gameOver( + position: Vector2(0, 20), + score: score, + 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/stories.dart b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart new file mode 100644 index 00000000..f85e6685 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart @@ -0,0 +1,27 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/backboard/game_over.dart'; +import 'package:sandbox/stories/backboard/waiting.dart'; + +void addBackboardStories(Dashbook dashbook) { + dashbook.storiesOf('Backboard') + ..add( + 'Waiting mode', + (context) => GameWidget( + game: BackboardWaitingGame(), + ), + codeLink: buildSourceLink('backboard/waiting.dart'), + info: BackboardWaitingGame.info, + ) + ..add( + 'Game over', + (context) => GameWidget( + game: BackboardGameOverGame( + context.numberProperty('score', 9000000000).toInt(), + ), + ), + codeLink: buildSourceLink('backboard/game_over.dart'), + info: BackboardGameOverGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/waiting.dart b/packages/pinball_components/sandbox/lib/stories/backboard/waiting.dart new file mode 100644 index 00000000..71f5c09a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/waiting.dart @@ -0,0 +1,19 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BackboardWaitingGame extends BasicGame { + static const info = ''' + Simple example showing the waiting mode of the backboard. + '''; + + @override + Future onLoad() async { + camera + ..followVector2(Vector2.zero()) + ..zoom = 5; + + final backboard = Backboard.waiting(position: Vector2(0, 20)); + await add(backboard); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index cdcf0825..338ca384 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,4 +1,5 @@ export 'alien_zone/stories.dart'; +export 'backboard/stories.dart'; export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index 33553ba6..61af3a8a 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -149,6 +149,13 @@ 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: diff --git a/packages/pinball_components/test/helpers/test_game.dart b/packages/pinball_components/test/helpers/test_game.dart index a1219868..5bd4b30d 100644 --- a/packages/pinball_components/test/helpers/test_game.dart +++ b/packages/pinball_components/test/helpers/test_game.dart @@ -1,3 +1,4 @@ +import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class TestGame extends Forge2DGame { @@ -5,3 +6,5 @@ class TestGame extends Forge2DGame { images.prefix = ''; } } + +class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {} diff --git a/packages/pinball_components/test/src/components/backboard_test.dart b/packages/pinball_components/test/src/components/backboard_test.dart index 2d95cc47..5e868bfc 100644 --- a/packages/pinball_components/test/src/components/backboard_test.dart +++ b/packages/pinball_components/test/src/components/backboard_test.dart @@ -1,7 +1,9 @@ -// 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'; @@ -9,7 +11,7 @@ import '../../helpers/helpers.dart'; void main() { group('Backboard', () { - final tester = FlameTester(TestGame.new); + final tester = FlameTester(KeyboardTestGame.new); group('on waitingMode', () { tester.testGameWidget( @@ -17,7 +19,7 @@ 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))); }, verify: (game, tester) async { await expectLater( @@ -34,20 +36,145 @@ 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, + 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, + 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, + 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/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/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/flame/keyboard_input_controller_test.dart b/packages/pinball_components/test/src/flame/keyboard_input_controller_test.dart new file mode 100644 index 00000000..991f1143 --- /dev/null +++ b/packages/pinball_components/test/src/flame/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_components/pinball_components.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/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index fb543814..e83c35d3 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,7 +21,6 @@ void main() { const GameState( score: 0, balls: 2, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ), @@ -41,14 +40,12 @@ void main() { const GameState( score: 2, balls: 3, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 5, balls: 3, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ), @@ -69,21 +66,18 @@ 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: [], ), @@ -91,103 +85,6 @@ void main() { ); }); - 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', @@ -200,21 +97,18 @@ void main() { 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], ), @@ -222,6 +116,33 @@ void main() { ); }); + 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, + activatedDashNests: {}, + bonusHistory: [GameBonus.googleWord], + ), + GameState( + score: 0, + balls: 3, + activatedDashNests: {}, + bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], + ), + ], + ); + }, + ); + group('SparkyTurboChargeActivated', () { blocTest( 'adds game bonus', @@ -231,7 +152,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..ef2a9f54 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -41,61 +41,51 @@ 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); - }); + 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'))), - ); - }); + 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..81ca29f1 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -10,7 +10,6 @@ void main() { GameState( score: 0, balls: 0, - activatedBonusLetters: const [], activatedDashNests: const {}, bonusHistory: const [], ), @@ -18,7 +17,6 @@ void main() { const GameState( score: 0, balls: 0, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ), @@ -32,7 +30,6 @@ void main() { const GameState( score: 0, balls: 0, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ), @@ -49,7 +46,6 @@ void main() { () => GameState( balls: -1, score: 0, - activatedBonusLetters: const [], activatedDashNests: const {}, bonusHistory: const [], ), @@ -66,7 +62,6 @@ void main() { () => GameState( balls: 0, score: -1, - activatedBonusLetters: const [], activatedDashNests: const {}, bonusHistory: const [], ), @@ -82,7 +77,6 @@ void main() { const gameState = GameState( balls: 0, score: 0, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ); @@ -95,7 +89,6 @@ void main() { const gameState = GameState( balls: 1, score: 0, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ); @@ -103,36 +96,6 @@ void main() { }); }); - 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,7 +104,6 @@ void main() { const gameState = GameState( balls: 0, score: 2, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ); @@ -159,7 +121,6 @@ void main() { const gameState = GameState( balls: 0, score: 2, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ); @@ -177,16 +138,14 @@ 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,7 +153,6 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, - activatedBonusLetters: otherGameState.activatedBonusLetters, activatedDashNests: otherGameState.activatedDashNests, bonusHistory: otherGameState.bonusHistory, ), diff --git a/test/game/components/alien_zone_test.dart b/test/game/components/alien_zone_test.dart index 68a2c2f1..863bef31 100644 --- a/test/game/components/alien_zone_test.dart +++ b/test/game/components/alien_zone_test.dart @@ -13,7 +13,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); group('AlienZone', () { flameTester.test( @@ -52,7 +52,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 9f2a5260..0a1928ab 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_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('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..96c67dd4 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, ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 5446a672..3c0fc1b0 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,22 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () { + final bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + activatedDashNests: {}, + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); group('FlipperController', () { group('onKeyEvent', () { @@ -48,6 +64,20 @@ void main() { ); }); + testRawKeyDownEvents(leftKeys, (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); + }, + ); + }); + testRawKeyUpEvents(leftKeys, (event) { flameTester.test( 'moves downwards ' @@ -119,6 +149,20 @@ void main() { ); }); + 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); + }, + ); + }); + testRawKeyUpEvents(leftKeys, (event) { flameTester.test( 'does nothing ' diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index 02bf7f24..a377487e 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -1,5 +1,6 @@ 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'; @@ -11,7 +12,22 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () { + final bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + activatedDashNests: {}, + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); group('PlungerController', () { group('onKeyEvent', () { @@ -73,6 +89,20 @@ void main() { }, ); }); + + 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 index a3e13486..944afca3 100644 --- a/test/game/components/controlled_sparky_computer_test.dart +++ b/test/game/components/controlled_sparky_computer_test.dart @@ -10,7 +10,7 @@ import '../../helpers/helpers.dart'; void main() { group('SparkyComputerController', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); late ControlledSparkyComputer controlledSparkyComputer; diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index e9e58985..7ad5a3de 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -12,7 +12,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); group('FlutterForest', () { flameTester.test( @@ -95,7 +95,6 @@ void main() { const state = GameState( score: 0, balls: 3, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], ); @@ -158,7 +157,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index dc1d9ab8..8bb81a6c 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -15,7 +15,6 @@ void main() { final state = GameState( score: 10, balls: 0, - activatedBonusLetters: const [], bonusHistory: const [], activatedDashNests: const {}, ); @@ -42,7 +41,12 @@ void main() { gameFlowController = GameFlowController(game); overlays = MockActiveOverlaysNotifier(); - when(backboard.gameOverMode).thenAnswer((_) async {}); + when( + () => backboard.gameOverMode( + score: any(named: 'score'), + onSubmit: any(named: 'onSubmit'), + ), + ).thenAnswer((_) async {}); when(backboard.waitingMode).thenAnswer((_) async {}); when(cameraController.focusOnBackboard).thenAnswer((_) async {}); when(cameraController.focusOnGame).thenAnswer((_) async {}); @@ -61,13 +65,17 @@ void main() { GameState( score: 10, balls: 0, - activatedBonusLetters: const [], bonusHistory: const [], activatedDashNests: const {}, ), ); - verify(backboard.gameOverMode).called(1); + verify( + () => backboard.gameOverMode( + score: 0, + onSubmit: any(named: 'onSubmit'), + ), + ).called(1); verify(cameraController.focusOnBackboard).called(1); }, ); diff --git a/test/game/components/google_word_test.dart b/test/game/components/google_word_test.dart new file mode 100644 index 00000000..fee7bdd0 --- /dev/null +++ b/test/game/components/google_word_test.dart @@ -0,0 +1,73 @@ +// 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('GoogleWord', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameTester = FlameTester(EmptyPinballTestGame.new); + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + + 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)); + }, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.googleWord to the game when all letters are activated', + setUp: (game, _) async { + final ball = Ball(baseColor: const Color(0xFFFF0000)); + final googleWord = GoogleWord(position: Vector2.zero()); + await game.ensureAddAll([googleWord, ball]); + + final letters = googleWord.children.whereType(); + expect(letters, isNotEmpty); + for (final letter in letters) { + beginContact(game, letter, ball); + await game.ready(); + + if (letter == letters.last) { + verify( + () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), + ).called(1); + } else { + verifyNever( + () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), + ); + } + } + }, + ); + }); +} diff --git a/test/game/components/score_effect_controller_test.dart b/test/game/components/score_effect_controller_test.dart index 241f040b..9d2b5310 100644 --- a/test/game/components/score_effect_controller_test.dart +++ b/test/game/components/score_effect_controller_test.dart @@ -30,7 +30,6 @@ void main() { const current = GameState( score: 10, balls: 3, - activatedBonusLetters: [], bonusHistory: [], activatedDashNests: {}, ); @@ -44,7 +43,6 @@ void main() { const current = GameState( score: 10, balls: 3, - activatedBonusLetters: [], bonusHistory: [], activatedDashNests: {}, ); @@ -70,7 +68,6 @@ void main() { const state = GameState( score: 10, balls: 3, - activatedBonusLetters: [], bonusHistory: [], activatedDashNests: {}, ); @@ -89,7 +86,6 @@ void main() { const GameState( score: 10, balls: 3, - activatedBonusLetters: [], bonusHistory: [], activatedDashNests: {}, ), @@ -99,7 +95,6 @@ void main() { const GameState( score: 14, balls: 3, - activatedBonusLetters: [], bonusHistory: [], activatedDashNests: {}, ), diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart index da8d8404..692af291 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_fire_zone_test.dart @@ -13,7 +13,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); + final flameTester = FlameTester(EmptyPinballTestGame.new); group('SparkyFireZone', () { flameTester.test( @@ -59,7 +59,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index f8e7483c..63a39991 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,7 +110,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index ef55b399..c29ee315 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -12,8 +12,8 @@ import '../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.new); - final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); + final flameTester = FlameTester(PinballTestGame.new); + final debugModeFlameTester = FlameTester(DebugPinballTestGame.new); group('PinballGame', () { // TODO(alestiago): test if [PinballGame] registers @@ -88,7 +88,7 @@ void main() { }); final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballGameTest.new, + gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); @@ -206,7 +206,7 @@ void main() { final debugModeFlameBlocTester = FlameBlocTester( - gameBuilder: DebugPinballGameTest.new, + gameBuilder: DebugPinballTestGame.new, blocBuilder: () => gameBloc, ); diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 953b89eb..2d5f50d9 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -12,7 +12,6 @@ void main() { const initialState = GameState( score: 10, balls: 2, - activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 7a1419fb..85f9cfc3 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -11,7 +11,7 @@ import '../../helpers/helpers.dart'; void main() { const theme = PinballTheme(characterTheme: DashTheme()); - final game = PinballGameTest(); + final game = PinballTestGame(); group('PinballGamePage', () { testWidgets('renders PinballGameView', (tester) async { 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..12e6d366 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -64,8 +64,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 {} 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/extensions.dart b/test/helpers/test_games.dart similarity index 56% rename from test/helpers/extensions.dart rename to test/helpers/test_games.dart index 8e054fe0..3747a231 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/test_games.dart @@ -1,12 +1,20 @@ // ignore_for_file: must_call_super +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'helpers.dart'; -class PinballGameTest extends PinballGame { - PinballGameTest() +class TestGame extends Forge2DGame with FlameBloc { + TestGame() { + images.prefix = ''; + } +} + +class PinballTestGame extends PinballGame { + PinballTestGame() : super( audio: MockPinballAudio(), theme: const PinballTheme( @@ -15,8 +23,8 @@ class PinballGameTest extends PinballGame { ); } -class DebugPinballGameTest extends DebugPinballGame { - DebugPinballGameTest() +class DebugPinballTestGame extends DebugPinballGame { + DebugPinballTestGame() : super( audio: MockPinballAudio(), theme: const PinballTheme( @@ -25,7 +33,7 @@ class DebugPinballGameTest extends DebugPinballGame { ); } -class EmptyPinballGameTest extends PinballGameTest { +class EmptyPinballTestGame extends PinballTestGame { @override Future onLoad() async {} }