From 573e16d0d5047ae82e0ea4ca906208b234489986 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 23 Mar 2022 22:24:18 +0100 Subject: [PATCH 1/2] refactor: leaderboard model (#78) * refactor: changed name of LeaderboardEntry at LeaderboardRepository to LeaderboardEntryData * chore: doc and analysis errors * refactor: removed findNested extensions (#77) * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * fix: leaderboard gen file * refactor: moved leaderboard models to separate path * test: fixed tests with leaderboard model * chore: doc Co-authored-by: Alejandro Santiago Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/leaderboard/bloc/leaderboard_bloc.dart | 9 ++- lib/leaderboard/bloc/leaderboard_event.dart | 8 +- lib/leaderboard/leaderboard.dart | 1 + .../models/leader_board_entry.dart | 80 +++++++++++++++++++ .../lib/src/leaderboard_repository.dart | 12 +-- ...entry.dart => leaderboard_entry_data.dart} | 28 +++---- ...y.g.dart => leaderboard_entry_data.g.dart} | 8 +- .../lib/src/models/leaderboard_ranking.dart | 8 +- .../lib/src/models/models.dart | 2 +- .../test/src/leaderboard_repository_test.dart | 4 +- ....dart => leaderboard_entry_data_test.dart} | 10 +-- test/helpers/mocks.dart | 3 + .../bloc/leaderboard_bloc_test.dart | 43 +++++++++- .../bloc/leaderboard_event_test.dart | 2 +- .../bloc/leaderboard_state_test.dart | 8 +- 15 files changed, 179 insertions(+), 47 deletions(-) create mode 100644 lib/leaderboard/models/leader_board_entry.dart rename packages/leaderboard_repository/lib/src/models/{leaderboard_entry.dart => leaderboard_entry_data.dart} (64%) rename packages/leaderboard_repository/lib/src/models/{leaderboard_entry.g.dart => leaderboard_entry_data.g.dart} (76%) rename packages/leaderboard_repository/test/src/models/{leaderboard_entry_test.dart => leaderboard_entry_data_test.dart} (72%) diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart index 6542548d..49a35474 100644 --- a/lib/leaderboard/bloc/leaderboard_bloc.dart +++ b/lib/leaderboard/bloc/leaderboard_bloc.dart @@ -3,6 +3,7 @@ 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'; @@ -30,10 +31,16 @@ class LeaderboardBloc extends Bloc { 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: top10Leaderboard, + leaderboard: leaderboardEntries, ), ); } catch (error) { diff --git a/lib/leaderboard/bloc/leaderboard_event.dart b/lib/leaderboard/bloc/leaderboard_event.dart index 34152163..b9e6955a 100644 --- a/lib/leaderboard/bloc/leaderboard_event.dart +++ b/lib/leaderboard/bloc/leaderboard_event.dart @@ -9,7 +9,7 @@ abstract class LeaderboardEvent extends Equatable { } /// {@template top_10_fetched} -/// Request the top 10 [LeaderboardEntry]s. +/// Request the top 10 [LeaderboardEntryData]s. /// {endtemplate} class Top10Fetched extends LeaderboardEvent { /// {@macro top_10_fetched} @@ -20,7 +20,7 @@ class Top10Fetched extends LeaderboardEvent { } /// {@template leaderboard_entry_added} -/// Writes a new [LeaderboardEntry]. +/// Writes a new [LeaderboardEntryData]. /// /// Should be added when a player finishes a game. /// {endtemplate} @@ -28,8 +28,8 @@ class LeaderboardEntryAdded extends LeaderboardEvent { /// {@macro leaderboard_entry_added} const LeaderboardEntryAdded({required this.entry}); - /// [LeaderboardEntry] to be written to the remote storage. - final LeaderboardEntry entry; + /// [LeaderboardEntryData] to be written to the remote storage. + final LeaderboardEntryData entry; @override List get props => [entry]; diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart index 13d71e40..156b7f78 100644 --- a/lib/leaderboard/leaderboard.dart +++ b/lib/leaderboard/leaderboard.dart @@ -1 +1,2 @@ export 'bloc/leaderboard_bloc.dart'; +export 'models/leader_board_entry.dart'; diff --git a/lib/leaderboard/models/leader_board_entry.dart b/lib/leaderboard/models/leader_board_entry.dart new file mode 100644 index 00000000..194f7cb6 --- /dev/null +++ b/lib/leaderboard/models/leader_board_entry.dart @@ -0,0 +1,80 @@ +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template leaderboard_entry} +/// A model representing a leaderboard entry containing the ranking position, +/// player's initials, score, and chosen character. +/// +/// {@endtemplate} +class LeaderboardEntry { + /// {@macro leaderboard_entry} + LeaderboardEntry({ + required this.rank, + required this.playerInitials, + required this.score, + required this.character, + }); + + /// Ranking position for [LeaderboardEntry]. + final String rank; + + /// Player's chosen initials for [LeaderboardEntry]. + final String playerInitials; + + /// Score for [LeaderboardEntry]. + final int score; + + /// [CharacterTheme] for [LeaderboardEntry]. + final AssetGenImage character; +} + +/// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry]. +extension LeaderboardEntryDataX on LeaderboardEntryData { + /// Conversion method to [LeaderboardEntry] + LeaderboardEntry toEntry(int position) { + return LeaderboardEntry( + rank: position.toString(), + playerInitials: playerInitials, + score: score, + character: character.toTheme.characterAsset, + ); + } +} + +/// Converts [CharacterType] to [CharacterTheme] to show on UI character theme +/// from repository. +extension CharacterTypeX on CharacterType { + /// Conversion method to [CharacterTheme] + CharacterTheme get toTheme { + switch (this) { + case CharacterType.dash: + return const DashTheme(); + case CharacterType.sparky: + return const SparkyTheme(); + case CharacterType.android: + return const AndroidTheme(); + case CharacterType.dino: + return const DinoTheme(); + } + } +} + +/// Converts [CharacterTheme] to [CharacterType] to persist at repository the +/// character theme from UI. +extension CharacterThemeX on CharacterTheme { + /// Conversion method to [CharacterType] + CharacterType get toType { + switch (runtimeType) { + case DashTheme: + return CharacterType.dash; + case SparkyTheme: + return CharacterType.sparky; + case AndroidTheme: + return CharacterType.android; + case DinoTheme: + return CharacterType.dino; + default: + return CharacterType.dash; + } + } +} diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 5a5fa42c..d75a88b3 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -83,9 +83,9 @@ class LeaderboardRepository { final FirebaseFirestore _firebaseFirestore; - /// Acquires top 10 [LeaderboardEntry]s. - Future> fetchTop10Leaderboard() async { - final leaderboardEntries = []; + /// Acquires top 10 [LeaderboardEntryData]s. + Future> fetchTop10Leaderboard() async { + final leaderboardEntries = []; late List documents; try { @@ -103,7 +103,7 @@ class LeaderboardRepository { final data = document.data() as Map?; if (data != null) { try { - leaderboardEntries.add(LeaderboardEntry.fromJson(data)); + leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); } catch (error, stackTrace) { throw LeaderboardDeserializationException(error, stackTrace); } @@ -115,7 +115,9 @@ class LeaderboardRepository { /// Adds player's score entry to the leaderboard and gets their /// [LeaderboardRanking]. - Future addLeaderboardEntry(LeaderboardEntry entry) async { + Future addLeaderboardEntry( + LeaderboardEntryData entry, + ) async { late DocumentReference entryReference; try { entryReference = await _firebaseFirestore diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.dart similarity index 64% rename from packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart rename to packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.dart index 86cb2464..c8137520 100644 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -part 'leaderboard_entry.g.dart'; +part 'leaderboard_entry_data.g.dart'; -/// Google character type associated with a [LeaderboardEntry]. +/// Google character type associated with a [LeaderboardEntryData]. enum CharacterType { /// Dash character. dash, @@ -18,7 +18,7 @@ enum CharacterType { dino, } -/// {@template leaderboard_entry} +/// {@template leaderboard_entry_data} /// A model representing a leaderboard entry containing the player's initials, /// score, and chosen character. /// @@ -34,42 +34,42 @@ enum CharacterType { /// ``` /// {@endtemplate} @JsonSerializable() -class LeaderboardEntry extends Equatable { - /// {@macro leaderboard_entry} - const LeaderboardEntry({ +class LeaderboardEntryData extends Equatable { + /// {@macro leaderboard_entry_data} + const LeaderboardEntryData({ required this.playerInitials, required this.score, required this.character, }); - /// Factory which converts a [Map] into a [LeaderboardEntry]. - factory LeaderboardEntry.fromJson(Map json) { + /// Factory which converts a [Map] into a [LeaderboardEntryData]. + factory LeaderboardEntryData.fromJson(Map json) { return _$LeaderboardEntryFromJson(json); } - /// Converts the [LeaderboardEntry] to [Map]. + /// Converts the [LeaderboardEntryData] to [Map]. Map toJson() => _$LeaderboardEntryToJson(this); - /// Player's chosen initials for [LeaderboardEntry]. + /// Player's chosen initials for [LeaderboardEntryData]. /// /// Example: 'ABC'. @JsonKey(name: 'playerInitials') final String playerInitials; - /// Score for [LeaderboardEntry]. + /// Score for [LeaderboardEntryData]. /// /// Example: 1500. @JsonKey(name: 'score') final int score; - /// [CharacterType] for [LeaderboardEntry]. + /// [CharacterType] for [LeaderboardEntryData]. /// /// Example: [CharacterType.dash]. @JsonKey(name: 'character') final CharacterType character; - /// An empty [LeaderboardEntry] object. - static const empty = LeaderboardEntry( + /// An empty [LeaderboardEntryData] object. + static const empty = LeaderboardEntryData( playerInitials: '', score: 0, character: CharacterType.dash, diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.g.dart similarity index 76% rename from packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart rename to packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.g.dart index fc685220..e57e43b8 100644 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.g.dart @@ -1,19 +1,19 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'leaderboard_entry.dart'; +part of 'leaderboard_entry_data.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -LeaderboardEntry _$LeaderboardEntryFromJson(Map json) => - LeaderboardEntry( +LeaderboardEntryData _$LeaderboardEntryFromJson(Map json) => + LeaderboardEntryData( playerInitials: json['playerInitials'] as String, score: json['score'] as int, character: $enumDecode(_$CharacterTypeEnumMap, json['character']), ); -Map _$LeaderboardEntryToJson(LeaderboardEntry instance) => +Map _$LeaderboardEntryToJson(LeaderboardEntryData instance) => { 'playerInitials': instance.playerInitials, 'score': instance.score, diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart index 7ec90ef4..4a322e00 100644 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart @@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; /// {@template leaderboard_ranking} -/// Contains [ranking] for a single [LeaderboardEntry] and the number of players -/// the [ranking] is [outOf]. +/// Contains [ranking] for a single [LeaderboardEntryData] and the number of +/// players the [ranking] is [outOf]. /// {@endtemplate} class LeaderboardRanking extends Equatable { /// {@macro leaderboard_ranking} const LeaderboardRanking({required this.ranking, required this.outOf}); - /// Place ranking by score for a [LeaderboardEntry]. + /// Place ranking by score for a [LeaderboardEntryData]. final int ranking; - /// Number of [LeaderboardEntry]s at the time of score entry. + /// Number of [LeaderboardEntryData]s at the time of score entry. final int outOf; @override diff --git a/packages/leaderboard_repository/lib/src/models/models.dart b/packages/leaderboard_repository/lib/src/models/models.dart index 3dabe2bf..e10a743b 100644 --- a/packages/leaderboard_repository/lib/src/models/models.dart +++ b/packages/leaderboard_repository/lib/src/models/models.dart @@ -1,2 +1,2 @@ -export 'leaderboard_entry.dart'; +export 'leaderboard_entry_data.dart'; export 'leaderboard_ranking.dart'; diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index cd632638..592425ec 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -57,7 +57,7 @@ void main() { final top10Leaderboard = top10Scores .map( - (score) => LeaderboardEntry( + (score) => LeaderboardEntryData( playerInitials: 'user$score', score: score, character: CharacterType.dash, @@ -144,7 +144,7 @@ void main() { entryScore, 1000, ]; - final leaderboardEntry = LeaderboardEntry( + final leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: entryScore, character: CharacterType.dash, diff --git a/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart b/packages/leaderboard_repository/test/src/models/leaderboard_entry_data_test.dart similarity index 72% rename from packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart rename to packages/leaderboard_repository/test/src/models/leaderboard_entry_data_test.dart index 21056529..f6e27e8a 100644 --- a/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart +++ b/packages/leaderboard_repository/test/src/models/leaderboard_entry_data_test.dart @@ -9,21 +9,21 @@ void main() { 'character': 'dash', }; - const leaderboardEntry = LeaderboardEntry( + const leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: 1500, character: CharacterType.dash, ); test('can be instantiated', () { - const leaderboardEntry = LeaderboardEntry.empty; + const leaderboardEntry = LeaderboardEntryData.empty; expect(leaderboardEntry, isNotNull); }); test('supports value equality.', () { - const leaderboardEntry = LeaderboardEntry.empty; - const leaderboardEntry2 = LeaderboardEntry.empty; + const leaderboardEntry = LeaderboardEntryData.empty; + const leaderboardEntry2 = LeaderboardEntryData.empty; expect(leaderboardEntry, equals(leaderboardEntry2)); }); @@ -33,7 +33,7 @@ void main() { }); test('can be obtained from json', () { - final leaderboardEntryFrom = LeaderboardEntry.fromJson(data); + final leaderboardEntryFrom = LeaderboardEntryData.fromJson(data); expect(leaderboardEntry, equals(leaderboardEntryFrom)); }); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index e1bd8a0c..1e6b7289 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,6 +2,7 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; 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/theme/theme.dart'; @@ -32,6 +33,8 @@ class MockGameState extends Mock implements GameState {} class MockThemeCubit extends Mock implements ThemeCubit {} +class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} + class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart index c44f7d3a..2b217704 100644 --- a/test/leaderboard/bloc/leaderboard_bloc_test.dart +++ b/test/leaderboard/bloc/leaderboard_bloc_test.dart @@ -5,8 +5,9 @@ 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'; -class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} +import '../../helpers/helpers.dart'; void main() { group('LeaderboardBloc', () { @@ -42,7 +43,7 @@ void main() { final top10Leaderboard = top10Scores .map( - (score) => LeaderboardEntry( + (score) => LeaderboardEntryData( playerInitials: 'user$score', score: score, character: CharacterType.dash, @@ -101,7 +102,7 @@ void main() { }); group('LeaderboardEntryAdded', () { - final leaderboardEntry = LeaderboardEntry( + final leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: 1500, character: CharacterType.dash, @@ -163,4 +164,40 @@ void main() { ); }); }); + + 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 index f74296af..33199ca1 100644 --- a/test/leaderboard/bloc/leaderboard_event_test.dart +++ b/test/leaderboard/bloc/leaderboard_event_test.dart @@ -20,7 +20,7 @@ void main() { }); group('LeaderboardEntryAdded', () { - const leaderboardEntry = LeaderboardEntry( + const leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: 1500, character: CharacterType.dash, diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart index 6ff5df13..a40a1cdb 100644 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ b/test/leaderboard/bloc/leaderboard_state_test.dart @@ -3,6 +3,7 @@ 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', () { @@ -25,10 +26,11 @@ void main() { }); group('copyWith', () { - const leaderboardEntry = LeaderboardEntry( + final leaderboardEntry = LeaderboardEntry( + rank: '1', playerInitials: 'ABC', score: 1500, - character: CharacterType.dash, + character: DashTheme().characterAsset, ); test( @@ -51,7 +53,7 @@ void main() { final otherLeaderboardState = LeaderboardState( status: LeaderboardStatus.success, ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: const [leaderboardEntry], + leaderboard: [leaderboardEntry], ); expect(leaderboardState, isNot(equals(otherLeaderboardState))); From f1b35d3eb217ed4941c9c4523309eeb0b942b77c Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 23 Mar 2022 23:08:15 +0100 Subject: [PATCH 2/2] feat: create ellipses from geometry (#84) * feat: create ellipses from geometry * test: geometry test for ellipse * feat: removed required angle and added tests * test: completed tests for geometry * chore: unused import * Update lib/game/components/pathway.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: renaming params * chore: missed test saved Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/pathway.dart | 40 ++++++++++++++ lib/game/pinball_game.dart | 1 - packages/geometry/lib/src/geometry.dart | 53 +++++++++++++++---- packages/geometry/test/src/geometry_test.dart | 40 ++++++++++++++ test/game/components/pathway_test.dart | 36 +++++++++++++ 5 files changed, 159 insertions(+), 11 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 8604e0f3..819ed5f4 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -144,6 +144,46 @@ class Pathway extends BodyComponent with InitialPosition, Layered { ); } + /// Creates an ellipse [Pathway]. + /// + /// Does so with two [ChainShape]s separated by a [width]. Can + /// be rotated by a given [rotation] in radians. + /// + /// If [singleWall] is true, just one [ChainShape] is created. + factory Pathway.ellipse({ + Color? color, + required Vector2 center, + required double width, + required double majorRadius, + required double minorRadius, + double rotation = 0, + bool singleWall = false, + }) { + final paths = >[]; + + // TODO(ruialonso): Refactor repetitive logic + final outerWall = calculateEllipse( + center: center, + majorRadius: majorRadius, + minorRadius: minorRadius, + ).map((vector) => vector..rotate(rotation)).toList(); + paths.add(outerWall); + + if (!singleWall) { + final innerWall = calculateEllipse( + center: center, + majorRadius: majorRadius - width, + minorRadius: minorRadius - width, + ).map((vector) => vector..rotate(rotation)).toList(); + paths.add(innerWall); + } + + return Pathway._( + color: color, + paths: paths, + ); + } + final List> _paths; /// Constructs different [ChainShape]s to form the [Pathway] shape. diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 44a7ec01..3ce7fd77 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,5 +1,4 @@ // ignore_for_file: public_member_api_docs - import 'dart:async'; import 'package:flame/extensions.dart'; import 'package:flame/input.dart'; diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index 8574bc73..6975f8cb 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -23,10 +23,45 @@ List calculateArc({ final points = []; for (var i = 0; i < precision; i++) { - final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle); - final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle); + final x = center.x + radius * math.cos((stepAngle * i) + offsetAngle); + final y = center.y - radius * math.sin((stepAngle * i) + offsetAngle); - final point = Vector2(xCoord, yCoord); + final point = Vector2(x, y); + points.add(point); + } + + return points; +} + +/// Calculates all [Vector2]s of an ellipse. +/// +/// An ellipse can be achieved by specifying a [center], a [majorRadius] and a +/// [minorRadius]. +/// +/// The higher the [precision], the more [Vector2]s will be calculated; +/// achieving a more rounded ellipse. +/// +/// For more information read: https://en.wikipedia.org/wiki/Ellipse. +List calculateEllipse({ + required Vector2 center, + required double majorRadius, + required double minorRadius, + int precision = 100, +}) { + assert( + 0 < minorRadius && minorRadius <= majorRadius, + 'smallRadius ($minorRadius) and bigRadius ($majorRadius) must be in ' + 'range 0 < smallRadius <= bigRadius', + ); + + final stepAngle = 2 * math.pi / (precision - 1); + + final points = []; + for (var i = 0; i < precision; i++) { + final x = center.x + minorRadius * math.cos(stepAngle * i); + final y = center.y - majorRadius * math.sin(stepAngle * i); + + final point = Vector2(x, y); points.add(point); } @@ -63,17 +98,15 @@ List calculateBezierCurve({ final points = []; do { - var xCoord = 0.0; - var yCoord = 0.0; + var x = 0.0; + var y = 0.0; for (var i = 0; i <= n; i++) { final point = controlPoints[i]; - xCoord += - binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; - yCoord += - binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y; + x += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; + y += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y; } - points.add(Vector2(xCoord, yCoord)); + points.add(Vector2(x, y)); t = t + step; } while (t <= 1); diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index 5c33d70f..7a49b2b2 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -33,6 +33,46 @@ void main() { }); }); + group('calculateEllipse', () { + test('returns by default 100 points as indicated by precision', () { + final points = calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 50, + ); + expect(points.length, 100); + }); + + test('returns as many points as indicated by precision', () { + final points = calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 50, + precision: 50, + ); + expect(points.length, 50); + }); + + test('fails if radius not in range', () { + expect( + () => calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 150, + ), + throwsA(isA()), + ); + expect( + () => calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 0, + ), + throwsA(isA()), + ); + }); + }); + group('calculateBezierCurve', () { test('fails if step not in range', () { expect( diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index 03b67c62..63e74d4d 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -165,6 +165,42 @@ void main() { }); }); + group('ellipse', () { + flameTester.test( + 'loads correctly', + (game) async { + final pathway = Pathway.ellipse( + center: Vector2.zero(), + width: width, + majorRadius: 150, + minorRadius: 70, + ); + await game.ready(); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'is static', + (game) async { + final pathway = Pathway.ellipse( + center: Vector2.zero(), + width: width, + majorRadius: 150, + minorRadius: 70, + ); + await game.ready(); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + }); + group('bezier curve', () { final controlPoints = [ Vector2(0, 0),