fix: merge conflicts fixed

pull/296/head
RuiAlonso 3 years ago
commit 3ed2057212

@ -10,13 +10,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Deploy Development name: Deploy Development
steps: steps:
- uses: actions/checkout@v2 - name: Checkout Repo
- uses: subosito/flutter-action@v2 uses: actions/checkout@v2
- name: Setup Flutter
uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
- run: flutter packages get
- run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release - name: Build Flutter App
- uses: FirebaseExtended/action-hosting-deploy@v0 run: |
flutter packages get
flutter build web --target lib/main_development.dart --web-renderer canvaskit --release
- name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0
with: with:
repoToken: "${{ secrets.GITHUB_TOKEN }}" repoToken: "${{ secrets.GITHUB_TOKEN }}"
firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -28,6 +29,11 @@ class AndroidAcres extends Component {
), ),
SpaceshipRail(), SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringBehavior(points: Points.twentyThousand),
@ -43,6 +49,13 @@ class AndroidAcres extends Component {
ScoringBehavior(points: Points.twentyThousand), ScoringBehavior(points: Points.twentyThousand),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.5, -13.8),
AndroidSpaceshipBonusBehavior(),
], ],
); );
/// Creates [AndroidAcres] without any children.
///
/// This can be used for testing [AndroidAcres]'s behaviors in isolation.
@visibleForTesting
AndroidAcres.test();
} }

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<AndroidAcres> {
@override
void onMount() {
super.onMount();
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
androidSpaceship.bloc.stream.listen((state) {
final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return;
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.androidSpaceship));
androidSpaceship.bloc.onBonusAwarded();
});
}
}

@ -1,2 +1,3 @@
export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart'; export 'ramp_bonus_behavior.dart';
export 'ramp_shot_behavior.dart'; export 'ramp_shot_behavior.dart';

@ -4,7 +4,7 @@ export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain.dart'; export 'drain.dart';
export 'flutter_forest/flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';

@ -0,0 +1 @@
export 'chrome_dino_bonus_behavior.dart';

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino].
class ChromeDinoBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<DinoDesert> {
@override
void onMount() {
super.onMount();
final chromeDino = parent.firstChild<ChromeDino>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
chromeDino.bloc.stream.listen((state) {
final listenWhen = state.status == ChromeDinoStatus.chomping;
if (!listenWhen) return;
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dinoChomp));
});
}
}

@ -1,11 +1,13 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template dino_desert} /// {@template dino_desert}
/// Area located next to the [Launcher] containing the [ChromeDino] and /// Area located next to the [Launcher] containing the [ChromeDino],
/// [DinoWalls]. /// [DinoWalls], and the [Slingshots].
/// {@endtemplate} /// {@endtemplate}
class DinoDesert extends Component { class DinoDesert extends Component {
/// {@macro dino_desert} /// {@macro dino_desert}
@ -21,8 +23,15 @@ class DinoDesert extends Component {
_BarrierBehindDino(), _BarrierBehindDino(),
DinoWalls(), DinoWalls(),
Slingshots(), Slingshots(),
ChromeDinoBonusBehavior(),
], ],
); );
/// Creates [DinoDesert] without any children.
///
/// This can be used for testing [DinoDesert]'s behaviors in isolation.
@visibleForTesting
DinoDesert.test();
} }
class _BarrierBehindDino extends BodyComponent { class _BarrierBehindDino extends BodyComponent {

@ -3,7 +3,10 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] /// Bonus obtained at the [FlutterForest].
///
/// When all [DashNestBumper]s are hit at least once three times, the [Signpost]
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. /// is awarded, and the [DashNestBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component class FlutterForestBonusBehavior extends Component
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> { with ParentIsA<FlutterForest>, HasGameRef<PinballGame> {
@ -12,28 +15,36 @@ class FlutterForestBonusBehavior extends Component
super.onMount(); super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>(); final bumpers = parent.children.whereType<DashNestBumper>();
final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!;
final canvas = gameRef.firstChild<ZCanvasComponent>()!;
for (final bumper in bumpers) { for (final bumper in bumpers) {
// TODO(alestiago): Refactor subscription management once the following is // TODO(alestiago): Refactor subscription management once the following is
// merged: // merged:
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
bumper.bloc.stream.listen((state) { bumper.bloc.stream.listen((state) {
final achievedBonus = bumpers.every( final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active, (bumper) => bumper.bloc.state == DashNestBumperState.active,
); );
if (achievedBonus) { if (activatedAllBumpers) {
gameRef signpost.bloc.onProgressed();
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
gameRef.firstChild<ZCanvasComponent>()!.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(17.2, -52.7),
);
parent.firstChild<DashAnimatronic>()?.playing = true;
for (final bumper in bumpers) { for (final bumper in bumpers) {
bumper.bloc.onReset(); bumper.bloc.onReset();
} }
if (signpost.bloc.isFullyProgressed()) {
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
canvas.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(17.2, -52.7),
);
animatronic.playing = true;
signpost.bloc.onProgressed();
}
} }
}); });
} }

@ -39,6 +39,7 @@ class GameFlowController extends ComponentController<PinballGame>
/// Puts the game on a playing state /// Puts the game on a playing state
void start() { void start() {
component.audio.backgroundMusic();
component.firstChild<Backboard>()?.waitingMode(); component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame(); component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay); component.overlays.remove(PinballGame.playButtonOverlay);

@ -1,5 +1,4 @@
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -134,7 +133,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName),
images.load(Assets.images.components.background.path),
]; ];
} }
} }

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/game/pinball_game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template play_button_overlay} /// {@template play_button_overlay}
/// [Widget] that renders the button responsible to starting the game /// [Widget] that renders the button responsible to starting the game
@ -20,14 +21,12 @@ class PlayButtonOverlay extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Center( return PinballButton(
child: ElevatedButton( text: l10n.play,
onPressed: () async { onTap: () async {
_game.gameFlowController.start(); _game.gameFlowController.start();
await showCharacterSelectionDialog(context); await showCharacterSelectionDialog(context);
}, },
child: Text(l10n.play),
),
); );
} }
} }

@ -44,10 +44,6 @@ class $AssetsImagesBonusAnimationGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png');
/// File path: assets/images/components/key.png /// File path: assets/images/components/key.png
AssetGenImage get key => AssetGenImage get key =>
const AssetGenImage('assets/images/components/key.png'); const AssetGenImage('assets/images/components/key.png');

@ -138,8 +138,8 @@ class _Character extends StatelessWidget {
return Expanded( return Expanded(
child: Opacity( child: Opacity(
opacity: isSelected ? 1 : 0.3, opacity: isSelected ? 1 : 0.3,
child: InkWell( child: TextButton(
onTap: () => onPressed: () =>
context.read<CharacterThemeCubit>().characterSelected(character), context.read<CharacterThemeCubit>().characterSelected(character),
child: character.icon.image(fit: BoxFit.contain), child: character.icon.image(fit: BoxFit.contain),
), ),

@ -3,9 +3,9 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
class MockFirebaseAuth extends Mock implements FirebaseAuth {} class _MockFirebaseAuth extends Mock implements FirebaseAuth {}
class MockUserCredential extends Mock implements UserCredential {} class _MockUserCredential extends Mock implements UserCredential {}
void main() { void main() {
late FirebaseAuth firebaseAuth; late FirebaseAuth firebaseAuth;
@ -14,8 +14,8 @@ void main() {
group('AuthenticationRepository', () { group('AuthenticationRepository', () {
setUp(() { setUp(() {
firebaseAuth = MockFirebaseAuth(); firebaseAuth = _MockFirebaseAuth();
userCredential = MockUserCredential(); userCredential = _MockUserCredential();
authenticationRepository = AuthenticationRepository(firebaseAuth); authenticationRepository = AuthenticationRepository(firebaseAuth);
}); });

@ -5,23 +5,23 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class MockFirebaseFirestore extends Mock implements FirebaseFirestore {} class _MockFirebaseFirestore extends Mock implements FirebaseFirestore {}
class MockCollectionReference extends Mock class _MockCollectionReference extends Mock
implements CollectionReference<Map<String, dynamic>> {} implements CollectionReference<Map<String, dynamic>> {}
class MockQuery extends Mock implements Query<Map<String, dynamic>> {} class _MockQuery extends Mock implements Query<Map<String, dynamic>> {}
class MockQuerySnapshot extends Mock class _MockQuerySnapshot extends Mock
implements QuerySnapshot<Map<String, dynamic>> {} implements QuerySnapshot<Map<String, dynamic>> {}
class MockQueryDocumentSnapshot extends Mock class _MockQueryDocumentSnapshot extends Mock
implements QueryDocumentSnapshot<Map<String, dynamic>> {} implements QueryDocumentSnapshot<Map<String, dynamic>> {}
class MockDocumentReference extends Mock class _MockDocumentReference extends Mock
implements DocumentReference<Map<String, dynamic>> {} implements DocumentReference<Map<String, dynamic>> {}
class MockDocumentSnapshot extends Mock class _MockDocumentSnapshot extends Mock
implements DocumentSnapshot<Map<String, dynamic>> {} implements DocumentSnapshot<Map<String, dynamic>> {}
void main() { void main() {
@ -29,7 +29,7 @@ void main() {
late FirebaseFirestore firestore; late FirebaseFirestore firestore;
setUp(() { setUp(() {
firestore = MockFirebaseFirestore(); firestore = _MockFirebaseFirestore();
}); });
test('can be instantiated', () { test('can be instantiated', () {
@ -70,11 +70,11 @@ void main() {
setUp(() { setUp(() {
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
collectionReference = MockCollectionReference(); collectionReference = _MockCollectionReference();
query = MockQuery(); query = _MockQuery();
querySnapshot = MockQuerySnapshot(); querySnapshot = _MockQuerySnapshot();
queryDocumentSnapshots = top10Scores.map((score) { queryDocumentSnapshots = top10Scores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'user$score', 'playerInitials': 'user$score',
@ -119,7 +119,7 @@ void main() {
'playerInitials': 'ABC', 'playerInitials': 'ABC',
'score': 1500, 'score': 1500,
}; };
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]); when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]);
when(queryDocumentSnapshot.data) when(queryDocumentSnapshot.data)
.thenReturn(top10LeaderboardDataMalformed); .thenReturn(top10LeaderboardDataMalformed);
@ -156,12 +156,12 @@ void main() {
setUp(() { setUp(() {
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
collectionReference = MockCollectionReference(); collectionReference = _MockCollectionReference();
documentReference = MockDocumentReference(); documentReference = _MockDocumentReference();
query = MockQuery(); query = _MockQuery();
querySnapshot = MockQuerySnapshot(); querySnapshot = _MockQuerySnapshot();
queryDocumentSnapshots = leaderboardScores.map((score) { queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -228,7 +228,7 @@ void main() {
5000 5000
]; ];
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -248,8 +248,8 @@ void main() {
test( test(
'throws DeleteLeaderboardException ' 'throws DeleteLeaderboardException '
'when deleting scores outside the top 10 fails', () async { 'when deleting scores outside the top 10 fails', () async {
final deleteQuery = MockQuery(); final deleteQuery = _MockQuery();
final deleteQuerySnapshot = MockQuerySnapshot(); final deleteQuerySnapshot = _MockQuerySnapshot();
final newScore = LeaderboardEntryData( final newScore = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 15000, score: 15000,
@ -269,7 +269,7 @@ void main() {
5000, 5000,
]; ];
final deleteDocumentSnapshots = [5500, 5000].map((score) { final deleteDocumentSnapshots = [5500, 5000].map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -284,7 +284,7 @@ void main() {
when(() => deleteQuerySnapshot.docs) when(() => deleteQuerySnapshot.docs)
.thenReturn(deleteDocumentSnapshots); .thenReturn(deleteDocumentSnapshots);
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -310,8 +310,8 @@ void main() {
'saves the new score when there are more than 10 scores in the ' 'saves the new score when there are more than 10 scores in the '
'leaderboard and the new score is higher than the lowest top 10, and ' 'leaderboard and the new score is higher than the lowest top 10, and '
'deletes the scores that are not in the top 10 anymore', () async { 'deletes the scores that are not in the top 10 anymore', () async {
final deleteQuery = MockQuery(); final deleteQuery = _MockQuery();
final deleteQuerySnapshot = MockQuerySnapshot(); final deleteQuerySnapshot = _MockQuerySnapshot();
final newScore = LeaderboardEntryData( final newScore = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 15000, score: 15000,
@ -331,7 +331,7 @@ void main() {
5000, 5000,
]; ];
final deleteDocumentSnapshots = [5500, 5000].map((score) { final deleteDocumentSnapshots = [5500, 5000].map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -346,7 +346,7 @@ void main() {
when(() => deleteQuerySnapshot.docs) when(() => deleteQuerySnapshot.docs)
.thenReturn(deleteDocumentSnapshots); .thenReturn(deleteDocumentSnapshots);
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash', 'character': 'dash',
'playerInitials': 'AAA', 'playerInitials': 'AAA',
@ -376,9 +376,9 @@ void main() {
late DocumentSnapshot<Map<String, dynamic>> documentSnapshot; late DocumentSnapshot<Map<String, dynamic>> documentSnapshot;
setUp(() async { setUp(() async {
collectionReference = MockCollectionReference(); collectionReference = _MockCollectionReference();
documentReference = MockDocumentReference(); documentReference = _MockDocumentReference();
documentSnapshot = MockDocumentSnapshot(); documentSnapshot = _MockDocumentSnapshot();
leaderboardRepository = LeaderboardRepository(firestore); leaderboardRepository = LeaderboardRepository(firestore);
when(() => firestore.collection('prohibitedInitials')) when(() => firestore.collection('prohibitedInitials'))

@ -5,16 +5,23 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsMusicGen {
const $AssetsMusicGen();
String get background => 'assets/music/background.mp3';
}
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get google => 'assets/sfx/google.ogg'; String get google => 'assets/sfx/google.mp3';
String get plim => 'assets/sfx/plim.ogg'; String get plim => 'assets/sfx/plim.mp3';
} }
class Assets { class Assets {
Assets._(); Assets._();
static const $AssetsMusicGen music = $AssetsMusicGen();
static const $AssetsSfxGen sfx = $AssetsSfxGen(); static const $AssetsSfxGen sfx = $AssetsSfxGen();
} }

@ -17,6 +17,14 @@ typedef CreateAudioPool = Future<AudioPool> Function(
/// audio /// audio
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for looping a single
/// audio
typedef LoopSingleAudio = Future<void> Function(String);
/// Function that defines the contract for pre fetching an
/// audio
typedef PreCacheSingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring /// Function that defines the contract for configuring
/// an [AudioCache] instance /// an [AudioCache] instance
typedef ConfigureAudioCache = void Function(AudioCache); typedef ConfigureAudioCache = void Function(AudioCache);
@ -29,9 +37,14 @@ class PinballAudio {
PinballAudio({ PinballAudio({
CreateAudioPool? createAudioPool, CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio, PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio,
PreCacheSingleAudio? preCacheSingleAudio,
ConfigureAudioCache? configureAudioCache, ConfigureAudioCache? configureAudioCache,
}) : _createAudioPool = createAudioPool ?? AudioPool.create, }) : _createAudioPool = createAudioPool ?? AudioPool.create,
_playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play,
_loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop,
_preCacheSingleAudio =
preCacheSingleAudio ?? FlameAudio.audioCache.load,
_configureAudioCache = configureAudioCache ?? _configureAudioCache = configureAudioCache ??
((AudioCache a) { ((AudioCache a) {
a.prefix = ''; a.prefix = '';
@ -41,6 +54,10 @@ class PinballAudio {
final PlaySingleAudio _playSingleAudio; final PlaySingleAudio _playSingleAudio;
final LoopSingleAudio _loopSingleAudio;
final PreCacheSingleAudio _preCacheSingleAudio;
final ConfigureAudioCache _configureAudioCache; final ConfigureAudioCache _configureAudioCache;
late AudioPool _scorePool; late AudioPool _scorePool;
@ -48,11 +65,17 @@ class PinballAudio {
/// Loads the sounds effects into the memory /// Loads the sounds effects into the memory
Future<void> load() async { Future<void> load() async {
_configureAudioCache(FlameAudio.audioCache); _configureAudioCache(FlameAudio.audioCache);
_scorePool = await _createAudioPool( _scorePool = await _createAudioPool(
_prefixFile(Assets.sfx.plim), _prefixFile(Assets.sfx.plim),
maxPlayers: 4, maxPlayers: 4,
prefix: '', prefix: '',
); );
await Future.wait([
_preCacheSingleAudio(_prefixFile(Assets.sfx.google)),
_preCacheSingleAudio(_prefixFile(Assets.music.background)),
]);
} }
/// Plays the basic score sound /// Plays the basic score sound
@ -65,6 +88,11 @@ class PinballAudio {
_playSingleAudio(_prefixFile(Assets.sfx.google)); _playSingleAudio(_prefixFile(Assets.sfx.google));
} }
/// Plays the background music
void backgroundMusic() {
_loopSingleAudio(_prefixFile(Assets.music.background));
}
String _prefixFile(String file) { String _prefixFile(String file) {
return 'packages/pinball_audio/$file'; return 'packages/pinball_audio/$file';
} }

@ -26,3 +26,4 @@ flutter_gen:
flutter: flutter:
assets: assets:
- assets/sfx/ - assets/sfx/
- assets/music/

@ -1,34 +0,0 @@
// ignore_for_file: one_member_abstracts
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:mocktail/mocktail.dart';
abstract class _CreateAudioPoolStub {
Future<AudioPool> onCall(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
}
class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {}
abstract class _ConfigureAudioCacheStub {
void onCall(AudioCache cache);
}
class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub {
}
abstract class _PlaySingleAudioStub {
Future<void> onCall(String url);
}
class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {}
class MockAudioPool extends Mock implements AudioPool {}
class MockAudioCache extends Mock implements AudioCache {}

@ -1,50 +1,92 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors, one_member_abstracts
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import '../helpers/helpers.dart'; class _MockAudioPool extends Mock implements AudioPool {}
class _MockAudioCache extends Mock implements AudioCache {}
class _MockCreateAudioPool extends Mock {
Future<AudioPool> onCall(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
}
class _MockConfigureAudioCache extends Mock {
void onCall(AudioCache cache);
}
class _MockPlaySingleAudio extends Mock {
Future<void> onCall(String url);
}
class _MockLoopSingleAudio extends Mock {
Future<void> onCall(String url);
}
abstract class _PreCacheSingleAudio {
Future<void> onCall(String url);
}
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
void main() { void main() {
group('PinballAudio', () { group('PinballAudio', () {
test('can be instantiated', () { late _MockCreateAudioPool createAudioPool;
expect(PinballAudio(), isNotNull); late _MockConfigureAudioCache configureAudioCache;
}); late _MockPlaySingleAudio playSingleAudio;
late _MockLoopSingleAudio loopSingleAudio;
late CreateAudioPoolStub createAudioPool; late _PreCacheSingleAudio preCacheSingleAudio;
late ConfigureAudioCacheStub configureAudioCache;
late PlaySingleAudioStub playSingleAudio;
late PinballAudio audio; late PinballAudio audio;
setUpAll(() { setUpAll(() {
registerFallbackValue(MockAudioCache()); registerFallbackValue(_MockAudioCache());
}); });
setUp(() { setUp(() {
createAudioPool = CreateAudioPoolStub(); createAudioPool = _MockCreateAudioPool();
when( when(
() => createAudioPool.onCall( () => createAudioPool.onCall(
any(), any(),
maxPlayers: any(named: 'maxPlayers'), maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'), prefix: any(named: 'prefix'),
), ),
).thenAnswer((_) async => MockAudioPool()); ).thenAnswer((_) async => _MockAudioPool());
configureAudioCache = ConfigureAudioCacheStub(); configureAudioCache = _MockConfigureAudioCache();
when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {});
playSingleAudio = PlaySingleAudioStub(); playSingleAudio = _MockPlaySingleAudio();
when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {});
loopSingleAudio = _MockLoopSingleAudio();
when(() => loopSingleAudio.onCall(any())).thenAnswer((_) async {});
preCacheSingleAudio = _MockPreCacheSingleAudio();
when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {});
audio = PinballAudio( audio = PinballAudio(
configureAudioCache: configureAudioCache.onCall, configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
loopSingleAudio: loopSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
}); });
test('can be instantiated', () {
expect(PinballAudio(), isNotNull);
});
group('load', () { group('load', () {
test('creates the score pool', () async { test('creates the score pool', () async {
await audio.load(); await audio.load();
@ -69,16 +111,30 @@ void main() {
audio = PinballAudio( audio = PinballAudio(
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
await audio.load(); await audio.load();
expect(FlameAudio.audioCache.prefix, equals('')); expect(FlameAudio.audioCache.prefix, equals(''));
}); });
test('pre cache the assets', () async {
await audio.load();
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'),
).called(1);
});
}); });
group('score', () { group('score', () {
test('plays the score sound pool', () async { test('plays the score sound pool', () async {
final audioPool = MockAudioPool(); final audioPool = _MockAudioPool();
when(audioPool.start).thenAnswer((_) async => () {}); when(audioPool.start).thenAnswer((_) async => () {});
when( when(
() => createAudioPool.onCall( () => createAudioPool.onCall(
@ -106,5 +162,17 @@ void main() {
).called(1); ).called(1);
}); });
}); });
group('backgroundMusic', () {
test('plays the correct file', () async {
await audio.load();
audio.backgroundMusic();
verify(
() => loopSingleAudio
.onCall('packages/pinball_audio/${Assets.music.background}'),
).called(1);
});
});
}); });
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 45 KiB

@ -0,0 +1,71 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_animatronic}
/// Animated Android that sits on top of the [AndroidSpaceship].
/// {@endtemplate}
class AndroidAnimatronic extends BodyComponent
with InitialPosition, Layered, ZIndex {
/// {@macro android_animatronic}
AndroidAnimatronic({Iterable<Component>? children})
: super(
children: [
_AndroidAnimatronicSpriteAnimationComponent(),
...?children,
],
renderBody: false,
) {
layer = Layer.spaceship;
zIndex = ZIndexes.androidHead;
}
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3.1,
minorRadius: 2,
)..rotate(1.4);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixtureFromShape(shape);
}
}
class _AndroidAnimatronicSpriteAnimationComponent
extends SpriteAnimationComponent with HasGameRef {
_AndroidAnimatronicSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-0.24, -2.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
);
const amountPerRow = 18;
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
}
}

@ -5,17 +5,25 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart';
class AndroidSpaceship extends Component { class AndroidSpaceship extends Component {
AndroidSpaceship({required Vector2 position}) AndroidSpaceship({
: super( required Vector2 position,
}) : bloc = AndroidSpaceshipCubit(),
super(
children: [ children: [
_SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position,
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25), AndroidSpaceshipEntrance(
children: [AndroidSpaceshipEntranceBallContactBehavior()],
),
_SpaceshipHole( _SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail, outsideLayer: Layer.spaceshipExitRail,
outsidePriority: ZIndexes.ballOnSpaceshipRail, outsidePriority: ZIndexes.ballOnSpaceshipRail,
@ -26,6 +34,27 @@ class AndroidSpaceship extends Component {
)..initialPosition = position - Vector2(-7.5, -1.1), )..initialPosition = position - Vector2(-7.5, -1.1),
], ],
); );
/// Creates an [AndroidSpaceship] without any children.
///
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final AndroidSpaceshipCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
} }
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
@ -123,62 +152,32 @@ class _LightBeamSpriteComponent extends SpriteComponent
} }
} }
class _AndroidHead extends BodyComponent with InitialPosition, Layered, ZIndex { class AndroidSpaceshipEntrance extends BodyComponent
_AndroidHead() with ParentIsA<AndroidSpaceship>, Layered {
AndroidSpaceshipEntrance({Iterable<Component>? children})
: super( : super(
children: [_AndroidHeadSpriteAnimationComponent()], children: children,
renderBody: false, renderBody: false,
) { ) {
layer = Layer.spaceship; layer = Layer.spaceship;
zIndex = ZIndexes.androidHead;
} }
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = PolygonShape()
center: Vector2.zero(), ..setAsBox(
majorRadius: 3.1, 2,
minorRadius: 2, 0.1,
)..rotate(1.4); Vector2(-27.4, -37.2),
final bodyDef = BodyDef(position: initialPosition); -0.12,
);
return world.createBody(bodyDef)..createFixtureFromShape(shape); final fixtureDef = FixtureDef(
} shape,
} isSensor: true,
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_AndroidHeadSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-0.24, -2.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
); );
final bodyDef = BodyDef();
const amountPerRow = 18; return world.createBody(bodyDef)..createFixture(fixtureDef);
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
} }
} }

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class AndroidSpaceshipEntranceBallContactBehavior
extends ContactBehavior<AndroidSpaceshipEntrance> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.parent.bloc.onBallEntered();
}
}

@ -0,0 +1 @@
export 'android_spaceship_entrance_ball_contact_behavior.dart.dart';

@ -0,0 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'android_spaceship_state.dart';
class AndroidSpaceshipCubit extends Cubit<AndroidSpaceshipState> {
AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus);
void onBallEntered() => emit(AndroidSpaceshipState.withBonus);
void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus);
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'android_spaceship_cubit.dart';
enum AndroidSpaceshipState {
withoutBonus,
withBonus,
}

@ -18,10 +18,17 @@ class ChromeDinoCubit extends Cubit<ChromeDinoState> {
} }
void onChomp(Ball ball) { void onChomp(Ball ball) {
emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); if (ball != state.ball) {
emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball));
}
} }
void onSpit() { void onSpit() {
emit(state.copyWith(status: ChromeDinoStatus.idle)); emit(
ChromeDinoState(
status: ChromeDinoStatus.idle,
isMouthOpen: state.isMouthOpen,
),
);
} }
} }

@ -1,5 +1,6 @@
export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship.dart'; export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
@ -27,7 +28,7 @@ export 'plunger.dart';
export 'rocket.dart'; export 'rocket.dart';
export 'score_component.dart'; export 'score_component.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart'; export 'signpost/signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp/spaceship_ramp.dart'; export 'spaceship_ramp/spaceship_ramp.dart';

@ -36,7 +36,7 @@ class Kicker extends BodyComponent with InitialPosition {
}) : _side = side, }) : _side = side,
super( super(
children: [ children: [
BumpingBehavior(strength: 20)..applyTo(['bouncy_edge']), BumpingBehavior(strength: 25)..applyTo(['bouncy_edge']),
KickerBallContactBehavior()..applyTo(['bouncy_edge']), KickerBallContactBehavior()..applyTo(['bouncy_edge']),
KickerBlinkingBehavior(), KickerBlinkingBehavior(),
_KickerSpriteGroupComponent( _KickerSpriteGroupComponent(

@ -1,101 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// Represents the [Signpost]'s current [Sprite] state.
@visibleForTesting
enum SignpostSpriteState {
/// Signpost with no active dashes.
inactive,
/// Signpost with a single sign of active dashes.
active1,
/// Signpost with two signs of active dashes.
active2,
/// Signpost with all signs of active dashes.
active3,
}
extension on SignpostSpriteState {
String get path {
switch (this) {
case SignpostSpriteState.inactive:
return Assets.images.signpost.inactive.keyName;
case SignpostSpriteState.active1:
return Assets.images.signpost.active1.keyName;
case SignpostSpriteState.active2:
return Assets.images.signpost.active2.keyName;
case SignpostSpriteState.active3:
return Assets.images.signpost.active3.keyName;
}
}
SignpostSpriteState get next {
return SignpostSpriteState
.values[(index + 1) % SignpostSpriteState.values.length];
}
}
/// {@template signpost}
/// A sign, found in the Flutter Forest.
///
/// Lights up a new sign whenever all three [DashNestBumper]s are hit.
/// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}
Signpost({
Iterable<Component>? children,
}) : super(
renderBody: false,
children: [
_SignpostSpriteComponent(),
...?children,
],
);
/// Forwards the sprite to the next [SignpostSpriteState].
///
/// If the current state is the last one it cycles back to the initial state.
void progress() => firstChild<_SignpostSpriteComponent>()!.progress();
@override
Body createBody() {
final shape = CircleShape()..radius = 0.25;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _SignpostSpriteComponent extends SpriteGroupComponent<SignpostSpriteState>
with HasGameRef {
_SignpostSpriteComponent()
: super(
anchor: Anchor.bottomCenter,
position: Vector2(0.65, 0.45),
);
void progress() => current = current?.next;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = <SignpostSpriteState, Sprite>{};
this.sprites = sprites;
for (final spriteState in SignpostSpriteState.values) {
sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = SignpostSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}

@ -0,0 +1,18 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'signpost_state.dart';
class SignpostCubit extends Cubit<SignpostState> {
SignpostCubit() : super(SignpostState.inactive);
void onProgressed() {
final index = SignpostState.values.indexOf(state);
emit(
SignpostState.values[(index + 1) % SignpostState.values.length],
);
}
bool isFullyProgressed() => state == SignpostState.active3;
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
part of 'signpost_cubit.dart';
enum SignpostState {
/// Signpost with no active eggs.
inactive,
/// Signpost with a single sign of lit up eggs.
active1,
/// Signpost with two signs of lit up eggs.
active2,
/// Signpost with all signs of lit up eggs.
active3,
}

@ -0,0 +1,109 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/signpost_cubit.dart';
/// {@template signpost}
/// A sign, found in the Flutter Forest.
///
/// Lights up a new sign whenever all three [DashNestBumper]s are hit.
/// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}
Signpost({
Iterable<Component>? children,
}) : this._(
children: children,
bloc: SignpostCubit(),
);
Signpost._({
Iterable<Component>? children,
required this.bloc,
}) : super(
renderBody: false,
children: [
_SignpostSpriteComponent(
current: bloc.state,
),
...?children,
],
);
/// Creates a [Signpost] without any children.
///
/// This can be used for testing [Signpost]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
Signpost.test({
required this.bloc,
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SignpostCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.25;
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixtureFromShape(shape);
}
}
class _SignpostSpriteComponent extends SpriteGroupComponent<SignpostState>
with HasGameRef, ParentIsA<Signpost> {
_SignpostSpriteComponent({
required SignpostState current,
}) : super(
anchor: Anchor.bottomCenter,
position: Vector2(0.65, 0.45),
current: current,
);
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state);
final sprites = <SignpostState, Sprite>{};
this.sprites = sprites;
for (final spriteState in SignpostState.values) {
sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = SignpostState.inactive;
size = sprites[current]!.originalSize / 10;
}
}
extension on SignpostState {
String get path {
switch (this) {
case SignpostState.inactive:
return Assets.images.signpost.inactive.keyName;
case SignpostState.active1:
return Assets.images.signpost.active1.keyName;
case SignpostState.active2:
return Assets.images.signpost.active2.keyName;
case SignpostState.active3:
return Assets.images.signpost.active3.keyName;
}
}
}

@ -17,7 +17,7 @@ class AndroidSpaceshipGame extends BallGame {
); );
static const description = ''' static const description = '''
Shows how the AndroidSpaceship is rendered. Shows how the AndroidSpaceship and AndroidAnimatronic are rendered.
- Activate the "trace" parameter to overlay the body. - Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a Ball into the game. - Tap anywhere on the screen to spawn a Ball into the game.
@ -28,8 +28,11 @@ class AndroidSpaceshipGame extends BallGame {
await super.onLoad(); await super.onLoad();
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await add( await addAll(
AndroidSpaceship(position: Vector2.zero()), [
AndroidSpaceship(position: Vector2.zero()),
AndroidAnimatronic(),
],
); );
await traceAllBodies(); await traceAllBodies();

@ -34,6 +34,6 @@ class SignpostGame extends BallGame {
@override @override
void onTap() { void onTap() {
super.onTap(); super.onTap();
firstChild<Signpost>()!.progress(); firstChild<Signpost>()!.bloc.onProgressed();
} }
} }

@ -1,2 +1 @@
export 'mocks.dart';
export 'test_game.dart'; export 'test_game.dart';

@ -1,34 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {}
class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {}
class MockGame extends Mock implements Forge2DGame {}
class MockContact extends Mock implements Contact {}
class MockComponent extends Mock implements Component {}
class MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {}
class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}
class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
class MockRampSensorCubit extends Mock implements RampSensorCubit {}

@ -0,0 +1,70 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.android.spaceship.animatronic.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
group('AndroidAnimatronic', () {
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.load(asset);
await game.ensureAdd(AndroidAnimatronic());
game.camera.followVector2(Vector2.zero());
await tester.pump();
},
verify: (game, tester) async {
final animationDuration = game
.firstChild<AndroidAnimatronic>()!
.firstChild<SpriteAnimationComponent>()!
.animation!
.totalDuration();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_animatronic/start.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_animatronic/middle.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_animatronic/end.png'),
);
},
);
flameTester.test(
'loads correctly',
(game) async {
final androidAnimatronic = AndroidAnimatronic();
await game.ensureAdd(androidAnimatronic);
expect(game.contains(androidAnimatronic), isTrue);
},
);
flameTester.test('adds new children', (game) async {
final component = Component();
final androidAnimatronic = AndroidAnimatronic(
children: [component],
);
await game.ensureAdd(androidAnimatronic);
expect(androidAnimatronic.children, contains(component));
});
});
}

@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/bumping_behavior.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -46,7 +48,7 @@ void main() {
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = MockAndroidBumperCubit(); final bloc = _MockAndroidBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<AndroidBumperState>.empty(), const Stream<AndroidBumperState>.empty(),

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/android_bumper/behaviors/behav
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -27,7 +34,7 @@ void main() {
'beginContact emits onBallContacted when contacts with a ball', 'beginContact emits onBallContacted when contacts with a ball',
(game) async { (game) async {
final behavior = AndroidBumperBallContactBehavior(); final behavior = AndroidBumperBallContactBehavior();
final bloc = MockAndroidBumperCubit(); final bloc = _MockAndroidBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<AndroidBumperState>.empty(), const Stream<AndroidBumperState>.empty(),
@ -38,7 +45,7 @@ void main() {
await androidBumper.add(behavior); await androidBumper.add(behavior);
await game.ensureAdd(androidBumper); await game.ensureAdd(androidBumper);
behavior.beginContact(MockBall(), MockContact()); behavior.beginContact(_MockBall(), _MockContact());
verify(androidBumper.bloc.onBallContacted).called(1); verify(androidBumper.bloc.onBallContacted).called(1);
}, },

@ -9,6 +9,8 @@ import 'package:pinball_components/src/components/android_bumper/behaviors/behav
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -20,7 +22,7 @@ void main() {
'calls onBlinked after 0.05 seconds when dimmed', 'calls onBlinked after 0.05 seconds when dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = AndroidBumperBlinkingBehavior(); final behavior = AndroidBumperBlinkingBehavior();
final bloc = MockAndroidBumperCubit(); final bloc = _MockAndroidBumperCubit();
final streamController = StreamController<AndroidBumperState>(); final streamController = StreamController<AndroidBumperState>();
whenListen( whenListen(
bloc, bloc,

@ -0,0 +1,109 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../helpers/helpers.dart';
class _MockAndroidSpaceshipCubit extends Mock implements AndroidSpaceshipCubit {
}
void main() {
group('AndroidSpaceship', () {
final assets = [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
final component = AndroidSpaceship(position: Vector2.zero());
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final canvas = ZCanvasComponent(
children: [AndroidSpaceship(position: Vector2.zero())],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
await game.ready();
await tester.pump();
},
verify: (game, tester) async {
const goldenFilePath = '../golden/android_spaceship/';
final animationDuration = game
.descendants()
.whereType<SpriteAnimationComponent>()
.single
.animation!
.totalDuration();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}start.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}middle.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}end.png'),
);
},
);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockAndroidSpaceshipCubit();
whenListen(
bloc,
const Stream<AndroidSpaceshipState>.empty(),
initialState: AndroidSpaceshipState.withoutBonus,
);
when(bloc.close).thenAnswer((_) async {});
final androidSpaceship = AndroidSpaceship.test(bloc: bloc);
await game.ensureAdd(androidSpaceship);
game.remove(androidSpaceship);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test(
'AndroidSpaceshipEntrance has an '
'AndroidSpaceshipEntranceBallContactBehavior', (game) async {
final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
await game.ensureAdd(androidSpaceship);
final androidSpaceshipEntrance =
androidSpaceship.firstChild<AndroidSpaceshipEntrance>();
expect(
androidSpaceshipEntrance!.children
.whereType<AndroidSpaceshipEntranceBallContactBehavior>()
.single,
isNotNull,
);
});
});
}

@ -0,0 +1,60 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockAndroidSpaceshipCubit extends Mock implements AndroidSpaceshipCubit {
}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'AndroidSpaceshipEntranceBallContactBehavior',
() {
test('can be instantiated', () {
expect(
AndroidSpaceshipEntranceBallContactBehavior(),
isA<AndroidSpaceshipEntranceBallContactBehavior>(),
);
});
flameTester.test(
'beginContact calls onBallEntered when entrance contacts with a ball',
(game) async {
final behavior = AndroidSpaceshipEntranceBallContactBehavior();
final bloc = _MockAndroidSpaceshipCubit();
whenListen(
bloc,
const Stream<AndroidSpaceshipState>.empty(),
initialState: AndroidSpaceshipState.withoutBonus,
);
final entrance = AndroidSpaceshipEntrance();
final androidSpaceship = AndroidSpaceship.test(
bloc: bloc,
children: [entrance],
);
await entrance.add(behavior);
await game.ensureAdd(androidSpaceship);
behavior.beginContact(_MockBall(), _MockContact());
verify(androidSpaceship.bloc.onBallEntered).called(1);
},
);
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'AndroidSpaceshipCubit',
() {
blocTest<AndroidSpaceshipCubit, AndroidSpaceshipState>(
'onBallEntered emits withBonus',
build: AndroidSpaceshipCubit.new,
act: (bloc) => bloc.onBallEntered(),
expect: () => [AndroidSpaceshipState.withBonus],
);
blocTest<AndroidSpaceshipCubit, AndroidSpaceshipState>(
'onBonusAwarded emits withoutBonus',
build: AndroidSpaceshipCubit.new,
act: (bloc) => bloc.onBonusAwarded(),
expect: () => [AndroidSpaceshipState.withoutBonus],
);
},
);
}

@ -1,67 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
void main() {
group('AndroidSpaceship', () {
final assets = [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
final component = AndroidSpaceship(position: Vector2.zero());
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final canvas = ZCanvasComponent(
children: [AndroidSpaceship(position: Vector2.zero())],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
await game.ready();
await tester.pump();
},
verify: (game, tester) async {
final animationDuration = game
.descendants()
.whereType<SpriteAnimationComponent>()
.last
.animation!
.totalDuration();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_spaceship/start.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_spaceship/middle.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_spaceship/end.png'),
);
},
);
});
}

@ -2,6 +2,7 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -11,6 +12,12 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
class _MockContact extends Mock implements Contact {}
class _MockFixture extends Mock implements Fixture {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -30,7 +37,7 @@ void main() {
(game) async { (game) async {
final ball = Ball(baseColor: Colors.red); final ball = Ball(baseColor: Colors.red);
final behavior = ChromeDinoChompingBehavior(); final behavior = ChromeDinoChompingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
@ -44,8 +51,8 @@ void main() {
await chromeDino.add(behavior); await chromeDino.add(behavior);
await game.ensureAddAll([chromeDino, ball]); await game.ensureAddAll([chromeDino, ball]);
final contact = MockContact(); final contact = _MockContact();
final fixture = MockFixture(); final fixture = _MockFixture();
when(() => contact.fixtureA).thenReturn(fixture); when(() => contact.fixtureA).thenReturn(fixture);
when(() => fixture.userData).thenReturn('inside_mouth'); when(() => fixture.userData).thenReturn('inside_mouth');

@ -10,6 +10,14 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
class _MockContact extends Mock implements Contact {}
class _MockFixture extends Mock implements Fixture {}
class _MockBall extends Mock implements Ball {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -29,7 +37,7 @@ void main() {
'and there is not ball in the mouth', 'and there is not ball in the mouth',
(game) async { (game) async {
final behavior = ChromeDinoMouthOpeningBehavior(); final behavior = ChromeDinoMouthOpeningBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
@ -43,12 +51,12 @@ void main() {
await chromeDino.add(behavior); await chromeDino.add(behavior);
await game.ensureAdd(chromeDino); await game.ensureAdd(chromeDino);
final contact = MockContact(); final contact = _MockContact();
final fixture = MockFixture(); final fixture = _MockFixture();
when(() => contact.fixtureA).thenReturn(fixture); when(() => contact.fixtureA).thenReturn(fixture);
when(() => fixture.userData).thenReturn('mouth_opening'); when(() => fixture.userData).thenReturn('mouth_opening');
behavior.preSolve(MockBall(), contact, Manifold()); behavior.preSolve(_MockBall(), contact, Manifold());
verify(() => contact.setEnabled(false)).called(1); verify(() => contact.setEnabled(false)).called(1);
}, },

@ -13,6 +13,8 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -33,7 +35,7 @@ void main() {
(game) async { (game) async {
final ball = Ball(baseColor: Colors.red); final ball = Ball(baseColor: Colors.red);
final behavior = ChromeDinoSpittingBehavior(); final behavior = ChromeDinoSpittingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
final streamController = StreamController<ChromeDinoState>(); final streamController = StreamController<ChromeDinoState>();
final chompingState = ChromeDinoState( final chompingState = ChromeDinoState(
status: ChromeDinoStatus.chomping, status: ChromeDinoStatus.chomping,
@ -71,7 +73,7 @@ void main() {
(game) async { (game) async {
final ball = Ball(baseColor: Colors.red); final ball = Ball(baseColor: Colors.red);
final behavior = ChromeDinoSpittingBehavior(); final behavior = ChromeDinoSpittingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
final streamController = StreamController<ChromeDinoState>(); final streamController = StreamController<ChromeDinoState>();
final chompingState = ChromeDinoState( final chompingState = ChromeDinoState(
status: ChromeDinoStatus.chomping, status: ChromeDinoStatus.chomping,

@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -30,7 +32,7 @@ void main() {
'creates a RevoluteJoint', 'creates a RevoluteJoint',
(game) async { (game) async {
final behavior = ChromeDinoSwivelingBehavior(); final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
@ -52,7 +54,7 @@ void main() {
'reverses swivel direction on each timer tick', 'reverses swivel direction on each timer tick',
(game) async { (game) async {
final behavior = ChromeDinoSwivelingBehavior(); final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
@ -84,7 +86,7 @@ void main() {
'and mouth is open', 'and mouth is open',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = ChromeDinoSwivelingBehavior(); final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
@ -113,7 +115,7 @@ void main() {
'and mouth is closed', 'and mouth is closed',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = ChromeDinoSwivelingBehavior(); final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
@ -141,7 +143,7 @@ void main() {
'and mouth is closed', 'and mouth is closed',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = ChromeDinoSwivelingBehavior(); final behavior = ChromeDinoSwivelingBehavior();
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),

@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -73,7 +75,7 @@ void main() {
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),

@ -36,7 +36,8 @@ void main() {
); );
blocTest<ChromeDinoCubit, ChromeDinoState>( blocTest<ChromeDinoCubit, ChromeDinoState>(
'onChomp emits ChromeDinoStatus.chomping and chomped ball', 'onChomp emits ChromeDinoStatus.chomping and chomped ball '
'when the ball is not in the mouth',
build: ChromeDinoCubit.new, build: ChromeDinoCubit.new,
act: (bloc) => bloc.onChomp(ball), act: (bloc) => bloc.onChomp(ball),
expect: () => [ expect: () => [
@ -55,7 +56,15 @@ void main() {
); );
blocTest<ChromeDinoCubit, ChromeDinoState>( blocTest<ChromeDinoCubit, ChromeDinoState>(
'onSpit emits ChromeDinoStatus.idle', 'onChomp emits nothing when the ball is already in the mouth',
build: ChromeDinoCubit.new,
seed: () => const ChromeDinoState.inital().copyWith(ball: ball),
act: (bloc) => bloc.onChomp(ball),
expect: () => <ChromeDinoState>[],
);
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onSpit emits ChromeDinoStatus.idle and removes ball',
build: ChromeDinoCubit.new, build: ChromeDinoCubit.new,
act: (bloc) => bloc.onSpit(), act: (bloc) => bloc.onSpit(),
expect: () => [ expect: () => [
@ -63,7 +72,11 @@ void main() {
(state) => state.status, (state) => state.status,
'status', 'status',
ChromeDinoStatus.idle, ChromeDinoStatus.idle,
) )..having(
(state) => state.ball,
'ball',
null,
)
], ],
); );
}, },

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/beh
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -27,7 +34,7 @@ void main() {
'beginContact emits onBallContacted when contacts with a ball', 'beginContact emits onBallContacted when contacts with a ball',
(game) async { (game) async {
final behavior = DashNestBumperBallContactBehavior(); final behavior = DashNestBumperBallContactBehavior();
final bloc = MockDashNestBumperCubit(); final bloc = _MockDashNestBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<DashNestBumperState>.empty(), const Stream<DashNestBumperState>.empty(),
@ -38,7 +45,7 @@ void main() {
await dashNestBumper.add(behavior); await dashNestBumper.add(behavior);
await game.ensureAdd(dashNestBumper); await game.ensureAdd(dashNestBumper);
behavior.beginContact(MockBall(), MockContact()); behavior.beginContact(_MockBall(), _MockContact());
verify(dashNestBumper.bloc.onBallContacted).called(1); verify(dashNestBumper.bloc.onBallContacted).called(1);
}, },

@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/beh
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -48,7 +50,7 @@ void main() {
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = MockDashNestBumperCubit(); final bloc = _MockDashNestBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<DashNestBumperState>.empty(), const Stream<DashNestBumperState>.empty(),

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/google_letter/behaviors/behavi
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -27,7 +34,7 @@ void main() {
'beginContact emits onBallContacted when contacts with a ball', 'beginContact emits onBallContacted when contacts with a ball',
(game) async { (game) async {
final behavior = GoogleLetterBallContactBehavior(); final behavior = GoogleLetterBallContactBehavior();
final bloc = MockGoogleLetterCubit(); final bloc = _MockGoogleLetterCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<GoogleLetterState>.empty(), const Stream<GoogleLetterState>.empty(),
@ -38,7 +45,7 @@ void main() {
await googleLetter.add(behavior); await googleLetter.add(behavior);
await game.ensureAdd(googleLetter); await game.ensureAdd(googleLetter);
behavior.beginContact(MockBall(), MockContact()); behavior.beginContact(_MockBall(), _MockContact());
verify(googleLetter.bloc.onBallContacted).called(1); verify(googleLetter.bloc.onBallContacted).called(1);
}, },

@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/google_letter/behaviors/behavi
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -104,7 +106,7 @@ void main() {
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = MockGoogleLetterCubit(); final bloc = _MockGoogleLetterCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<GoogleLetterState>.empty(), const Stream<GoogleLetterState>.empty(),

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -11,6 +12,10 @@ import '../../../../helpers/helpers.dart';
class _MockKickerCubit extends Mock implements KickerCubit {} class _MockKickerCubit extends Mock implements KickerCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -43,7 +48,7 @@ void main() {
await kicker.add(behavior); await kicker.add(behavior);
await game.ensureAdd(kicker); await game.ensureAdd(kicker);
behavior.beginContact(MockBall(), MockContact()); behavior.beginContact(_MockBall(), _MockContact());
verify(kicker.bloc.onBallContacted).called(1); verify(kicker.bloc.onBallContacted).called(1);
}, },

@ -7,6 +7,12 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
class TestLayerSensor extends LayerSensor { class TestLayerSensor extends LayerSensor {
TestLayerSensor({ TestLayerSensor({
required LayerEntranceOrientation orientation, required LayerEntranceOrientation orientation,
@ -115,8 +121,8 @@ void main() {
late Layer insideLayer; late Layer insideLayer;
setUp(() { setUp(() {
ball = MockBall(); ball = _MockBall();
body = MockBody(); body = _MockBody();
insideZIndex = 1; insideZIndex = 1;
insideLayer = Layer.spaceshipEntranceRamp; insideLayer = Layer.spaceshipEntranceRamp;
@ -136,13 +142,13 @@ void main() {
when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
sensor.beginContact(ball, MockContact()); sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1); verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insideZIndex).called(1); verify(() => ball.zIndex = insideZIndex).called(1);
when(() => ball.layer).thenReturn(insideLayer); when(() => ball.layer).thenReturn(insideLayer);
sensor.beginContact(ball, MockContact()); sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
}); });
@ -159,13 +165,13 @@ void main() {
when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
sensor.beginContact(ball, MockContact()); sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1); verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insidePriority).called(1); verify(() => ball.zIndex = insidePriority).called(1);
when(() => ball.layer).thenReturn(insideLayer); when(() => ball.layer).thenReturn(insideLayer);
sensor.beginContact(ball, MockContact()); sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
}); });

@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/multiball/behaviors/behaviors.
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockMultiballCubit extends Mock implements MultiballCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -22,7 +24,7 @@ void main() {
'calls onBlink every 0.1 seconds when animation state is animated', 'calls onBlink every 0.1 seconds when animation state is animated',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior(); final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit(); final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>(); final streamController = StreamController<MultiballState>();
whenListen( whenListen(
bloc, bloc,
@ -57,7 +59,7 @@ void main() {
'calls onStop when animation state is stopped', 'calls onStop when animation state is stopped',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior(); final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit(); final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>(); final streamController = StreamController<MultiballState>();
whenListen( whenListen(
bloc, bloc,
@ -94,7 +96,7 @@ void main() {
'onTick stops when there is no animation', 'onTick stops when there is no animation',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior(); final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit(); final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>(); final streamController = StreamController<MultiballState>();
whenListen( whenListen(
bloc, bloc,
@ -125,7 +127,7 @@ void main() {
'onTick stops after 10 blinks repetitions', 'onTick stops after 10 blinks repetitions',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior(); final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit(); final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>(); final streamController = StreamController<MultiballState>();
whenListen( whenListen(
bloc, bloc,

@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/multiball/behaviors/behaviors.
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockMultiballCubit extends Mock implements MultiballCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -50,7 +52,7 @@ void main() {
flameTester.test( flameTester.test(
'closes bloc when removed', 'closes bloc when removed',
(game) async { (game) async {
final bloc = MockMultiballCubit(); final bloc = _MockMultiballCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<MultiballLightState>.empty(), const Stream<MultiballLightState>.empty(),

@ -9,9 +9,9 @@ import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { class _MockMultiplierCubit extends Mock implements MultiplierCubit {}
final bloc = MockMultiplierCubit();
void main() {
group('Multiplier', () { group('Multiplier', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -27,6 +27,11 @@ void main() {
Assets.images.multiplier.x6.dimmed.keyName, Assets.images.multiplier.x6.dimmed.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
late MultiplierCubit bloc;
setUp(() {
bloc = _MockMultiplierCubit();
});
flameTester.test('"x2" loads correctly', (game) async { flameTester.test('"x2" loads correctly', (game) async {
final multiplier = Multiplier.x2( final multiplier = Multiplier.x2(

@ -0,0 +1,39 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('SignpostCubit', () {
blocTest<SignpostCubit, SignpostState>(
'onProgressed progresses',
build: SignpostCubit.new,
act: (bloc) {
bloc
..onProgressed()
..onProgressed()
..onProgressed()
..onProgressed();
},
expect: () => [
SignpostState.active1,
SignpostState.active2,
SignpostState.active3,
SignpostState.inactive,
],
);
test('isFullyProgressed when on active3', () {
final bloc = SignpostCubit();
expect(bloc.isFullyProgressed(), isFalse);
bloc.onProgressed();
expect(bloc.isFullyProgressed(), isFalse);
bloc.onProgressed();
expect(bloc.isFullyProgressed(), isFalse);
bloc.onProgressed();
expect(bloc.isFullyProgressed(), isTrue);
});
});
}

@ -1,11 +1,15 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockSignpostCubit extends Mock implements SignpostCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -18,13 +22,13 @@ void main() {
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
group('Signpost', () { group('Signpost', () {
const goldenPath = '../golden/signpost/';
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final signpost = Signpost(); final signpost = Signpost();
await game.ready();
await game.ensureAdd(signpost); await game.ensureAdd(signpost);
expect(game.contains(signpost), isTrue); expect(game.contains(signpost), isTrue);
}, },
); );
@ -39,8 +43,8 @@ void main() {
await tester.pump(); await tester.pump();
expect( expect(
signpost.firstChild<SpriteGroupComponent>()!.current, signpost.bloc.state,
SignpostSpriteState.inactive, equals(SignpostState.inactive),
); );
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
@ -48,7 +52,7 @@ void main() {
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/inactive.png'), matchesGoldenFile('${goldenPath}inactive.png'),
); );
}, },
); );
@ -59,12 +63,12 @@ void main() {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final signpost = Signpost(); final signpost = Signpost();
await game.ensureAdd(signpost); await game.ensureAdd(signpost);
signpost.progress(); signpost.bloc.onProgressed();
await tester.pump(); await tester.pump();
expect( expect(
signpost.firstChild<SpriteGroupComponent>()!.current, signpost.bloc.state,
SignpostSpriteState.active1, equals(SignpostState.active1),
); );
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
@ -72,7 +76,7 @@ void main() {
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/active1.png'), matchesGoldenFile('${goldenPath}active1.png'),
); );
}, },
); );
@ -83,14 +87,14 @@ void main() {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final signpost = Signpost(); final signpost = Signpost();
await game.ensureAdd(signpost); await game.ensureAdd(signpost);
signpost signpost.bloc
..progress() ..onProgressed()
..progress(); ..onProgressed();
await tester.pump(); await tester.pump();
expect( expect(
signpost.firstChild<SpriteGroupComponent>()!.current, signpost.bloc.state,
SignpostSpriteState.active2, equals(SignpostState.active2),
); );
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
@ -98,7 +102,7 @@ void main() {
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/active2.png'), matchesGoldenFile('${goldenPath}active2.png'),
); );
}, },
); );
@ -109,15 +113,16 @@ void main() {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final signpost = Signpost(); final signpost = Signpost();
await game.ensureAdd(signpost); await game.ensureAdd(signpost);
signpost
..progress() signpost.bloc
..progress() ..onProgressed()
..progress(); ..onProgressed()
..onProgressed();
await tester.pump(); await tester.pump();
expect( expect(
signpost.firstChild<SpriteGroupComponent>()!.current, signpost.bloc.state,
SignpostSpriteState.active3, equals(SignpostState.active3),
); );
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
@ -125,33 +130,12 @@ void main() {
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/active3.png'), matchesGoldenFile('${goldenPath}active3.png'),
); );
}, },
); );
}); });
flameTester.test(
'progress correctly cycles through all sprites',
(game) async {
final signpost = Signpost();
await game.ready();
await game.ensureAdd(signpost);
final spriteComponent = signpost.firstChild<SpriteGroupComponent>()!;
expect(spriteComponent.current, SignpostSpriteState.inactive);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.active1);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.active2);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.active3);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.inactive);
},
);
flameTester.test('adds new children', (game) async { flameTester.test('adds new children', (game) async {
final component = Component(); final component = Component();
final signpost = Signpost( final signpost = Signpost(
@ -160,5 +144,22 @@ void main() {
await game.ensureAdd(signpost); await game.ensureAdd(signpost);
expect(signpost.children, contains(component)); expect(signpost.children, contains(component));
}); });
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSignpostCubit();
whenListen(
bloc,
const Stream<SignpostCubit>.empty(),
initialState: SignpostState.inactive,
);
when(bloc.close).thenAnswer((_) async {});
final component = Signpost.test(bloc: bloc);
await game.ensureAdd(component);
game.remove(component);
await game.ready();
verify(bloc.close).called(1);
});
}); });
} }

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -10,6 +11,10 @@ import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavi
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class MockContact extends Mock implements Contact {}
class MockRampSensorCubit extends Mock implements RampSensorCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [

@ -8,6 +8,8 @@ import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class MockRampSensorCubit extends Mock implements RampSensorCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/sparky_bumper/behaviors/behavi
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -27,7 +34,7 @@ void main() {
'beginContact emits onBallContacted when contacts with a ball', 'beginContact emits onBallContacted when contacts with a ball',
(game) async { (game) async {
final behavior = SparkyBumperBallContactBehavior(); final behavior = SparkyBumperBallContactBehavior();
final bloc = MockSparkyBumperCubit(); final bloc = _MockSparkyBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<SparkyBumperState>.empty(), const Stream<SparkyBumperState>.empty(),
@ -38,7 +45,7 @@ void main() {
await sparkyBumper.add(behavior); await sparkyBumper.add(behavior);
await game.ensureAdd(sparkyBumper); await game.ensureAdd(sparkyBumper);
behavior.beginContact(MockBall(), MockContact()); behavior.beginContact(_MockBall(), _MockContact());
verify(sparkyBumper.bloc.onBallContacted).called(1); verify(sparkyBumper.bloc.onBallContacted).called(1);
}, },

@ -9,6 +9,8 @@ import 'package:pinball_components/src/components/sparky_bumper/behaviors/behavi
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -20,7 +22,7 @@ void main() {
'calls onBlinked after 0.05 seconds when dimmed', 'calls onBlinked after 0.05 seconds when dimmed',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = SparkyBumperBlinkingBehavior(); final behavior = SparkyBumperBlinkingBehavior();
final bloc = MockSparkyBumperCubit(); final bloc = _MockSparkyBumperCubit();
final streamController = StreamController<SparkyBumperState>(); final streamController = StreamController<SparkyBumperState>();
whenListen( whenListen(
bloc, bloc,

@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/sparky_bumper/behaviors/behavi
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -46,7 +48,7 @@ void main() {
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = MockSparkyBumperCubit(); final bloc = _MockSparkyBumperCubit();
whenListen( whenListen(
bloc, bloc,
const Stream<SparkyBumperState>.empty(), const Stream<SparkyBumperState>.empty(),

@ -1,7 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
class MockForge2DGame extends Mock implements Forge2DGame {}
class MockComponent extends Mock implements Component {}

@ -6,13 +6,13 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
abstract class _KeyCallStub { abstract class _KeyCall {
bool onCall(); bool onCall();
} }
class KeyCallStub extends Mock implements _KeyCallStub {} class _MockKeyCall extends Mock implements _KeyCall {}
class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString(); return super.toString();
@ -20,7 +20,7 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
} }
RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) {
final event = MockRawKeyUpEvent(); final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(key); when(() => event.logicalKey).thenReturn(key);
return event; return event;
} }
@ -28,7 +28,7 @@ RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) {
void main() { void main() {
group('KeyboardInputController', () { group('KeyboardInputController', () {
test('calls registered handlers', () { test('calls registered handlers', () {
final stub = KeyCallStub(); final stub = _MockKeyCall();
when(stub.onCall).thenReturn(true); when(stub.onCall).thenReturn(true);
final input = KeyboardInputController( final input = KeyboardInputController(
@ -44,7 +44,7 @@ void main() {
test( test(
'returns false the handler return value', 'returns false the handler return value',
() { () {
final stub = KeyCallStub(); final stub = _MockKeyCall();
when(stub.onCall).thenReturn(false); when(stub.onCall).thenReturn(false);
final input = KeyboardInputController( final input = KeyboardInputController(
@ -63,7 +63,7 @@ void main() {
test( test(
'returns true (allowing event to bubble) when no handler is registered', 'returns true (allowing event to bubble) when no handler is registered',
() { () {
final stub = KeyCallStub(); final stub = _MockKeyCall();
when(stub.onCall).thenReturn(true); when(stub.onCall).thenReturn(true);
final input = KeyboardInputController(); final input = KeyboardInputController();

@ -3,12 +3,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class MockSpriteAnimationController extends Mock class _MockSpriteAnimationController extends Mock
implements SpriteAnimationController {} implements SpriteAnimationController {}
class MockSpriteAnimation extends Mock implements SpriteAnimation {} class _MockSpriteAnimation extends Mock implements SpriteAnimation {}
class MockSprite extends Mock implements Sprite {} class _MockSprite extends Mock implements Sprite {}
// TODO(arturplaczek): Remove when this PR will be merged. // TODO(arturplaczek): Remove when this PR will be merged.
// https://github.com/flame-engine/flame/pull/1552 // https://github.com/flame-engine/flame/pull/1552
@ -20,9 +20,9 @@ void main() {
late Sprite sprite; late Sprite sprite;
setUp(() { setUp(() {
controller = MockSpriteAnimationController(); controller = _MockSpriteAnimationController();
animation = MockSpriteAnimation(); animation = _MockSpriteAnimation();
sprite = MockSprite(); sprite = _MockSprite();
when(() => controller.animation).thenAnswer((_) => animation); when(() => controller.animation).thenAnswer((_) => animation);

@ -21,26 +21,29 @@ class PinballButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DecoratedBox( return Material(
decoration: BoxDecoration( color: PinballColors.transparent,
image: DecorationImage( child: DecoratedBox(
image: AssetImage(Assets.images.button.pinballButton.keyName), decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(Assets.images.button.pinballButton.keyName),
),
), ),
), child: Center(
child: Center( child: InkWell(
child: InkWell( onTap: onTap,
onTap: onTap, child: Padding(
child: Padding( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 32,
horizontal: 32, vertical: 16,
vertical: 16, ),
), child: Text(
child: Text( text,
text, style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .headline3!
.headline3! .copyWith(color: PinballColors.white),
.copyWith(color: PinballColors.white), ),
), ),
), ),
), ),

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save