mirror of https://github.com/flutter/pinball.git
parent
19bfd035cc
commit
26cc086105
@ -1,46 +1,115 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/gen/gen.dart';
|
||||
|
||||
/// {@template game_hud}
|
||||
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
|
||||
/// [GameState.balls].
|
||||
/// Overlay of a [PinballGame] that displays the current [GameState.score],
|
||||
/// [GameState.balls] and animation when the player get the bonus.
|
||||
/// {@endtemplate}
|
||||
class GameHud extends StatelessWidget {
|
||||
class GameHud extends StatefulWidget {
|
||||
/// {@macro game_hud}
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<GameBloc>().state;
|
||||
|
||||
return Container(
|
||||
color: Colors.redAccent,
|
||||
width: 200,
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${state.score}',
|
||||
style: Theme.of(context).textTheme.headline3,
|
||||
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
|
||||
|
||||
return _ScoreViewDecoration(
|
||||
child: SizedBox(
|
||||
height: _width / _ratio,
|
||||
width: _width,
|
||||
child: BlocListener<GameBloc, GameState>(
|
||||
listenWhen: _listenWhen,
|
||||
listener: (_, __) => setState(() => showAnimation = true),
|
||||
child: AnimatedSwitcher(
|
||||
duration: kThemeAnimationDuration,
|
||||
child: showAnimation && !isGameOver
|
||||
? _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),
|
||||
child: CircleAvatar(
|
||||
radius: 8,
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool _listenWhen(GameState previous, GameState current) {
|
||||
final previousCount = previous.bonusHistory.length;
|
||||
final currentCount = current.bonusHistory.length;
|
||||
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…
Reference in new issue