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/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index 86857ee4..8f1b46e8 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -40,7 +40,7 @@ class FlutterForestBonusBehavior extends Component .add(const BonusActivated(GameBonus.dashNest)); canvas.add( ControlledBall.bonus(characterTheme: gameRef.characterTheme) - ..initialPosition = Vector2(17.2, -52.7), + ..initialPosition = Vector2(29.5, -24.5), ); animatronic.playing = true; signpost.bloc.onProgressed(); 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/game_assets.dart b/lib/game/game_assets.dart index 621ffb53..2a847ce0 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -38,6 +38,7 @@ extension PinballGameAssetsX on PinballGame { ), images.load(components.Assets.images.dino.bottomWall.keyName), images.load(components.Assets.images.dino.topWall.keyName), + images.load(components.Assets.images.dino.topWallTunnel.keyName), images.load(components.Assets.images.dino.animatronic.head.keyName), images.load(components.Assets.images.dino.animatronic.mouth.keyName), images.load(components.Assets.images.dash.animatronic.keyName), diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 35516bbc..fb45b3eb 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 1b3bdfb9..0f68e170 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -15,6 +15,7 @@ class $AssetsSfxGen { const $AssetsSfxGen(); String get google => 'assets/sfx/google.mp3'; + String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; String get plim => 'assets/sfx/plim.mp3'; } diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 8bda14e5..f87b05d1 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -74,6 +74,7 @@ class PinballAudio { await Future.wait([ _preCacheSingleAudio(_prefixFile(Assets.sfx.google)), + _preCacheSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)), _preCacheSingleAudio(_prefixFile(Assets.music.background)), ]); } @@ -88,6 +89,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 c92b876d..9d6dff98 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -125,6 +125,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'), @@ -163,6 +168,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/assets/images/dino/top-wall-tunnel.png b/packages/pinball_components/assets/images/dino/top-wall-tunnel.png new file mode 100644 index 00000000..07cb9c46 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/top-wall-tunnel.png differ diff --git a/packages/pinball_components/assets/images/dino/top-wall.png b/packages/pinball_components/assets/images/dino/top-wall.png index 72b912e3..7ee69411 100644 Binary files a/packages/pinball_components/assets/images/dino/top-wall.png and b/packages/pinball_components/assets/images/dino/top-wall.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 3e2faad1..da9747c4 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -124,6 +124,10 @@ class $AssetsImagesDinoGen { AssetGenImage get bottomWall => const AssetGenImage('assets/images/dino/bottom-wall.png'); + /// File path: assets/images/dino/top-wall-tunnel.png + AssetGenImage get topWallTunnel => + const AssetGenImage('assets/images/dino/top-wall-tunnel.png'); + /// File path: assets/images/dino/top-wall.png AssetGenImage get topWall => const AssetGenImage('assets/images/dino/top-wall.png'); diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart similarity index 74% rename from packages/pinball_components/lib/src/components/ball.dart rename to packages/pinball_components/lib/src/components/ball/ball.dart index 7469396a..dea4c0b4 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,11 +1,12 @@ 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_gravitating_behavior.dart'; +import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template ball} @@ -20,6 +21,8 @@ class Ball extends BodyComponent renderBody: false, children: [ _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + BallScalingBehavior(), + BallGravitatingBehavior(), ], ) { // TODO(ruimiguel): while developing Ball can be launched by clicking mouse, @@ -30,6 +33,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); @@ -76,48 +88,6 @@ class Ball extends BodyComponent body.linearVelocity = impulse; await add(_TurboChargeSpriteAnimationComponent()); } - - @override - 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; - const maxXGravityPercentage = - (1 - BoardDimensions.perspectiveShrinkFactor) / 2; - final xDeviationFromCenter = body.position.x; - - final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) * - maxXGravityPercentage) * - defaultGravity; - - final positionalYForce = math.sqrt( - math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2), - ); - - body.gravityOverride = Vector2(positionalXForce, positionalYForce); - } } class _BallSpriteComponent extends SpriteComponent with HasGameRef { diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_gravitating_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_gravitating_behavior.dart new file mode 100644 index 00000000..bad129a6 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_gravitating_behavior.dart @@ -0,0 +1,35 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Scales the ball's gravity according to its position on the board. +class BallGravitatingBehavior extends Component + with ParentIsA, HasGameRef { + @override + void update(double dt) { + super.update(dt); + final defaultGravity = gameRef.world.gravity.y; + + final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2; + const maxXGravityPercentage = + (1 - BoardDimensions.perspectiveShrinkFactor) / 2; + final xDeviationFromCenter = parent.body.position.x; + + final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) * + maxXGravityPercentage) * + defaultGravity; + final positionalYForce = math.sqrt( + math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2), + ); + + final gravityOverride = parent.body.gravityOverride; + if (gravityOverride != null) { + gravityOverride.setValues(positionalXForce, positionalYForce); + } else { + parent.body.gravityOverride = Vector2(positionalXForce, positionalYForce); + } + } +} 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..038b7833 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'ball_gravitating_behavior.dart'; +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/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 5ca9e8c4..5125d4bc 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -23,59 +23,70 @@ class DinoWalls extends Component { /// {@template dino_top_wall} /// Wall segment located above [ChromeDino]. /// {@endtemplate} -class _DinoTopWall extends BodyComponent with InitialPosition, ZIndex { +class _DinoTopWall extends BodyComponent with InitialPosition { ///{@macro dino_top_wall} _DinoTopWall() : super( - children: [_DinoTopWallSpriteComponent()], + children: [ + _DinoTopWallSpriteComponent(), + _DinoTopWallTunnelSpriteComponent(), + ], renderBody: false, - ) { - zIndex = ZIndexes.dinoTopWall; - } + ); List _createFixtureDefs() { - final topStraightShape = EdgeShape() + final topEdgeShape = EdgeShape() ..set( - Vector2(28.65, -34.3), - Vector2(29.5, -34.3), + Vector2(29.25, -35.27), + Vector2(28.4, -34.77), ); final topCurveShape = BezierCurveShape( controlPoints: [ - topStraightShape.vertex1, - Vector2(18.8, -26.2), - Vector2(26.6, -20.2), + topEdgeShape.vertex2, + Vector2(21.35, -28.72), + Vector2(23.45, -24.62), ], ); - final middleCurveShape = BezierCurveShape( - controlPoints: [ + final tunnelTopEdgeShape = EdgeShape() + ..set( topCurveShape.vertices.last, - Vector2(27.8, -19.3), - Vector2(26.8, -18.7), - ], - ); + Vector2(30.35, -27.32), + ); - final bottomCurveShape = BezierCurveShape( - controlPoints: [ - middleCurveShape.vertices.last, - Vector2(23, -14.2), - Vector2(27, -14.2), - ], - ); + final tunnelBottomEdgeShape = EdgeShape() + ..set( + Vector2(30.75, -23.17), + Vector2(25.45, -21.22), + ); - final bottomStraightShape = EdgeShape() + final middleEdgeShape = EdgeShape() ..set( - bottomCurveShape.vertices.last, - Vector2(31, -13.7), + tunnelBottomEdgeShape.vertex2, + Vector2(27.45, -19.32), + ); + + final bottomEdgeShape = EdgeShape() + ..set( + middleEdgeShape.vertex2, + Vector2(24.65, -15.02), + ); + + final undersideEdgeShape = EdgeShape() + ..set( + bottomEdgeShape.vertex2, + Vector2(31.75, -13.77), ); return [ - FixtureDef(topStraightShape), + FixtureDef(topEdgeShape), FixtureDef(topCurveShape), - FixtureDef(middleCurveShape), - FixtureDef(bottomCurveShape), - FixtureDef(bottomStraightShape), + FixtureDef(tunnelTopEdgeShape), + FixtureDef(tunnelBottomEdgeShape), + FixtureDef(middleEdgeShape), + FixtureDef(bottomEdgeShape), + FixtureDef(undersideEdgeShape), ]; } @@ -93,7 +104,15 @@ class _DinoTopWall extends BodyComponent with InitialPosition, ZIndex { } } -class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { +class _DinoTopWallSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _DinoTopWallSpriteComponent() + : super( + position: Vector2(22.75, -38.07), + ) { + zIndex = ZIndexes.dinoTopWall; + } + @override Future onLoad() async { await super.onLoad(); @@ -104,7 +123,26 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22.8, -38.1); + } +} + +class _DinoTopWallTunnelSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _DinoTopWallTunnelSpriteComponent() + : super(position: Vector2(23.31, -26.01)) { + zIndex = ZIndexes.dinoTopWallTunnel; + } + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.topWallTunnel.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; } } @@ -122,7 +160,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex { } List _createFixtureDefs() { - final topStraightShape = EdgeShape() + final topEdgeShape = EdgeShape() ..set( Vector2(32.4, -8.8), Vector2(25, -7.7), @@ -130,29 +168,29 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex { final topLeftCurveShape = BezierCurveShape( controlPoints: [ - topStraightShape.vertex2, + topEdgeShape.vertex2, Vector2(21.8, -7), Vector2(29.8, 13.8), ], ); - final bottomLeftStraightShape = EdgeShape() + final bottomLeftEdgeShape = EdgeShape() ..set( topLeftCurveShape.vertices.last, Vector2(31.9, 44.1), ); - final bottomStraightShape = EdgeShape() + final bottomEdgeShape = EdgeShape() ..set( - bottomLeftStraightShape.vertex2, + bottomLeftEdgeShape.vertex2, Vector2(37.8, 44.1), ); return [ - FixtureDef(topStraightShape), + FixtureDef(topEdgeShape), FixtureDef(topLeftCurveShape), - FixtureDef(bottomLeftStraightShape), - FixtureDef(bottomStraightShape), + FixtureDef(bottomLeftEdgeShape), + FixtureDef(bottomEdgeShape), ]; } diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index 440bd1fe..b8371273 100644 --- a/packages/pinball_components/lib/src/components/z_indexes.dart +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -53,6 +53,8 @@ abstract class ZIndexes { static const dinoTopWall = _above + ballOnBoard; + static const dinoTopWallTunnel = _below + ballOnBoard; + static const dino = _above + dinoTopWall; static const dinoBottomWall = _above + dino; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 9fdee65a..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'; @@ -14,19 +8,17 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); - addChromeDinoStories(dashbook); addFlutterForestStories(dashbook); - addBottomGroupStories(dashbook); - addPlungerStories(dashbook); - addSlingshotStories(dashbook); addSparkyScorchStories(dashbook); addAndroidAcresStories(dashbook); + addDinoDesertStories(dashbook); + addBottomGroupStories(dashbook); + addPlungerStories(dashbook); addBoundariesStories(dashbook); addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreStories(dashbook); addBackboardStories(dashbook); - addDinoWallStories(dashbook); addMultiballStories(dashbook); addMultipliersStories(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart deleted file mode 100644 index a4c70c03..00000000 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart'; - -void addChromeDinoStories(Dashbook dashbook) { - dashbook.storiesOf('Chrome Dino').addGame( - title: 'Traced', - description: ChromeDinoGame.description, - gameBuilder: (_) => ChromeDinoGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/chrome_dino_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart rename to packages/pinball_components/sandbox/lib/stories/dino_desert/chrome_dino_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/dino_walls_game.dart similarity index 85% rename from packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart rename to packages/pinball_components/sandbox/lib/stories/dino_desert/dino_walls_game.dart index 0d213aa4..849e17a8 100644 --- a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/dino_desert/dino_walls_game.dart @@ -4,8 +4,8 @@ import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class DinoWallGame extends BallGame { - DinoWallGame() : super(); +class DinoWallsGame extends BallGame { + DinoWallsGame() : super(); static const description = ''' Shows how DinoWalls are rendered. @@ -20,6 +20,7 @@ class DinoWallGame extends BallGame { await images.loadAll([ Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, ]); diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/slingshots_game.dart similarity index 92% rename from packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart rename to packages/pinball_components/sandbox/lib/stories/dino_desert/slingshots_game.dart index 11c38c0c..1a9eb2e5 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/dino_desert/slingshots_game.dart @@ -2,8 +2,8 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SlingshotGame extends BallGame { - SlingshotGame() +class SlingshotsGame extends BallGame { + SlingshotsGame() : super( imagesFileNames: [ Assets.images.slingshot.upper.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/dino_desert/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/stories.dart new file mode 100644 index 00000000..bd8fc329 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_desert/stories.dart @@ -0,0 +1,24 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/dino_desert/chrome_dino_game.dart'; +import 'package:sandbox/stories/dino_desert/dino_walls_game.dart'; +import 'package:sandbox/stories/dino_desert/slingshots_game.dart'; + +void addDinoDesertStories(Dashbook dashbook) { + dashbook.storiesOf('Dino Desert') + ..addGame( + title: 'Chrome Dino', + description: ChromeDinoGame.description, + gameBuilder: (_) => ChromeDinoGame(), + ) + ..addGame( + title: 'Dino Walls', + description: DinoWallsGame.description, + gameBuilder: (_) => DinoWallsGame(), + ) + ..addGame( + title: 'Slingshots', + description: SlingshotsGame.description, + gameBuilder: (_) => SlingshotsGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart deleted file mode 100644 index e24d26cc..00000000 --- a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/dino_wall/dino_wall_game.dart'; - -void addDinoWallStories(Dashbook dashbook) { - dashbook.storiesOf('DinoWall').addGame( - title: 'Traced', - description: DinoWallGame.description, - gameBuilder: (_) => DinoWallGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart deleted file mode 100644 index e4c04a0f..00000000 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/slingshot/slingshot_game.dart'; - -void addSlingshotStories(Dashbook dashbook) { - dashbook.storiesOf('Slingshots').addGame( - title: 'Traced', - description: SlingshotGame.description, - gameBuilder: (_) => SlingshotGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 8cdd38b1..b8bc567c 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -3,8 +3,7 @@ export 'backboard/stories.dart'; export 'ball/stories.dart'; export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; -export 'chrome_dino/stories.dart'; -export 'dino_wall/stories.dart'; +export 'dino_desert/stories.dart'; export 'effects/stories.dart'; export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; @@ -14,5 +13,4 @@ export 'multiball/stories.dart'; export 'multipliers/stories.dart'; export 'plunger/stories.dart'; export 'score/stories.dart'; -export 'slingshot/stories.dart'; export 'sparky_scorch/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 75% 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..02175f16 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,31 @@ void main() { }, ); + group('adds', () { + flameTester.test('a BallScalingBehavior', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + expect( + ball.descendants().whereType().length, + equals(1), + ); + }); + + flameTester.test('a BallGravitatingBehavior', (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 +69,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 +77,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 +92,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 +102,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 +113,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 +125,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 +139,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 +147,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 +154,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 +167,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 +183,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 +194,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 +208,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_gravitating_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart new file mode 100644 index 00000000..de291f21 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart @@ -0,0 +1,63 @@ +// 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('BallGravitatingBehavior', () { + const baseColor = Color(0xFFFFFFFF); + test('can be instantiated', () { + expect( + BallGravitatingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallGravitatingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test( + "overrides the body's horizontal gravity symmetrically", + (game) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(10, 0); + await ball1.add(BallGravitatingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(-10, 0); + await ball2.add(BallGravitatingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + expect( + ball1.body.gravityOverride!.x, + equals(-ball2.body.gravityOverride!.x), + ); + expect( + ball1.body.gravityOverride!.y, + equals(ball2.body.gravityOverride!.y), + ); + }, + ); + }); +} 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..cd0a0486 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -0,0 +1,85 @@ +// 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('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.test( + 'scales the sprite', + (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 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_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index a93c6a3c..5e4471e5 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -12,6 +12,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index 592c83fb..c7d55c04 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ 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/components/dino_desert/dino_desert_test.dart b/test/game/components/dino_desert/dino_desert_test.dart index 262ddc2d..20c9ad38 100644 --- a/test/game/components/dino_desert/dino_desert_test.dart +++ b/test/game/components/dino_desert/dino_desert_test.dart @@ -14,6 +14,7 @@ void main() { Assets.images.dino.animatronic.head.keyName, Assets.images.dino.animatronic.mouth.keyName, Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, Assets.images.slingshot.upper.keyName, Assets.images.slingshot.lower.keyName, diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 52ce156d..884037f4 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -48,6 +48,7 @@ void main() { Assets.images.dino.animatronic.mouth.keyName, Assets.images.dino.animatronic.head.keyName, Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, Assets.images.dash.animatronic.keyName, Assets.images.dash.bumper.a.active.keyName, diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 802f49f8..3afdaefe 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -7,6 +7,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'; @@ -68,7 +69,6 @@ void main() { Stream.value(initialAssetsState), initialState: initialAssetsState, ); - await tester.pumpApp( PinballGameView( game: game, @@ -76,14 +76,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); + }, + ); }); }