Merge branch 'main' into fix/ramp-opening

fix/ramp-opening
RuiAlonso 3 years ago
commit 0e33b847ff

@ -6,4 +6,5 @@ export 'camera_focusing_behavior.dart';
export 'character_selection_behavior.dart'; export 'character_selection_behavior.dart';
export 'cow_bumper_noise_behavior.dart'; export 'cow_bumper_noise_behavior.dart';
export 'kicker_noise_behavior.dart'; export 'kicker_noise_behavior.dart';
export 'rollover_noise_behavior.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';

@ -0,0 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class RolloverNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballAudioPlayer>().play(PinballAudio.rollover);
}
}

@ -19,6 +19,7 @@ class GoogleWordBonusBehavior extends Component {
.add(const BonusActivated(GameBonus.googleWord)); .add(const BonusActivated(GameBonus.googleWord));
readBloc<GoogleWordCubit, GoogleWordState>().onBonusAwarded(); readBloc<GoogleWordCubit, GoogleWordState>().onBonusAwarded();
add(BonusBallSpawningBehavior()); add(BonusBallSpawningBehavior());
add(GoogleWordAnimatingBehavior());
}, },
), ),
); );

@ -22,12 +22,14 @@ class GoogleGallery extends Component with ZIndex {
side: BoardSide.right, side: BoardSide.right,
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
RolloverNoiseBehavior(),
], ],
), ),
GoogleRollover( GoogleRollover(
side: BoardSide.left, side: BoardSide.left,
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
RolloverNoiseBehavior(),
], ],
), ),
GoogleWord(position: Vector2(-4.45, 1.8)), GoogleWord(position: Vector2(-4.45, 1.8)),

@ -125,6 +125,7 @@ class PinballGame extends PinballForge2DGame
SkillShot( SkillShot(
children: [ children: [
ScoringContactBehavior(points: Points.oneMillion), ScoringContactBehavior(points: Points.oneMillion),
RolloverNoiseBehavior(),
], ],
), ),
AndroidAcres(), AndroidAcres(),

@ -132,10 +132,15 @@ class _Character extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Opacity( child: Opacity(
opacity: isSelected ? 1 : 0.3, opacity: isSelected ? 1 : 0.4,
child: TextButton( child: TextButton(
onPressed: () => onPressed: () =>
context.read<CharacterThemeCubit>().characterSelected(character), context.read<CharacterThemeCubit>().characterSelected(character),
style: ButtonStyle(
overlayColor: MaterialStateProperty.all(
PinballColors.transparent,
),
),
child: character.icon.image(fit: BoxFit.contain), child: character.icon.image(fit: BoxFit.contain),
), ),
), ),

@ -26,6 +26,7 @@ class $AssetsSfxGen {
String get kickerA => 'assets/sfx/kicker_a.mp3'; String get kickerA => 'assets/sfx/kicker_a.mp3';
String get kickerB => 'assets/sfx/kicker_b.mp3'; String get kickerB => 'assets/sfx/kicker_b.mp3';
String get launcher => 'assets/sfx/launcher.mp3'; String get launcher => 'assets/sfx/launcher.mp3';
String get rollover => 'assets/sfx/rollover.mp3';
String get sparky => 'assets/sfx/sparky.mp3'; String get sparky => 'assets/sfx/sparky.mp3';
} }

@ -33,6 +33,9 @@ enum PinballAudio {
/// Kicker. /// Kicker.
kicker, kicker,
/// Rollover.
rollover,
/// Sparky. /// Sparky.
sparky, sparky,
@ -56,7 +59,7 @@ typedef CreateAudioPool = Future<AudioPool> Function(
}); });
/// Defines the contract for playing a single audio. /// Defines the contract for playing a single audio.
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String, {double volume});
/// Defines the contract for looping a single audio. /// Defines the contract for looping a single audio.
typedef LoopSingleAudio = Future<void> Function(String, {double volume}); typedef LoopSingleAudio = Future<void> Function(String, {double volume});
@ -81,18 +84,20 @@ class _SimplePlayAudio extends _Audio {
required this.preCacheSingleAudio, required this.preCacheSingleAudio,
required this.playSingleAudio, required this.playSingleAudio,
required this.path, required this.path,
this.volume,
}); });
final PreCacheSingleAudio preCacheSingleAudio; final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio; final PlaySingleAudio playSingleAudio;
final String path; final String path;
final double? volume;
@override @override
Future<void> load() => preCacheSingleAudio(prefixFile(path)); Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override @override
void play() { void play() {
playSingleAudio(prefixFile(path)); playSingleAudio(prefixFile(path), volume: volume ?? 1);
} }
} }
@ -266,6 +271,12 @@ class PinballAudioPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.launcher, path: Assets.sfx.launcher,
), ),
PinballAudio.rollover: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.rollover,
volume: 0.3,
),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,

@ -29,15 +29,15 @@ class _MockConfigureAudioCache extends Mock {
} }
class _MockPlaySingleAudio extends Mock { class _MockPlaySingleAudio extends Mock {
Future<void> onCall(String url); Future<void> onCall(String path, {double volume});
} }
class _MockLoopSingleAudio extends Mock { class _MockLoopSingleAudio extends Mock {
Future<void> onCall(String url, {double volume}); Future<void> onCall(String path, {double volume});
} }
abstract class _PreCacheSingleAudio { abstract class _PreCacheSingleAudio {
Future<void> onCall(String url); Future<void> onCall(String path);
} }
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
@ -74,7 +74,8 @@ void main() {
when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {});
playSingleAudio = _MockPlaySingleAudio(); playSingleAudio = _MockPlaySingleAudio();
when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume')))
.thenAnswer((_) async {});
loopSingleAudio = _MockLoopSingleAudio(); loopSingleAudio = _MockLoopSingleAudio();
when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume'))) when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume')))
@ -195,6 +196,10 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/rollover.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'), .onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'),
@ -346,8 +351,10 @@ void main() {
audioPlayer.play(PinballAudio.google); audioPlayer.play(PinballAudio.google);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.google}'), 'packages/pinball_audio/${Assets.sfx.google}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
@ -358,8 +365,10 @@ void main() {
audioPlayer.play(PinballAudio.sparky); audioPlayer.play(PinballAudio.sparky);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.sparky}'), 'packages/pinball_audio/${Assets.sfx.sparky}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
@ -370,8 +379,10 @@ void main() {
audioPlayer.play(PinballAudio.dino); audioPlayer.play(PinballAudio.dino);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.dino}'), 'packages/pinball_audio/${Assets.sfx.dino}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
@ -382,8 +393,10 @@ void main() {
audioPlayer.play(PinballAudio.android); audioPlayer.play(PinballAudio.android);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.android}'), 'packages/pinball_audio/${Assets.sfx.android}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
@ -394,8 +407,10 @@ void main() {
audioPlayer.play(PinballAudio.dash); audioPlayer.play(PinballAudio.dash);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.dash}'), 'packages/pinball_audio/${Assets.sfx.dash}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
@ -406,8 +421,24 @@ void main() {
audioPlayer.play(PinballAudio.launcher); audioPlayer.play(PinballAudio.launcher);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.launcher}'), 'packages/pinball_audio/${Assets.sfx.launcher}',
volume: any(named: 'volume'),
),
).called(1);
});
});
group('rollover', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.rollover);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.rollover}',
volume: .3,
),
).called(1); ).called(1);
}); });
}); });
@ -420,6 +451,7 @@ void main() {
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}', 'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}',
volume: any(named: 'volume'),
), ),
).called(1); ).called(1);
}); });
@ -433,6 +465,7 @@ void main() {
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}', 'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}',
volume: any(named: 'volume'),
), ),
).called(1); ).called(1);
}); });

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart';
class GoogleWordAnimatingBehavior extends TimerComponent
with FlameBlocReader<GoogleWordCubit, GoogleWordState> {
GoogleWordAnimatingBehavior() : super(period: 0.35, repeat: true);
final _maxBlinks = 7;
int _blinks = 0;
@override
void onTick() {
super.onTick();
if (_blinks != _maxBlinks * 2) {
bloc.switched();
_blinks++;
} else {
timer.stop();
bloc.onAnimationFinished();
shouldRemove = true;
}
}
}

@ -23,7 +23,52 @@ class GoogleWordCubit extends Cubit<GoogleWordState> {
} }
} }
void switched() {
switch (state.letterSpriteStates[0]!) {
case GoogleLetterSpriteState.lit:
emit(
GoogleWordState(
letterSpriteStates: {
for (int i = 0; i < _lettersInGoogle; i++)
if (i.isEven)
i: GoogleLetterSpriteState.dimmed
else
i: GoogleLetterSpriteState.lit
},
),
);
break;
case GoogleLetterSpriteState.dimmed:
emit(
GoogleWordState(
letterSpriteStates: {
for (int i = 0; i < _lettersInGoogle; i++)
if (i.isEven)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
},
),
);
break;
}
}
void onBonusAwarded() { void onBonusAwarded() {
emit(
GoogleWordState(
letterSpriteStates: {
for (int i = 0; i < _lettersInGoogle; i++)
if (i.isEven)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
},
),
);
}
void onAnimationFinished() {
emit(GoogleWordState.initial()); emit(GoogleWordState.initial());
_lastLitLetter = 0; _lastLitLetter = 0;
} }

@ -1,6 +1,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart';
export 'cubit/google_word_cubit.dart'; export 'cubit/google_word_cubit.dart';
/// {@template google_word} /// {@template google_word}

@ -0,0 +1,64 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.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';
class _TestGame extends Forge2DGame {
Future<void> pump(
GoogleWordAnimatingBehavior child, {
required GoogleWordCubit bloc,
}) async {
await ensureAdd(
FlameBlocProvider<GoogleWordCubit, GoogleWordState>.value(
value: bloc,
children: [child],
),
);
}
}
class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('GoogleWordAnimatingBehavior', () {
flameTester.testGameWidget(
'calls switched after timer period reached',
setUp: (game, tester) async {
final behavior = GoogleWordAnimatingBehavior();
final bloc = _MockGoogleWordCubit();
await game.pump(behavior, bloc: bloc);
game.update(behavior.timer.limit);
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls onAnimationFinished and removes itself '
'after all blinks complete',
setUp: (game, tester) async {
final behavior = GoogleWordAnimatingBehavior();
final bloc = _MockGoogleWordCubit();
await game.pump(behavior, bloc: bloc);
for (var i = 0; i <= 14; i++) {
game.update(behavior.timer.limit);
}
await game.ready();
verify(bloc.onAnimationFinished).called(1);
expect(
game.descendants().whereType<GoogleWordAnimatingBehavior>().isEmpty,
isTrue,
);
},
);
});
}

@ -6,6 +6,21 @@ void main() {
group( group(
'GoogleWordCubit', 'GoogleWordCubit',
() { () {
final litEvens = {
for (int i = 0; i < 6; i++)
if (i.isEven)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
};
final litOdds = {
for (int i = 0; i < 6; i++)
if (i.isOdd)
i: GoogleLetterSpriteState.lit
else
i: GoogleLetterSpriteState.dimmed
};
blocTest<GoogleWordCubit, GoogleWordState>( blocTest<GoogleWordCubit, GoogleWordState>(
'onRolloverContacted emits first letter lit', 'onRolloverContacted emits first letter lit',
build: GoogleWordCubit.new, build: GoogleWordCubit.new,
@ -25,9 +40,31 @@ void main() {
); );
blocTest<GoogleWordCubit, GoogleWordState>( blocTest<GoogleWordCubit, GoogleWordState>(
'onBonusAwarded emits initial state', 'switched emits all even letters lit when first letter is dimmed',
build: GoogleWordCubit.new,
act: (bloc) => bloc.switched(),
expect: () => [GoogleWordState(letterSpriteStates: litEvens)],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'switched emits all odd letters lit when first letter is lit',
build: GoogleWordCubit.new,
seed: () => GoogleWordState(letterSpriteStates: litEvens),
act: (bloc) => bloc.switched(),
expect: () => [GoogleWordState(letterSpriteStates: litOdds)],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'onBonusAwarded emits all even letters lit',
build: GoogleWordCubit.new, build: GoogleWordCubit.new,
act: (bloc) => bloc.onBonusAwarded(), act: (bloc) => bloc.onBonusAwarded(),
expect: () => [GoogleWordState(letterSpriteStates: litEvens)],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'onAnimationFinished emits initial state',
build: GoogleWordCubit.new,
act: (bloc) => bloc.onAnimationFinished(),
expect: () => [GoogleWordState.initial()], expect: () => [GoogleWordState.initial()],
); );
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

After

Width:  |  Height:  |  Size: 366 KiB

@ -30,7 +30,7 @@ class PinballButton extends StatelessWidget {
), ),
), ),
child: Center( child: Center(
child: InkWell( child: GestureDetector(
onTap: onTap, onTap: onTap,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(

@ -53,7 +53,7 @@ class PinballDpadButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: PinballColors.transparent, color: PinballColors.transparent,
child: InkWell( child: GestureDetector(
onTap: onTap, onTap: onTap,
child: Image.asset( child: Image.asset(
direction.toAsset(), direction.toAsset(),

@ -0,0 +1,58 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
_TestBodyComponent child, {
required PinballAudioPlayer audioPlayer,
}) {
return ensureAdd(
FlameProvider<PinballAudioPlayer>.value(
audioPlayer,
children: [child],
),
);
}
}
class _TestBodyComponent extends BodyComponent {
@override
Body createBody() => world.createBody(BodyDef());
}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RolloverNoiseBehavior', () {
late PinballAudioPlayer audioPlayer;
final flameTester = FlameTester(_TestGame.new);
setUp(() {
audioPlayer = _MockPinballAudioPlayer();
});
flameTester.testGameWidget(
'plays rollover sound on contact',
setUp: (game, _) async {
final behavior = RolloverNoiseBehavior();
final parent = _TestBodyComponent();
await game.pump(parent, audioPlayer: audioPlayer);
await parent.ensureAdd(behavior);
behavior.beginContact(Object(), _MockContact());
},
verify: (_, __) async {
verify(() => audioPlayer.play(PinballAudio.rollover)).called(1);
},
);
});
}

@ -119,8 +119,8 @@ void main() {
); );
flameTester.testGameWidget( flameTester.testGameWidget(
'adds BonusBallSpawningBehavior to the game when all letters ' 'adds BonusBallSpawningBehavior and GoogleWordAnimatingBehavior '
'in google word are activated', 'to the game when all letters in google word are activated',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = GoogleWordBonusBehavior(); final behavior = GoogleWordBonusBehavior();
final parent = GoogleGallery.test(); final parent = GoogleGallery.test();
@ -161,6 +161,10 @@ void main() {
game.descendants().whereType<BonusBallSpawningBehavior>().length, game.descendants().whereType<BonusBallSpawningBehavior>().length,
equals(1), equals(1),
); );
expect(
game.descendants().whereType<GoogleWordAnimatingBehavior>().length,
equals(1),
);
}, },
); );
}); });

@ -97,6 +97,20 @@ void main() {
}, },
); );
flameTester.test(
'RolloverNoiseBehavior to GoogleRollovers',
(game) async {
await game.pump(GoogleGallery());
game.descendants().whereType<GoogleRollover>().forEach(
(rollover) => expect(
rollover.firstChild<RolloverNoiseBehavior>(),
isNotNull,
),
);
},
);
flameTester.test('a GoogleWordBonusBehavior', (game) async { flameTester.test('a GoogleWordBonusBehavior', (game) async {
final component = GoogleGallery(); final component = GoogleGallery();
await game.pump(component); await game.pump(component);

Loading…
Cancel
Save