diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index de3f0c1a..c1fa5c90 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; @@ -10,6 +12,8 @@ class GameBloc extends Bloc { on(_onGameScoreDecreased); on(_onGameOver); on(_onGameSectionCompleted); + on(_onGameWingsIncreased); + on(_onGameWingsDecreased); } void _onGameScoreIncreased( @@ -34,6 +38,28 @@ class GameBloc extends Bloc { ); } + void _onGameWingsIncreased( + GameWingsIncreased event, + Emitter emit, + ) { + emit( + state.copyWith( + wingsQty: min(state.wingsQty + event.by, GameState.maxWingsQty), + ), + ); + } + + void _onGameWingsDecreased( + GameWingsDecreased event, + Emitter emit, + ) { + emit( + state.copyWith( + wingsQty: state.wingsQty - event.by, + ), + ); + } + void _onGameOver( GameOver event, Emitter emit, diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index ada714f5..d7433ac5 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -25,6 +25,24 @@ final class GameScoreDecreased extends GameEvent { List get props => [by]; } +final class GameWingsIncreased extends GameEvent { + const GameWingsIncreased({this.by = 1}); + + final int by; + + @override + List get props => [by]; +} + +final class GameWingsDecreased extends GameEvent { + const GameWingsDecreased({this.by = 1}); + + final int by; + + @override + List get props => [by]; +} + final class GameOver extends GameEvent { const GameOver(); } diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index ef93f9f3..4d3e7f07 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -3,31 +3,38 @@ part of 'game_bloc.dart'; class GameState extends Equatable { const GameState({ required this.score, + required this.wingsQty, required this.currentLevel, required this.currentSection, }); const GameState.initial() : score = 0, + wingsQty = 0, currentLevel = 1, currentSection = 0; final int score; + final int wingsQty; final int currentLevel; final int currentSection; + static const maxWingsQty = 5; + GameState copyWith({ int? score, + int? wingsQty, int? currentLevel, int? currentSection, }) { return GameState( score: score ?? this.score, + wingsQty: wingsQty ?? this.wingsQty, currentLevel: currentLevel ?? this.currentLevel, currentSection: currentSection ?? this.currentSection, ); } @override - List get props => [score, currentLevel, currentSection]; + List get props => [score, wingsQty, currentLevel, currentSection]; } diff --git a/lib/game/entities/player.dart b/lib/game/entities/player.dart index b6963b79..facd305a 100644 --- a/lib/game/entities/player.dart +++ b/lib/game/entities/player.dart @@ -27,7 +27,6 @@ class Player extends JumperCharacter { late final PlayerStateBehavior stateBehavior = findBehavior(); - bool hasGoldenFeather = false; bool isPlayerInvincible = false; bool isPlayerTeleporting = false; bool isPlayerRespawning = false; @@ -42,6 +41,8 @@ class Player extends JumperCharacter { @override int get priority => 1; + bool get hasGoldenFeather => gameRef.state.wingsQty > 0; + void jumpEffects() { final jumpSound = hasGoldenFeather ? Sfx.phoenixJump : Sfx.jump; gameRef.audioController.playSfx(jumpSound); @@ -125,7 +126,9 @@ class Player extends JumperCharacter { } void addPowerUp() { - hasGoldenFeather = true; + gameRef.gameBloc.add( + const GameWingsIncreased(), + ); if (stateBehavior.state == DashState.idle) { stateBehavior.state = DashState.phoenixIdle; @@ -172,7 +175,10 @@ class Player extends JumperCharacter { } // If player has a golden feather, use it to avoid death. - hasGoldenFeather = false; + gameRef.gameBloc.add( + const GameWingsDecreased(), + ); + return respawn(); } @@ -213,7 +219,10 @@ class Player extends JumperCharacter { } // If player has a golden feather, use it to avoid death. - hasGoldenFeather = false; + gameRef.gameBloc.add( + const GameWingsDecreased(), + ); + return respawn(); } } diff --git a/lib/game/widgets/score_label.dart b/lib/game/widgets/score_label.dart index 23518724..bbce2e59 100644 --- a/lib/game/widgets/score_label.dart +++ b/lib/game/widgets/score_label.dart @@ -12,8 +12,8 @@ class ScoreLabel extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; final textTheme = Theme.of(context).textTheme; - final score = context.select( - (GameBloc bloc) => bloc.state.score, + final state = context.select( + (GameBloc bloc) => (score: bloc.state.score, wings: bloc.state.wingsQty), ); return SafeArea( @@ -37,7 +37,18 @@ class ScoreLabel extends StatelessWidget { ), const SizedBox(width: 10), Text( - l10n.gameScoreLabel(score), + l10n.gameScoreLabel(state.score), + style: textTheme.titleLarge?.copyWith( + color: const Color(0xFF4D5B92), + ), + ), + const SizedBox(width: 10), + Assets.images.powerfulWingsInstruction.image( + width: 40, + height: 40, + ), + Text( + l10n.wingsQtyLabel(state.wings), style: textTheme.titleLarge?.copyWith( color: const Color(0xFF4D5B92), ), diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3e4486c7..5f66d983 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -185,6 +185,15 @@ } } }, + "wingsQtyLabel": "{points}x Wings", + "@wingsQtyLabel": { + "description": "Text shown on the game wings label", + "placeholders": { + "points": { + "type": "int" + } + } + }, "leaderboardPageLeaderboardHeadline": "Leaderboard", "@leaderboardPageLeaderboardHeadline": { "description": "Text shown in the leaderboard page headline" diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 31039f16..36fe47e1 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -13,6 +13,7 @@ void main() { score: 100, currentLevel: 2, currentSection: 2, + wingsQty: 2, ), act: (bloc) => bloc.add(GameOver()), expect: () => const [GameState.initial()], @@ -35,5 +36,34 @@ void main() { act: (bloc) => bloc.add(GameScoreDecreased(by: 2)), expect: () => [const GameState.initial().copyWith(score: 98)], ); + + blocTest( + 'emits GameState with wings increased correctly ' + 'when dash collect a wing', + build: GameBloc.new, + seed: () => const GameState.initial().copyWith(wingsQty: 1), + act: (bloc) => bloc.add(GameWingsIncreased()), + expect: () => [const GameState.initial().copyWith(wingsQty: 2)], + ); + + blocTest( + 'emits GameState with wings decreased correctly ' + 'when dash lost a wing', + build: GameBloc.new, + seed: () => const GameState.initial().copyWith(wingsQty: 1), + act: (bloc) => bloc.add(GameWingsDecreased()), + expect: () => [const GameState.initial().copyWith(wingsQty: 0)], + ); + + blocTest( + 'not emits GameState when dash collect a ' + 'new wing and exceed the max wings qty', + build: GameBloc.new, + seed: () => const GameState.initial().copyWith( + wingsQty: GameState.maxWingsQty, + ), + act: (bloc) => bloc.add(GameWingsIncreased()), + expect: () => [], + ); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index f22c083c..d3ec3f67 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -21,7 +21,7 @@ void main() { test('returns object with updated score when score is passed', () { expect( GameState.initial().copyWith(score: 100), - GameState(score: 100, currentLevel: 1, currentSection: 0), + GameState(score: 100, currentLevel: 1, currentSection: 0, wingsQty: 0), ); }); @@ -30,7 +30,7 @@ void main() { () { expect( GameState.initial().copyWith(currentLevel: 2), - GameState(score: 0, currentLevel: 2, currentSection: 0), + GameState(score: 0, currentLevel: 2, currentSection: 0, wingsQty: 0), ); }, ); @@ -38,7 +38,14 @@ void main() { test('returns object with updated currentSection when score is passed', () { expect( GameState.initial().copyWith(currentSection: 3), - GameState(score: 0, currentLevel: 1, currentSection: 3), + GameState(score: 0, currentLevel: 1, currentSection: 3, wingsQty: 0), + ); + }); + + test('returns object with updated wings when wings is passed', () { + expect( + GameState.initial().copyWith(wingsQty: 2), + GameState(score: 0, currentLevel: 1, currentSection: 0, wingsQty: 2), ); }); });