diff --git a/lib/app/app.dart b/lib/app/app.dart index 2b135918..f23ab3c8 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,8 +1 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - export 'view/app.dart'; diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 528954a6..d778b55b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - // ignore_for_file: public_member_api_docs import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/assets_manager/assets_manager.dart b/lib/assets_manager/assets_manager.dart new file mode 100644 index 00000000..438b75d1 --- /dev/null +++ b/lib/assets_manager/assets_manager.dart @@ -0,0 +1,2 @@ +export 'cubit/assets_manager_cubit.dart'; +export 'views/views.dart'; diff --git a/lib/game/assets_manager/cubit/assets_manager_cubit.dart b/lib/assets_manager/cubit/assets_manager_cubit.dart similarity index 100% rename from lib/game/assets_manager/cubit/assets_manager_cubit.dart rename to lib/assets_manager/cubit/assets_manager_cubit.dart diff --git a/lib/game/assets_manager/cubit/assets_manager_state.dart b/lib/assets_manager/cubit/assets_manager_state.dart similarity index 100% rename from lib/game/assets_manager/cubit/assets_manager_state.dart rename to lib/assets_manager/cubit/assets_manager_state.dart diff --git a/lib/assets_manager/views/assets_loading_page.dart b/lib/assets_manager/views/assets_loading_page.dart new file mode 100644 index 00000000..ddb76803 --- /dev/null +++ b/lib/assets_manager/views/assets_loading_page.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template assets_loading_page} +/// Widget used to indicate the loading progress of the different assets used +/// in the game +/// {@endtemplate} +class AssetsLoadingPage extends StatelessWidget { + /// {@macro assets_loading_page} + const AssetsLoadingPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final headline1 = Theme.of(context).textTheme.headline1; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.ioPinball, + style: headline1!.copyWith(fontSize: 80), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + AnimatedEllipsisText( + l10n.loading, + style: headline1, + ), + const SizedBox(height: 40), + FractionallySizedBox( + widthFactor: 0.8, + child: BlocBuilder( + builder: (context, state) { + return PinballLoadingIndicator(value: state.progress); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/assets_manager/views/views.dart b/lib/assets_manager/views/views.dart new file mode 100644 index 00000000..8c60627f --- /dev/null +++ b/lib/assets_manager/views/views.dart @@ -0,0 +1 @@ +export 'assets_loading_page.dart'; diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index bbd87f0c..c5e42951 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - // ignore_for_file: public_member_api_docs import 'dart:async'; diff --git a/lib/game/game.dart b/lib/game/game.dart index 7de964eb..ad02533d 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,4 +1,3 @@ -export 'assets_manager/cubit/assets_manager_cubit.dart'; export 'bloc/game_bloc.dart'; export 'components/components.dart'; export 'game_assets.dart'; diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 9ac25cfe..4557c243 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -4,10 +4,12 @@ import 'package:flame/game.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; class PinballGamePage extends StatelessWidget { const PinballGamePage({ @@ -71,32 +73,13 @@ class PinballGameView extends StatelessWidget { final isLoading = context.select( (AssetsManagerCubit bloc) => bloc.state.progress != 1, ); - - return Scaffold( - backgroundColor: Colors.blue, - body: isLoading - ? const _PinballGameLoadingView() - : PinballGameLoadedView(game: game), - ); - } -} - -class _PinballGameLoadingView extends StatelessWidget { - const _PinballGameLoadingView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final loadingProgress = context.select( - (AssetsManagerCubit bloc) => bloc.state.progress, - ); - - return Padding( - padding: const EdgeInsets.all(24), - child: Center( - child: LinearProgressIndicator( - color: Colors.white, - value: loadingProgress, - ), + return Container( + decoration: const CrtBackground(), + child: Scaffold( + backgroundColor: PinballColors.transparent, + body: isLoading + ? const AssetsLoadingPage() + : PinballGameLoadedView(game: game), ), ); } diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index 3dc2c62b..e91698f5 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -3,8 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; import 'package:platform_helper/platform_helper.dart'; @@ -50,10 +52,13 @@ extension on Control { } Future showHowToPlayDialog(BuildContext context) { + final audio = context.read(); return showDialog( context: context, builder: (_) => HowToPlayDialog(), - ); + ).then((_) { + audio.ioPinballVoiceOver(); + }); } class HowToPlayDialog extends StatefulWidget { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 562d9b1f..5566066f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -20,7 +20,7 @@ "@flipperControls": { "description": "Text displayed on the how to play dialog with the flipper controls" }, - "tapAndHoldRocket": "Tap & Hold Rocket", + "tapAndHoldRocket": "Tap Rocket", "@tapAndHoldRocket": { "description": "Text displayed on the how to launch on mobile" }, @@ -123,5 +123,13 @@ "footerGoogleIOText": "Google I/O", "@footerGoogleIOText": { "description": "Text shown on the footer which mentions Google I/O" + }, + "loading": "Loading", + "@loading": { + "description": "Text shown to indicate loading times" + }, + "ioPinball": "I/O Pinball", + "@ioPinball": { + "description": "I/O Pinball - Name of the game" } } diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 548a81a6..0945f30f 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - // ignore_for_file: public_member_api_docs import 'package:flutter/widgets.dart'; diff --git a/lib/main_development.dart b/lib/main_development.dart index 529c66e2..21166057 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/main_production.dart b/lib/main_production.dart index 529c66e2..21166057 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 529c66e2..21166057 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; diff --git a/packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3 b/packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3 new file mode 100644 index 00000000..7829086c Binary files /dev/null and b/packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3 differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index fad57bd7..5bb8fea8 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -17,6 +17,7 @@ class $AssetsSfxGen { String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3'; String get google => 'assets/sfx/google.mp3'; + String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; } class Assets { diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index d589bcb2..134d8b33 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -88,6 +88,7 @@ class PinballAudio { await Future.wait([ _preCacheSingleAudio(_prefixFile(Assets.sfx.google)), + _preCacheSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)), _preCacheSingleAudio(_prefixFile(Assets.music.background)), ]); } @@ -102,6 +103,11 @@ class PinballAudio { _playSingleAudio(_prefixFile(Assets.sfx.google)); } + /// Plays the I/O Pinball voice over audio. + void ioPinballVoiceOver() { + _playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)); + } + /// Plays the background music void backgroundMusic() { _loopSingleAudio(_prefixFile(Assets.music.background)); diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index ef91c36b..916d0f34 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -141,6 +141,11 @@ void main() { () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/google.mp3'), ).called(1); + verify( + () => preCacheSingleAudio.onCall( + 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', + ), + ).called(1); verify( () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/music/background.mp3'), @@ -209,6 +214,19 @@ void main() { }); }); + group('ioPinballVoiceOver', () { + test('plays the correct file', () async { + await audio.load(); + audio.ioPinballVoiceOver(); + + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}', + ), + ).called(1); + }); + }); + group('backgroundMusic', () { test('plays the correct file', () async { await audio.load(); diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart similarity index 88% rename from packages/pinball_components/lib/src/components/ball.dart rename to packages/pinball_components/lib/src/components/ball/ball.dart index 7469396a..4f913c2c 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:math' as math; -import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template ball} @@ -20,6 +20,7 @@ class Ball extends BodyComponent renderBody: false, children: [ _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + BallScalingBehavior(), ], ) { // TODO(ruimiguel): while developing Ball can be launched by clicking mouse, @@ -30,6 +31,15 @@ class Ball extends BodyComponent layer = Layer.board; } + /// Creates a [Ball] without any behaviors. + /// + /// This can be used for testing [Ball]'s behaviors in isolation. + @visibleForTesting + Ball.test({required this.baseColor}) + : super( + children: [_BallSpriteComponent()], + ); + /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); @@ -81,26 +91,9 @@ class Ball extends BodyComponent void update(double dt) { super.update(dt); - _rescaleSize(); _setPositionalGravity(); } - void _rescaleSize() { - final boardHeight = BoardDimensions.bounds.height; - const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; - - final standardizedYPosition = body.position.y + (boardHeight / 2); - - final scaleFactor = maxShrinkValue + - ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); - - body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; - - // TODO(alestiago): Revisit and see if there's a better way to do this. - final spriteComponent = firstChild<_BallSpriteComponent>(); - spriteComponent?.scale = Vector2.all(scaleFactor); - } - void _setPositionalGravity() { final defaultGravity = gameRef.world.gravity.y; final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2; diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart new file mode 100644 index 00000000..7fc06fb1 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Scales the ball's body and sprite according to its position on the board. +class BallScalingBehavior extends Component with ParentIsA { + @override + void update(double dt) { + super.update(dt); + final boardHeight = BoardDimensions.bounds.height; + const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; + + final standardizedYPosition = parent.body.position.y + (boardHeight / 2); + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); + + parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor; + + parent.firstChild()!.scale.setValues( + scaleFactor, + scaleFactor, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart new file mode 100644 index 00000000..22928734 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'ball_scaling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index c5ea7f9f..6e79ac56 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -2,7 +2,7 @@ export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; export 'android_spaceship/android_spaceship.dart'; export 'backboard/backboard.dart'; -export 'ball.dart'; +export 'ball/ball.dart'; export 'baseboard.dart'; export 'board_background_sprite_component.dart'; export 'board_dimensions.dart'; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 9bce5632..cb268b41 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -1,9 +1,3 @@ -// Copyright (c) 2022, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; import 'package:sandbox/stories/stories.dart'; diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart similarity index 78% rename from packages/pinball_components/test/src/components/ball_test.dart rename to packages/pinball_components/test/src/components/ball/ball_test.dart index 26a03886..321e137b 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -6,18 +6,29 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); group('Ball', () { + const baseColor = Color(0xFFFFFFFF); + + test( + 'can be instantiated', + () { + expect(Ball(baseColor: baseColor), isA()); + expect(Ball.test(baseColor: baseColor), isA()); + }, + ); + flameTester.test( 'loads correctly', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ready(); await game.ensureAdd(ball); @@ -25,11 +36,20 @@ void main() { }, ); + flameTester.test('add a BallScalingBehavior', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + expect( + ball.descendants().whereType().length, + equals(1), + ); + }); + group('body', () { flameTester.test( 'is dynamic', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.bodyType, equals(BodyType.dynamic)); @@ -38,7 +58,7 @@ void main() { group('can be moved', () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); game.update(1); @@ -46,7 +66,7 @@ void main() { }); flameTester.test('by applying velocity', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.body.gravityScale = Vector2.zero(); @@ -61,7 +81,7 @@ void main() { flameTester.test( 'exists', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.fixtures[0], isA()); @@ -71,7 +91,7 @@ void main() { flameTester.test( 'is dense', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -82,7 +102,7 @@ void main() { flameTester.test( 'shape is circular', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -94,7 +114,7 @@ void main() { flameTester.test( 'has Layer.all as default filter maskBits', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ready(); await game.ensureAdd(ball); await game.ready(); @@ -108,7 +128,7 @@ void main() { group('stop', () { group("can't be moved", () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); @@ -116,19 +136,6 @@ void main() { expect(ball.body.position, equals(ball.initialPosition)); }); }); - - // TODO(allisonryan0002): delete or retest this if/when solution is added - // to prevent forces on a ball while stopped. - - // flameTester.test('by applying velocity', (game) async { - // final ball = Ball(baseColor: Colors.blue); - // await game.ensureAdd(ball); - // ball.stop(); - - // ball.body.linearVelocity.setValues(10, 10); - // game.update(1); - // expect(ball.body.position, equals(ball.initialPosition)); - // }); }); group('resume', () { @@ -136,7 +143,7 @@ void main() { flameTester.test( 'by its weight when previously stopped', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -149,7 +156,7 @@ void main() { flameTester.test( 'by applying velocity when previously stopped', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -165,7 +172,7 @@ void main() { group('boost', () { flameTester.test('applies an impulse to the ball', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.linearVelocity, equals(Vector2.zero())); @@ -176,7 +183,7 @@ void main() { }); flameTester.test('adds TurboChargeSpriteAnimation', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); await ball.boost(Vector2.all(10)); @@ -190,7 +197,7 @@ void main() { flameTester.test('removes TurboChargeSpriteAnimation after it finishes', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); await ball.boost(Vector2.all(10)); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart new file mode 100644 index 00000000..42cd9073 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -0,0 +1,99 @@ +// ignore_for_file: cascade_invocations + +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final asset = Assets.images.ball.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + group('BallScalingBehavior', () { + const baseColor = Color(0xFFFFFFFF); + test('can be instantiated', () { + expect( + BallScalingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallScalingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallScalingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test('scales the shape radius', (game) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, 10); + await ball1.add(BallScalingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, -10); + await ball2.add(BallScalingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + final shape1 = ball1.body.fixtures.first.shape; + final shape2 = ball2.body.fixtures.first.shape; + expect( + shape1.radius, + greaterThan(shape2.radius), + ); + }); + + flameTester.testGameWidget( + 'scales the sprite', + setUp: (game, tester) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, 10); + await ball1.add(BallScalingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, -10); + await ball2.add(BallScalingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + await tester.pump(); + await game.ready(); + + final sprite1 = ball1.firstChild()!; + final sprite2 = ball2.firstChild()!; + expect( + sprite1.scale.x, + greaterThan(sprite2.scale.x), + ); + expect( + sprite1.scale.y, + greaterThan(sprite2.scale.y), + ); + }, + ); + }); +} diff --git a/packages/pinball_ui/lib/src/theme/pinball_colors.dart b/packages/pinball_ui/lib/src/theme/pinball_colors.dart index 5db27229..df1ddce6 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_colors.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_colors.dart @@ -8,4 +8,9 @@ abstract class PinballColors { static const Color orange = Color(0xFFE5AB05); static const Color blue = Color(0xFF4B94F6); static const Color transparent = Color(0x00000000); + static const Color loadingDarkRed = Color(0xFFE33B2D); + static const Color loadingLightRed = Color(0xFFEC5E2B); + static const Color loadingDarkBlue = Color(0xFF4087F8); + static const Color loadingLightBlue = Color(0xFF6CCAE4); + static const Color crtBackground = Color(0xFF274E54); } diff --git a/packages/pinball_ui/lib/src/widgets/animated_ellipsis_text.dart b/packages/pinball_ui/lib/src/widgets/animated_ellipsis_text.dart new file mode 100644 index 00000000..9b52d604 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/animated_ellipsis_text.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// {@tempalte animated_ellipsis_text} +/// Every 500 milliseconds, it will add a new `.` at the end of the given +/// [text]. Once 3 `.` have been added (e.g. `Loading...`), it will reset to +/// zero ellipsis and start over again. +/// {@endtemplate} +class AnimatedEllipsisText extends StatefulWidget { + /// {@macro animated_ellipsis_text} + const AnimatedEllipsisText( + this.text, { + Key? key, + this.style, + }) : super(key: key); + + /// The text that will be animated. + final String text; + + /// Optional [TextStyle] of the given [text]. + final TextStyle? style; + + @override + State createState() => _AnimatedEllipsisText(); +} + +class _AnimatedEllipsisText extends State + with SingleTickerProviderStateMixin { + late final Timer timer; + var _numberOfEllipsis = 0; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(milliseconds: 500), (_) { + setState(() { + _numberOfEllipsis++; + _numberOfEllipsis = _numberOfEllipsis % 4; + }); + }); + } + + @override + void dispose() { + if (timer.isActive) timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text( + '${widget.text}${_numberOfEllipsis.toEllipsis()}', + style: widget.style, + ); + } +} + +extension on int { + String toEllipsis() => '.' * this; +} diff --git a/packages/pinball_ui/lib/src/widgets/crt_background.dart b/packages/pinball_ui/lib/src/widgets/crt_background.dart new file mode 100644 index 00000000..202af1d3 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/crt_background.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template crt_background} +/// [BoxDecoration] that provides a CRT-like background efffect. +/// {@endtemplate} +class CrtBackground extends BoxDecoration { + /// {@macro crt_background} + const CrtBackground() + : super( + gradient: const LinearGradient( + begin: Alignment(1, 0.015), + stops: [0.0, 0.5, 0.5, 1], + colors: [ + PinballColors.darkBlue, + PinballColors.darkBlue, + PinballColors.crtBackground, + PinballColors.crtBackground, + ], + tileMode: TileMode.repeated, + ), + ); +} diff --git a/packages/pinball_ui/lib/src/widgets/pinball_loading_indicator.dart b/packages/pinball_ui/lib/src/widgets/pinball_loading_indicator.dart new file mode 100644 index 00000000..ac9b4f46 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/pinball_loading_indicator.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_loading_indicator} +/// Pixel-art loading indicator +/// {@endtemplate} +class PinballLoadingIndicator extends StatelessWidget { + /// {@macro pinball_loading_indicator} + const PinballLoadingIndicator({ + Key? key, + required this.value, + }) : assert( + value >= 0.0 && value <= 1.0, + 'Progress must be between 0 and 1', + ), + super(key: key); + + /// Progress value + final double value; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _InnerIndicator(value: value, widthFactor: 0.95), + _InnerIndicator(value: value, widthFactor: 0.98), + _InnerIndicator(value: value), + _InnerIndicator(value: value), + _InnerIndicator(value: value, widthFactor: 0.98), + _InnerIndicator(value: value, widthFactor: 0.95) + ], + ); + } +} + +class _InnerIndicator extends StatelessWidget { + const _InnerIndicator({ + Key? key, + required this.value, + this.widthFactor = 1.0, + }) : super(key: key); + + final double value; + final double widthFactor; + + @override + Widget build(BuildContext context) { + return FractionallySizedBox( + widthFactor: widthFactor, + child: Column( + children: [ + LinearProgressIndicator( + backgroundColor: PinballColors.loadingDarkBlue, + color: PinballColors.loadingDarkRed, + value: value, + ), + LinearProgressIndicator( + backgroundColor: PinballColors.loadingLightBlue, + color: PinballColors.loadingLightRed, + value: value, + ), + ], + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/widgets/widgets.dart b/packages/pinball_ui/lib/src/widgets/widgets.dart index 34d952b6..3aa96c3e 100644 --- a/packages/pinball_ui/lib/src/widgets/widgets.dart +++ b/packages/pinball_ui/lib/src/widgets/widgets.dart @@ -1 +1,4 @@ +export 'animated_ellipsis_text.dart'; +export 'crt_background.dart'; export 'pinball_button.dart'; +export 'pinball_loading_indicator.dart'; diff --git a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart index 36e45c0d..3c54c60b 100644 --- a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart @@ -27,5 +27,25 @@ void main() { test('transparent is 0x00000000', () { expect(PinballColors.transparent, const Color(0x00000000)); }); + + test('loadingDarkRed is 0xFFE33B2D', () { + expect(PinballColors.loadingDarkRed, const Color(0xFFE33B2D)); + }); + + test('loadingLightRed is 0xFFEC5E2B', () { + expect(PinballColors.loadingLightRed, const Color(0xFFEC5E2B)); + }); + + test('loadingDarkBlue is 0xFF4087F8', () { + expect(PinballColors.loadingDarkBlue, const Color(0xFF4087F8)); + }); + + test('loadingLightBlue is 0xFF6CCAE4', () { + expect(PinballColors.loadingLightBlue, const Color(0xFF6CCAE4)); + }); + + test('crtBackground is 0xFF274E54', () { + expect(PinballColors.crtBackground, const Color(0xFF274E54)); + }); }); } diff --git a/packages/pinball_ui/test/src/widgets/animated_ellipsis_text_test.dart b/packages/pinball_ui/test/src/widgets/animated_ellipsis_text_test.dart new file mode 100644 index 00000000..3800cfed --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/animated_ellipsis_text_test.dart @@ -0,0 +1,30 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('AnimatedEllipsisText', () { + testWidgets( + 'adds a new `.` every 500ms and ' + 'resets back to zero after adding 3', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AnimatedEllipsisText('test'), + ), + ), + ); + expect(find.text('test'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test.'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test..'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test...'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test'), findsOneWidget); + }); + }); +} diff --git a/packages/pinball_ui/test/src/widgets/crt_background_test.dart b/packages/pinball_ui/test/src/widgets/crt_background_test.dart new file mode 100644 index 00000000..65f27456 --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/crt_background_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('CrtBackground', () { + test('is a BoxDecoration with a LinearGradient', () { + // ignore: prefer_const_constructors + final crtBg = CrtBackground(); + const expectedGradient = LinearGradient( + begin: Alignment(1, 0.015), + stops: [0.0, 0.5, 0.5, 1], + colors: [ + PinballColors.darkBlue, + PinballColors.darkBlue, + PinballColors.crtBackground, + PinballColors.crtBackground, + ], + tileMode: TileMode.repeated, + ); + expect(crtBg, isA()); + expect(crtBg.gradient, expectedGradient); + }); + }); +} diff --git a/packages/pinball_ui/test/src/widgets/pinball_loading_indicator_test.dart b/packages/pinball_ui/test/src/widgets/pinball_loading_indicator_test.dart new file mode 100644 index 00000000..a2cc6d1a --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/pinball_loading_indicator_test.dart @@ -0,0 +1,45 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballLoadingIndicator', () { + group('assert value', () { + test('throws error if value <= 0.0', () { + expect( + () => PinballLoadingIndicator(value: -0.5), + throwsA(isA()), + ); + }); + + test('throws error if value >= 1.0', () { + expect( + () => PinballLoadingIndicator(value: 1.5), + throwsA(isA()), + ); + }); + }); + + testWidgets( + 'renders 12 LinearProgressIndicators and ' + '6 FractionallySizedBox to indicate progress', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PinballLoadingIndicator(value: 0.75), + ), + ), + ); + expect(find.byType(FractionallySizedBox), findsNWidgets(6)); + expect(find.byType(LinearProgressIndicator), findsNWidgets(12)); + final progressIndicators = tester.widgetList( + find.byType(LinearProgressIndicator), + ); + for (final i in progressIndicators) { + expect(i.value, 0.75); + } + }); + }); +} diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 5ba5aca7..ca1cedff 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; diff --git a/test/game/assets_manager/cubit/assets_manager_cubit_test.dart b/test/assets_manager/cubit/assets_manager_cubit_test.dart similarity index 93% rename from test/game/assets_manager/cubit/assets_manager_cubit_test.dart rename to test/assets_manager/cubit/assets_manager_cubit_test.dart index d0afee34..27d9cedb 100644 --- a/test/game/assets_manager/cubit/assets_manager_cubit_test.dart +++ b/test/assets_manager/cubit/assets_manager_cubit_test.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; void main() { group('AssetsManagerCubit', () { diff --git a/test/game/assets_manager/cubit/assets_manager_state_test.dart b/test/assets_manager/cubit/assets_manager_state_test.dart similarity index 98% rename from test/game/assets_manager/cubit/assets_manager_state_test.dart rename to test/assets_manager/cubit/assets_manager_state_test.dart index 12a42485..4882f880 100644 --- a/test/game/assets_manager/cubit/assets_manager_state_test.dart +++ b/test/assets_manager/cubit/assets_manager_state_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; void main() { group('AssetsManagerState', () { diff --git a/test/assets_manager/views/assets_loading_page_test.dart b/test/assets_manager/views/assets_loading_page_test.dart new file mode 100644 index 00000000..a6210e0c --- /dev/null +++ b/test/assets_manager/views/assets_loading_page_test.dart @@ -0,0 +1,38 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +import '../../helpers/helpers.dart'; + +class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} + +void main() { + late AssetsManagerCubit assetsManagerCubit; + + setUp(() { + final initialAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: const [], + ); + assetsManagerCubit = _MockAssetsManagerCubit(); + whenListen( + assetsManagerCubit, + Stream.value(initialAssetsState), + initialState: initialAssetsState, + ); + }); + + group('AssetsLoadingPage', () { + testWidgets('renders an animated text and a pinball loading indicator', + (tester) async { + await tester.pumpApp( + const AssetsLoadingPage(), + assetsManagerCubit: assetsManagerCubit, + ); + expect(find.byType(AnimatedEllipsisText), findsOneWidget); + expect(find.byType(PinballLoadingIndicator), findsOneWidget); + }); + }); +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index d3b32d85..0ed6e744 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -5,6 +5,7 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; @@ -66,7 +67,6 @@ void main() { Stream.value(initialAssetsState), initialState: initialAssetsState, ); - await tester.pumpApp( PinballGameView( game: game, @@ -74,14 +74,7 @@ void main() { assetsManagerCubit: assetsManagerCubit, characterThemeCubit: characterThemeCubit, ); - - expect( - find.byWidgetPredicate( - (widget) => - widget is LinearProgressIndicator && widget.value == 0.0, - ), - findsOneWidget, - ); + expect(find.byType(AssetsLoadingPage), findsOneWidget); }, ); diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 50bb9bc1..febf8d36 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1,9 +1,3 @@ -// -// Copyright (c) 2021, Very Good Ventures -// Use of this source code is governed by an MIT-style -// https://opensource.org/licenses/MIT. -// https://verygood.ventures -// license that can be found in the LICENSE file or at export 'builders.dart'; export 'fakes.dart'; export 'forge2d.dart'; diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 7347989d..a7d7ae67 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,6 +5,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart index 2570c846..232aa1d5 100644 --- a/test/how_to_play/how_to_play_dialog_test.dart +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -3,10 +3,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:platform_helper/platform_helper.dart'; import '../helpers/helpers.dart'; +class _MockPinballAudio extends Mock implements PinballAudio {} + class _MockPlatformHelper extends Mock implements PlatformHelper {} void main() { @@ -93,5 +96,30 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(HowToPlayDialog), findsNothing); }); + + testWidgets( + 'plays the I/O Pinball voice over audio on dismiss', + (tester) async { + final audio = _MockPinballAudio(); + await tester.pumpApp( + Builder( + builder: (context) { + return TextButton( + onPressed: () => showHowToPlayDialog(context), + child: const Text('test'), + ); + }, + ), + pinballAudio: audio, + ); + expect(find.byType(HowToPlayDialog), findsNothing); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + + await tester.tapAt(Offset.zero); + await tester.pumpAndSettle(); + verify(audio.ioPinballVoiceOver).called(1); + }, + ); }); }