diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..8a6476eb --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,25 @@ +name: deploy + +on: + push: + branches: + - main + +jobs: + deploy-dev: + runs-on: ubuntu-latest + name: Deploy Development + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter packages get + - run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}" + projectId: ashehwkdkdjruejdnensjsjdne + expires: 30d + channelId: live \ No newline at end of file diff --git a/.github/workflows/platform_helper.yaml b/.github/workflows/platform_helper.yaml new file mode 100644 index 00000000..0c1c61e7 --- /dev/null +++ b/.github/workflows/platform_helper.yaml @@ -0,0 +1,23 @@ +name: platform_helper + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + + pull_request: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/platform_helper + coverage_excludes: "lib/gen/*.dart" diff --git a/assets/images/components/key.png b/assets/images/components/key.png new file mode 100644 index 00000000..588c2b89 Binary files /dev/null and b/assets/images/components/key.png differ diff --git a/assets/images/components/space.png b/assets/images/components/space.png new file mode 100644 index 00000000..9949b383 Binary files /dev/null and b/assets/images/components/space.png differ diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres.dart index 314986ce..57855ed6 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres.dart @@ -16,12 +16,12 @@ class AndroidAcres extends Blueprint { components: [ AndroidBumper.a( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-32.52, -9.1), AndroidBumper.b( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-22.89, -17.35), ], diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index 92ba0ed0..921a8e58 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -48,6 +48,9 @@ class _BottomGroupSide extends Component { ); final kicker = Kicker( side: _side, + children: [ + ScoringBehavior(points: 5000), + ], )..initialPosition = Vector2( (22.4 * direction) + centerXAdjustment, 25, diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index d7447543..7508d5c3 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -23,17 +23,17 @@ class FlutterForest extends Component { )..initialPosition = Vector2(8.35, -58.3), DashNestBumper.main( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 200000), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(23.3, -46.75), DashAnimatronic()..position = Vector2(20, -66), diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart index 9a9faa9a..63999fe1 100644 --- a/lib/game/components/google_word/google_word.dart +++ b/lib/game/components/google_word/google_word.dart @@ -1,6 +1,7 @@ import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template google_word} @@ -12,12 +13,30 @@ class GoogleWord extends Component { required Vector2 position, }) : super( children: [ - GoogleLetter(0)..initialPosition = position + Vector2(-12.92, 1.82), - GoogleLetter(1)..initialPosition = position + Vector2(-8.33, -0.65), - GoogleLetter(2)..initialPosition = position + Vector2(-2.88, -1.75), - GoogleLetter(3)..initialPosition = position + Vector2(2.88, -1.75), - GoogleLetter(4)..initialPosition = position + Vector2(8.33, -0.65), - GoogleLetter(5)..initialPosition = position + Vector2(12.92, 1.82), + GoogleLetter( + 0, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-12.92, 1.82), + GoogleLetter( + 1, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-8.33, -0.65), + GoogleLetter( + 2, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-2.88, -1.75), + GoogleLetter( + 3, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(2.88, -1.75), + GoogleLetter( + 4, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(8.33, -0.65), + GoogleLetter( + 5, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(12.92, 1.82), GoogleWordBonusBehavior(), ], ); diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index 5c00e5c9..a37c2469 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -18,17 +18,17 @@ class SparkyFireZone extends Blueprint { components: [ SparkyBumper.a( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.b( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-21.25, -57.9), SparkyBumper.c( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-3.3, -52.55), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), @@ -47,7 +47,13 @@ class SparkyFireZone extends Blueprint { class SparkyComputerSensor extends BodyComponent with InitialPosition, ContactCallbacks { /// {@macro sparky_computer_sensor} - SparkyComputerSensor() : super(renderBody: false); + SparkyComputerSensor() + : super( + renderBody: false, + children: [ + ScoringBehavior(points: 200000), + ], + ); @override Body createBody() { diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart index 30135cd2..b8f67c26 100644 --- a/lib/game/view/widgets/round_count_display.dart +++ b/lib/game/view/widgets/round_count_display.dart @@ -21,7 +21,7 @@ class RoundCountDisplay extends StatelessWidget { Text( l10n.rounds, style: AppTextStyle.subtitle1.copyWith( - color: AppColors.orange, + color: AppColors.yellow, ), ), const SizedBox(width: 8), @@ -53,7 +53,7 @@ class RoundIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128); + final color = isActive ? AppColors.yellow : AppColors.yellow.withAlpha(128); const size = 8.0; return Padding( diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart index 288ea05c..40b33c35 100644 --- a/lib/game/view/widgets/score_view.dart +++ b/lib/game/view/widgets/score_view.dart @@ -59,7 +59,7 @@ class _ScoreDisplay extends StatelessWidget { Text( l10n.score.toLowerCase(), style: AppTextStyle.subtitle1.copyWith( - color: AppColors.orange, + color: AppColors.yellow, ), ), const _ScoreText(), diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index f5b935a5..9559fd45 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -47,6 +47,14 @@ class $AssetsImagesComponentsGen { /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); + + /// File path: assets/images/components/key.png + AssetGenImage get key => + const AssetGenImage('assets/images/components/key.png'); + + /// File path: assets/images/components/space.png + AssetGenImage get space => + const AssetGenImage('assets/images/components/space.png'); } class $AssetsImagesScoreGen { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 9655d8be..19b12296 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -8,6 +8,10 @@ "@howToPlay": { "description": "Text displayed on the landing page how to play button" }, + "tipsForFlips": "Tips for flips", + "@tipsForFlips": { + "description": "Text displayed on the landing page how to play button" + }, "launchControls": "Launch Controls", "@launchControls": { "description": "Text displayed on the how to play dialog with the launch controls" @@ -16,6 +20,26 @@ "@flipperControls": { "description": "Text displayed on the how to play dialog with the flipper controls" }, + "tapAndHoldRocket": "Tap & Hold Rocket", + "@tapAndHoldRocket": { + "description": "Text displayed on the how to launch on mobile" + }, + "to": "to", + "@to": { + "description": "Text displayed for the word to" + }, + "launch": "LAUNCH", + "@launch": { + "description": "Text displayed for the word launch" + }, + "tapLeftRightScreen": "Tap left/right screen", + "@tapLeftRightScreen": { + "description": "Text displayed on the how to flip on mobile" + }, + "flip": "FLIP", + "@flip": { + "description": "Text displayed for the word FLIP" + }, "start": "Start", "@start": { "description": "Text displayed on the character selection page start button" @@ -24,6 +48,10 @@ "@select": { "description": "Text displayed on the character selection page select button" }, + "space": "Space", + "@space": { + "description": "Text displayed on space control button" + }, "characterSelectionTitle": "Choose your character!", "@characterSelectionTitle": { "description": "Title text displayed on the character selection page" @@ -80,4 +108,4 @@ "@rounds": { "description": "Text displayed on the scoreboard widget to indicate rounds left" } -} +} \ No newline at end of file diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart deleted file mode 100644 index 49a35474..00000000 --- a/lib/leaderboard/bloc/leaderboard_bloc.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; - -part 'leaderboard_event.dart'; -part 'leaderboard_state.dart'; - -/// {@template leaderboard_bloc} -/// Manages leaderboard events. -/// -/// Uses a [LeaderboardRepository] to request and update players participations. -/// {@endtemplate} -class LeaderboardBloc extends Bloc { - /// {@macro leaderboard_bloc} - LeaderboardBloc(this._leaderboardRepository) - : super(const LeaderboardState.initial()) { - on(_onTop10Fetched); - on(_onLeaderboardEntryAdded); - } - - final LeaderboardRepository _leaderboardRepository; - - Future _onTop10Fetched( - Top10Fetched event, - Emitter emit, - ) async { - emit(state.copyWith(status: LeaderboardStatus.loading)); - try { - final top10Leaderboard = - await _leaderboardRepository.fetchTop10Leaderboard(); - - final leaderboardEntries = []; - top10Leaderboard.asMap().forEach( - (index, value) => leaderboardEntries.add(value.toEntry(index + 1)), - ); - - emit( - state.copyWith( - status: LeaderboardStatus.success, - leaderboard: leaderboardEntries, - ), - ); - } catch (error) { - emit(state.copyWith(status: LeaderboardStatus.error)); - addError(error); - } - } - - Future _onLeaderboardEntryAdded( - LeaderboardEntryAdded event, - Emitter emit, - ) async { - emit(state.copyWith(status: LeaderboardStatus.loading)); - try { - final ranking = - await _leaderboardRepository.addLeaderboardEntry(event.entry); - emit( - state.copyWith( - status: LeaderboardStatus.success, - ranking: ranking, - ), - ); - } catch (error) { - emit(state.copyWith(status: LeaderboardStatus.error)); - addError(error); - } - } -} diff --git a/lib/leaderboard/bloc/leaderboard_event.dart b/lib/leaderboard/bloc/leaderboard_event.dart deleted file mode 100644 index b9e6955a..00000000 --- a/lib/leaderboard/bloc/leaderboard_event.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'leaderboard_bloc.dart'; - -/// {@template leaderboard_event} -/// Represents the events available for [LeaderboardBloc]. -/// {endtemplate} -abstract class LeaderboardEvent extends Equatable { - /// {@macro leaderboard_event} - const LeaderboardEvent(); -} - -/// {@template top_10_fetched} -/// Request the top 10 [LeaderboardEntryData]s. -/// {endtemplate} -class Top10Fetched extends LeaderboardEvent { - /// {@macro top_10_fetched} - const Top10Fetched(); - - @override - List get props => []; -} - -/// {@template leaderboard_entry_added} -/// Writes a new [LeaderboardEntryData]. -/// -/// Should be added when a player finishes a game. -/// {endtemplate} -class LeaderboardEntryAdded extends LeaderboardEvent { - /// {@macro leaderboard_entry_added} - const LeaderboardEntryAdded({required this.entry}); - - /// [LeaderboardEntryData] to be written to the remote storage. - final LeaderboardEntryData entry; - - @override - List get props => [entry]; -} diff --git a/lib/leaderboard/bloc/leaderboard_state.dart b/lib/leaderboard/bloc/leaderboard_state.dart deleted file mode 100644 index 20d68f0d..00000000 --- a/lib/leaderboard/bloc/leaderboard_state.dart +++ /dev/null @@ -1,59 +0,0 @@ -// ignore_for_file: public_member_api_docs - -part of 'leaderboard_bloc.dart'; - -/// Defines the request status. -enum LeaderboardStatus { - /// Request is being loaded. - loading, - - /// Request was processed successfully and received a valid response. - success, - - /// Request was processed unsuccessfully and received an error. - error, -} - -/// {@template leaderboard_state} -/// Represents the state of the leaderboard. -/// {@endtemplate} -class LeaderboardState extends Equatable { - /// {@macro leaderboard_state} - const LeaderboardState({ - required this.status, - required this.ranking, - required this.leaderboard, - }); - - const LeaderboardState.initial() - : status = LeaderboardStatus.loading, - ranking = const LeaderboardRanking( - ranking: 0, - outOf: 0, - ), - leaderboard = const []; - - /// The current [LeaderboardStatus] of the state. - final LeaderboardStatus status; - - /// Rank of the current player. - final LeaderboardRanking ranking; - - /// List of top-ranked players. - final List leaderboard; - - @override - List get props => [status, ranking, leaderboard]; - - LeaderboardState copyWith({ - LeaderboardStatus? status, - LeaderboardRanking? ranking, - List? leaderboard, - }) { - return LeaderboardState( - status: status ?? this.status, - ranking: ranking ?? this.ranking, - leaderboard: leaderboard ?? this.leaderboard, - ); - } -} diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart deleted file mode 100644 index 08765743..00000000 --- a/lib/leaderboard/leaderboard.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'bloc/leaderboard_bloc.dart'; -export 'models/leader_board_entry.dart'; -export 'view/leaderboard_page.dart'; diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart deleted file mode 100644 index b9866111..00000000 --- a/lib/leaderboard/view/leaderboard_page.dart +++ /dev/null @@ -1,306 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/select_character/select_character.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -class LeaderboardPage extends StatelessWidget { - const LeaderboardPage({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - static Route route({required CharacterTheme theme}) { - return MaterialPageRoute( - builder: (_) => LeaderboardPage(theme: theme), - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LeaderboardBloc( - context.read(), - )..add(const Top10Fetched()), - child: LeaderboardView(theme: theme), - ); - } -} - -class LeaderboardView extends StatelessWidget { - const LeaderboardView({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 80), - Text( - l10n.leaderboard, - style: Theme.of(context).textTheme.headline3, - ), - const SizedBox(height: 80), - BlocBuilder( - builder: (context, state) { - switch (state.status) { - case LeaderboardStatus.loading: - return _LeaderboardLoading(theme: theme); - case LeaderboardStatus.success: - return _LeaderboardRanking( - ranking: state.leaderboard, - theme: theme, - ); - case LeaderboardStatus.error: - return _LeaderboardError(theme: theme); - } - }, - ), - const SizedBox(height: 20), - TextButton( - onPressed: () => Navigator.of(context).push( - CharacterSelectionDialog.route(), - ), - child: Text(l10n.retry), - ), - ], - ), - ), - ), - ); - } -} - -class _LeaderboardLoading extends StatelessWidget { - const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return const Center( - child: CircularProgressIndicator(), - ); - } -} - -class _LeaderboardError extends StatelessWidget { - const _LeaderboardError({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: Text( - 'There was en error loading data!', - style: - Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red), - ), - ); - } -} - -class _LeaderboardRanking extends StatelessWidget { - const _LeaderboardRanking({ - Key? key, - required this.ranking, - required this.theme, - }) : super(key: key); - - final List ranking; - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardHeaders(theme: theme), - _LeaderboardList( - ranking: ranking, - theme: theme, - ), - ], - ), - ); - } -} - -class _LeaderboardHeaders extends StatelessWidget { - const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardHeaderItem(title: l10n.rank, theme: theme), - _LeaderboardHeaderItem(title: l10n.character, theme: theme), - _LeaderboardHeaderItem(title: l10n.username, theme: theme), - _LeaderboardHeaderItem(title: l10n.score, theme: theme), - ], - ); - } -} - -class _LeaderboardHeaderItem extends StatelessWidget { - const _LeaderboardHeaderItem({ - Key? key, - required this.title, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final String title; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: theme.ballColor, - ), - child: Text( - title, - style: Theme.of(context).textTheme.headline5, - ), - ), - ); - } -} - -class _LeaderboardList extends StatelessWidget { - const _LeaderboardList({ - Key? key, - required this.ranking, - required this.theme, - }) : super(key: key); - - final List ranking; - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return ListView.builder( - shrinkWrap: true, - itemBuilder: (_, index) => _LeaderBoardCompetitor( - entry: ranking[index], - theme: theme, - ), - itemCount: ranking.length, - ); - } -} - -class _LeaderBoardCompetitor extends StatelessWidget { - const _LeaderBoardCompetitor({ - Key? key, - required this.entry, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - - final LeaderboardEntry entry; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardCompetitorField( - text: entry.rank, - theme: theme, - ), - _LeaderboardCompetitorCharacter( - characterAsset: entry.character, - theme: theme, - ), - _LeaderboardCompetitorField( - text: entry.playerInitials, - theme: theme, - ), - _LeaderboardCompetitorField( - text: entry.score.toString(), - theme: theme, - ), - ], - ); - } -} - -class _LeaderboardCompetitorField extends StatelessWidget { - const _LeaderboardCompetitorField({ - Key? key, - required this.text, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final String text; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: theme.ballColor, - width: 2, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(text), - ), - ), - ); - } -} - -class _LeaderboardCompetitorCharacter extends StatelessWidget { - const _LeaderboardCompetitorCharacter({ - Key? key, - required this.characterAsset, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final AssetGenImage characterAsset; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: theme.ballColor, - width: 2, - ), - ), - child: SizedBox( - height: 30, - child: characterAsset.image(), - ), - ), - ); - } -} diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 83dc6ee6..863722e6 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -49,14 +49,13 @@ class CharacterSelectionView extends StatelessWidget { Navigator.of(context).pop(); // TODO(arturplaczek): remove after merge StarBlocListener final height = MediaQuery.of(context).size.height * 0.5; - showDialog( context: context, builder: (_) => Center( child: SizedBox( height: height, width: height * 1.4, - child: const HowToPlayDialog(), + child: HowToPlayDialog(), ), ), ); diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index bc5166e4..1665d35d 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -1,33 +1,236 @@ // ignore_for_file: public_member_api_docs +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; +import 'package:platform_helper/platform_helper.dart'; + +@visibleForTesting +enum Control { + left, + right, + down, + a, + d, + s, + space, +} + +extension on Control { + bool get isArrow => isDown || isRight || isLeft; + + bool get isDown => this == Control.down; + + bool get isRight => this == Control.right; + + bool get isLeft => this == Control.left; + + bool get isSpace => this == Control.space; + + String getCharacter(BuildContext context) { + switch (this) { + case Control.a: + return 'A'; + case Control.d: + return 'D'; + case Control.down: + return '>'; // Will be rotated + case Control.left: + return '<'; + case Control.right: + return '>'; + case Control.s: + return 'S'; + case Control.space: + return context.l10n.space; + } + } +} + +class HowToPlayDialog extends StatefulWidget { + HowToPlayDialog({ + Key? key, + @visibleForTesting PlatformHelper? platformHelper, + }) : platformHelper = platformHelper ?? PlatformHelper(), + super(key: key); + + final PlatformHelper platformHelper; + + @override + State createState() => _HowToPlayDialogState(); +} + +class _HowToPlayDialogState extends State { + late Timer closeTimer; + @override + void initState() { + super.initState(); + closeTimer = Timer(const Duration(seconds: 3), () { + if (mounted) { + Navigator.of(context).maybePop(); + } + }); + } + + @override + void dispose() { + closeTimer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = widget.platformHelper.isMobile; + return PixelatedDecoration( + header: const _HowToPlayHeader(), + body: isMobile ? const _MobileBody() : const _DesktopBody(), + ); + } +} + +class _MobileBody extends StatelessWidget { + const _MobileBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final paddingWidth = MediaQuery.of(context).size.width * 0.15; + final paddingHeight = MediaQuery.of(context).size.height * 0.075; + return FittedBox( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: paddingWidth, + ), + child: Column( + children: [ + const _MobileLaunchControls(), + SizedBox(height: paddingHeight), + const _MobileFlipperControls(), + ], + ), + ), + ); + } +} + +class _MobileLaunchControls extends StatelessWidget { + const _MobileLaunchControls({Key? key}) : super(key: key); -class HowToPlayDialog extends StatelessWidget { - const HowToPlayDialog({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const textStyle = AppTextStyle.subtitle3; + return Column( + children: [ + Text( + l10n.tapAndHoldRocket, + style: textStyle, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${l10n.to} ', + style: textStyle, + ), + TextSpan( + text: l10n.launch, + style: textStyle.copyWith( + color: AppColors.blue, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _MobileFlipperControls extends StatelessWidget { + const _MobileFlipperControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final l10n = context.l10n; + const textStyle = AppTextStyle.subtitle3; + return Column( + children: [ + Text( + l10n.tapLeftRightScreen, + style: textStyle, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${l10n.to} ', + style: textStyle, + ), + TextSpan( + text: l10n.flip, + style: textStyle.copyWith( + color: AppColors.orange, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _DesktopBody extends StatelessWidget { + const _DesktopBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { const spacing = SizedBox(height: 16); + return ListView( + children: const [ + spacing, + _DesktopLaunchControls(), + spacing, + _DesktopFlipperControls(), + ], + ); + } +} - return PixelatedDecoration( - header: Text(l10n.howToPlay), - body: ListView( - children: const [ - spacing, - _LaunchControls(), - spacing, - _FlipperControls(), +class _HowToPlayHeader extends StatelessWidget { + const _HowToPlayHeader({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const headerTextStyle = AppTextStyle.title; + + return FittedBox( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.howToPlay, + style: headerTextStyle.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + l10n.tipsForFlips, + style: headerTextStyle, + ), ], ), ); } } -class _LaunchControls extends StatelessWidget { - const _LaunchControls({Key? key}) : super(key: key); +class _DesktopLaunchControls extends StatelessWidget { + const _DesktopLaunchControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -36,15 +239,18 @@ class _LaunchControls extends StatelessWidget { return Column( children: [ - Text(l10n.launchControls), + Text( + l10n.launchControls, + style: AppTextStyle.headline4, + ), const SizedBox(height: 10), Wrap( children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down), + KeyButton(control: Control.down), spacing, - KeyIndicator.fromKeyName(keyName: 'SPACE'), + KeyButton(control: Control.space), spacing, - KeyIndicator.fromKeyName(keyName: 'S'), + KeyButton(control: Control.s), ], ) ], @@ -52,8 +258,8 @@ class _LaunchControls extends StatelessWidget { } } -class _FlipperControls extends StatelessWidget { - const _FlipperControls({Key? key}) : super(key: key); +class _DesktopFlipperControls extends StatelessWidget { + const _DesktopFlipperControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -62,7 +268,10 @@ class _FlipperControls extends StatelessWidget { return Column( children: [ - Text(l10n.flipperControls), + Text( + l10n.flipperControls, + style: AppTextStyle.subtitle2, + ), const SizedBox(height: 10), Column( children: [ @@ -70,17 +279,17 @@ class _FlipperControls extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), + KeyButton(control: Control.left), rowSpacing, - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right), + KeyButton(control: Control.right), ], ), const SizedBox(height: 8), Wrap( children: const [ - KeyIndicator.fromKeyName(keyName: 'A'), + KeyButton(control: Control.a), rowSpacing, - KeyIndicator.fromKeyName(keyName: 'D'), + KeyButton(control: Control.d), ], ) ], @@ -90,65 +299,46 @@ class _FlipperControls extends StatelessWidget { } } -// TODO(allisonryan0002): remove visibility when adding final UI. @visibleForTesting -class KeyIndicator extends StatelessWidget { - const KeyIndicator._({ +class KeyButton extends StatelessWidget { + const KeyButton({ Key? key, - required String keyName, - required IconData keyIcon, - required bool fromIcon, - }) : _keyName = keyName, - _keyIcon = keyIcon, - _fromIcon = fromIcon, + required Control control, + }) : _control = control, super(key: key); - const KeyIndicator.fromKeyName({Key? key, required String keyName}) - : this._( - key: key, - keyName: keyName, - keyIcon: Icons.keyboard_arrow_down, - fromIcon: false, - ); - - const KeyIndicator.fromIcon({Key? key, required IconData keyIcon}) - : this._( - key: key, - keyName: '', - keyIcon: keyIcon, - fromIcon: true, - ); - - final String _keyName; - - final IconData _keyIcon; - - final bool _fromIcon; + final Control _control; @override Widget build(BuildContext context) { - const iconPadding = EdgeInsets.all(15); - const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22); - final boarderColor = Colors.blue.withOpacity(0.5); - final color = Colors.blue.withOpacity(0.7); - + final textStyle = + _control.isArrow ? AppTextStyle.headline1 : AppTextStyle.headline3; + const height = 60.0; + final width = _control.isSpace ? height * 2.83 : height; return DecoratedBox( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: boarderColor, - width: 3, + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage( + _control.isSpace + ? Assets.images.components.space.keyName + : Assets.images.components.key.keyName, + ), ), ), - child: _fromIcon - ? Padding( - padding: iconPadding, - child: Icon(_keyIcon, color: color), - ) - : Padding( - padding: textPadding, - child: Text(_keyName, style: TextStyle(color: color)), + child: SizedBox( + width: width, + height: height, + child: Center( + child: RotatedBox( + quarterTurns: _control.isDown ? 1 : 0, + child: Text( + _control.getCharacter(context), + style: textStyle.copyWith(color: AppColors.white), ), + ), + ), + ), ); } } diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index 2d3899a6..a12d3edc 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -7,7 +7,9 @@ abstract class AppColors { static const Color darkBlue = Color(0xFF0C32A4); - static const Color orange = Color(0xFFFFEE02); + static const Color yellow = Color(0xFFFFEE02); + + static const Color orange = Color(0xFFE5AB05); static const Color blue = Color(0xFF4B94F6); diff --git a/lib/theme/app_text_style.dart b/lib/theme/app_text_style.dart index 8104ca11..084936e9 100644 --- a/lib/theme/app_text_style.dart +++ b/lib/theme/app_text_style.dart @@ -27,6 +27,35 @@ abstract class AppTextStyle { fontFamily: _primaryFontFamily, ); + static const headline4 = TextStyle( + color: AppColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const title = TextStyle( + color: AppColors.darkBlue, + fontSize: 20, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle3 = TextStyle( + color: AppColors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle2 = TextStyle( + color: AppColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + static const subtitle1 = TextStyle( fontSize: 10, fontFamily: _primaryFontFamily, diff --git a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart index 63207e01..a865acf8 100644 --- a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart +++ b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart @@ -13,12 +13,14 @@ export 'cubit/google_letter_cubit.dart'; class GoogleLetter extends BodyComponent with InitialPosition { /// {@macro google_letter} GoogleLetter( - int index, - ) : bloc = GoogleLetterCubit(), + int index, { + Iterable? children, + }) : bloc = GoogleLetterCubit(), super( children: [ GoogleLetterBallContactBehavior(), - _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]) + _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]), + ...?children, ], ); diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index 12cd638d..527ffde4 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -16,9 +16,13 @@ class Kicker extends BodyComponent with InitialPosition { /// {@macro kicker} Kicker({ required BoardSide side, + Iterable? children, }) : _side = side, super( - children: [_KickerSpriteComponent(side: side)], + children: [ + _KickerSpriteComponent(side: side), + ...?children, + ], renderBody: false, ); diff --git a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart index 624168b9..7ad0e64b 100644 --- a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -80,6 +81,16 @@ void main() { }, ); + flameTester.test('adds new children', (game) async { + final component = Component(); + final googleLetter = GoogleLetter( + 1, + children: [component], + ); + await game.ensureAdd(googleLetter); + expect(googleLetter.children, contains(component)); + }); + test('throws error when index out of range', () { expect(() => GoogleLetter(-1), throwsA(isA())); expect(() => GoogleLetter(6), throwsA(isA())); diff --git a/packages/pinball_components/test/src/components/kicker_test.dart b/packages/pinball_components/test/src/components/kicker_test.dart index 8c48a1fb..aebf9380 100644 --- a/packages/pinball_components/test/src/components/kicker_test.dart +++ b/packages/pinball_components/test/src/components/kicker_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -44,6 +45,16 @@ void main() { }, ); + flameTester.test('adds new children', (game) async { + final component = Component(); + final kicker = Kicker( + side: BoardSide.left, + children: [component], + ); + await game.ensureAdd(kicker); + expect(kicker.children, contains(component)); + }); + flameTester.test( 'body is static', (game) async { diff --git a/packages/platform_helper/.gitignore b/packages/platform_helper/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/platform_helper/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/platform_helper/README.md b/packages/platform_helper/README.md new file mode 100644 index 00000000..7a96e658 --- /dev/null +++ b/packages/platform_helper/README.md @@ -0,0 +1,11 @@ +# platform_helper + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Platform helper for Pinball application. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/platform_helper/analysis_options.yaml b/packages/platform_helper/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/platform_helper/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/platform_helper/lib/platform_helper.dart b/packages/platform_helper/lib/platform_helper.dart new file mode 100644 index 00000000..4b6d7f48 --- /dev/null +++ b/packages/platform_helper/lib/platform_helper.dart @@ -0,0 +1,3 @@ +library platform_helper; + +export 'src/platform_helper.dart'; diff --git a/packages/platform_helper/lib/src/platform_helper.dart b/packages/platform_helper/lib/src/platform_helper.dart new file mode 100644 index 00000000..638d1ab6 --- /dev/null +++ b/packages/platform_helper/lib/src/platform_helper.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; + +/// {@template platform_helper} +/// Returns whether the current platform is running on a mobile device. +/// {@endtemplate} +class PlatformHelper { + /// {@macro platform_helper} + bool get isMobile { + return defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android; + } +} diff --git a/packages/platform_helper/pubspec.yaml b/packages/platform_helper/pubspec.yaml new file mode 100644 index 00000000..edff346a --- /dev/null +++ b/packages/platform_helper/pubspec.yaml @@ -0,0 +1,16 @@ +name: platform_helper +description: Platform helper for Pinball application. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/packages/platform_helper/test/src/platform_helper_test.dart b/packages/platform_helper/test/src/platform_helper_test.dart new file mode 100644 index 00000000..69bec3a8 --- /dev/null +++ b/packages/platform_helper/test/src/platform_helper_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_helper/platform_helper.dart'; + +void main() { + group('PlatformHelper', () { + test('can be instantiated', () { + expect(PlatformHelper(), isNotNull); + }); + + group('isMobile', () { + tearDown(() async { + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is iOS', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is android', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test( + 'returns false when defaultTargetPlatform is niether iOS nor android', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + expect(PlatformHelper().isMobile, isFalse); + debugDefaultTargetPlatformOverride = null; + }, + ); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 4b71c77b..ab39378a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -513,6 +513,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_helper: + dependency: "direct main" + description: + path: "packages/platform_helper" + relative: true + source: path + version: "1.0.0+1" plugin_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f129ea19..51c85cd5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: path: packages/pinball_theme pinball_ui: path: packages/pinball_ui + platform_helper: + path: packages/platform_helper dev_dependencies: bloc_test: ^9.0.2 diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index dfa28869..8f5f7f13 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -108,7 +108,7 @@ void main() { expect( find.byWidgetPredicate( - (widget) => widget is Container && widget.color == AppColors.orange, + (widget) => widget is Container && widget.color == AppColors.yellow, ), findsOneWidget, ); @@ -125,7 +125,7 @@ void main() { find.byWidgetPredicate( (widget) => widget is Container && - widget.color == AppColors.orange.withAlpha(128), + widget.color == AppColors.yellow.withAlpha(128), ), findsOneWidget, ); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 6aab19a2..14a286e2 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -35,8 +34,6 @@ class MockGameState extends Mock implements GameState {} class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} -class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} - class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart deleted file mode 100644 index 2b217704..00000000 --- a/test/leaderboard/bloc/leaderboard_bloc_test.dart +++ /dev/null @@ -1,203 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LeaderboardBloc', () { - late LeaderboardRepository leaderboardRepository; - - setUp(() { - leaderboardRepository = MockLeaderboardRepository(); - }); - - test('initial state has state loading no ranking and empty leaderboard', - () { - final bloc = LeaderboardBloc(leaderboardRepository); - expect(bloc.state.status, equals(LeaderboardStatus.loading)); - expect(bloc.state.ranking.ranking, equals(0)); - expect(bloc.state.ranking.outOf, equals(0)); - expect(bloc.state.leaderboard.isEmpty, isTrue); - }); - - group('Top10Fetched', () { - const top10Scores = [ - 2500, - 2200, - 2200, - 2000, - 1800, - 1400, - 1300, - 1000, - 600, - 300, - 100, - ]; - - final top10Leaderboard = top10Scores - .map( - (score) => LeaderboardEntryData( - playerInitials: 'user$score', - score: score, - character: CharacterType.dash, - ), - ) - .toList(); - - blocTest( - 'emits [loading, success] statuses ' - 'when fetchTop10Leaderboard succeeds', - setUp: () { - when(() => leaderboardRepository.fetchTop10Leaderboard()).thenAnswer( - (_) async => top10Leaderboard, - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(Top10Fetched()), - expect: () => [ - LeaderboardState.initial(), - isA() - ..having( - (element) => element.status, - 'status', - equals(LeaderboardStatus.success), - ) - ..having( - (element) => element.leaderboard.length, - 'leaderboard', - equals(top10Leaderboard.length), - ) - ], - verify: (_) => - verify(() => leaderboardRepository.fetchTop10Leaderboard()) - .called(1), - ); - - blocTest( - 'emits [loading, error] statuses ' - 'when fetchTop10Leaderboard fails', - setUp: () { - when(() => leaderboardRepository.fetchTop10Leaderboard()).thenThrow( - Exception(), - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(Top10Fetched()), - expect: () => [ - LeaderboardState.initial(), - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), - ], - verify: (_) => - verify(() => leaderboardRepository.fetchTop10Leaderboard()) - .called(1), - errors: () => [isA()], - ); - }); - - group('LeaderboardEntryAdded', () { - final leaderboardEntry = LeaderboardEntryData( - playerInitials: 'ABC', - score: 1500, - character: CharacterType.dash, - ); - - final ranking = LeaderboardRanking(ranking: 3, outOf: 4); - - blocTest( - 'emits [loading, success] statuses ' - 'when addLeaderboardEntry succeeds', - setUp: () { - when( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).thenAnswer( - (_) async => ranking, - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), - expect: () => [ - LeaderboardState.initial(), - isA() - ..having( - (element) => element.status, - 'status', - equals(LeaderboardStatus.success), - ) - ..having( - (element) => element.ranking, - 'ranking', - equals(ranking), - ) - ], - verify: (_) => verify( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).called(1), - ); - - blocTest( - 'emits [loading, error] statuses ' - 'when addLeaderboardEntry fails', - setUp: () { - when( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).thenThrow( - Exception(), - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), - expect: () => [ - LeaderboardState.initial(), - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), - ], - verify: (_) => verify( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).called(1), - errors: () => [isA()], - ); - }); - }); - - group('CharacterTypeX', () { - test('converts CharacterType.android to AndroidTheme', () { - expect(CharacterType.android.toTheme, equals(AndroidTheme())); - }); - - test('converts CharacterType.dash to DashTheme', () { - expect(CharacterType.dash.toTheme, equals(DashTheme())); - }); - - test('converts CharacterType.dino to DinoTheme', () { - expect(CharacterType.dino.toTheme, equals(DinoTheme())); - }); - - test('converts CharacterType.sparky to SparkyTheme', () { - expect(CharacterType.sparky.toTheme, equals(SparkyTheme())); - }); - }); - - group('CharacterThemeX', () { - test('converts AndroidTheme to CharacterType.android', () { - expect(AndroidTheme().toType, equals(CharacterType.android)); - }); - - test('converts DashTheme to CharacterType.dash', () { - expect(DashTheme().toType, equals(CharacterType.dash)); - }); - - test('converts DinoTheme to CharacterType.dino', () { - expect(DinoTheme().toType, equals(CharacterType.dino)); - }); - - test('converts SparkyTheme to CharacterType.sparky', () { - expect(SparkyTheme().toType, equals(CharacterType.sparky)); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_event_test.dart b/test/leaderboard/bloc/leaderboard_event_test.dart deleted file mode 100644 index 33199ca1..00000000 --- a/test/leaderboard/bloc/leaderboard_event_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; - -void main() { - group('GameEvent', () { - group('Top10Fetched', () { - test('can be instantiated', () { - expect(const Top10Fetched(), isNotNull); - }); - - test('supports value equality', () { - expect( - Top10Fetched(), - equals(const Top10Fetched()), - ); - }); - }); - - group('LeaderboardEntryAdded', () { - const leaderboardEntry = LeaderboardEntryData( - playerInitials: 'ABC', - score: 1500, - character: CharacterType.dash, - ); - - test('can be instantiated', () { - expect(const LeaderboardEntryAdded(entry: leaderboardEntry), isNotNull); - }); - - test('supports value equality', () { - expect( - LeaderboardEntryAdded(entry: leaderboardEntry), - equals(const LeaderboardEntryAdded(entry: leaderboardEntry)), - ); - }); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart deleted file mode 100644 index 1b5d41d9..00000000 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('LeaderboardState', () { - test('supports value equality', () { - expect( - LeaderboardState.initial(), - equals( - LeaderboardState.initial(), - ), - ); - }); - - group('constructor', () { - test('can be instantiated', () { - expect( - LeaderboardState.initial(), - isNotNull, - ); - }); - }); - - group('copyWith', () { - final leaderboardEntry = LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 1500, - character: DashTheme().leaderboardIcon, - ); - - test( - 'copies correctly ' - 'when no argument specified', - () { - const leaderboardState = LeaderboardState.initial(); - expect( - leaderboardState.copyWith(), - equals(leaderboardState), - ); - }, - ); - - test( - 'copies correctly ' - 'when all arguments specified', - () { - const leaderboardState = LeaderboardState.initial(); - final otherLeaderboardState = LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: [leaderboardEntry], - ); - expect(leaderboardState, isNot(equals(otherLeaderboardState))); - - expect( - leaderboardState.copyWith( - status: otherLeaderboardState.status, - ranking: otherLeaderboardState.ranking, - leaderboard: otherLeaderboardState.leaderboard, - ), - equals(otherLeaderboardState), - ); - }, - ); - }); - }); -} diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart deleted file mode 100644 index daacb4a7..00000000 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LeaderboardPage', () { - testWidgets('renders LeaderboardView', (tester) async { - await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), - ), - ); - - expect(find.byType(LeaderboardView), findsOneWidget); - }); - - testWidgets('route returns a valid navigation route', (tester) async { - await expectNavigatesToRoute( - tester, - LeaderboardPage.route( - theme: DashTheme(), - ), - ); - }); - }); - - group('LeaderboardView', () { - late LeaderboardBloc leaderboardBloc; - - setUp(() { - leaderboardBloc = MockLeaderboardBloc(); - }); - - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.text(l10n.leaderboard), findsOneWidget); - expect(find.text(l10n.retry), findsOneWidget); - }); - - testWidgets('renders loading view when bloc emits [loading]', - (tester) async { - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - expect(find.text('There was en error loading data!'), findsNothing); - expect(find.byType(ListView), findsNothing); - }); - - testWidgets('renders error view when bloc emits [error]', (tester) async { - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial().copyWith( - status: LeaderboardStatus.error, - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('There was en error loading data!'), findsOneWidget); - expect(find.byType(ListView), findsNothing); - }); - - testWidgets('renders success view when bloc emits [success]', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: [ - LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 10000, - character: DashTheme().leaderboardIcon, - ), - ], - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('There was en error loading data!'), findsNothing); - expect(find.text(l10n.rank), findsOneWidget); - expect(find.text(l10n.character), findsOneWidget); - expect(find.text(l10n.username), findsOneWidget); - expect(find.text(l10n.score), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); - }); - - testWidgets('navigates to CharacterSelectionPage when retry is tapped', - (tester) async { - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - - await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), - ), - navigator: navigator, - ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - - verify(() => navigator.push(any())).called(1); - }); - }); -} diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 0dda92d7..dc5d70ea 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: prefer_const_constructors +import 'dart:async'; + import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -84,17 +86,68 @@ void main() { .called(1); }); - testWidgets('displays how to play dialog when start is tapped', + group('HowToPlayDialog', () { + testWidgets( + 'is displayed for 3 seconds when start is tapped', (tester) async { - await tester.pumpApp( - CharacterSelectionView(), - characterThemeCubit: characterThemeCubit, + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionDialog.route()); + }, + child: Text('Tap me'), + ); + }, + ), + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.pump(Duration(seconds: 3)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }, ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); + testWidgets( + 'can be dismissed manually before 3 seconds have passed', + (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionDialog.route()); + }, + child: Text('Tap me'), + ); + }, + ), + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.tapAt(Offset(1, 1)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }, + ); }); }); diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart index c31ac1a3..1de4c2ad 100644 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -2,41 +2,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/start_game/start_game.dart'; +import 'package:platform_helper/platform_helper.dart'; import '../../helpers/helpers.dart'; +class MockPlatformHelper extends Mock implements PlatformHelper {} + void main() { group('HowToPlayDialog', () { - testWidgets('displays content', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - - await tester.pumpApp(HowToPlayDialog()); + late AppLocalizations l10n; + late PlatformHelper platformHelper; - expect(find.text(l10n.launchControls), findsOneWidget); + setUp(() async { + l10n = await AppLocalizations.delegate.load(Locale('en')); + platformHelper = MockPlatformHelper(); }); - }); - group('KeyIndicator', () { - testWidgets('fromKeyName renders correctly', (tester) async { - const keyName = 'A'; + testWidgets( + 'can be instantiated without passing in a platform helper', + (tester) async { + await tester.pumpApp(HowToPlayDialog()); + expect(find.byType(HowToPlayDialog), findsOneWidget); + }, + ); + testWidgets('displays content for desktop', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => false); await tester.pumpApp( - KeyIndicator.fromKeyName(keyName: keyName), + HowToPlayDialog( + platformHelper: platformHelper, + ), ); - - expect(find.text(keyName), findsOneWidget); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.launchControls), findsOneWidget); + expect(find.text(l10n.flipperControls), findsOneWidget); + expect(find.byType(KeyButton), findsNWidgets(7)); }); - testWidgets('fromIcon renders correctly', (tester) async { - const keyIcon = Icons.keyboard_arrow_down; + testWidgets('displays content for mobile', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => true); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); + expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); + }); + }); + group('KeyButton', () { + testWidgets('renders correctly', (tester) async { await tester.pumpApp( - KeyIndicator.fromIcon(keyIcon: keyIcon), + KeyButton(control: Control.a), ); - expect(find.byIcon(keyIcon), findsOneWidget); + expect(find.text('A'), findsOneWidget); }); }); } diff --git a/web/index.html b/web/index.html index 37e17170..e1544bf2 100644 --- a/web/index.html +++ b/web/index.html @@ -56,6 +56,13 @@ I/O Pinball Machine - Flutter + +