feat: update GameHud

pull/206/head
arturplaczek 3 years ago
parent 19bfd035cc
commit 26cc086105

@ -1,46 +1,115 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
/// {@template game_hud} /// {@template game_hud}
/// Overlay of a [PinballGame] that displays the current [GameState.score] and /// Overlay of a [PinballGame] that displays the current [GameState.score],
/// [GameState.balls]. /// [GameState.balls] and animation when the player get the bonus.
/// {@endtemplate} /// {@endtemplate}
class GameHud extends StatelessWidget { class GameHud extends StatefulWidget {
/// {@macro game_hud} /// {@macro game_hud}
const GameHud({Key? key}) : super(key: key); const GameHud({Key? key}) : super(key: key);
@override
State<GameHud> createState() => _GameHudState();
}
class _GameHudState extends State<GameHud> {
bool showAnimation = false;
/// Ratio from sprite frame (width 500, height 144) w / h = ratio
static const _ratio = 3.47;
static const _width = 265.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final state = context.watch<GameBloc>().state; final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
return Container( return _ScoreViewDecoration(
color: Colors.redAccent, child: SizedBox(
width: 200, height: _width / _ratio,
height: 100, width: _width,
padding: const EdgeInsets.all(16), child: BlocListener<GameBloc, GameState>(
child: Row( listenWhen: _listenWhen,
mainAxisAlignment: MainAxisAlignment.spaceBetween, listener: (_, __) => setState(() => showAnimation = true),
children: [ child: AnimatedSwitcher(
Text( duration: kThemeAnimationDuration,
'${state.score}', child: showAnimation && !isGameOver
style: Theme.of(context).textTheme.headline3, ? _AnimationView(
onComplete: () {
if (mounted) {
setState(() => showAnimation = false);
}
},
)
: const ScoreView(),
), ),
Wrap( ),
direction: Axis.vertical, ),
children: [ );
for (var i = 0; i < state.balls; i++) }
const Padding(
padding: EdgeInsets.only(top: 6, right: 6), bool _listenWhen(GameState previous, GameState current) {
child: CircleAvatar( final previousCount = previous.bonusHistory.length;
radius: 8, final currentCount = current.bonusHistory.length;
backgroundColor: Colors.black, return previousCount != currentCount;
), }
), }
],
class _ScoreViewDecoration extends StatelessWidget {
const _ScoreViewDecoration({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
Assets.images.score.miniScoreBackground.path,
),
fit: BoxFit.cover,
), ),
], ),
child: child,
), ),
); );
} }
} }
class _AnimationView extends StatelessWidget {
const _AnimationView({
Key? key,
required this.onComplete,
}) : super(key: key);
final VoidCallback onComplete;
@override
Widget build(BuildContext context) {
final lastBonus = context.select(
(GameBloc bloc) => bloc.state.bonusHistory.last,
);
switch (lastBonus) {
case GameBonus.dashNest:
return BonusAnimation.dashNest(onCompleted: onComplete);
case GameBonus.sparkyTurboCharge:
return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete);
case GameBonus.dino:
return BonusAnimation.dino(onCompleted: onComplete);
case GameBonus.googleWord:
return BonusAnimation.google(onCompleted: onComplete);
case GameBonus.android:
return BonusAnimation.android(onCompleted: onComplete);
}
}
}

@ -1,83 +0,0 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('GameHud', () {
late GameBloc gameBloc;
const initialState = GameState(
score: 10,
balls: 2,
bonusHistory: [],
);
void _mockState(GameState state) {
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
}
Future<void> _pumpHud(WidgetTester tester) async {
await tester.pumpApp(
GameHud(),
gameBloc: gameBloc,
);
}
setUp(() {
gameBloc = MockGameBloc();
_mockState(initialState);
});
testWidgets(
'renders the current score',
(tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
},
);
testWidgets(
'renders the current ball number',
(tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
},
);
testWidgets('updates the score', (tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
_mockState(initialState.copyWith(score: 20));
await tester.pump();
expect(find.text('20'), findsOneWidget);
});
testWidgets('updates the ball number', (tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
_mockState(initialState.copyWith(balls: 1));
await tester.pump();
expect(
find.byType(CircleAvatar),
findsNWidgets(1),
);
});
});
}

@ -0,0 +1,136 @@
// ignore_for_file: prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import '../../../helpers/helpers.dart';
void main() {
group('GameHud', () {
late GameBloc gameBloc;
late StreamController<GameState> stateController;
const initialState = GameState(
score: 10,
balls: 2,
bonusHistory: [],
);
setUp(() async {
gameBloc = MockGameBloc();
stateController = StreamController<GameState>()..add(initialState);
await BonusAnimation.loadAssets();
whenListen(
gameBloc,
stateController.stream,
initialState: initialState,
);
});
// We cannot use pumpApp when we are testing animation because
// animation tests needs to be run and check in tester.runAsync
Future<void> _pumpAppWithWidget(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: BlocProvider<GameBloc>.value(
value: gameBloc,
child: GameHud(),
),
),
),
);
}
group('renders ScoreView widget', () {
testWidgets(
'with the score',
(tester) async {
await tester.pumpApp(
GameHud(),
gameBloc: gameBloc,
);
expect(find.text(initialState.score.toString()), findsOneWidget);
},
);
testWidgets(
'on game over',
(tester) async {
final state = initialState.copyWith(
bonusHistory: [GameBonus.dashNest],
);
stateController.add(state);
await tester.pumpApp(
GameHud(),
gameBloc: gameBloc,
);
expect(find.byType(ScoreView), findsOneWidget);
},
);
});
for (final gameBonus in GameBonus.values) {
testWidgets('renders BonusAnimation for $gameBonus', (tester) async {
await tester.runAsync(() async {
final state = initialState.copyWith(
bonusHistory: [gameBonus],
);
whenListen(
gameBloc,
Stream.value(state),
initialState: initialState,
);
await _pumpAppWithWidget(tester);
await tester.pump();
expect(find.byType(BonusAnimation), findsOneWidget);
});
});
}
testWidgets(
'is going back to ScoreView after the animation',
(tester) async {
await tester.runAsync(() async {
final state = initialState.copyWith(
bonusHistory: [GameBonus.dashNest],
);
whenListen(
gameBloc,
Stream.value(state),
initialState: initialState,
);
await _pumpAppWithWidget(tester);
await tester.pump();
await Future<void>.delayed(const Duration(seconds: 4));
await expectLater(find.byType(ScoreView), findsOneWidget);
});
},
);
});
}
Loading…
Cancel
Save