diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 2812a049..fa948320 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -49,6 +49,11 @@ class GameState extends Equatable { /// Determines when the player has only one ball left. bool get isLastBall => balls == 1; + /// Shortcut method to check if the given [i] + /// is activated on the state + bool isLetterActivated(int i) => + activatedBonusLetters.contains(i); + GameState copyWith({ int? score, int? balls, diff --git a/lib/game/components/bonus_letter.dart b/lib/game/components/bonus_letter.dart index 0296d665..4dbdd42e 100644 --- a/lib/game/components/bonus_letter.dart +++ b/lib/game/components/bonus_letter.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_renaming_method_parameters 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'; @@ -13,7 +14,6 @@ import 'package:pinball/game/game.dart'; /// {@endtemplate} class BonusLetter extends BodyComponent with BlocComponent { - /// {@macro bonus_letter} BonusLetter({ required Vector2 position, @@ -22,14 +22,14 @@ class BonusLetter extends BodyComponent }) : _position = position, _letter = letter, _index = index { - paint = _disablePaint; + paint = Paint()..color = _disableColor; } /// The area size of this bonus letter static final areaSize = Vector2.all(4); - static final _activePaint = Paint()..color = Colors.green; - static final _disablePaint = Paint()..color = Colors.red; + static const _activeColor = Colors.green; + static const _disableColor = Colors.red; final Vector2 _position; final String _letter; @@ -41,7 +41,7 @@ class BonusLetter extends BodyComponent await add( TextComponent( - position: Vector2(-1, 1), + position: Vector2(-1, -1), text: _letter, textRenderer: TextPaint( style: const TextStyle(fontSize: 2, color: Colors.white), @@ -64,12 +64,35 @@ class BonusLetter extends BodyComponent 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); + + final color = isActive ? _activeColor : _disableColor; + + add( + ColorEffect( + color, + const Offset(0, 1), + EffectController(duration: 0.25), + ), + ); + } + /// When called, will activate this letter, if still not activated void activate() { - // TODO - //gameRef.read().add(BonusLetterActivated(_index)); - - paint = _activePaint; + final isActive = state?.isLetterActivated(_index) ?? false; + if (!isActive) { + gameRef.read().add(BonusLetterActivated(_index)); + } } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2e85c535..09fba8ec 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -130,12 +130,3 @@ class DebugPinballGame extends PinballGame with TapDetector { add(Ball(position: info.eventPosition.game)); } } - -class DebugPinballGame extends PinballGame with TapDetector { - DebugPinballGame({required PinballTheme theme}) : super(theme: theme); - - @override - void onTapUp(TapUpInfo info) { - add(Ball(position: info.eventPosition.game)); - } -} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 4af2168e..21bd4074 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -63,6 +63,8 @@ class _PinballGameViewState extends State { @override Widget build(BuildContext context) { return BlocListener( + listenWhen: (previous, current) => + previous.isGameOver != current.isGameOver, listener: (context, state) { if (state.isGameOver) { showDialog( diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 7b060984..8ab72e6c 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -126,6 +126,34 @@ void main() { ); }); + group('isLetterActivated', () { + test( + 'is true when the letter is activated', + () { + const gameState = GameState( + balls: 3, + score: 0, + activatedBonusLetters: [1], + bonusHistory: [], + ); + expect(gameState.isLetterActivated(1), isTrue); + }, + ); + + test( + 'is false when the letter is not activated', + () { + const gameState = GameState( + balls: 3, + score: 0, + activatedBonusLetters: [1], + bonusHistory: [], + ); + expect(gameState.isLetterActivated(0), isFalse); + }, + ); + }); + group('copyWith', () { test( 'throws AssertionError ' diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 0b115055..cff2e65b 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -114,7 +114,7 @@ void main() { (game, tester) async { await game.ready(); - game.children.whereType().first.removeFromParent(); + game.children.whereType().first.lost(); await game.ready(); // Making sure that all additions are done expect( diff --git a/test/game/components/bonus_letter_test.dart b/test/game/components/bonus_letter_test.dart new file mode 100644 index 00000000..b0a7513f --- /dev/null +++ b/test/game/components/bonus_letter_test.dart @@ -0,0 +1,221 @@ +// 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_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('BonusLetter', () { + final flameTester = FlameTester(PinballGameTest.create); + + flameTester.test( + 'loads correctly', + (game) async { + final bonusLetter = BonusLetter( + position: Vector2.zero(), + letter: 'G', + index: 0, + ); + await game.ensureAdd(bonusLetter); + await game.ready(); + + expect(game.contains(bonusLetter), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final bonusLetter = BonusLetter( + position: position, + letter: 'G', + index: 0, + ); + await game.ensureAdd(bonusLetter); + game.contains(bonusLetter); + + expect(bonusLetter.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final bonusLetter = BonusLetter( + position: Vector2.zero(), + letter: 'G', + index: 0, + ); + await game.ensureAdd(bonusLetter); + + expect(bonusLetter.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final bonusLetter = BonusLetter( + position: Vector2.zero(), + letter: 'G', + index: 0, + ); + await game.ensureAdd(bonusLetter); + + expect(bonusLetter.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'is sensor', + (game) async { + final bonusLetter = BonusLetter( + position: Vector2.zero(), + 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( + position: Vector2.zero(), + 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(2)); + }, + ); + }); + + group('bonus letter activation', () { + final gameBloc = MockGameBloc(); + + setUp(() { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final tester = flameBlocTester(gameBloc: gameBloc); + + tester.widgetTest( + 'adds BonusLetterActivated to GameBloc when not activated', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.activate(); + await tester.pump(); + + verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); + }, + ); + + tester.widgetTest( + "don't add BonusLetterActivated to GameBloc when is already activated", + (game, tester) async { + const state = GameState( + score: 0, + balls: 2, + activatedBonusLetters: [0], + bonusHistory: [], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + await game.ready(); + + game.children.whereType().first.activate(); + await game.ready(); // Making sure that all additions are done + + verifyNever(() => gameBloc.add(const BonusLetterActivated(0))); + }, + ); + + tester.widgetTest( + 'adds a ColorEffect when it gets activated', + (game, tester) async { + await game.ready(); + await tester.pump(); + + const state = GameState( + score: 0, + balls: 2, + activatedBonusLetters: [0], + bonusHistory: [], + ); + + final bonusLetter = game.children.whereType().first; + + bonusLetter.onNewState(state); + await tester.pump(); + + expect( + bonusLetter.children.whereType().length, + equals(1), + ); + }, + ); + + tester.widgetTest( + 'only listen when there an change on the letter status', + (game, tester) async { + await game.ready(); + await tester.pump(); + + const state = GameState( + score: 0, + balls: 2, + activatedBonusLetters: [0], + bonusHistory: [], + ); + + final bonusLetter = game.children.whereType().first; + + expect( + bonusLetter.listenWhen(const GameState.initial(), state), + isTrue, + ); + }, + ); + }); + + group('BonusLetterBallContactCallback', () { + test('calls ball.activate', () { + final ball = MockBall(); + final bonusLetter = MockBonusLetter(); + + final contactCallback = BonusLetterBallContactCallback(); + contactCallback.begin(ball, bonusLetter, MockContact()); + + verify(bonusLetter.activate).called(1); + }); + }); + }); +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 9de36cde..dcf0c001 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -94,7 +94,7 @@ void main() { whenListen( gameBloc, Stream.value(state), - initialState: state, + initialState: GameState.initial(), ); await tester.pumpApp( diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 46886752..c1c59377 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -37,3 +37,5 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { class MockTapUpInfo extends Mock implements TapUpInfo {} class MockEventPosition extends Mock implements EventPosition {} + +class MockBonusLetter extends Mock implements BonusLetter {}