From 5b355ff406fca431dedde4eb44c48455187d8833 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 22 Mar 2022 12:57:11 +0000 Subject: [PATCH 01/13] refactor: used size instead of width and height (#69) --- lib/game/components/board.dart | 10 +++++----- lib/game/components/flipper.dart | 25 +++++++++++-------------- test/game/components/flipper_test.dart | 4 ++-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index f7b80bd8..6107048e 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -94,7 +94,7 @@ class _BottomGroup extends Component { @override Future onLoad() async { - final spacing = this.spacing + Flipper.width / 2; + final spacing = this.spacing + Flipper.size.x / 2; final rightSide = _BottomGroupSide( side: BoardSide.right, position: position + Vector2(spacing, 0), @@ -135,15 +135,15 @@ class _BottomGroupSide extends Component { final baseboard = Baseboard(side: _side) ..initialPosition = _position + Vector2( - (Flipper.width * direction) - direction, - Flipper.height, + (Flipper.size.x * direction) - direction, + Flipper.size.y, ); final slingShot = SlingShot( side: _side, )..initialPosition = _position + Vector2( - (Flipper.width) * direction, - Flipper.height + SlingShot.size.y, + (Flipper.size.x) * direction, + Flipper.size.y + SlingShot.size.y, ); await addAll([flipper, baseboard, slingShot]); diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 77238acd..c4f18389 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -58,11 +58,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// Sprite is preloaded by [PinballGameAssetsX]. static const spritePath = 'components/flipper.png'; - /// The width of the [Flipper]. - static const width = 12.0; - - /// The height of the [Flipper]. - static const height = 2.8; + /// The size of the [Flipper]. + static final size = Vector2(12, 2.8); /// The speed required to move the [Flipper] to its highest position. /// @@ -97,7 +94,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { final sprite = await gameRef.loadSprite(spritePath); final spriteComponent = SpriteComponent( sprite: sprite, - size: Vector2(width, height), + size: size, anchor: Anchor.center, ); @@ -134,21 +131,21 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { final fixturesDef = []; final isLeft = side.isLeft; - final bigCircleShape = CircleShape()..radius = height / 2; + final bigCircleShape = CircleShape()..radius = 1.75; bigCircleShape.position.setValues( isLeft - ? -(width / 2) + bigCircleShape.radius - : (width / 2) - bigCircleShape.radius, + ? -(size.x / 2) + bigCircleShape.radius + : (size.x / 2) - bigCircleShape.radius, 0, ); final bigCircleFixtureDef = FixtureDef(bigCircleShape); fixturesDef.add(bigCircleFixtureDef); - final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; + final smallCircleShape = CircleShape()..radius = 0.9; smallCircleShape.position.setValues( isLeft - ? (width / 2) - smallCircleShape.radius - : -(width / 2) + smallCircleShape.radius, + ? (size.x / 2) - smallCircleShape.radius + : -(size.x / 2) + smallCircleShape.radius, 0, ); final smallCircleFixtureDef = FixtureDef(smallCircleShape); @@ -227,8 +224,8 @@ class FlipperAnchor extends JointAnchor { }) { initialPosition = Vector2( flipper.side.isLeft - ? flipper.body.position.x - Flipper.width / 2 - : flipper.body.position.x + Flipper.width / 2, + ? flipper.body.position.x - Flipper.size.x / 2 + : flipper.body.position.x + Flipper.size.x / 2, flipper.body.position.y, ); } diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index e6e9ba23..64d2f77b 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -282,7 +282,7 @@ void main() { final flipperAnchor = FlipperAnchor(flipper: flipper); await game.ensureAdd(flipperAnchor); - expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2)); + expect(flipperAnchor.body.position.x, equals(-Flipper.size.x / 2)); }, ); @@ -297,7 +297,7 @@ void main() { final flipperAnchor = FlipperAnchor(flipper: flipper); await game.ensureAdd(flipperAnchor); - expect(flipperAnchor.body.position.x, equals(Flipper.width / 2)); + expect(flipperAnchor.body.position.x, equals(Flipper.size.x / 2)); }, ); }); From 79bb95bef97f20525e7cb0d19345dab0ce8a3b46 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 22 Mar 2022 13:21:04 +0000 Subject: [PATCH 02/13] refactor: rename `SlingShot` to `Kicker` (#68) * refactor: renamed SlingShot to Kicker --- lib/game/components/board.dart | 10 +++---- lib/game/components/board_side.dart | 2 +- lib/game/components/components.dart | 2 +- .../{sling_shot.dart => kicker.dart} | 26 +++++++++---------- test/game/components/board_test.dart | 6 ++--- ...{sling_shot_test.dart => kicker_test.dart} | 26 +++++++++---------- 6 files changed, 36 insertions(+), 36 deletions(-) rename lib/game/components/{sling_shot.dart => kicker.dart} (86%) rename test/game/components/{sling_shot_test.dart => kicker_test.dart} (65%) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 6107048e..9c34a263 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart'; /// {@template board} /// The main flat surface of the [PinballGame], where the [Flipper]s, -/// [RoundBumper]s, [SlingShot]s are arranged. +/// [RoundBumper]s, [Kicker]s are arranged. /// {entemplate} class Board extends Component { /// {@macro board} @@ -76,7 +76,7 @@ class _FlutterForest extends Component { /// {@template bottom_group} /// Grouping of the board's bottom [Component]s. /// -/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [SlingShot]s. +/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s. /// {@endtemplate} // TODO(alestiago): Consider renaming once entire Board is defined. class _BottomGroup extends Component { @@ -138,14 +138,14 @@ class _BottomGroupSide extends Component { (Flipper.size.x * direction) - direction, Flipper.size.y, ); - final slingShot = SlingShot( + final kicker = Kicker( side: _side, )..initialPosition = _position + Vector2( (Flipper.size.x) * direction, - Flipper.size.y + SlingShot.size.y, + Flipper.size.y + Kicker.size.y, ); - await addAll([flipper, baseboard, slingShot]); + await addAll([flipper, baseboard, kicker]); } } diff --git a/lib/game/components/board_side.dart b/lib/game/components/board_side.dart index f7587f47..2ef8d651 100644 --- a/lib/game/components/board_side.dart +++ b/lib/game/components/board_side.dart @@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart'; /// Indicates a side of the board. /// /// Usually used to position or mirror elements of a [PinballGame]; such as a -/// [Flipper] or [SlingShot]. +/// [Flipper] or [Kicker]. enum BoardSide { /// The left side of the board. left, diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 84525166..a255e652 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -7,6 +7,7 @@ export 'flipper.dart'; export 'initial_position.dart'; export 'jetpack_ramp.dart'; export 'joint_anchor.dart'; +export 'kicker.dart'; export 'launcher_ramp.dart'; export 'layer.dart'; export 'pathway.dart'; @@ -14,6 +15,5 @@ export 'plunger.dart'; export 'ramp_opening.dart'; export 'round_bumper.dart'; export 'score_points.dart'; -export 'sling_shot.dart'; export 'spaceship.dart'; export 'wall.dart'; diff --git a/lib/game/components/sling_shot.dart b/lib/game/components/kicker.dart similarity index 86% rename from lib/game/components/sling_shot.dart rename to lib/game/components/kicker.dart index 53160213..e4b2824d 100644 --- a/lib/game/components/sling_shot.dart +++ b/lib/game/components/kicker.dart @@ -6,15 +6,15 @@ import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:pinball/game/game.dart'; -/// {@template sling_shot} +/// {@template kicker} /// Triangular [BodyType.static] body that propels the [Ball] towards the /// opposite side. /// -/// [SlingShot]s are usually positioned above each [Flipper]. -/// {@endtemplate sling_shot} -class SlingShot extends BodyComponent with InitialPosition { - /// {@macro sling_shot} - SlingShot({ +/// [Kicker]s are usually positioned above each [Flipper]. +/// {@endtemplate kicker} +class Kicker extends BodyComponent with InitialPosition { + /// {@macro kicker} + Kicker({ required BoardSide side, }) : _side = side { // TODO(alestiago): Use sprite instead of color when provided. @@ -23,14 +23,14 @@ class SlingShot extends BodyComponent with InitialPosition { ..style = PaintingStyle.fill; } - /// Whether the [SlingShot] is on the left or right side of the board. + /// Whether the [Kicker] is on the left or right side of the board. /// - /// A [SlingShot] with [BoardSide.left] propels the [Ball] to the right, - /// whereas a [SlingShot] with [BoardSide.right] propels the [Ball] to the + /// A [Kicker] with [BoardSide.left] propels the [Ball] to the right, + /// whereas a [Kicker] with [BoardSide.right] propels the [Ball] to the /// left. final BoardSide _side; - /// The size of the [SlingShot] body. + /// The size of the [Kicker] body. // TODO(alestiago): Use size from PositionedBodyComponent instead, // once a sprite is given. static final Vector2 size = Vector2(4, 10); @@ -78,7 +78,7 @@ class SlingShot extends BodyComponent with InitialPosition { final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0; fixturesDefs.add(bottomLineFixtureDef); - final kickerEdge = EdgeShape() + final bouncyEdge = EdgeShape() ..set( upperCircle.position + Vector2( @@ -92,11 +92,11 @@ class SlingShot extends BodyComponent with InitialPosition { ), ); - final kickerFixtureDef = FixtureDef(kickerEdge) + final bouncyFixtureDef = FixtureDef(bouncyEdge) // TODO(alestiago): Play with restitution value once game is bundled. ..restitution = 10.0 ..friction = 0; - fixturesDefs.add(kickerFixtureDef); + fixturesDefs.add(bouncyFixtureDef); // TODO(alestiago): Evaluate if there is value on centering the fixtures. final centroid = geometry.centroid( diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index ccf599ec..1b87fd24 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -65,14 +65,14 @@ void main() { ); flameTester.test( - 'has two SlingShots', + 'has two Kickers', (game) async { final board = Board(size: Vector2.all(500)); await game.ready(); await game.ensureAdd(board); - final slingShots = board.findNestedChildren(); - expect(slingShots.length, equals(2)); + final kickers = board.findNestedChildren(); + expect(kickers.length, equals(2)); }, ); diff --git a/test/game/components/sling_shot_test.dart b/test/game/components/kicker_test.dart similarity index 65% rename from test/game/components/sling_shot_test.dart rename to test/game/components/kicker_test.dart index 2bfb2355..211ff8ad 100644 --- a/test/game/components/sling_shot_test.dart +++ b/test/game/components/kicker_test.dart @@ -6,43 +6,43 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; void main() { - group('SlingShot', () { + group('Kicker', () { // TODO(alestiago): Include golden tests for left and right. final flameTester = FlameTester(Forge2DGame.new); flameTester.test( 'loads correctly', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - expect(game.contains(slingShot), isTrue); + expect(game.contains(kicker), isTrue); }, ); flameTester.test( 'body is static', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - expect(slingShot.body.bodyType, equals(BodyType.static)); + expect(kicker.body.bodyType, equals(BodyType.static)); }, ); flameTester.test( 'has restitution', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - final totalRestitution = slingShot.body.fixtures.fold( + final totalRestitution = kicker.body.fixtures.fold( 0, (total, fixture) => total + fixture.restitution, ); @@ -53,12 +53,12 @@ void main() { flameTester.test( 'has no friction', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - final totalFriction = slingShot.body.fixtures.fold( + final totalFriction = kicker.body.fixtures.fold( 0, (total, fixture) => total + fixture.friction, ); From e0e8fabce5ea622207947e7e40ccfdf4c56fa2b8 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Tue, 22 Mar 2022 14:51:04 +0100 Subject: [PATCH 03/13] feat: leaderboard bloc (#57) * feat: added bloc for request ranking * feat: added empty repository * test: tests for bloc * refactor: adapt bloc events and models to what leaderboard repository has * feat: extension to convert between CharacterTheme and CharacterType * doc: documented leaderboard bloc * refactor: merge with leaderboard_repository * doc: completed doc * chore: unused import, trailing comma * chore: removed ios files * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Erick * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Erick * refactor: remove props from abstract event class to force childs to implement it * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_event.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_event.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_event.dart Co-authored-by: Alejandro Santiago * chore: ignore doc for file * chore: wrong reference at doc * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * Update lib/leaderboard/bloc/leaderboard_state.dart Co-authored-by: Alejandro Santiago * refactor: leaderboard state initial method Co-authored-by: Erick Co-authored-by: Alejandro Santiago --- lib/leaderboard/bloc/leaderboard_bloc.dart | 64 +++++++ lib/leaderboard/bloc/leaderboard_event.dart | 36 ++++ lib/leaderboard/bloc/leaderboard_state.dart | 59 +++++++ lib/leaderboard/leaderboard.dart | 1 + .../bloc/leaderboard_bloc_test.dart | 166 ++++++++++++++++++ .../bloc/leaderboard_event_test.dart | 41 +++++ .../bloc/leaderboard_state_test.dart | 70 ++++++++ 7 files changed, 437 insertions(+) create mode 100644 lib/leaderboard/bloc/leaderboard_bloc.dart create mode 100644 lib/leaderboard/bloc/leaderboard_event.dart create mode 100644 lib/leaderboard/bloc/leaderboard_state.dart create mode 100644 lib/leaderboard/leaderboard.dart create mode 100644 test/leaderboard/bloc/leaderboard_bloc_test.dart create mode 100644 test/leaderboard/bloc/leaderboard_event_test.dart create mode 100644 test/leaderboard/bloc/leaderboard_state_test.dart diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart new file mode 100644 index 00000000..6542548d --- /dev/null +++ b/lib/leaderboard/bloc/leaderboard_bloc.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:leaderboard_repository/leaderboard_repository.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(); + emit( + state.copyWith( + status: LeaderboardStatus.success, + leaderboard: top10Leaderboard, + ), + ); + } 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 new file mode 100644 index 00000000..34152163 --- /dev/null +++ b/lib/leaderboard/bloc/leaderboard_event.dart @@ -0,0 +1,36 @@ +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 [LeaderboardEntry]s. +/// {endtemplate} +class Top10Fetched extends LeaderboardEvent { + /// {@macro top_10_fetched} + const Top10Fetched(); + + @override + List get props => []; +} + +/// {@template leaderboard_entry_added} +/// Writes a new [LeaderboardEntry]. +/// +/// Should be added when a player finishes a game. +/// {endtemplate} +class LeaderboardEntryAdded extends LeaderboardEvent { + /// {@macro leaderboard_entry_added} + const LeaderboardEntryAdded({required this.entry}); + + /// [LeaderboardEntry] to be written to the remote storage. + final LeaderboardEntry entry; + + @override + List get props => [entry]; +} diff --git a/lib/leaderboard/bloc/leaderboard_state.dart b/lib/leaderboard/bloc/leaderboard_state.dart new file mode 100644 index 00000000..20d68f0d --- /dev/null +++ b/lib/leaderboard/bloc/leaderboard_state.dart @@ -0,0 +1,59 @@ +// 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 new file mode 100644 index 00000000..13d71e40 --- /dev/null +++ b/lib/leaderboard/leaderboard.dart @@ -0,0 +1 @@ +export 'bloc/leaderboard_bloc.dart'; diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart new file mode 100644 index 00000000..c44f7d3a --- /dev/null +++ b/test/leaderboard/bloc/leaderboard_bloc_test.dart @@ -0,0 +1,166 @@ +// 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'; + +class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} + +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) => LeaderboardEntry( + 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 = LeaderboardEntry( + 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()], + ); + }); + }); +} diff --git a/test/leaderboard/bloc/leaderboard_event_test.dart b/test/leaderboard/bloc/leaderboard_event_test.dart new file mode 100644 index 00000000..f74296af --- /dev/null +++ b/test/leaderboard/bloc/leaderboard_event_test.dart @@ -0,0 +1,41 @@ +// 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 = LeaderboardEntry( + 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 new file mode 100644 index 00000000..6ff5df13 --- /dev/null +++ b/test/leaderboard/bloc/leaderboard_state_test.dart @@ -0,0 +1,70 @@ +// 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('LeaderboardState', () { + test('supports value equality', () { + expect( + LeaderboardState.initial(), + equals( + LeaderboardState.initial(), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + LeaderboardState.initial(), + isNotNull, + ); + }); + }); + + group('copyWith', () { + const leaderboardEntry = LeaderboardEntry( + playerInitials: 'ABC', + score: 1500, + character: CharacterType.dash, + ); + + 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: const [leaderboardEntry], + ); + expect(leaderboardState, isNot(equals(otherLeaderboardState))); + + expect( + leaderboardState.copyWith( + status: otherLeaderboardState.status, + ranking: otherLeaderboardState.ranking, + leaderboard: otherLeaderboardState.leaderboard, + ), + equals(otherLeaderboardState), + ); + }, + ); + }); + }); +} From 4af09b64fa871f2c962deb43d31918e3bdfdcedd Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 22 Mar 2022 15:58:30 +0000 Subject: [PATCH 04/13] chore: bumped flame versions (#71) * chore: increased version * fix: changes due to versions --- lib/game/components/flipper.dart | 1 - lib/game/components/plunger.dart | 2 +- pubspec.lock | 8 ++++---- pubspec.yaml | 6 +++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index c4f18389..cf3fed4f 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flame/components.dart'; -import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 61abaf1d..934cd8ac 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,4 +1,4 @@ -import 'package:flame/input.dart'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; diff --git a/pubspec.lock b/pubspec.lock index 71647cae..ac5fa36d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -182,21 +182,21 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-releasecandidate.5" + version: "1.1.0-releasecandidate.6" flame_bloc: dependency: "direct main" description: name: flame_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-releasecandidate.5" + version: "1.2.0-releasecandidate.6" flame_forge2d: dependency: "direct main" description: name: flame_forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.9.0-releasecandidate.5" + version: "0.9.0-releasecandidate.6" flame_test: dependency: "direct dev" description: @@ -237,7 +237,7 @@ packages: name: forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.8.2" + version: "0.9.0" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 35d8190f..25c8fbb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,9 +10,9 @@ dependencies: bloc: ^8.0.2 cloud_firestore: ^3.1.10 equatable: ^2.0.3 - flame: ^1.1.0-releasecandidate.5 - flame_bloc: ^1.2.0-releasecandidate.5 - flame_forge2d: ^0.9.0-releasecandidate.5 + flame: ^1.1.0-releasecandidate.6 + flame_bloc: ^1.2.0-releasecandidate.6 + flame_forge2d: ^0.9.0-releasecandidate.6 flutter: sdk: flutter flutter_bloc: ^8.0.1 From a111fd417edbae19f3a60605e66e831ab511fd25 Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 22 Mar 2022 13:54:06 -0300 Subject: [PATCH 05/13] feat: fixed positioning (#70) * feat: game uses fixed positioning now * feat: fixed positioning * feat: pr suggestion * lint --- lib/game/components/board.dart | 12 ++--- lib/game/components/jetpack_ramp.dart | 39 ++++++++------ lib/game/components/launcher_ramp.dart | 25 ++++----- lib/game/components/pathway.dart | 5 +- lib/game/components/spaceship.dart | 5 +- lib/game/components/wall.dart | 18 +++---- lib/game/pinball_game.dart | 70 +++++++++++++------------- test/game/components/board_test.dart | 12 ++--- 8 files changed, 98 insertions(+), 88 deletions(-) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 9c34a263..5bd4d92b 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -7,25 +7,23 @@ import 'package:pinball/game/game.dart'; /// {entemplate} class Board extends Component { /// {@macro board} - Board({required Vector2 size}) : _size = size; - - final Vector2 _size; + Board(); @override Future onLoad() async { // TODO(alestiago): adjust positioning once sprites are added. final bottomGroup = _BottomGroup( position: Vector2( - _size.x / 2, - _size.y / 1.25, + PinballGame.boardBounds.center.dx, + PinballGame.boardBounds.bottom + 10, ), spacing: 2, ); final dashForest = _FlutterForest( position: Vector2( - _size.x / 1.25, - _size.y / 4.25, + PinballGame.boardBounds.right - 20, + PinballGame.boardBounds.top - 20, ), ); diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index 985c8f7d..aa5a2d3d 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -30,18 +30,23 @@ class JetpackRamp extends Component with HasGameRef { // TODO(ruialonso): Use a bezier curve once control points are defined. color: const Color.fromARGB(255, 8, 218, 241), center: position, - width: 62, - radius: 200, + width: 5, + radius: 18, angle: math.pi, + rotation: math.pi, + )..layer = layer; + + final leftOpening = _JetpackRampOpening( + outsideLayer: Layer.spaceship, + rotation: math.pi, ) - ..initialPosition = position - ..layer = layer; - final leftOpening = _JetpackRampOpening(outsideLayer: Layer.spaceship) - ..initialPosition = position + Vector2(-27.6, 25.3) + ..initialPosition = position - Vector2(2, 22) ..layer = Layer.jetpack; - final rightOpening = _JetpackRampOpening() - ..initialPosition = position + Vector2(-10.6, 25.3) + final rightOpening = _JetpackRampOpening( + rotation: math.pi, + ) + ..initialPosition = position - Vector2(-13, 22) ..layer = Layer.opening; await addAll([ @@ -60,20 +65,26 @@ class _JetpackRampOpening extends RampOpening { /// {@macro jetpack_ramp_opening} _JetpackRampOpening({ Layer? outsideLayer, - }) : super( + required double rotation, + }) : _rotation = rotation, + super( pathwayLayer: Layer.jetpack, outsideLayer: outsideLayer, orientation: RampOrientation.down, ); - // TODO(ruialonso): Avoid magic number 2, should be proportional to + final double _rotation; + + // TODO(ruialonso): Avoid magic number 3, should be propotional to // [JetpackRamp]. - static const _size = 2; + static final Vector2 _size = Vector2(3, .1); @override Shape get shape => PolygonShape() - ..setAsEdge( - Vector2(initialPosition.x - _size, initialPosition.y), - Vector2(initialPosition.x + _size, initialPosition.y), + ..setAsBox( + _size.x, + _size.y, + initialPosition, + _rotation, ); } diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index 21d4d666..5fdabcdb 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -28,26 +28,27 @@ class LauncherRamp extends Component with HasGameRef { final straightPath = Pathway.straight( color: const Color.fromARGB(255, 34, 255, 0), - start: Vector2(0, 0), - end: Vector2(0, 700), - width: 80, + start: Vector2(position.x, position.y), + end: Vector2(position.x, 74), + width: 5, ) ..initialPosition = position ..layer = layer; + final curvedPath = Pathway.arc( color: const Color.fromARGB(255, 251, 255, 0), - center: position + Vector2(-29, -8), - radius: 300, - angle: 10 * math.pi / 9, - width: 80, - ) - ..initialPosition = position + Vector2(-28.8, -6) - ..layer = layer; + center: position + Vector2(-1, 68), + radius: 20, + angle: 8 * math.pi / 9, + width: 5, + rotation: math.pi, + )..layer = layer; + final leftOpening = _LauncherRampOpening(rotation: 13 * math.pi / 180) - ..initialPosition = position + Vector2(-72.5, 12) + ..initialPosition = position + Vector2(1, 49) ..layer = Layer.opening; final rightOpening = _LauncherRampOpening(rotation: 0) - ..initialPosition = position + Vector2(-46.8, 17) + ..initialPosition = position + Vector2(-16, 46) ..layer = Layer.opening; await addAll([ diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 414442d3..0c29dd7b 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -150,10 +150,7 @@ class Pathway extends BodyComponent with InitialPosition, Layered { final fixturesDef = []; for (final path in _paths) { - final chain = ChainShape() - ..createChain( - path.map(gameRef.screenToWorld).toList(), - ); + final chain = ChainShape()..createChain(path); fixturesDef.add(FixtureDef(chain)); } diff --git a/lib/game/components/spaceship.dart b/lib/game/components/spaceship.dart index f934d943..d933a79f 100644 --- a/lib/game/components/spaceship.dart +++ b/lib/game/components/spaceship.dart @@ -15,7 +15,10 @@ class Spaceship extends Forge2DBlueprint { @override void build() { - final position = Vector2(30, -50); + final position = Vector2( + PinballGame.boardBounds.left + radius + 0.5, + PinballGame.boardBounds.center.dy + 34, + ); addAllContactCallback([ SpaceshipHoleBallContactCallback(), diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 017f8c4d..62f9033f 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -1,7 +1,9 @@ // ignore_for_file: avoid_renaming_method_parameters +import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; +import 'package:pinball/game/pinball_game.dart'; /// {@template wall} /// A continuous generic and [BodyType.static] barrier that divides a game area. @@ -39,15 +41,16 @@ class Wall extends BodyComponent { /// Create top, left, and right [Wall]s for the game board. List createBoundaries(Forge2DGame game) { - final topLeft = Vector2.zero(); - final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); + final topLeft = PinballGame.boardBounds.topLeft.toVector2(); + final bottomRight = PinballGame.boardBounds.bottomRight.toVector2(); + final topRight = Vector2(bottomRight.x, topLeft.y); final bottomLeft = Vector2(topLeft.x, bottomRight.y); return [ Wall(start: topLeft, end: topRight), Wall(start: topRight, end: bottomRight), - Wall(start: bottomLeft, end: topLeft), + Wall(start: topLeft, end: bottomLeft), ]; } @@ -59,13 +62,10 @@ List createBoundaries(Forge2DGame game) { /// {@endtemplate} class BottomWall extends Wall { /// {@macro bottom_wall} - BottomWall(Forge2DGame game) + BottomWall() : super( - start: game.screenToWorld(game.camera.viewport.effectiveSize), - end: Vector2( - 0, - game.screenToWorld(game.camera.viewport.effectiveSize).y, - ), + start: PinballGame.boardBounds.bottomLeft.toVector2(), + end: PinballGame.boardBounds.bottomRight.toVector2(), ); } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 86bceef6..c23fa095 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,6 +1,7 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; +import 'package:flame/extensions.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -16,6 +17,13 @@ class PinballGame extends Forge2DGame late final Plunger plunger; + static final boardSize = Vector2(72, 128); + static final boardBounds = Rect.fromCenter( + center: Offset.zero, + width: boardSize.x, + height: -boardSize.y, + ); + @override void onAttach() { super.onAttach(); @@ -27,11 +35,16 @@ class PinballGame extends Forge2DGame _addContactCallbacks(); await _addGameBoundaries(); - unawaited(_addBoard()); + unawaited(add(Board())); unawaited(_addPlunger()); unawaited(_addBonusWord()); unawaited(_addPaths()); unawaited(addFromBlueprint(Spaceship())); + + // Fix camera on the center of the board size + camera + ..followVector2(screenToWorld(boardSize / 2)) + ..zoom = size.y / 14; } void _addContactCallbacks() { @@ -41,44 +54,27 @@ class PinballGame extends Forge2DGame } Future _addGameBoundaries() async { - await add(BottomWall(this)); + await add(BottomWall()); createBoundaries(this).forEach(add); } - Future _addBoard() async { - final board = Board( - size: screenToWorld( - Vector2( - camera.viewport.effectiveSize.x, - camera.viewport.effectiveSize.y, - ), - ), - ); - await add(board); - } - Future _addPlunger() async { - plunger = Plunger( - compressionDistance: camera.viewport.effectiveSize.y / 12, - ); - plunger.initialPosition = screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2 + 450, - camera.viewport.effectiveSize.y - plunger.compressionDistance, - ), - ); + plunger = Plunger(compressionDistance: 2); + plunger.initialPosition = boardBounds.bottomRight.toVector2() - + Vector2( + 8, + -10, + ); await add(plunger); } Future _addBonusWord() async { await add( BonusWord( - position: screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2, - camera.viewport.effectiveSize.y - 50, - ), + position: Vector2( + boardBounds.center.dx, + boardBounds.bottom + 10, ), ), ); @@ -86,18 +82,22 @@ class PinballGame extends Forge2DGame Future _addPaths() async { final jetpackRamp = JetpackRamp( - position: Vector2(42.6, -45), + position: Vector2( + PinballGame.boardBounds.left + 25, + PinballGame.boardBounds.top - 20, + ), ); final launcherRamp = LauncherRamp( - position: screenToWorld( - Vector2( - camera.viewport.effectiveSize.x / 2 + 400, - camera.viewport.effectiveSize.y / 2 - 330, - ), + position: Vector2( + PinballGame.boardBounds.right - 23, + PinballGame.boardBounds.bottom + 40, ), ); - await addAll([jetpackRamp, launcherRamp]); + await addAll([ + jetpackRamp, + launcherRamp, + ]); } void spawnBall() { diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 1b87fd24..04847dec 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -15,7 +15,7 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final board = Board(size: Vector2.all(500)); + final board = Board(); await game.ready(); await game.ensureAdd(board); @@ -27,7 +27,7 @@ void main() { flameTester.test( 'has one left flipper', (game) async { - final board = Board(size: Vector2.all(500)); + final board = Board(); await game.ready(); await game.ensureAdd(board); @@ -41,7 +41,7 @@ void main() { flameTester.test( 'has one right flipper', (game) async { - final board = Board(size: Vector2.all(500)); + final board = Board(); await game.ready(); await game.ensureAdd(board); @@ -55,7 +55,7 @@ void main() { flameTester.test( 'has two Baseboards', (game) async { - final board = Board(size: Vector2.all(500)); + final board = Board(); await game.ready(); await game.ensureAdd(board); @@ -67,7 +67,7 @@ void main() { flameTester.test( 'has two Kickers', (game) async { - final board = Board(size: Vector2.all(500)); + final board = Board(); await game.ready(); await game.ensureAdd(board); @@ -80,7 +80,7 @@ void main() { 'has three RoundBumpers', (game) async { // TODO(alestiago): change to [NestBumpers] once provided. - final board = Board(size: Vector2.all(500)); + final board = Board(); await game.ready(); await game.ensureAdd(board); From 5f90e622586346de334aa091a023dd78af17e5de Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Tue, 22 Mar 2022 19:44:40 +0100 Subject: [PATCH 06/13] fix: deactivate bonus word while effect (#61) * fix: fixed error activating bottom letter while bonus word animation * test: coverage for test isEnabled * Update lib/game/components/bonus_word.dart Co-authored-by: Alejandro Santiago * chore: test method name Co-authored-by: Alejandro Santiago Co-authored-by: Erick --- lib/game/components/bonus_word.dart | 61 +++++++++++++++-------- test/game/components/bonus_word_test.dart | 16 +++++- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart index 03f64a11..cc6391e8 100644 --- a/lib/game/components/bonus_word.dart +++ b/lib/game/components/bonus_word.dart @@ -36,31 +36,39 @@ class BonusWord extends Component with BlocComponent { for (var i = 0; i < letters.length; i++) { final letter = letters[i]; - letter.add( - SequenceEffect( - [ - ColorEffect( - i.isOdd ? BonusLetter._activeColor : BonusLetter._disableColor, - const Offset(0, 1), - EffectController(duration: 0.25), - ), - ColorEffect( - i.isOdd ? BonusLetter._disableColor : BonusLetter._activeColor, - const Offset(0, 1), - EffectController(duration: 0.25), - ), - ], - repeatCount: 4, - )..onFinishCallback = () { - letter.add( + letter + ..isEnabled = false + ..add( + SequenceEffect( + [ ColorEffect( - BonusLetter._disableColor, + i.isOdd + ? BonusLetter._activeColor + : BonusLetter._disableColor, const Offset(0, 1), EffectController(duration: 0.25), ), - ); - }, - ); + ColorEffect( + i.isOdd + ? BonusLetter._disableColor + : BonusLetter._activeColor, + const Offset(0, 1), + EffectController(duration: 0.25), + ), + ], + repeatCount: 4, + )..onFinishCallback = () { + letter + ..isEnabled = true + ..add( + ColorEffect( + BonusLetter._disableColor, + const Offset(0, 1), + EffectController(duration: 0.25), + ), + ); + }, + ); } } } @@ -107,6 +115,13 @@ class BonusLetter extends BodyComponent final String _letter; final int _index; + /// Indicates if a [BonusLetter] can be activated on [Ball] contact. + /// + /// It is disabled whilst animating and enabled again once the animation + /// completes. The animation is triggered when [GameBonus.word] is + /// awarded. + bool isEnabled = true; + @override Future onLoad() async { await super.onLoad(); @@ -172,6 +187,8 @@ class BonusLetterBallContactCallback extends ContactCallback { @override void begin(Ball ball, BonusLetter bonusLetter, Contact contact) { - bonusLetter.activate(); + if (bonusLetter.isEnabled) { + bonusLetter.activate(); + } } } diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index a9af305e..47a7e257 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -300,12 +300,26 @@ void main() { test('calls ball.activate', () { final ball = MockBall(); final bonusLetter = MockBonusLetter(); - final contactCallback = BonusLetterBallContactCallback(); + + when(() => bonusLetter.isEnabled).thenReturn(true); + contactCallback.begin(ball, bonusLetter, MockContact()); verify(bonusLetter.activate).called(1); }); + + test("doesn't call ball.activate when letter is disabled", () { + final ball = MockBall(); + final bonusLetter = MockBonusLetter(); + final contactCallback = BonusLetterBallContactCallback(); + + when(() => bonusLetter.isEnabled).thenReturn(false); + + contactCallback.begin(ball, bonusLetter, MockContact()); + + verifyNever(bonusLetter.activate); + }); }); }); } From c0f451b6f7559572b766dd8d492728419f6c7c9f Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 22 Mar 2022 17:03:33 -0300 Subject: [PATCH 07/13] fix: camera position (#74) * fix: camera position * Update lib/game/pinball_game.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/pinball_game.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index c23fa095..e77a1f73 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -41,9 +41,9 @@ class PinballGame extends Forge2DGame unawaited(_addPaths()); unawaited(addFromBlueprint(Spaceship())); - // Fix camera on the center of the board size + // Fix camera on the center of the board. camera - ..followVector2(screenToWorld(boardSize / 2)) + ..followVector2(Vector2.zero()) ..zoom = size.y / 14; } From c03cf6b2a27b86a778b7d6d282c39aa04df64c87 Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 22 Mar 2022 17:31:42 -0300 Subject: [PATCH 08/13] chore: refactoring to use flutter gen on the game (#73) * chore: refactoring to use flutter gen on the game * fix: lint * Update lib/game/components/spaceship.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * fix: lint Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- .github/workflows/main.yaml | 1 + analysis_options.yaml | 3 + lib/game/components/ball.dart | 8 +-- lib/game/components/flipper.dart | 10 ++- lib/game/components/spaceship.dart | 32 ++++------ lib/game/game_assets.dart | 13 ++-- lib/game/pinball_game.dart | 4 +- lib/gen/assets.gen.dart | 96 ++++++++++++++++++++++++++++ pubspec.yaml | 3 + test/game/components/board_test.dart | 3 +- test/helpers/extensions.dart | 2 +- 11 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 lib/gen/assets.gen.dart diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e4db22a8..f3e3fd99 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -8,3 +8,4 @@ jobs: with: flutter_channel: stable flutter_version: 2.10.0 + coverage_excludes: "lib/gen/*.dart" diff --git a/analysis_options.yaml b/analysis_options.yaml index 44aef9ac..f8155aa6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,4 @@ include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 3dc068c2..def21929 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -1,6 +1,7 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/assets.gen.dart'; /// {@template ball} /// A solid, [BodyType.dynamic] sphere that rolls and bounces along the @@ -20,15 +21,10 @@ class Ball extends BodyComponent with InitialPosition, Layered { /// The size of the [Ball] final Vector2 size = Vector2.all(2); - /// Asset location of the sprite that renders with the [Ball]. - /// - /// Sprite is preloaded by [PinballGameAssetsX]. - static const spritePath = 'components/ball.png'; - @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite(spritePath); + final sprite = await gameRef.loadSprite(Assets.images.components.ball.path); final tint = gameRef.theme.characterTheme.ballColor.withOpacity(0.5); await add( SpriteComponent( diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index cf3fed4f..92b2ddd4 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -6,6 +6,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/assets.gen.dart'; /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. @@ -52,11 +53,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { } } - /// Asset location of the sprite that renders with the [Flipper]. - /// - /// Sprite is preloaded by [PinballGameAssetsX]. - static const spritePath = 'components/flipper.png'; - /// The size of the [Flipper]. static final size = Vector2(12, 2.8); @@ -90,7 +86,9 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// Loads the sprite that renders with the [Flipper]. Future _loadSprite() async { - final sprite = await gameRef.loadSprite(spritePath); + final sprite = await gameRef.loadSprite( + Assets.images.components.flipper.path, + ); final spriteComponent = SpriteComponent( sprite: sprite, size: size, diff --git a/lib/game/components/spaceship.dart b/lib/game/components/spaceship.dart index d933a79f..51dbc291 100644 --- a/lib/game/components/spaceship.dart +++ b/lib/game/components/spaceship.dart @@ -7,6 +7,7 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/assets.gen.dart'; /// A [Blueprint] which creates the spaceship feature. class Spaceship extends Forge2DBlueprint { @@ -46,18 +47,12 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { layer = Layer.spaceship; } - /// Path for the base sprite - static const saucerSpritePath = 'components/spaceship/saucer.png'; - - /// Path for the upper wall sprite - static const upperWallPath = 'components/spaceship/upper.png'; - @override Future onLoad() async { await super.onLoad(); final sprites = await Future.wait([ - gameRef.loadSprite(saucerSpritePath), - gameRef.loadSprite(upperWallPath), + gameRef.loadSprite(Assets.images.components.spaceship.saucer.path), + gameRef.loadSprite(Assets.images.components.spaceship.upper.path), ]); await add( @@ -104,14 +99,13 @@ class SpaceshipBridgeTop extends BodyComponent with InitialPosition { /// {@macro spaceship_bridge_top} SpaceshipBridgeTop() : super(priority: 6); - /// Path to the top of this sprite - static const spritePath = 'components/spaceship/android-top.png'; - @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite(spritePath); + final sprite = await gameRef.loadSprite( + Assets.images.components.spaceship.androidTop.path, + ); await add( SpriteComponent( sprite: sprite, @@ -142,16 +136,15 @@ class SpaceshipBridge extends BodyComponent with InitialPosition, Layered { layer = Layer.spaceship; } - /// Path to the spaceship bridge - static const spritePath = 'components/spaceship/android-bottom.png'; - @override Future onLoad() async { await super.onLoad(); renderBody = false; - final sprite = await gameRef.images.load(spritePath); + final sprite = await gameRef.images.load( + Assets.images.components.spaceship.androidBottom.path, + ); await add( SpriteAnimationComponent.fromFrameData( sprite, @@ -253,14 +246,13 @@ class SpaceshipWall extends BodyComponent with InitialPosition, Layered { layer = Layer.spaceship; } - /// Sprite path for the lower wall - static const lowerWallPath = 'components/spaceship/lower.png'; - @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite(lowerWallPath); + final sprite = await gameRef.loadSprite( + Assets.images.components.spaceship.lower.path, + ); await add( SpriteComponent( diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 00e9d09c..d19ef177 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -1,16 +1,17 @@ import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/assets.gen.dart'; /// Add methods to help loading and caching game assets. extension PinballGameAssetsX on PinballGame { /// Pre load the initial assets of the game. Future preLoadAssets() async { await Future.wait([ - images.load(Ball.spritePath), - images.load(Flipper.spritePath), - images.load(SpaceshipBridge.spritePath), - images.load(SpaceshipBridgeTop.spritePath), - images.load(SpaceshipWall.lowerWallPath), - images.load(SpaceshipSaucer.upperWallPath), + images.load(Assets.images.components.ball.path), + images.load(Assets.images.components.flipper.path), + images.load(Assets.images.components.spaceship.androidTop.path), + images.load(Assets.images.components.spaceship.androidBottom.path), + images.load(Assets.images.components.spaceship.lower.path), + images.load(Assets.images.components.spaceship.upper.path), ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index e77a1f73..d1abb70e 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -11,7 +11,9 @@ import 'package:pinball_theme/pinball_theme.dart'; class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents { - PinballGame({required this.theme}); + PinballGame({required this.theme}) { + images.prefix = ''; + } final PinballTheme theme; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart new file mode 100644 index 00000000..fe4bad8b --- /dev/null +++ b/lib/gen/assets.gen.dart @@ -0,0 +1,96 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + $AssetsImagesComponentsGen get components => + const $AssetsImagesComponentsGen(); +} + +class $AssetsImagesComponentsGen { + const $AssetsImagesComponentsGen(); + + AssetGenImage get ball => + const AssetGenImage('assets/images/components/ball.png'); + AssetGenImage get flipper => + const AssetGenImage('assets/images/components/flipper.png'); + AssetGenImage get sauce => + const AssetGenImage('assets/images/components/sauce.png'); + $AssetsImagesComponentsSpaceshipGen get spaceship => + const $AssetsImagesComponentsSpaceshipGen(); +} + +class $AssetsImagesComponentsSpaceshipGen { + const $AssetsImagesComponentsSpaceshipGen(); + + AssetGenImage get androidBottom => const AssetGenImage( + 'assets/images/components/spaceship/android-bottom.png'); + AssetGenImage get androidTop => + const AssetGenImage('assets/images/components/spaceship/android-top.png'); + AssetGenImage get lower => + const AssetGenImage('assets/images/components/spaceship/lower.png'); + AssetGenImage get saucer => + const AssetGenImage('assets/images/components/spaceship/saucer.png'); + AssetGenImage get upper => + const AssetGenImage('assets/images/components/spaceship/upper.png'); +} + +class Assets { + Assets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) : super(assetName); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/pubspec.yaml b/pubspec.yaml index 25c8fbb9..838dd9ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,3 +42,6 @@ flutter: assets: - assets/images/components/ - assets/images/components/spaceship/ + +flutter_gen: + line_length: 80 diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 04847dec..7791d891 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: cascade_invocations -import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; @@ -9,7 +8,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final flameTester = FlameTester(PinballGameTest.create); group('Board', () { flameTester.test( diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index a5c56a86..abea191d 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -9,7 +9,7 @@ extension PinballGameTest on PinballGame { theme: const PinballTheme( characterTheme: DashTheme(), ), - ); + )..images.prefix = ''; } /// [DebugPinballGame] extension to reduce boilerplate in tests. From ed36a162d6b9655ca970360dc7770a3a5483ee69 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Wed, 23 Mar 2022 03:10:08 -0500 Subject: [PATCH 09/13] refactor: dimension boundaries according to designs (#75) * refactor: correctly dimension boundaries * refactor: adjust zoom level --- lib/game/components/board.dart | 4 ++-- lib/game/components/jetpack_ramp.dart | 4 ++-- lib/game/components/launcher_ramp.dart | 22 +++++++++++++--------- lib/game/components/spaceship.dart | 4 ++-- lib/game/components/wall.dart | 8 +++++--- lib/game/pinball_game.dart | 16 ++++++++-------- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 5bd4d92b..af03efdd 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -22,8 +22,8 @@ class Board extends Component { final dashForest = _FlutterForest( position: Vector2( - PinballGame.boardBounds.right - 20, - PinballGame.boardBounds.top - 20, + PinballGame.boardBounds.center.dx + 20, + PinballGame.boardBounds.center.dy + 48, ), ); diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index aa5a2d3d..4afc36d1 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -40,13 +40,13 @@ class JetpackRamp extends Component with HasGameRef { outsideLayer: Layer.spaceship, rotation: math.pi, ) - ..initialPosition = position - Vector2(2, 22) + ..initialPosition = position + Vector2(-2.5, -20.2) ..layer = Layer.jetpack; final rightOpening = _JetpackRampOpening( rotation: math.pi, ) - ..initialPosition = position - Vector2(-13, 22) + ..initialPosition = position + Vector2(12.9, -20.2) ..layer = Layer.opening; await addAll([ diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index 5fdabcdb..0b6e4dbf 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -26,29 +26,33 @@ class LauncherRamp extends Component with HasGameRef { RampOpeningBallContactCallback<_LauncherRampOpening>(), ); + final launcherRampRotation = + -math.atan(18.6 / PinballGame.boardBounds.height); + final straightPath = Pathway.straight( color: const Color.fromARGB(255, 34, 255, 0), - start: Vector2(position.x, position.y), - end: Vector2(position.x, 74), + start: position + Vector2(-1.2, 10), + end: position + Vector2(-1.2, 117), width: 5, + rotation: launcherRampRotation, ) ..initialPosition = position ..layer = layer; final curvedPath = Pathway.arc( color: const Color.fromARGB(255, 251, 255, 0), - center: position + Vector2(-1, 68), - radius: 20, - angle: 8 * math.pi / 9, + center: position + Vector2(-2.8, 87.2), + radius: 16.3, + angle: math.pi / 2, width: 5, - rotation: math.pi, + rotation: 3 * math.pi / 2, )..layer = layer; - final leftOpening = _LauncherRampOpening(rotation: 13 * math.pi / 180) - ..initialPosition = position + Vector2(1, 49) + final leftOpening = _LauncherRampOpening(rotation: math.pi / 2) + ..initialPosition = position + Vector2(-11.8, 66.3) ..layer = Layer.opening; final rightOpening = _LauncherRampOpening(rotation: 0) - ..initialPosition = position + Vector2(-16, 46) + ..initialPosition = position + Vector2(-4.9, 59.4) ..layer = Layer.opening; await addAll([ diff --git a/lib/game/components/spaceship.dart b/lib/game/components/spaceship.dart index 51dbc291..8243a974 100644 --- a/lib/game/components/spaceship.dart +++ b/lib/game/components/spaceship.dart @@ -17,8 +17,8 @@ class Spaceship extends Forge2DBlueprint { @override void build() { final position = Vector2( - PinballGame.boardBounds.left + radius + 0.5, - PinballGame.boardBounds.center.dy + 34, + PinballGame.boardBounds.left + radius + 15, + PinballGame.boardBounds.center.dy + 30, ); addAllContactCallback([ diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 62f9033f..7475715e 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -41,11 +41,13 @@ class Wall extends BodyComponent { /// Create top, left, and right [Wall]s for the game board. List createBoundaries(Forge2DGame game) { - final topLeft = PinballGame.boardBounds.topLeft.toVector2(); + final topLeft = + PinballGame.boardBounds.topLeft.toVector2() + Vector2(18.6, 0); final bottomRight = PinballGame.boardBounds.bottomRight.toVector2(); - final topRight = Vector2(bottomRight.x, topLeft.y); - final bottomLeft = Vector2(topLeft.x, bottomRight.y); + final topRight = + PinballGame.boardBounds.topRight.toVector2() - Vector2(18.6, 0); + final bottomLeft = PinballGame.boardBounds.bottomLeft.toVector2(); return [ Wall(start: topLeft, end: topRight), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index d1abb70e..44a7ec01 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -19,7 +19,7 @@ class PinballGame extends Forge2DGame late final Plunger plunger; - static final boardSize = Vector2(72, 128); + static final boardSize = Vector2(101.6, 143.8); static final boardBounds = Rect.fromCenter( center: Offset.zero, width: boardSize.x, @@ -46,7 +46,7 @@ class PinballGame extends Forge2DGame // Fix camera on the center of the board. camera ..followVector2(Vector2.zero()) - ..zoom = size.y / 14; + ..zoom = size.y / 16; } void _addContactCallbacks() { @@ -63,10 +63,10 @@ class PinballGame extends Forge2DGame Future _addPlunger() async { plunger = Plunger(compressionDistance: 2); - plunger.initialPosition = boardBounds.bottomRight.toVector2() - + plunger.initialPosition = boardBounds.bottomRight.toVector2() + Vector2( - 8, - -10, + -5, + 10, ); await add(plunger); } @@ -85,13 +85,13 @@ class PinballGame extends Forge2DGame Future _addPaths() async { final jetpackRamp = JetpackRamp( position: Vector2( - PinballGame.boardBounds.left + 25, - PinballGame.boardBounds.top - 20, + PinballGame.boardBounds.left + 40.5, + PinballGame.boardBounds.top - 31.5, ), ); final launcherRamp = LauncherRamp( position: Vector2( - PinballGame.boardBounds.right - 23, + PinballGame.boardBounds.right - 30, PinballGame.boardBounds.bottom + 40, ), ); From d5f41277c8f13294d94e0578359e27a71672a7b2 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 23 Mar 2022 09:23:13 +0000 Subject: [PATCH 10/13] fix: set userData for ScorePoints #67 --- lib/game/components/score_points.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index b4ad1bdf..da894652 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -9,6 +9,12 @@ import 'package:pinball/game/game.dart'; mixin ScorePoints on BodyComponent { /// {@macro score_points} int get points; + + @override + Future onLoad() async { + await super.onLoad(); + body.userData = this; + } } /// Adds points to the score when a [Ball] collides with a [BodyComponent] that From 823cdb6af58ee52d01361f09bbba5d21078e6ff5 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 23 Mar 2022 16:15:06 +0000 Subject: [PATCH 11/13] feat: include DashNest bonus (#80) * feat: included dash nest logic Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/bloc/game_bloc.dart | 25 +++++++++++ lib/game/bloc/game_event.dart | 9 ++++ lib/game/bloc/game_state.dart | 12 +++++ test/game/bloc/game_bloc_test.dart | 52 ++++++++++++++++++++++ test/game/bloc/game_event_test.dart | 17 +++++++ test/game/bloc/game_state_test.dart | 16 +++++++ test/game/components/ball_test.dart | 1 + test/game/components/board_test.dart | 19 ++++---- test/game/components/bonus_word_test.dart | 3 ++ test/game/view/game_hud_test.dart | 1 + test/game/view/pinball_game_page_test.dart | 1 + test/helpers/extensions.dart | 39 ---------------- 12 files changed, 146 insertions(+), 49 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 663fee35..c02417a7 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -12,6 +12,7 @@ class GameBloc extends Bloc { on(_onBallLost); on(_onScored); on(_onBonusLetterActivated); + on(_onDashNestActivated); } static const bonusWord = 'GOOGLE'; @@ -52,4 +53,28 @@ class GameBloc extends Bloc { ); } } + + void _onDashNestActivated(DashNestActivated event, Emitter emit) { + const nestsRequiredForBonus = 3; + + final newNests = { + ...state.activatedDashNests, + event.nestId, + }; + if (newNests.length == nestsRequiredForBonus) { + emit( + state.copyWith( + activatedDashNests: {}, + bonusHistory: [ + ...state.bonusHistory, + GameBonus.dashNest, + ], + ), + ); + } else { + emit( + state.copyWith(activatedDashNests: newNests), + ); + } + } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index 0edc91ab..b05c5336 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -45,3 +45,12 @@ class BonusLetterActivated extends GameEvent { @override List get props => [letterIndex]; } + +class DashNestActivated extends GameEvent { + const DashNestActivated(this.nestId); + + final String nestId; + + @override + List get props => [nestId]; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index e2c39d1f..5c722946 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -7,6 +7,10 @@ enum GameBonus { /// Bonus achieved when the user activate all of the bonus /// letters on the board, forming the bonus word word, + + /// Bonus achieved when the user activates all of the Dash + /// nests on the board, adding a new ball to the board. + dashNest, } /// {@template game_state} @@ -19,6 +23,7 @@ class GameState extends Equatable { required this.balls, required this.activatedBonusLetters, required this.bonusHistory, + required this.activatedDashNests, }) : assert(score >= 0, "Score can't be negative"), assert(balls >= 0, "Number of balls can't be negative"); @@ -26,6 +31,7 @@ class GameState extends Equatable { : score = 0, balls = 3, activatedBonusLetters = const [], + activatedDashNests = const {}, bonusHistory = const []; /// The current score of the game. @@ -39,6 +45,9 @@ class GameState extends Equatable { /// Active bonus letters. final List activatedBonusLetters; + /// Active dash nests. + final Set activatedDashNests; + /// Holds the history of all the [GameBonus]es earned by the player during a /// PinballGame. final List bonusHistory; @@ -57,6 +66,7 @@ class GameState extends Equatable { int? score, int? balls, List? activatedBonusLetters, + Set? activatedDashNests, List? bonusHistory, }) { assert( @@ -69,6 +79,7 @@ class GameState extends Equatable { balls: balls ?? this.balls, activatedBonusLetters: activatedBonusLetters ?? this.activatedBonusLetters, + activatedDashNests: activatedDashNests ?? this.activatedDashNests, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -78,6 +89,7 @@ class GameState extends Equatable { score, balls, activatedBonusLetters, + activatedDashNests, bonusHistory, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 18e50858..f4b79001 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -25,18 +25,21 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 1, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -56,12 +59,14 @@ void main() { score: 2, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 5, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -82,18 +87,21 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 1, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -113,18 +121,21 @@ void main() { score: 0, balls: 3, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -145,46 +156,87 @@ void main() { score: 0, balls: 3, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2, 3], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2, 3, 4], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [GameBonus.word], ), GameState( score: GameBloc.bonusWordScore, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [GameBonus.word], ), ], ); }); + + group('DashNestActivated', () { + blocTest( + 'adds the bonus when all nests are activated', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const DashNestActivated('0')) + ..add(const DashNestActivated('1')) + ..add(const DashNestActivated('2')), + expect: () => const [ + GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {'0'}, + bonusHistory: [], + ), + GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {'0', '1'}, + bonusHistory: [], + ), + GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {}, + bonusHistory: [GameBonus.dashNest], + ), + ], + ); + }); }); } diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index d6d2278b..af9f6148 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -67,5 +67,22 @@ void main() { }, ); }); + + group('DashNestActivated', () { + test('can be instantiated', () { + expect(const DashNestActivated('0'), isNotNull); + }); + + test('supports value equality', () { + expect( + DashNestActivated('0'), + equals(DashNestActivated('0')), + ); + expect( + DashNestActivated('0'), + isNot(equals(DashNestActivated('1'))), + ); + }); + }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 8ab72e6c..aa8bdf66 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -11,6 +11,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: const [], + activatedDashNests: const {}, bonusHistory: const [], ), equals( @@ -18,6 +19,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ), @@ -31,6 +33,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), isNotNull, @@ -47,6 +50,7 @@ void main() { balls: -1, score: 0, activatedBonusLetters: const [], + activatedDashNests: const {}, bonusHistory: const [], ), throwsAssertionError, @@ -63,6 +67,7 @@ void main() { balls: 0, score: -1, activatedBonusLetters: const [], + activatedDashNests: const {}, bonusHistory: const [], ), throwsAssertionError, @@ -78,6 +83,7 @@ void main() { balls: 0, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isGameOver, isTrue); @@ -90,6 +96,7 @@ void main() { balls: 1, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); @@ -105,6 +112,7 @@ void main() { balls: 1, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLastBall, isTrue); @@ -119,6 +127,7 @@ void main() { balls: 2, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLastBall, isFalse); @@ -134,6 +143,7 @@ void main() { balls: 3, score: 0, activatedBonusLetters: [1], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLetterActivated(1), isTrue); @@ -147,6 +157,7 @@ void main() { balls: 3, score: 0, activatedBonusLetters: [1], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLetterActivated(0), isFalse); @@ -163,6 +174,7 @@ void main() { balls: 0, score: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect( @@ -180,6 +192,7 @@ void main() { balls: 0, score: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect( @@ -197,12 +210,14 @@ void main() { score: 2, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, activatedBonusLetters: const [0], + activatedDashNests: const {'1'}, bonusHistory: const [GameBonus.word], ); expect(gameState, isNot(equals(otherGameState))); @@ -212,6 +227,7 @@ void main() { score: otherGameState.score, balls: otherGameState.balls, activatedBonusLetters: otherGameState.activatedBonusLetters, + activatedDashNests: otherGameState.activatedDashNests, bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 7a48b21f..f94c1526 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -156,6 +156,7 @@ void main() { score: 10, balls: 1, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ); diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 7791d891..f0cd0e16 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -30,9 +30,9 @@ void main() { await game.ready(); await game.ensureAdd(board); - final leftFlippers = board.findNestedChildren( - condition: (flipper) => flipper.side.isLeft, - ); + final leftFlippers = board.descendants().whereType().where( + (flipper) => flipper.side.isLeft, + ); expect(leftFlippers.length, equals(1)); }, ); @@ -43,10 +43,9 @@ void main() { final board = Board(); await game.ready(); await game.ensureAdd(board); - - final rightFlippers = board.findNestedChildren( - condition: (flipper) => flipper.side.isRight, - ); + final rightFlippers = board.descendants().whereType().where( + (flipper) => flipper.side.isRight, + ); expect(rightFlippers.length, equals(1)); }, ); @@ -58,7 +57,7 @@ void main() { await game.ready(); await game.ensureAdd(board); - final baseboards = board.findNestedChildren(); + final baseboards = board.descendants().whereType(); expect(baseboards.length, equals(2)); }, ); @@ -70,7 +69,7 @@ void main() { await game.ready(); await game.ensureAdd(board); - final kickers = board.findNestedChildren(); + final kickers = board.descendants().whereType(); expect(kickers.length, equals(2)); }, ); @@ -83,7 +82,7 @@ void main() { await game.ready(); await game.ensureAdd(board); - final roundBumpers = board.findNestedChildren(); + final roundBumpers = board.descendants().whereType(); expect(roundBumpers.length, equals(3)); }, ); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 47a7e257..a12a5a74 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -234,6 +234,7 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ); whenListen( @@ -259,6 +260,7 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ); @@ -283,6 +285,7 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 536edbad..953b89eb 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -13,6 +13,7 @@ void main() { score: 10, balls: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index dcf0c001..5298d6ac 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -88,6 +88,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index abea191d..b3c4c6f8 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,4 +1,3 @@ -import 'package:flame/components.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -21,41 +20,3 @@ extension DebugPinballGameTest on DebugPinballGame { ), ); } - -extension ComponentX on Component { - T findNestedChild({ - bool Function(T)? condition, - }) { - T? nestedChild; - propagateToChildren((child) { - final foundChild = (condition ?? (_) => true)(child); - if (foundChild) { - nestedChild = child; - } - - return !foundChild; - }); - - if (nestedChild == null) { - throw Exception('No child of type $T found.'); - } else { - return nestedChild!; - } - } - - List findNestedChildren({ - bool Function(T)? condition, - }) { - final nestedChildren = []; - propagateToChildren((child) { - final foundChild = (condition ?? (_) => true)(child); - if (foundChild) { - nestedChildren.add(child); - } - - return true; - }); - - return nestedChildren; - } -} From 5bbfd0c3628a978ea2aaf0159f3f50087ac5c451 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 23 Mar 2022 16:19:44 +0000 Subject: [PATCH 12/13] refactor: made Pathway createFixtureDefs public (#81) * refactor: made createFixtureDefs public --- lib/game/components/pathway.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 0c29dd7b..8604e0f3 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -146,7 +146,8 @@ class Pathway extends BodyComponent with InitialPosition, Layered { final List> _paths; - List _createFixtureDefs() { + /// Constructs different [ChainShape]s to form the [Pathway] shape. + List createFixtureDefs() { final fixturesDef = []; for (final path in _paths) { @@ -161,7 +162,7 @@ class Pathway extends BodyComponent with InitialPosition, Layered { Body createBody() { final bodyDef = BodyDef()..position = initialPosition; final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); + createFixtureDefs().forEach(body.createFixture); return body; } From 92404fbdba78f15f4f8b65f633e5aaadda81e9f0 Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 23 Mar 2022 14:16:48 -0300 Subject: [PATCH 13/13] feat: adding pinball components scaffold project (#82) * refactor: removed findNested extensions (#77) * feat: bootstraping the components package Co-authored-by: Alejandro Santiago --- .github/workflows/pinball_components.yaml | 19 +++++++++ packages/pinball_components/.gitignore | 39 +++++++++++++++++++ packages/pinball_components/README.md | 11 ++++++ .../pinball_components/analysis_options.yaml | 1 + .../lib/pinball_components.dart | 3 ++ .../lib/src/pinball_components.dart | 7 ++++ packages/pinball_components/pubspec.yaml | 16 ++++++++ .../test/src/pinball_components_test.dart | 11 ++++++ 8 files changed, 107 insertions(+) create mode 100644 .github/workflows/pinball_components.yaml create mode 100644 packages/pinball_components/.gitignore create mode 100644 packages/pinball_components/README.md create mode 100644 packages/pinball_components/analysis_options.yaml create mode 100644 packages/pinball_components/lib/pinball_components.dart create mode 100644 packages/pinball_components/lib/src/pinball_components.dart create mode 100644 packages/pinball_components/pubspec.yaml create mode 100644 packages/pinball_components/test/src/pinball_components_test.dart diff --git a/.github/workflows/pinball_components.yaml b/.github/workflows/pinball_components.yaml new file mode 100644 index 00000000..cab60a54 --- /dev/null +++ b/.github/workflows/pinball_components.yaml @@ -0,0 +1,19 @@ +name: pinball_components + +on: + push: + paths: + - "packages/pinball_components/**" + - ".github/workflows/pinball_components.yaml" + + pull_request: + paths: + - "packages/pinball_components/**" + - ".github/workflows/pinball_components.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_components + coverage_excludes: "lib/src/generated/*.dart" diff --git a/packages/pinball_components/.gitignore b/packages/pinball_components/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_components/.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/pinball_components/README.md b/packages/pinball_components/README.md new file mode 100644 index 00000000..81ea9893 --- /dev/null +++ b/packages/pinball_components/README.md @@ -0,0 +1,11 @@ +# pinball_components + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Package with the UI game components for the Pinball Game + +[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/pinball_components/analysis_options.yaml b/packages/pinball_components/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/pinball_components/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/pinball_components/lib/pinball_components.dart b/packages/pinball_components/lib/pinball_components.dart new file mode 100644 index 00000000..a08579e5 --- /dev/null +++ b/packages/pinball_components/lib/pinball_components.dart @@ -0,0 +1,3 @@ +library pinball_components; + +export 'src/pinball_components.dart'; diff --git a/packages/pinball_components/lib/src/pinball_components.dart b/packages/pinball_components/lib/src/pinball_components.dart new file mode 100644 index 00000000..dbc4d6fd --- /dev/null +++ b/packages/pinball_components/lib/src/pinball_components.dart @@ -0,0 +1,7 @@ +/// {@template pinball_components} +/// Package with the UI game components for the Pinball Game +/// {@endtemplate} +class PinballComponents { + /// {@macro pinball_components} + const PinballComponents(); +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml new file mode 100644 index 00000000..a3a990a0 --- /dev/null +++ b/packages/pinball_components/pubspec.yaml @@ -0,0 +1,16 @@ +name: pinball_components +description: Package with the UI game components for the Pinball Game +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/pinball_components/test/src/pinball_components_test.dart b/packages/pinball_components/test/src/pinball_components_test.dart new file mode 100644 index 00000000..7359ddcb --- /dev/null +++ b/packages/pinball_components/test/src/pinball_components_test.dart @@ -0,0 +1,11 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('PinballComponents', () { + test('can be instantiated', () { + expect(PinballComponents(), isNotNull); + }); + }); +}