fix: merge conflict on backbox bloc fixed

pull/359/head
RuiAlonso 3 years ago
commit 2c7a21576d

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@ -34,8 +34,11 @@ class Backbox extends PositionComponent with ZIndex {
anchor = Anchor.bottomCenter;
zIndex = ZIndexes.backbox;
_bloc.add(LeaderboardRequested());
await add(_BackboxSpriteComponent());
await add(_display = Component());
_build(_bloc.state);
_subscription = _bloc.stream.listen((state) {
_display.children.removeWhere((_) => true);
@ -52,6 +55,8 @@ class Backbox extends PositionComponent with ZIndex {
void _build(BackboxState state) {
if (state is LoadingState) {
_display.add(LoadingDisplay());
} else if (state is LeaderboardSuccessState) {
_display.add(LeaderboardDisplay(entries: state.entries));
} else if (state is InitialsFormState) {
_display.add(
InitialsInputDisplay(

@ -19,6 +19,7 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<ShareScoreRequested>(_onScoreShareRequested);
on<LeaderboardRequested>(_onLeaderboardRequested);
}
final LeaderboardRepository _leaderboardRepository;
@ -73,4 +74,20 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
),
);
}
Future<void> _onLeaderboardRequested(
LeaderboardRequested event,
Emitter<BackboxState> emit,
) async {
try {
emit(LoadingState());
final entries = await _leaderboardRepository.fetchTop10Leaderboard();
emit(LeaderboardSuccessState(entries: entries));
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(LeaderboardFailureState());
}
}
}

@ -75,3 +75,9 @@ class ShareScoreRequested extends BackboxEvent {
@override
List<Object?> get props => [score, initials, character];
}
/// Event that triggers the fetching of the leaderboard
class LeaderboardRequested extends BackboxEvent {
@override
List<Object?> get props => [];
}

@ -14,10 +14,18 @@ class LoadingState extends BackboxState {
List<Object?> get props => [];
}
/// {@template leaderboard_success_state}
/// State when the leaderboard was successfully loaded.
/// {@endtemplate}
class LeaderboardSuccessState extends BackboxState {
/// {@macro leaderboard_success_state}
const LeaderboardSuccessState({required this.entries});
/// Current entries
final List<LeaderboardEntryData> entries;
@override
List<Object?> get props => [];
List<Object?> get props => [entries];
}
/// State when the leaderboard failed to load.

@ -2,4 +2,5 @@ export 'game_over_info_display.dart';
export 'initials_input_display.dart';
export 'initials_submission_failure_display.dart';
export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart';
export 'loading_display.dart';

@ -0,0 +1,120 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/models/leader_board_entry.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
final _titleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 2,
color: PinballColors.red,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template leaderboard_display}
/// Component that builds the leaderboard list of the Backbox.
/// {@endtemplate}
class LeaderboardDisplay extends PositionComponent with HasGameRef {
/// {@macro leaderboard_display}
LeaderboardDisplay({required List<LeaderboardEntryData> entries})
: _entries = entries;
final List<LeaderboardEntryData> _entries;
double _calcY(int i) => (i * 3.2) + 3.2;
static const _columns = [-15.0, 0.0, 15.0];
String _rank(int number) {
switch (number) {
case 1:
return '${number}st';
case 2:
return '${number}nd';
case 3:
return '${number}rd';
default:
return '${number}th';
}
}
@override
Future<void> onLoad() async {
position = Vector2(0, -30);
final l10n = readProvider<AppLocalizations>();
final ranking = _entries.take(5).toList();
await add(
PositionComponent(
position: Vector2(0, 4),
children: [
PositionComponent(
children: [
TextComponent(
text: l10n.rank,
textRenderer: _titleTextPaint,
position: Vector2(_columns[0], 0),
anchor: Anchor.center,
),
TextComponent(
text: l10n.score,
textRenderer: _titleTextPaint,
position: Vector2(_columns[1], 0),
anchor: Anchor.center,
),
TextComponent(
text: l10n.name,
textRenderer: _titleTextPaint,
position: Vector2(_columns[2], 0),
anchor: Anchor.center,
),
],
),
for (var i = 0; i < ranking.length; i++)
PositionComponent(
children: [
TextComponent(
text: _rank(i + 1),
textRenderer: _bodyTextPaint,
position: Vector2(_columns[0], _calcY(i)),
anchor: Anchor.center,
),
TextComponent(
text: ranking[i].score.formatScore(),
textRenderer: _bodyTextPaint,
position: Vector2(_columns[1], _calcY(i)),
anchor: Anchor.center,
),
SpriteComponent.fromImage(
gameRef.images.fromCache(
ranking[i].character.toTheme.leaderboardIcon.keyName,
),
anchor: Anchor.center,
size: Vector2(1.8, 1.8),
position: Vector2(_columns[2] - 2.5, _calcY(i) + .25),
),
TextComponent(
text: ranking[i].playerInitials,
textRenderer: _bodyTextPaint,
position: Vector2(_columns[2] + 1, _calcY(i)),
anchor: Anchor.center,
),
],
),
],
),
);
}
}

@ -114,14 +114,6 @@ class PinballGameLoadedView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return StartGameListener(
child: Stack(
children: [
@ -149,16 +141,36 @@ class PinballGameLoadedView extends StatelessWidget {
},
),
),
Positioned(
top: 0,
left: clampedMargin,
child: Visibility(
visible: isPlaying,
child: const GameHud(),
),
),
const _PositionedGameHud(),
],
),
);
}
}
class _PositionedGameHud extends StatelessWidget {
const _PositionedGameHud({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
final isGameOver = context.select(
(GameBloc bloc) => bloc.state.status.isGameOver,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return Positioned(
top: 0,
left: clampedMargin,
child: Visibility(
visible: isPlaying && !isGameOver,
child: const GameHud(),
),
);
}
}

@ -14,6 +14,7 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
@ -53,6 +54,14 @@ class $AssetsImagesComponentsGen {
const AssetGenImage('assets/images/components/space.png');
}
class $AssetsImagesLinkBoxGen {
const $AssetsImagesLinkBoxGen();
/// File path: assets/images/link_box/info_icon.png
AssetGenImage get infoIcon =>
const AssetGenImage('assets/images/link_box/info_icon.png');
}
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();

@ -120,6 +120,46 @@
"@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O"
},
"linkBoxTitle": "Resources",
"@linkBoxTitle": {
"description": "Text shown on the link box title section."
},
"linkBoxMadeWithText": "Made with ",
"@linkBoxMadeWithText": {
"description": "Text shown on the link box which mentions technologies used to build the app."
},
"linkBoxFlutterLinkText": "Flutter",
"@linkBoxFlutterLinkText": {
"description": "Text on the link shown on the link box which navigates to the Flutter page"
},
"linkBoxFirebaseLinkText": "Firebase",
"@linkBoxFirebaseLinkText": {
"description": "Text on the link shown on the link box which navigates to the Firebase page"
},
"linkBoxOpenSourceCode": "Open Source Code",
"@linkBoxOpenSourceCode": {
"description": "Text shown on the link box which redirects to github project"
},
"linkBoxGoogleIOText": "Google I/O",
"@linkBoxGoogleIOText": {
"description": "Text shown on the link box which mentions Google I/O"
},
"linkBoxFlutterGames": "Flutter Games",
"@linkBoxFlutterGames": {
"description": "Text shown on the link box which redirects to flutter games article"
},
"linkBoxHowItsMade": "How its made",
"@linkBoxHowItsMade": {
"description": "Text shown on the link box which redirects to Very Good Blog article"
},
"linkBoxTermsOfService": "Terms of Service",
"@linkBoxTermsOfService": {
"description": "Text shown on the link box which redirect to terms of service"
},
"linkBoxPrivacyPolicy": "Privacy Policy",
"@linkBoxPrivacyPolicy": {
"description": "Text shown on the link box which redirect to privacy policy"
},
"loading": "Loading",
"@loading": {
"description": "Text shown to indicate loading times"

@ -37,6 +37,13 @@ abstract class PinballTextStyle {
fontFamily: _primaryFontFamily,
);
static const headline5 = TextStyle(
color: PinballColors.white,
fontSize: 14,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const subtitle2 = TextStyle(
color: PinballColors.white,
fontSize: 16,

@ -16,6 +16,7 @@ class PinballTheme {
headline2: PinballTextStyle.headline2,
headline3: PinballTextStyle.headline3,
headline4: PinballTextStyle.headline4,
headline5: PinballTextStyle.headline5,
subtitle1: PinballTextStyle.subtitle1,
subtitle2: PinballTextStyle.subtitle2,
);

@ -17,7 +17,7 @@ dependencies:
flame: ^1.1.1
flame_bloc: ^1.4.0
flame_forge2d:
git:
git:
url: https://github.com/flame-engine/flame/
path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
@ -60,6 +60,7 @@ flutter:
- assets/images/components/
- assets/images/bonus_animation/
- assets/images/score/
- assets/images/link_box/
flutter_gen:
line_length: 80

@ -42,6 +42,9 @@ class _TestGame extends Forge2DGame {
AndroidAcres child, {
required GameBloc gameBloc,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,

@ -81,6 +81,9 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get name => '';
@override
String get rank => '';
@override
String get enterInitials => '';
@ -129,7 +132,7 @@ void main() {
bloc = _MockBackboxBloc();
whenListen(
bloc,
Stream.value(LoadingState()),
Stream<BackboxState>.empty(),
initialState: LoadingState(),
);
});
@ -144,6 +147,16 @@ void main() {
},
);
flameTester.test(
'adds LeaderboardRequested when loaded',
(game) async {
final backbox = Backbox.test(bloc: bloc);
await game.pump(backbox);
verify(() => bloc.add(LeaderboardRequested())).called(1);
},
);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
@ -196,7 +209,7 @@ void main() {
);
whenListen(
bloc,
Stream.value(state),
Stream<BackboxState>.empty(),
initialState: state,
);
final backbox = Backbox.test(bloc: bloc);
@ -216,7 +229,7 @@ void main() {
);
flameTester.test(
'adds InfoDisplay on InitialsSuccessState',
'adds GameOverInfoDisplay on InitialsSuccessState',
(game) async {
final state = InitialsSuccessState(
score: 100,
@ -225,7 +238,7 @@ void main() {
);
whenListen(
bloc,
Stream.value(state),
const Stream<InitialsSuccessState>.empty(),
initialState: state,
);
final backbox = Backbox.test(bloc: bloc);
@ -275,7 +288,7 @@ void main() {
(game) async {
whenListen(
bloc,
Stream.value(InitialsFailureState()),
Stream<BackboxState>.empty(),
initialState: InitialsFailureState(),
);
final backbox = Backbox.test(bloc: bloc);
@ -291,6 +304,25 @@ void main() {
},
);
flameTester.test(
'adds LeaderboardDisplay on LeaderboardSuccessState',
(game) async {
whenListen(
bloc,
Stream<BackboxState>.empty(),
initialState: LeaderboardSuccessState(entries: const []),
);
final backbox = Backbox.test(bloc: bloc);
await game.pump(backbox);
expect(
game.descendants().whereType<LeaderboardDisplay>().length,
equals(1),
);
},
);
flameTester.test(
'closes the subscription when it is removed',
(game) async {

@ -93,26 +93,64 @@ void main() {
);
});
blocTest<BackboxBloc, BackboxState>(
'emits ShareState on ScoreShareRequested',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(
ShareScoreRequested(
score: 100,
initials: 'AAA',
character: AndroidTheme(),
),
),
expect: () => [
ShareState(
score: 100,
initials: 'AAA',
character: AndroidTheme(),
group('ShareScoreRequested', () {
blocTest<BackboxBloc, BackboxState>(
'emits ShareState',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(
ShareScoreRequested(
score: 100,
initials: 'AAA',
character: AndroidTheme(),
),
),
],
);
expect: () => [
ShareState(
score: 100,
initials: 'AAA',
character: AndroidTheme(),
),
],
);
});
group('LeaderboardRequested', () {
blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardSuccessState] when request succeeds',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
when(
() => leaderboardRepository.fetchTop10Leaderboard(),
).thenAnswer(
(_) async => [LeaderboardEntryData.empty],
);
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(LeaderboardRequested()),
expect: () => [
LoadingState(),
LeaderboardSuccessState(entries: const [LeaderboardEntryData.empty]),
],
);
blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardFailureState] when request fails',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
when(
() => leaderboardRepository.fetchTop10Leaderboard(),
).thenThrow(Exception('Error'));
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(LeaderboardRequested()),
expect: () => [
LoadingState(),
LeaderboardFailureState(),
],
);
});
});
}

@ -203,5 +203,15 @@ void main() {
);
});
});
group('LeaderboardRequested', () {
test('can be instantiated', () {
expect(LeaderboardRequested(), isNotNull);
});
test('supports value comparison', () {
expect(LeaderboardRequested(), equals(LeaderboardRequested()));
});
});
});
}

@ -1,6 +1,7 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball_theme/pinball_theme.dart';
@ -18,11 +19,30 @@ void main() {
group('LeaderboardSuccessState', () {
test('can be instantiated', () {
expect(LeaderboardSuccessState(), isNotNull);
expect(
LeaderboardSuccessState(entries: const []),
isNotNull,
);
});
test('supports value comparison', () {
expect(LeaderboardSuccessState(), equals(LeaderboardSuccessState()));
expect(
LeaderboardSuccessState(entries: const []),
equals(
LeaderboardSuccessState(entries: const []),
),
);
expect(
LeaderboardSuccessState(entries: const []),
isNot(
equals(
LeaderboardSuccessState(
entries: const [LeaderboardEntryData.empty],
),
),
),
);
});
});

@ -0,0 +1,109 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/leaderboard_display.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get rank => 'rank';
@override
String get score => 'score';
@override
String get name => 'name';
}
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
await super.onLoad();
images.prefix = '';
await images.load(const AndroidTheme().leaderboardIcon.keyName);
}
Future<void> pump(LeaderboardDisplay component) {
return ensureAdd(
FlameProvider.value(
_MockAppLocalizations(),
children: [component],
),
);
}
}
void main() {
group('LeaderboardDisplay', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
flameTester.test('renders the titles', (game) async {
await game.pump(LeaderboardDisplay(entries: const []));
final textComponents =
game.descendants().whereType<TextComponent>().toList();
expect(textComponents.length, equals(3));
expect(textComponents[0].text, equals('rank'));
expect(textComponents[1].text, equals('score'));
expect(textComponents[2].text, equals('name'));
});
flameTester.test('renders the entries', (game) async {
await game.pump(
LeaderboardDisplay(
entries: const [
LeaderboardEntryData(
playerInitials: 'AAA',
score: 123,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'BBB',
score: 1234,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'CCC',
score: 12345,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'DDD',
score: 12346,
character: CharacterType.android,
),
],
),
);
for (final text in [
'AAA',
'BBB',
'CCC',
'DDD',
'1st',
'2nd',
'3rd',
'4th'
]) {
expect(
game
.descendants()
.whereType<TextComponent>()
.where((textComponent) => textComponent.text == text)
.length,
equals(1),
);
}
});
});
}

@ -17,8 +17,11 @@ class _TestGame extends Forge2DGame {
]);
}
Future<void> pump(Multiballs child, {GameBloc? gameBloc}) {
return ensureAdd(
Future<void> pump(Multiballs child, {GameBloc? gameBloc}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc ?? GameBloc(),
children: [child],
@ -38,7 +41,9 @@ void main() {
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.pump(multiballs);
expect(game.descendants(), contains(multiballs));
},
verify: (game, tester) async {
expect(game.descendants().whereType<Multiballs>().length, equals(1));
},
);

@ -260,5 +260,36 @@ void main() {
findsOneWidget,
);
});
testWidgets('hide a hud on game over', (tester) async {
final startGameState = StartGameState.initial().copyWith(
status: StartGameStatus.play,
);
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
startGameBloc,
Stream.value(startGameState),
initialState: startGameState,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(
find.byType(GameHud),
findsNothing,
);
});
});
}

Loading…
Cancel
Save