feat: add arcade backgrounds (#405)

* feat: add arcade backgrounds

* chore: lighter assets

* chore: swap for smaller assets
pull/414/head
Allison Ryan 2 years ago committed by GitHub
parent b06f4bd6e9
commit c3094850fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -17,29 +17,32 @@ class AssetsLoadingPage extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),
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);
},
return Container(
decoration: const CrtBackground(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),
),
],
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);
},
),
),
],
),
),
);
}

@ -1,7 +1,7 @@
export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart';
export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart';
export 'character_selection_behavior.dart';
export 'scoring_behavior.dart';

@ -3,18 +3,25 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
/// Updates the launch [Ball] to reflect character selections.
class BallThemingBehavior extends Component
/// Updates the [ArcadeBackground] and launch [Ball] to reflect character
/// selections.
class CharacterSelectionBehavior extends Component
with
FlameBlocListenable<CharacterThemeCubit, CharacterThemeState>,
HasGameRef {
@override
void onNewState(CharacterThemeState state) {
gameRef
.descendants()
.whereType<ArcadeBackground>()
.single
.bloc
.onCharacterSelected(state.characterTheme);
gameRef
.descendants()
.whereType<Ball>()
.single
.bloc
.onThemeChanged(state.characterTheme);
.onCharacterSelected(state.characterTheme);
}
}

@ -139,13 +139,17 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(androidTheme.background.keyName),
images.load(androidTheme.ball.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(dashTheme.background.keyName),
images.load(dashTheme.ball.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(dinoTheme.background.keyName),
images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.background.keyName),
images.load(sparkyTheme.ball.keyName),
];
}

@ -90,7 +90,7 @@ class PinballGame extends PinballForge2DGame
BonusNoiseBehavior(),
GameBlocStatusListener(),
BallSpawningBehavior(),
BallThemingBehavior(),
CharacterSelectionBehavior(),
CameraFocusingBehavior(),
CanvasComponent(
onSpritePainted: (paint) {
@ -101,6 +101,7 @@ class PinballGame extends PinballForge2DGame
children: [
ZCanvasComponent(
children: [
ArcadeBackground(),
BoardBackgroundSpriteComponent(),
Boundaries(),
Backbox(

@ -43,14 +43,11 @@ class PinballGamePage extends StatelessWidget {
gameBloc: gameBloc,
);
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: BlocProvider(
create: (_) => AssetsManagerCubit(game, audioPlayer)..load(),
child: PinballGameView(game),
),
return Scaffold(
backgroundColor: PinballColors.black,
body: BlocProvider(
create: (_) => AssetsManagerCubit(game, audioPlayer)..load(),
child: PinballGameView(game),
),
);
}

@ -0,0 +1,92 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
export 'cubit/arcade_background_cubit.dart';
/// {@template arcade_background}
/// Background of the arcade that the pinball machine lives in.
/// {@endtemplate}
class ArcadeBackground extends Component with ZIndex {
/// {@macro arcade_background}
ArcadeBackground({String? assetPath})
: this._(
bloc: ArcadeBackgroundCubit(),
assetPath: assetPath,
);
ArcadeBackground._({required this.bloc, String? assetPath})
: super(
children: [
FlameBlocProvider<ArcadeBackgroundCubit,
ArcadeBackgroundState>.value(
value: bloc,
children: [ArcadeBackgroundSpriteComponent(assetPath: assetPath)],
)
],
) {
zIndex = ZIndexes.arcadeBackground;
}
/// Creates an [ArcadeBackground] without any behaviors.
///
/// This can be used for testing [ArcadeBackground]'s behaviors in isolation.
@visibleForTesting
ArcadeBackground.test({
ArcadeBackgroundCubit? bloc,
String? assetPath,
}) : bloc = bloc ?? ArcadeBackgroundCubit(),
super(
children: [
FlameBlocProvider<ArcadeBackgroundCubit,
ArcadeBackgroundState>.value(
value: bloc ?? ArcadeBackgroundCubit(),
children: [ArcadeBackgroundSpriteComponent(assetPath: assetPath)],
)
],
);
/// Bloc to update the arcade background sprite when a new character is
/// selected.
final ArcadeBackgroundCubit bloc;
}
/// {@template arcade_background_sprite_component}
/// [SpriteComponent] for the [ArcadeBackground].
/// {@endtemplate}
@visibleForTesting
class ArcadeBackgroundSpriteComponent extends SpriteComponent
with
FlameBlocListenable<ArcadeBackgroundCubit, ArcadeBackgroundState>,
HasGameRef {
/// {@macro arcade_background_sprite_component}
ArcadeBackgroundSpriteComponent({required String? assetPath})
: _assetPath = assetPath,
super(
anchor: Anchor.bottomCenter,
position: Vector2(0, 72.3),
);
final String? _assetPath;
@override
void onNewState(ArcadeBackgroundState state) {
sprite = Sprite(
gameRef.images.fromCache(state.characterTheme.background.keyName),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images
.fromCache(_assetPath ?? theme.Assets.images.dash.background.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';
part 'arcade_background_state.dart';
class ArcadeBackgroundCubit extends Cubit<ArcadeBackgroundState> {
ArcadeBackgroundCubit() : super(const ArcadeBackgroundState.initial());
void onCharacterSelected(CharacterTheme characterTheme) {
emit(ArcadeBackgroundState(characterTheme: characterTheme));
}
}

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
part of 'arcade_background_cubit.dart';
class ArcadeBackgroundState extends Equatable {
const ArcadeBackgroundState({required this.characterTheme});
const ArcadeBackgroundState.initial()
: this(characterTheme: const DashTheme());
final CharacterTheme characterTheme;
@override
List<Object> get props => [characterTheme];
}

@ -7,7 +7,7 @@ part 'ball_state.dart';
class BallCubit extends Cubit<BallState> {
BallCubit() : super(const BallState.initial());
void onThemeChanged(CharacterTheme characterTheme) {
void onCharacterSelected(CharacterTheme characterTheme) {
emit(BallState(characterTheme: characterTheme));
}
}

@ -1,6 +1,7 @@
export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart';
export 'arcade_background/arcade_background.dart';
export 'ball/ball.dart';
export 'baseboard.dart';
export 'board_background_sprite_component.dart';

@ -18,6 +18,8 @@ abstract class ZIndexes {
// Background
static const arcadeBackground = _below + boardBackground;
static const boardBackground = 5 * _below + _base;
static const decal = _above + boardBackground;

@ -0,0 +1,79 @@
// 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_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
theme.Assets.images.android.background.keyName,
theme.Assets.images.dash.background.keyName,
theme.Assets.images.dino.background.keyName,
theme.Assets.images.sparky.background.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('ArcadeBackground', () {
test(
'can be instantiated',
() {
expect(ArcadeBackground(), isA<ArcadeBackground>());
expect(ArcadeBackground.test(), isA<ArcadeBackground>());
},
);
flameTester.test(
'loads correctly',
(game) async {
final ball = ArcadeBackground();
await game.ready();
await game.ensureAdd(ball);
expect(game.contains(ball), isTrue);
},
);
flameTester.test(
'has only one SpriteComponent',
(game) async {
final ball = ArcadeBackground();
await game.ready();
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<SpriteComponent>().length,
equals(1),
);
},
);
flameTester.test(
'ArcadeBackgroundSpriteComponent changes sprite onNewState',
(game) async {
final ball = ArcadeBackground();
await game.ready();
await game.ensureAdd(ball);
final ballSprite = ball
.descendants()
.whereType<ArcadeBackgroundSpriteComponent>()
.single;
final originalSprite = ballSprite.sprite;
ballSprite.onNewState(
const ArcadeBackgroundState(characterTheme: theme.DinoTheme()),
);
await game.ready();
final newSprite = ballSprite.sprite;
expect(newSprite != originalSprite, isTrue);
},
);
});
}

@ -0,0 +1,22 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group(
'ArcadeBackgroundCubit',
() {
blocTest<ArcadeBackgroundCubit, ArcadeBackgroundState>(
'onCharacterSelected emits new theme',
build: ArcadeBackgroundCubit.new,
act: (bloc) => bloc.onCharacterSelected(const DinoTheme()),
expect: () => [
const ArcadeBackgroundState(
characterTheme: DinoTheme(),
),
],
);
},
);
}

@ -0,0 +1,32 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('ArcadeBackgroundState', () {
test('supports value equality', () {
expect(
ArcadeBackgroundState(characterTheme: DashTheme()),
equals(ArcadeBackgroundState(characterTheme: DashTheme())),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
ArcadeBackgroundState(characterTheme: DashTheme()),
isNotNull,
);
});
test('initial contains DashTheme', () {
expect(
ArcadeBackgroundState.initial().characterTheme,
DashTheme(),
);
});
});
});
}

@ -8,9 +8,9 @@ void main() {
'BallCubit',
() {
blocTest<BallCubit, BallState>(
'onThemeChanged emits new theme',
'onCharacterSelected emits new theme',
build: BallCubit.new,
act: (bloc) => bloc.onThemeChanged(const DinoTheme()),
act: (bloc) => bloc.onCharacterSelected(const DinoTheme()),
expect: () => [const BallState(characterTheme: DinoTheme())],
);
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

@ -32,9 +32,9 @@ class $AssetsImagesAndroidGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/android/animation.png');
/// File path: assets/images/android/background.png
/// File path: assets/images/android/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/android/background.png');
const AssetGenImage('assets/images/android/background.jpg');
/// File path: assets/images/android/ball.png
AssetGenImage get ball =>
@ -56,9 +56,9 @@ class $AssetsImagesDashGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/dash/animation.png');
/// File path: assets/images/dash/background.png
/// File path: assets/images/dash/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/dash/background.png');
const AssetGenImage('assets/images/dash/background.jpg');
/// File path: assets/images/dash/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png');
@ -78,9 +78,9 @@ class $AssetsImagesDinoGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/dino/animation.png');
/// File path: assets/images/dino/background.png
/// File path: assets/images/dino/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/dino/background.png');
const AssetGenImage('assets/images/dino/background.jpg');
/// File path: assets/images/dino/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png');
@ -100,9 +100,9 @@ class $AssetsImagesSparkyGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/sparky/animation.png');
/// File path: assets/images/sparky/background.png
/// File path: assets/images/sparky/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/sparky/background.png');
const AssetGenImage('assets/images/sparky/background.jpg');
/// File path: assets/images/sparky/ball.png
AssetGenImage get ball =>

@ -5,6 +5,9 @@ abstract class PinballColors {
/// Color: 0xFFFFFFFF
static const Color white = Color(0xFFFFFFFF);
/// Color: 0xFF000000
static const Color black = Color(0xFF000000);
/// Color: 0xFF0C32A4
static const Color darkBlue = Color(0xFF0C32A4);

@ -8,6 +8,10 @@ void main() {
expect(PinballColors.white, const Color(0xFFFFFFFF));
});
test('black is 0xFF000000', () {
expect(PinballColors.black, const Color(0xFF000000));
});
test('darkBlue is 0xFF0C32A4', () {
expect(PinballColors.darkBlue, const Color(0xFF0C32A4));
});

@ -20,6 +20,8 @@ class _TestGame extends Forge2DGame {
await images.loadAll([
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.dash.background.keyName,
theme.Assets.images.dino.background.keyName,
]);
}
@ -38,32 +40,66 @@ class _TestGame extends Forge2DGame {
class _MockBallCubit extends Mock implements BallCubit {}
class _MockArcadeBackgroundCubit extends Mock implements ArcadeBackgroundCubit {
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'BallThemingBehavior',
'CharacterSelectionBehavior',
() {
final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
BallThemingBehavior(),
isA<BallThemingBehavior>(),
CharacterSelectionBehavior(),
isA<CharacterSelectionBehavior>(),
);
});
flameTester.test(
'loads',
(game) async {
final behavior = BallThemingBehavior();
final behavior = CharacterSelectionBehavior();
await game.pump([behavior]);
expect(game.descendants(), contains(behavior));
},
);
flameTester.test(
'onNewState calls onThemeChanged on the ball bloc',
'onNewState calls onCharacterSelected on the arcade background bloc',
(game) async {
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen(
arcadeBackgroundBloc,
const Stream<ArcadeBackgroundState>.empty(),
initialState: const ArcadeBackgroundState.initial(),
);
final arcadeBackground =
ArcadeBackground.test(bloc: arcadeBackgroundBloc);
final behavior = CharacterSelectionBehavior();
await game.pump([
arcadeBackground,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Ball.test(),
]);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState);
await game.ready();
verify(
() => arcadeBackgroundBloc
.onCharacterSelected(dinoThemeState.characterTheme),
).called(1);
},
);
flameTester.test(
'onNewState calls onCharacterSelected on the ball bloc',
(game) async {
final ballBloc = _MockBallCubit();
whenListen(
@ -72,20 +108,22 @@ void main() {
initialState: const BallState.initial(),
);
final ball = Ball.test(bloc: ballBloc);
final behavior = BallThemingBehavior();
final behavior = CharacterSelectionBehavior();
await game.pump([
ball,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
ArcadeBackground.test(),
]);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState);
await game.ready();
verify(() => ballBloc.onThemeChanged(dinoThemeState.characterTheme))
.called(1);
verify(
() => ballBloc.onCharacterSelected(dinoThemeState.characterTheme),
).called(1);
},
);
},

@ -113,11 +113,11 @@ void main() {
);
flameTester.test(
'has only one BallThemingBehavior',
'has only one CharacterSelectionBehavior',
(game) async {
await game.ready();
expect(
game.descendants().whereType<BallThemingBehavior>().length,
game.descendants().whereType<CharacterSelectionBehavior>().length,
equals(1),
);
},

Loading…
Cancel
Save