Merge branch 'main' into feat/spaceship-ramp-shot-logic

pull/296/head
RuiAlonso 3 years ago
commit 2d5ea91923

@ -1,8 +1 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
export 'view/app.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs
import 'package:authentication_repository/authentication_repository.dart';

@ -0,0 +1,2 @@
export 'cubit/assets_manager_cubit.dart';
export 'views/views.dart';

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template assets_loading_page}
/// Widget used to indicate the loading progress of the different assets used
/// in the game
/// {@endtemplate}
class AssetsLoadingPage extends StatelessWidget {
/// {@macro assets_loading_page}
const AssetsLoadingPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.ioPinball,
style: headline1!.copyWith(fontSize: 80),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
),
),
],
),
);
}
}

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

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs
import 'dart:async';

@ -34,17 +34,17 @@ class AndroidAcres extends Component {
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(-20.5, -13.8),
AndroidSpaceshipBonusBehavior(),

@ -18,22 +18,22 @@ class FlutterForest extends Component with ZIndex {
children: [
Signpost(
children: [
ScoringBehavior(points: Points.fiveThousand),
BumperScoringBehavior(points: Points.fiveThousand),
],
)..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main(
children: [
ScoringBehavior(points: Points.twoHundredThousand),
BumperScoringBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66),

@ -23,7 +23,6 @@ class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points.value));
gameRef.audio.score();
gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreComponent(
points: _points,
@ -32,3 +31,23 @@ class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
);
}
}
/// {@template bumper_scoring_behavior}
/// A specific [ScoringBehavior] used for Bumpers.
/// In addition to its parent logic, also plays the
/// SFX for bumpers
/// {@endtemplate}
class BumperScoringBehavior extends ScoringBehavior {
/// {@macro bumper_scoring_behavior}
BumperScoringBehavior({
required Points points,
}) : super(points: points);
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.audio.bumper();
}
}

@ -16,17 +16,17 @@ class SparkyScorch extends Component {
children: [
SparkyBumper.a(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c(
children: [
ScoringBehavior(points: Points.twentyThousand),
BumperScoringBehavior(points: Points.twentyThousand),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9),

@ -1,4 +1,3 @@
export 'assets_manager/cubit/assets_manager_cubit.dart';
export 'bloc/game_bloc.dart';
export 'components/components.dart';
export 'game_assets.dart';

@ -5,7 +5,6 @@ import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
@ -14,7 +13,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends Forge2DGame
class PinballGame extends PinballForge2DGame
with
FlameBloc,
HasKeyboardHandlerComponents,

@ -4,10 +4,12 @@ import 'package:flame/game.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
class PinballGamePage extends StatelessWidget {
const PinballGamePage({
@ -71,32 +73,13 @@ class PinballGameView extends StatelessWidget {
final isLoading = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress != 1,
);
return Scaffold(
backgroundColor: Colors.blue,
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading
? const _PinballGameLoadingView()
? const AssetsLoadingPage()
: PinballGameLoadedView(game: game),
);
}
}
class _PinballGameLoadingView extends StatelessWidget {
const _PinballGameLoadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final loadingProgress = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress,
);
return Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: LinearProgressIndicator(
color: Colors.white,
value: loadingProgress,
),
),
);
}

@ -3,8 +3,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart';
@ -50,10 +52,13 @@ extension on Control {
}
Future<void> showHowToPlayDialog(BuildContext context) {
final audio = context.read<PinballAudio>();
return showDialog<void>(
context: context,
builder: (_) => HowToPlayDialog(),
);
).then((_) {
audio.ioPinballVoiceOver();
});
}
class HowToPlayDialog extends StatefulWidget {

@ -20,7 +20,7 @@
"@flipperControls": {
"description": "Text displayed on the how to play dialog with the flipper controls"
},
"tapAndHoldRocket": "Tap & Hold Rocket",
"tapAndHoldRocket": "Tap Rocket",
"@tapAndHoldRocket": {
"description": "Text displayed on the how to launch on mobile"
},
@ -123,5 +123,13 @@
"footerGoogleIOText": "Google I/O",
"@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O"
},
"loading": "Loading",
"@loading": {
"description": "Text shown to indicate loading times"
},
"ioPinball": "I/O Pinball",
"@ioPinball": {
"description": "I/O Pinball - Name of the game"
}
}

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs
import 'package:flutter/widgets.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';

@ -14,8 +14,10 @@ class $AssetsMusicGen {
class $AssetsSfxGen {
const $AssetsSfxGen();
String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3';
String get google => 'assets/sfx/google.mp3';
String get plim => 'assets/sfx/plim.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
}
class Assets {

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart';
@ -40,6 +42,7 @@ class PinballAudio {
LoopSingleAudio? loopSingleAudio,
PreCacheSingleAudio? preCacheSingleAudio,
ConfigureAudioCache? configureAudioCache,
Random? seed,
}) : _createAudioPool = createAudioPool ?? AudioPool.create,
_playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play,
_loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop,
@ -48,7 +51,8 @@ class PinballAudio {
_configureAudioCache = configureAudioCache ??
((AudioCache a) {
a.prefix = '';
});
}),
_seed = seed ?? Random();
final CreateAudioPool _createAudioPool;
@ -60,27 +64,38 @@ class PinballAudio {
final ConfigureAudioCache _configureAudioCache;
late AudioPool _scorePool;
final Random _seed;
late AudioPool _bumperAPool;
late AudioPool _bumperBPool;
/// Loads the sounds effects into the memory
Future<void> load() async {
_configureAudioCache(FlameAudio.audioCache);
_scorePool = await _createAudioPool(
_prefixFile(Assets.sfx.plim),
_bumperAPool = await _createAudioPool(
_prefixFile(Assets.sfx.bumperA),
maxPlayers: 4,
prefix: '',
);
_bumperBPool = await _createAudioPool(
_prefixFile(Assets.sfx.bumperB),
maxPlayers: 4,
prefix: '',
);
await Future.wait([
_preCacheSingleAudio(_prefixFile(Assets.sfx.google)),
_preCacheSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)),
_preCacheSingleAudio(_prefixFile(Assets.music.background)),
]);
}
/// Plays the basic score sound
void score() {
_scorePool.start();
/// Plays a random bumper sfx.
void bumper() {
(_seed.nextBool() ? _bumperAPool : _bumperBPool).start(volume: 0.6);
}
/// Plays the google word bonus
@ -88,6 +103,11 @@ class PinballAudio {
_playSingleAudio(_prefixFile(Assets.sfx.google));
}
/// Plays the I/O Pinball voice over audio.
void ioPinballVoiceOver() {
_playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver));
}
/// Plays the background music
void backgroundMusic() {
_loopSingleAudio(_prefixFile(Assets.music.background));

@ -1,4 +1,6 @@
// ignore_for_file: prefer_const_constructors, one_member_abstracts
import 'dart:math';
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart';
@ -39,6 +41,8 @@ abstract class _PreCacheSingleAudio {
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
class _MockRandom extends Mock implements Random {}
void main() {
group('PinballAudio', () {
late _MockCreateAudioPool createAudioPool;
@ -46,6 +50,7 @@ void main() {
late _MockPlaySingleAudio playSingleAudio;
late _MockLoopSingleAudio loopSingleAudio;
late _PreCacheSingleAudio preCacheSingleAudio;
late Random seed;
late PinballAudio audio;
setUpAll(() {
@ -74,12 +79,15 @@ void main() {
preCacheSingleAudio = _MockPreCacheSingleAudio();
when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {});
seed = _MockRandom();
audio = PinballAudio(
configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall,
loopSingleAudio: loopSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall,
seed: seed,
);
});
@ -88,12 +96,20 @@ void main() {
});
group('load', () {
test('creates the score pool', () async {
test('creates the bumpers pools', () async {
await audio.load();
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.plim}',
'packages/pinball_audio/${Assets.sfx.bumperA}',
maxPlayers: 4,
prefix: '',
),
).called(1);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
maxPlayers: 4,
prefix: '',
),
@ -125,6 +141,11 @@ void main() {
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'),
).called(1);
verify(
() => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -132,22 +153,52 @@ void main() {
});
});
group('score', () {
test('plays the score sound pool', () async {
final audioPool = _MockAudioPool();
when(audioPool.start).thenAnswer((_) async => () {});
group('bumper', () {
late AudioPool bumperAPool;
late AudioPool bumperBPool;
setUp(() {
bumperAPool = _MockAudioPool();
when(() => bumperAPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
any(),
'packages/pinball_audio/${Assets.sfx.bumperA}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => bumperAPool);
bumperBPool = _MockAudioPool();
when(() => bumperBPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => audioPool);
).thenAnswer((_) async => bumperBPool);
});
group('when seed is true', () {
test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true);
await audio.load();
audio.bumper();
verify(() => bumperAPool.start(volume: 0.6)).called(1);
});
});
group('when seed is false', () {
test('plays the bumper B sound pool', () async {
when(seed.nextBool).thenReturn(false);
await audio.load();
audio.score();
audio.bumper();
verify(audioPool.start).called(1);
verify(() => bumperBPool.start(volume: 0.6)).called(1);
});
});
});
@ -163,6 +214,19 @@ void main() {
});
});
group('ioPinballVoiceOver', () {
test('plays the correct file', () async {
await audio.load();
audio.ioPinballVoiceOver();
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}',
),
).called(1);
});
});
group('backgroundMusic', () {
test('plays the correct file', () async {
await audio.load();

@ -1,18 +1,18 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/ball_gravitating_behavior.dart';
import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate}
class Ball<T extends Forge2DGame> extends BodyComponent<T>
with Layered, InitialPosition, ZIndex {
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball}
Ball({
required this.baseColor,
@ -20,6 +20,8 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
renderBody: false,
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
BallScalingBehavior(),
BallGravitatingBehavior(),
],
) {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
@ -30,6 +32,15 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
layer = Layer.board;
}
/// Creates a [Ball] without any behaviors.
///
/// This can be used for testing [Ball]'s behaviors in isolation.
@visibleForTesting
Ball.test({required this.baseColor})
: super(
children: [_BallSpriteComponent()],
);
/// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13);
@ -76,48 +87,6 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
body.linearVelocity = impulse;
await add(_TurboChargeSpriteAnimationComponent());
}
@override
void update(double dt) {
super.update(dt);
_rescaleSize();
_setPositionalGravity();
}
void _rescaleSize() {
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
final standardizedYPosition = body.position.y + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
// TODO(alestiago): Revisit and see if there's a better way to do this.
final spriteComponent = firstChild<_BallSpriteComponent>();
spriteComponent?.scale = Vector2.all(scaleFactor);
}
void _setPositionalGravity() {
final defaultGravity = gameRef.world.gravity.y;
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
const maxXGravityPercentage =
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
final xDeviationFromCenter = body.position.x;
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
maxXGravityPercentage) *
defaultGravity;
final positionalYForce = math.sqrt(
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
);
body.gravityOverride = Vector2(positionalXForce, positionalYForce);
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {

@ -0,0 +1,35 @@
import 'dart:math' as math;
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';
/// Scales the ball's gravity according to its position on the board.
class BallGravitatingBehavior extends Component
with ParentIsA<Ball>, HasGameRef<Forge2DGame> {
@override
void update(double dt) {
super.update(dt);
final defaultGravity = gameRef.world.gravity.y;
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
const maxXGravityPercentage =
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
final xDeviationFromCenter = parent.body.position.x;
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
maxXGravityPercentage) *
defaultGravity;
final positionalYForce = math.sqrt(
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
);
final gravityOverride = parent.body.gravityOverride;
if (gravityOverride != null) {
gravityOverride.setValues(positionalXForce, positionalYForce);
} else {
parent.body.gravityOverride = Vector2(positionalXForce, positionalYForce);
}
}
}

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Scales the ball's body and sprite according to its position on the board.
class BallScalingBehavior extends Component with ParentIsA<Ball> {
@override
void update(double dt) {
super.update(dt);
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
final standardizedYPosition = parent.body.position.y + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;
parent.firstChild<SpriteComponent>()!.scale.setValues(
scaleFactor,
scaleFactor,
);
}
}

@ -0,0 +1,2 @@
export 'ball_gravitating_behavior.dart';
export 'ball_scaling_behavior.dart';

@ -2,7 +2,7 @@ export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart';
export 'ball.dart';
export 'ball/ball.dart';
export 'baseboard.dart';
export 'board_background_sprite_component.dart';
export 'board_dimensions.dart';

@ -5,7 +5,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
///
/// Note: If the [initialPosition] is set after the [BodyComponent] has been
/// loaded it will have no effect; defaulting to [Vector2.zero].
mixin InitialPosition<T extends Forge2DGame> on BodyComponent<T> {
mixin InitialPosition on BodyComponent {
final Vector2 _initialPosition = Vector2.zero();
set initialPosition(Vector2 value) {

@ -9,7 +9,7 @@ import 'package:flutter/material.dart';
/// ignoring others. This compatibility depends on bit masking operation
/// between layers. For more information read: https://en.wikipedia.org/wiki/Mask_(computing).
/// {@endtemplate}
mixin Layered<T extends Forge2DGame> on BodyComponent<T> {
mixin Layered on BodyComponent {
Layer _layer = Layer.all;
/// {@macro layered}

@ -1,9 +1,3 @@
// Copyright (c) 2022, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/stories/stories.dart';

@ -6,18 +6,29 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart';
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Ball', () {
const baseColor = Color(0xFFFFFFFF);
test(
'can be instantiated',
() {
expect(Ball(baseColor: baseColor), isA<Ball>());
expect(Ball.test(baseColor: baseColor), isA<Ball>());
},
);
flameTester.test(
'loads correctly',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ready();
await game.ensureAdd(ball);
@ -25,11 +36,31 @@ void main() {
},
);
group('adds', () {
flameTester.test('a BallScalingBehavior', (game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<BallScalingBehavior>().length,
equals(1),
);
});
flameTester.test('a BallGravitatingBehavior', (game) async {
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<BallGravitatingBehavior>().length,
equals(1),
);
});
});
group('body', () {
flameTester.test(
'is dynamic',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
expect(ball.body.bodyType, equals(BodyType.dynamic));
@ -38,7 +69,7 @@ void main() {
group('can be moved', () {
flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
game.update(1);
@ -46,7 +77,7 @@ void main() {
});
flameTester.test('by applying velocity', (game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
ball.body.gravityScale = Vector2.zero();
@ -61,7 +92,7 @@ void main() {
flameTester.test(
'exists',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
expect(ball.body.fixtures[0], isA<Fixture>());
@ -71,7 +102,7 @@ void main() {
flameTester.test(
'is dense',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
@ -82,7 +113,7 @@ void main() {
flameTester.test(
'shape is circular',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
@ -94,7 +125,7 @@ void main() {
flameTester.test(
'has Layer.all as default filter maskBits',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ready();
await game.ensureAdd(ball);
await game.ready();
@ -108,7 +139,7 @@ void main() {
group('stop', () {
group("can't be moved", () {
flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
ball.stop();
@ -116,19 +147,6 @@ void main() {
expect(ball.body.position, equals(ball.initialPosition));
});
});
// TODO(allisonryan0002): delete or retest this if/when solution is added
// to prevent forces on a ball while stopped.
// flameTester.test('by applying velocity', (game) async {
// final ball = Ball(baseColor: Colors.blue);
// await game.ensureAdd(ball);
// ball.stop();
// ball.body.linearVelocity.setValues(10, 10);
// game.update(1);
// expect(ball.body.position, equals(ball.initialPosition));
// });
});
group('resume', () {
@ -136,7 +154,7 @@ void main() {
flameTester.test(
'by its weight when previously stopped',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
ball.stop();
ball.resume();
@ -149,7 +167,7 @@ void main() {
flameTester.test(
'by applying velocity when previously stopped',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
ball.stop();
ball.resume();
@ -165,7 +183,7 @@ void main() {
group('boost', () {
flameTester.test('applies an impulse to the ball', (game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
expect(ball.body.linearVelocity, equals(Vector2.zero()));
@ -176,7 +194,7 @@ void main() {
});
flameTester.test('adds TurboChargeSpriteAnimation', (game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
await ball.boost(Vector2.all(10));
@ -190,7 +208,7 @@ void main() {
flameTester.test('removes TurboChargeSpriteAnimation after it finishes',
(game) async {
final ball = Ball(baseColor: Colors.blue);
final ball = Ball(baseColor: baseColor);
await game.ensureAdd(ball);
await ball.boost(Vector2.all(10));

@ -0,0 +1,63 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
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_components/src/components/ball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.ball.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
group('BallGravitatingBehavior', () {
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () {
expect(
BallGravitatingBehavior(),
isA<BallGravitatingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor);
final behavior = BallGravitatingBehavior();
await ball.add(behavior);
await game.ensureAdd(ball);
expect(
ball.firstChild<BallGravitatingBehavior>(),
equals(behavior),
);
});
flameTester.test(
"overrides the body's horizontal gravity symmetrically",
(game) async {
final ball1 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(10, 0);
await ball1.add(BallGravitatingBehavior());
final ball2 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(-10, 0);
await ball2.add(BallGravitatingBehavior());
await game.ensureAddAll([ball1, ball2]);
game.update(1);
expect(
ball1.body.gravityOverride!.x,
equals(-ball2.body.gravityOverride!.x),
);
expect(
ball1.body.gravityOverride!.y,
equals(ball2.body.gravityOverride!.y),
);
},
);
});
}

@ -0,0 +1,85 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
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_components/src/components/ball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.ball.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
group('BallScalingBehavior', () {
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () {
expect(
BallScalingBehavior(),
isA<BallScalingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor);
final behavior = BallScalingBehavior();
await ball.add(behavior);
await game.ensureAdd(ball);
expect(
ball.firstChild<BallScalingBehavior>(),
equals(behavior),
);
});
flameTester.test('scales the shape radius', (game) async {
final ball1 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, 10);
await ball1.add(BallScalingBehavior());
final ball2 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, -10);
await ball2.add(BallScalingBehavior());
await game.ensureAddAll([ball1, ball2]);
game.update(1);
final shape1 = ball1.body.fixtures.first.shape;
final shape2 = ball2.body.fixtures.first.shape;
expect(
shape1.radius,
greaterThan(shape2.radius),
);
});
flameTester.test(
'scales the sprite',
(game) async {
final ball1 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, 10);
await ball1.add(BallScalingBehavior());
final ball2 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, -10);
await ball2.add(BallScalingBehavior());
await game.ensureAddAll([ball1, ball2]);
game.update(1);
final sprite1 = ball1.firstChild<SpriteComponent>()!;
final sprite2 = ball2.firstChild<SpriteComponent>()!;
expect(
sprite1.scale.x,
greaterThan(sprite2.scale.x),
);
expect(
sprite1.scale.y,
greaterThan(sprite2.scale.y),
);
},
);
});
}

@ -4,5 +4,6 @@ export 'src/component_controller.dart';
export 'src/contact_behavior.dart';
export 'src/keyboard_input_controller.dart';
export 'src/parent_is_a.dart';
export 'src/pinball_forge2d_game.dart';
export 'src/sprite_animation.dart';
export 'src/z_canvas_component.dart';

@ -0,0 +1,44 @@
import 'dart:math';
import 'package:flame/game.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_forge2d/world_contact_listener.dart';
// NOTE(wolfen): This should be removed when https://github.com/flame-engine/flame/pull/1597 is solved.
/// {@template pinball_forge2d_game}
/// A [Game] that uses the Forge2D physics engine.
/// {@endtemplate}
class PinballForge2DGame extends FlameGame implements Forge2DGame {
/// {@macro pinball_forge2d_game}
PinballForge2DGame({
required Vector2 gravity,
}) : world = World(gravity),
super(camera: Camera()) {
camera.zoom = Forge2DGame.defaultZoom;
world.setContactListener(WorldContactListener());
}
@override
final World world;
@override
void update(double dt) {
super.update(dt);
world.stepDt(min(dt, 1 / 60));
}
@override
Vector2 screenToFlameWorld(Vector2 position) {
throw UnimplementedError();
}
@override
Vector2 screenToWorld(Vector2 position) {
throw UnimplementedError();
}
@override
Vector2 worldToScreen(Vector2 position) {
throw UnimplementedError();
}
}

@ -0,0 +1,51 @@
// ignore_for_file: cascade_invocations
import 'package:flame/extensions.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_flame/pinball_flame.dart';
void main() {
final flameTester = FlameTester(
() => PinballForge2DGame(gravity: Vector2.zero()),
);
group('PinballForge2DGame', () {
test('can instantiate', () {
expect(
() => PinballForge2DGame(gravity: Vector2.zero()),
returnsNormally,
);
});
flameTester.test(
'screenToFlameWorld throws UnimpelementedError',
(game) async {
expect(
() => game.screenToFlameWorld(Vector2.zero()),
throwsUnimplementedError,
);
},
);
flameTester.test(
'screenToWorld throws UnimpelementedError',
(game) async {
expect(
() => game.screenToWorld(Vector2.zero()),
throwsUnimplementedError,
);
},
);
flameTester.test(
'worldToScreen throws UnimpelementedError',
(game) async {
expect(
() => game.worldToScreen(Vector2.zero()),
throwsUnimplementedError,
);
},
);
});
}

@ -8,4 +8,9 @@ abstract class PinballColors {
static const Color orange = Color(0xFFE5AB05);
static const Color blue = Color(0xFF4B94F6);
static const Color transparent = Color(0x00000000);
static const Color loadingDarkRed = Color(0xFFE33B2D);
static const Color loadingLightRed = Color(0xFFEC5E2B);
static const Color loadingDarkBlue = Color(0xFF4087F8);
static const Color loadingLightBlue = Color(0xFF6CCAE4);
static const Color crtBackground = Color(0xFF274E54);
}

@ -0,0 +1,61 @@
import 'dart:async';
import 'package:flutter/material.dart';
/// {@tempalte animated_ellipsis_text}
/// Every 500 milliseconds, it will add a new `.` at the end of the given
/// [text]. Once 3 `.` have been added (e.g. `Loading...`), it will reset to
/// zero ellipsis and start over again.
/// {@endtemplate}
class AnimatedEllipsisText extends StatefulWidget {
/// {@macro animated_ellipsis_text}
const AnimatedEllipsisText(
this.text, {
Key? key,
this.style,
}) : super(key: key);
/// The text that will be animated.
final String text;
/// Optional [TextStyle] of the given [text].
final TextStyle? style;
@override
State<StatefulWidget> createState() => _AnimatedEllipsisText();
}
class _AnimatedEllipsisText extends State<AnimatedEllipsisText>
with SingleTickerProviderStateMixin {
late final Timer timer;
var _numberOfEllipsis = 0;
@override
void initState() {
super.initState();
timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
setState(() {
_numberOfEllipsis++;
_numberOfEllipsis = _numberOfEllipsis % 4;
});
});
}
@override
void dispose() {
if (timer.isActive) timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(
'${widget.text}${_numberOfEllipsis.toEllipsis()}',
style: widget.style,
);
}
}
extension on int {
String toEllipsis() => '.' * this;
}

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template crt_background}
/// [BoxDecoration] that provides a CRT-like background efffect.
/// {@endtemplate}
class CrtBackground extends BoxDecoration {
/// {@macro crt_background}
const CrtBackground()
: super(
gradient: const LinearGradient(
begin: Alignment(1, 0.015),
stops: [0.0, 0.5, 0.5, 1],
colors: [
PinballColors.darkBlue,
PinballColors.darkBlue,
PinballColors.crtBackground,
PinballColors.crtBackground,
],
tileMode: TileMode.repeated,
),
);
}

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template pinball_loading_indicator}
/// Pixel-art loading indicator
/// {@endtemplate}
class PinballLoadingIndicator extends StatelessWidget {
/// {@macro pinball_loading_indicator}
const PinballLoadingIndicator({
Key? key,
required this.value,
}) : assert(
value >= 0.0 && value <= 1.0,
'Progress must be between 0 and 1',
),
super(key: key);
/// Progress value
final double value;
@override
Widget build(BuildContext context) {
return Column(
children: [
_InnerIndicator(value: value, widthFactor: 0.95),
_InnerIndicator(value: value, widthFactor: 0.98),
_InnerIndicator(value: value),
_InnerIndicator(value: value),
_InnerIndicator(value: value, widthFactor: 0.98),
_InnerIndicator(value: value, widthFactor: 0.95)
],
);
}
}
class _InnerIndicator extends StatelessWidget {
const _InnerIndicator({
Key? key,
required this.value,
this.widthFactor = 1.0,
}) : super(key: key);
final double value;
final double widthFactor;
@override
Widget build(BuildContext context) {
return FractionallySizedBox(
widthFactor: widthFactor,
child: Column(
children: [
LinearProgressIndicator(
backgroundColor: PinballColors.loadingDarkBlue,
color: PinballColors.loadingDarkRed,
value: value,
),
LinearProgressIndicator(
backgroundColor: PinballColors.loadingLightBlue,
color: PinballColors.loadingLightRed,
value: value,
),
],
),
);
}
}

@ -1 +1,4 @@
export 'animated_ellipsis_text.dart';
export 'crt_background.dart';
export 'pinball_button.dart';
export 'pinball_loading_indicator.dart';

@ -27,5 +27,25 @@ void main() {
test('transparent is 0x00000000', () {
expect(PinballColors.transparent, const Color(0x00000000));
});
test('loadingDarkRed is 0xFFE33B2D', () {
expect(PinballColors.loadingDarkRed, const Color(0xFFE33B2D));
});
test('loadingLightRed is 0xFFEC5E2B', () {
expect(PinballColors.loadingLightRed, const Color(0xFFEC5E2B));
});
test('loadingDarkBlue is 0xFF4087F8', () {
expect(PinballColors.loadingDarkBlue, const Color(0xFF4087F8));
});
test('loadingLightBlue is 0xFF6CCAE4', () {
expect(PinballColors.loadingLightBlue, const Color(0xFF6CCAE4));
});
test('crtBackground is 0xFF274E54', () {
expect(PinballColors.crtBackground, const Color(0xFF274E54));
});
});
}

@ -0,0 +1,30 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('AnimatedEllipsisText', () {
testWidgets(
'adds a new `.` every 500ms and '
'resets back to zero after adding 3', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AnimatedEllipsisText('test'),
),
),
);
expect(find.text('test'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test.'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test..'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test...'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test'), findsOneWidget);
});
});
}

@ -0,0 +1,25 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('CrtBackground', () {
test('is a BoxDecoration with a LinearGradient', () {
// ignore: prefer_const_constructors
final crtBg = CrtBackground();
const expectedGradient = LinearGradient(
begin: Alignment(1, 0.015),
stops: [0.0, 0.5, 0.5, 1],
colors: [
PinballColors.darkBlue,
PinballColors.darkBlue,
PinballColors.crtBackground,
PinballColors.crtBackground,
],
tileMode: TileMode.repeated,
);
expect(crtBg, isA<BoxDecoration>());
expect(crtBg.gradient, expectedGradient);
});
});
}

@ -0,0 +1,45 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('PinballLoadingIndicator', () {
group('assert value', () {
test('throws error if value <= 0.0', () {
expect(
() => PinballLoadingIndicator(value: -0.5),
throwsA(isA<AssertionError>()),
);
});
test('throws error if value >= 1.0', () {
expect(
() => PinballLoadingIndicator(value: 1.5),
throwsA(isA<AssertionError>()),
);
});
});
testWidgets(
'renders 12 LinearProgressIndicators and '
'6 FractionallySizedBox to indicate progress', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PinballLoadingIndicator(value: 0.75),
),
),
);
expect(find.byType(FractionallySizedBox), findsNWidgets(6));
expect(find.byType(LinearProgressIndicator), findsNWidgets(12));
final progressIndicators = tester.widgetList<LinearProgressIndicator>(
find.byType(LinearProgressIndicator),
);
for (final i in progressIndicators) {
expect(i.value, 0.75);
}
});
});
}

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:authentication_repository/authentication_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
void main() {
group('AssetsManagerCubit', () {

@ -1,7 +1,7 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
void main() {
group('AssetsManagerState', () {

@ -0,0 +1,38 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball_ui/pinball_ui.dart';
import '../../helpers/helpers.dart';
class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}
void main() {
late AssetsManagerCubit assetsManagerCubit;
setUp(() {
final initialAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: const [],
);
assetsManagerCubit = _MockAssetsManagerCubit();
whenListen(
assetsManagerCubit,
Stream.value(initialAssetsState),
initialState: initialAssetsState,
);
});
group('AssetsLoadingPage', () {
testWidgets('renders an animated text and a pinball loading indicator',
(tester) async {
await tester.pumpApp(
const AssetsLoadingPage(),
assetsManagerCubit: assetsManagerCubit,
);
expect(find.byType(AnimatedEllipsisText), findsOneWidget);
expect(find.byType(PinballLoadingIndicator), findsOneWidget);
});
});
}

@ -2,7 +2,6 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/extensions.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';
@ -15,7 +14,7 @@ import '../../helpers/helpers.dart';
// TODO(allisonryan0002): remove once
// https://github.com/flame-engine/flame/pull/1520 is merged
class _WrappedBallController extends BallController {
_WrappedBallController(Ball<Forge2DGame> ball, this._gameRef) : super(ball);
_WrappedBallController(Ball ball, this._gameRef) : super(ball);
final PinballGame _gameRef;

@ -90,20 +90,6 @@ void main() {
},
);
flameBlocTester.testGameWidget(
'plays score sound',
setUp: (game, tester) async {
final scoringBehavior = ScoringBehavior(points: Points.oneMillion);
await parent.add(scoringBehavior);
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
scoringBehavior.beginContact(ball, _MockContact());
verify(audio.score).called(1);
},
);
flameBlocTester.testGameWidget(
"adds a ScoreComponent at Ball's position with points",
setUp: (game, tester) async {
@ -130,4 +116,57 @@ void main() {
);
});
});
group('BumperScoringBehavior', () {
group('beginContact', () {
late GameBloc bloc;
late PinballAudio audio;
late Ball ball;
late BodyComponent parent;
setUp(() {
audio = _MockPinballAudio();
ball = _MockBall();
final ballBody = _MockBody();
when(() => ball.body).thenReturn(ballBody);
when(() => ballBody.position).thenReturn(Vector2.all(4));
parent = _TestBodyComponent();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: () => EmptyPinballTestGame(
audio: audio,
),
blocBuilder: () {
bloc = _MockGameBloc();
const state = GameState(
score: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
assets: assets,
);
flameBlocTester.testGameWidget(
'plays bumper sound',
setUp: (game, tester) async {
final scoringBehavior = BumperScoringBehavior(
points: Points.oneMillion,
);
await parent.add(scoringBehavior);
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
scoringBehavior.beginContact(ball, _MockContact());
verify(audio.bumper).called(1);
},
);
});
});
}

@ -5,6 +5,7 @@ import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
@ -66,7 +67,6 @@ void main() {
Stream.value(initialAssetsState),
initialState: initialAssetsState,
);
await tester.pumpApp(
PinballGameView(
game: game,
@ -74,14 +74,7 @@ void main() {
assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit,
);
expect(
find.byWidgetPredicate(
(widget) =>
widget is LinearProgressIndicator && widget.value == 0.0,
),
findsOneWidget,
);
expect(find.byType(AssetsLoadingPage), findsOneWidget);
},
);

@ -1,9 +1,3 @@
//
// Copyright (c) 2021, Very Good Ventures
// Use of this source code is governed by an MIT-style
// https://opensource.org/licenses/MIT.
// https://verygood.ventures
// license that can be found in the LICENSE file or at
export 'builders.dart';
export 'fakes.dart';
export 'forge2d.dart';

@ -1,10 +1,3 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -12,6 +5,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';

@ -3,10 +3,13 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:platform_helper/platform_helper.dart';
import '../helpers/helpers.dart';
class _MockPinballAudio extends Mock implements PinballAudio {}
class _MockPlatformHelper extends Mock implements PlatformHelper {}
void main() {
@ -93,5 +96,30 @@ void main() {
await tester.pumpAndSettle();
expect(find.byType(HowToPlayDialog), findsNothing);
});
testWidgets(
'plays the I/O Pinball voice over audio on dismiss',
(tester) async {
final audio = _MockPinballAudio();
await tester.pumpApp(
Builder(
builder: (context) {
return TextButton(
onPressed: () => showHowToPlayDialog(context),
child: const Text('test'),
);
},
),
pinballAudio: audio,
);
expect(find.byType(HowToPlayDialog), findsNothing);
await tester.tap(find.text('test'));
await tester.pumpAndSettle();
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
verify(audio.ioPinballVoiceOver).called(1);
},
);
});
}

Loading…
Cancel
Save