Merge branch 'main' into refactor/removed-scoring-sensor

pull/395/head
Alejandro Santiago 3 years ago committed by GitHub
commit d1aa319002
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -1,27 +1,39 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
part 'assets_manager_state.dart';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
AssetsManagerCubit(this._game, this._player)
: super(const AssetsManagerState.initial());
final PinballGame _game;
final PinballPlayer _player;
/// Loads the assets
Future<void> load() async {
/// Assigning loadables is a very expensive operation. With this purposeful
/// delay here, which is a bit random in duration but enough to let the UI
/// do its job without adding too much delay for the user, we are letting
/// the UI paint first, and then we start loading the assets.
await Future<void>.delayed(const Duration(milliseconds: 300));
emit(
state.copyWith(
loadables: [
_game.preFetchLeaderboard(),
..._game.preLoadAssets(),
..._player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
],
),
);
final all = state.loadables.map((loadable) async {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable {
});
/// {@macro assets_manager_state}
const AssetsManagerState.initial({
required List<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
const AssetsManagerState.initial()
: this(loadables: const [], loaded: const []);
/// List of futures to load
final List<Future> loadables;
@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable {
final List<Future> loaded;
/// Returns a value between 0 and 1 to indicate the loading progress
double get progress => loaded.length / loadables.length;
double get progress =>
loadables.isEmpty ? 0 : loaded.length / loadables.length;
/// Only returns false if all the assets have been loaded
bool get isLoading => progress != 1;
/// Returns a copy of this instance with the given parameters
/// updated

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
@ -20,10 +21,9 @@ class AssetsLoadingPage extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.ioPinball,
style: headline1!.copyWith(fontSize: 80),
textAlign: TextAlign.center,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),
const SizedBox(height: 40),
AnimatedEllipsisText(

@ -5,7 +5,7 @@ enum GameBonus {
/// Bonus achieved when the ball activates all Google letters.
googleWord,
/// Bonus achieved when the user activates all dash nest bumpers.
/// Bonus achieved when the user activates all dash bumpers.
dashNest,
/// Bonus achieved when a ball enters Sparky's computer.

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
@ -15,42 +16,43 @@ class AndroidAcres extends Component {
AndroidAcres()
: super(
children: [
SpaceshipRamp(
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new,
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
);

@ -5,18 +5,21 @@ 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 ParentIsA<AndroidAcres>, FlameBlocReader<GameBloc, GameState> {
class AndroidSpaceshipBonusBehavior extends Component {
@override
void onMount() {
super.onMount();
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!;
androidSpaceship.bloc.stream.listen((state) {
final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return;
bloc.add(const BonusActivated(GameBonus.androidSpaceship));
androidSpaceship.bloc.onBonusAwarded();
});
Future<void> onLoad() async {
await super.onLoad();
await add(
FlameBlocListener<AndroidSpaceshipCubit, AndroidSpaceshipState>(
listenWhen: (_, state) => state == AndroidSpaceshipState.withBonus,
onNewState: (state) {
readBloc<GameBloc, GameState>().add(
const BonusActivated(GameBonus.androidSpaceship),
);
readBloc<AndroidSpaceshipCubit, AndroidSpaceshipState>()
.onBonusAwarded();
},
),
);
}
}

@ -7,9 +7,9 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Bonus obtained at the [FlutterForest].
///
/// When all [DashNestBumper]s are hit at least once three times, the [Signpost]
/// When all [DashBumper]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 [DashBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component
with
ParentIsA<FlutterForest>,
@ -19,14 +19,14 @@ class FlutterForestBonusBehavior extends Component
void onMount() {
super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>();
final bumpers = parent.children.whereType<DashBumper>();
final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!;
for (final bumper in bumpers) {
bumper.bloc.stream.listen((state) {
final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active,
(bumper) => bumper.bloc.state == DashBumperState.active,
);
if (activatedAllBumpers) {

@ -9,7 +9,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template flutter_forest}
/// Area positioned at the top right of the board where the [Ball] can bounce
/// off [DashNestBumper]s.
/// off [DashBumper]s.
/// {@endtemplate}
class FlutterForest extends Component with ZIndex {
/// {@macro flutter_forest}
@ -22,19 +22,19 @@ class FlutterForest extends Component with ZIndex {
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(7.95, -58.35),
DashNestBumper.main(
DashBumper.main(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
DashBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
DashBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),

@ -45,8 +45,11 @@ class GameBlocStatusListener extends Component
}
}
void _addFlipperKeyControls(Flipper flipper) =>
flipper.add(FlipperKeyControllingBehavior());
void _addFlipperKeyControls(Flipper flipper) {
flipper
..add(FlipperKeyControllingBehavior())
..moveDown();
}
void _removeFlipperKeyControls(Flipper flipper) => flipper
.descendants()

@ -1,3 +1,4 @@
import 'package:flame/extensions.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -5,7 +6,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded
List<Future> preLoadAssets() {
List<Future<Image>> preLoadAssets() {
const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme();

@ -21,12 +21,6 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode;
static Route route({bool isDebugMode = kDebugMode}) {
return MaterialPageRoute<void>(
builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
);
}
@override
Widget build(BuildContext context) {
final characterThemeBloc = context.read<CharacterThemeCubit>();
@ -48,53 +42,39 @@ class PinballGamePage extends StatelessWidget {
l10n: context.l10n,
gameBloc: gameBloc,
);
final loadables = [
game.preFetchLeaderboard(),
...game.preLoadAssets(),
...player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
return BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: BlocProvider(
create: (_) => AssetsManagerCubit(game, player)..load(),
child: PinballGameView(game),
),
),
);
}
}
class PinballGameView extends StatelessWidget {
const PinballGameView({
Key? key,
required this.game,
}) : super(key: key);
const PinballGameView(this.game, {Key? key}) : super(key: key);
final PinballGame game;
@override
Widget build(BuildContext context) {
final isLoading = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress != 1,
);
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading
return BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return state.isLoading
? const AssetsLoadingPage()
: PinballGameLoadedView(game: game),
),
: PinballGameLoadedView(game);
},
);
}
}
@visibleForTesting
class PinballGameLoadedView extends StatelessWidget {
const PinballGameLoadedView({
Key? key,
required this.game,
}) : super(key: key);
const PinballGameLoadedView(this.game, {Key? key}) : super(key: key);
final PinballGame game;

@ -15,6 +15,8 @@ class $AssetsImagesGen {
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen {
const AssetGenImage('assets/images/link_box/info_icon.png');
}
class $AssetsImagesLoadingGameGen {
const $AssetsImagesLoadingGameGen();
/// File path: assets/images/loading_game/io_pinball.png
AssetGenImage get ioPinball =>
const AssetGenImage('assets/images/loading_game/io_pinball.png');
}
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();

@ -148,10 +148,6 @@
"@loading": {
"description": "Text shown to indicate loading times"
},
"ioPinball": "I/O Pinball",
"@ioPinball": {
"description": "I/O Pinball - Name of the game"
},
"enter": "Enter",
"@enter": {
"description": "Text shown on the mobile controls enter button"

@ -11,10 +11,8 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart';
class AndroidSpaceship extends Component {
AndroidSpaceship({
required Vector2 position,
}) : bloc = AndroidSpaceshipCubit(),
super(
AndroidSpaceship({required Vector2 position})
: super(
children: [
_SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
@ -38,17 +36,8 @@ class AndroidSpaceship extends Component {
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
@visibleForTesting
AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
final AndroidSpaceshipCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {

@ -1,14 +1,18 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_bloc/flame_bloc.dart';
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> {
extends ContactBehavior<AndroidSpaceshipEntrance>
with FlameBlocReader<AndroidSpaceshipCubit, AndroidSpaceshipState> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.parent.bloc.onBallEntered();
bloc.onBallEntered();
}
}

@ -10,7 +10,7 @@ export 'boundaries.dart';
export 'camera_zoom.dart';
export 'chrome_dino/chrome_dino.dart';
export 'dash_animatronic.dart';
export 'dash_nest_bumper/dash_nest_bumper.dart';
export 'dash_bumper/dash_bumper.dart';
export 'dino_walls.dart';
export 'error_component.dart';
export 'fire_effect.dart';
@ -26,7 +26,7 @@ export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
export 'rocket.dart';
export 'score_component.dart';
export 'score_component/score_component.dart';
export 'shapes/shapes.dart';
export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_animatronic}
/// Animated Dash that sits on top of the [DashNestBumper.main].
/// Animated Dash that sits on top of the [DashBumper.main].
/// {@endtemplate}
class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro dash_animatronic}

@ -2,8 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class DashNestBumperBallContactBehavior
extends ContactBehavior<DashNestBumper> {
class DashBumperBallContactBehavior extends ContactBehavior<DashBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);

@ -0,0 +1,17 @@
import 'package:bloc/bloc.dart';
part 'dash_bumper_state.dart';
class DashBumperCubit extends Cubit<DashBumperState> {
DashBumperCubit() : super(DashBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashBumperState.inactive);
}
}

@ -0,0 +1,10 @@
part of 'dash_bumper_cubit.dart';
/// Indicates the [DashBumperCubit]'s current state.
enum DashBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -5,17 +5,17 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/dash_nest_bumper_cubit.dart';
export 'cubit/dash_bumper_cubit.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@template dash_bumper}
/// Bumper for the flutter forest.
/// {@endtemplate}
class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
class DashBumper extends BodyComponent with InitialPosition {
/// {@macro dash_bumper}
DashBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
@ -28,19 +28,22 @@ class DashNestBumper extends BodyComponent with InitialPosition {
super(
renderBody: false,
children: [
_DashNestBumperSpriteGroupComponent(
_DashBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
position: spritePosition,
current: bloc.state,
),
DashNestBumperBallContactBehavior(),
DashBumperBallContactBehavior(),
...?children,
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.main({
/// {@macro dash_bumper}
///
/// [DashBumper.main], usually positioned with a [DashAnimatronic] on top of
/// it.
DashBumper.main({
Iterable<Component>? children,
}) : this._(
majorRadius: 5.1,
@ -48,15 +51,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.a({
/// {@macro dash_bumper}
///
/// [DashBumper.a] is positioned at the right side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.a({
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
@ -64,15 +70,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.3, -1.3),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.b({
/// {@macro dash_bumper}
///
/// [DashBumper.b] is positioned at the left side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.b({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.1,
@ -80,25 +89,26 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.4, -1.2),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// Creates an [DashNestBumper] without any children.
/// Creates a [DashBumper] without any children.
///
/// This can be used for testing [DashNestBumper]'s behaviors in isolation.
/// This can be used for testing [DashBumper]'s behaviors in isolation.
@visibleForTesting
DashNestBumper.test({required this.bloc})
DashBumper.test({required this.bloc})
: _majorRadius = 3,
_minorRadius = 2.5;
final double _majorRadius;
final double _minorRadius;
final DashNestBumperCubit bloc;
// ignore: public_member_api_docs
final DashBumperCubit bloc;
@override
void onRemove() {
@ -121,14 +131,14 @@ class DashNestBumper extends BodyComponent with InitialPosition {
}
}
class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperState>
with HasGameRef, ParentIsA<DashNestBumper> {
_DashNestBumperSpriteGroupComponent({
class _DashBumperSpriteGroupComponent
extends SpriteGroupComponent<DashBumperState>
with HasGameRef, ParentIsA<DashBumper> {
_DashBumperSpriteGroupComponent({
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 position,
required DashNestBumperState current,
required DashBumperState current,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
super(
@ -146,9 +156,9 @@ class _DashNestBumperSpriteGroupComponent
parent.bloc.stream.listen((state) => current = state);
final sprites = {
DashNestBumperState.active:
DashBumperState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperState.inactive:
DashBumperState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
};
this.sprites = sprites;

@ -1,17 +0,0 @@
import 'package:bloc/bloc.dart';
part 'dash_nest_bumper_state.dart';
class DashNestBumperCubit extends Cubit<DashNestBumperState> {
DashNestBumperCubit() : super(DashNestBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashNestBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashNestBumperState.inactive);
}
}

@ -1,10 +0,0 @@
part of 'dash_nest_bumper_cubit.dart';
/// Indicates the [DashNestBumperCubit]'s current state.
enum DashNestBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -6,8 +6,6 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Joints the [Flipper] to allow pivoting around one end.
class FlipperJointingBehavior extends Component
with ParentIsA<Flipper>, HasGameRef {
late final RevoluteJoint _joint;
@override
Future<void> onLoad() async {
await super.onLoad();
@ -19,15 +17,7 @@ class FlipperJointingBehavior extends Component
flipper: parent,
anchor: anchor,
);
_joint = _FlipperJoint(jointDef);
parent.world.createJoint(_joint);
}
@override
void onMount() {
gameRef.ready().whenComplete(
() => parent.body.joints.whereType<_FlipperJoint>().first.unlock(),
);
parent.world.createJoint(RevoluteJoint(jointDef));
}
}
@ -58,46 +48,15 @@ class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) : side = flipper.side {
enableLimit = true;
}) {
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
}
final BoardSide side;
}
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the pivoting motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def) {
lock();
}
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.611;
final BoardSide side;
/// Locks the [Flipper] to its resting position.
///
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.
void unlock() {
const angle = _halfSweepingAngle;
setLimits(-angle, angle);
enableLimit = true;
upperAngle = 0.611;
lowerAngle = -upperAngle;
}
}

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Scales a [ScoreComponent] according to its position on the board.
class ScoreComponentScalingBehavior extends Component
with ParentIsA<SpriteComponent> {
@override
void update(double dt) {
super.update(dt);
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = 0.83;
final augmentedPosition = parent.position.y * 3;
final standardizedYPosition = augmentedPosition + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.scale.setValues(
scaleFactor,
scaleFactor,
);
}
}

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/score_component/behaviors/score_component_scaling_behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
enum Points {
@ -26,10 +28,25 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
super(
position: position,
anchor: Anchor.center,
children: [ScoreComponentScalingBehavior()],
) {
zIndex = ZIndexes.score;
}
/// Creates a [ScoreComponent] without any children.
///
/// This can be used for testing [ScoreComponent]'s behaviors in isolation.
@visibleForTesting
ScoreComponent.test({
required this.points,
required Vector2 position,
required EffectController effectController,
}) : _effectController = effectController,
super(
position: position,
anchor: Anchor.center,
);
late Points points;
late final Effect _effect;

@ -9,7 +9,7 @@ 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.
/// Lights up a new sign whenever all three [DashBumper]s are hit.
/// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}

@ -11,7 +11,7 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_bumper_cubit.dart';
/// {@template sparky_bumper}
/// Bumper for Sparky area.
/// Bumper for the Sparky Scorch.
/// {@endtemplate}
class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
/// {@macro sparky_bumper}

@ -106,7 +106,7 @@ abstract class ZIndexes {
// Score
static const score = _above + spaceshipRampForegroundRailing;
static const score = _above + sparkyAnimatronic;
// Debug information

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperAGame extends BallGame {
SmallDashNestBumperAGame()
class DashBumperAGame extends BallGame {
DashBumperAGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.a.active.keyName,
@ -14,7 +14,7 @@ class SmallDashNestBumperAGame extends BallGame {
);
static const description = '''
Shows how a SmallDashNestBumper ("a") is rendered.
Shows how the "a" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -24,7 +24,7 @@ class SmallDashNestBumperAGame extends BallGame {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(DashNestBumper.a()..priority = 1);
await add(DashBumper.a()..priority = 1);
await traceAllBodies();
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperBGame extends BallGame {
SmallDashNestBumperBGame()
class DashBumperBGame extends BallGame {
DashBumperBGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.b.active.keyName,
@ -14,7 +14,7 @@ class SmallDashNestBumperBGame extends BallGame {
);
static const description = '''
Shows how a SmallDashNestBumper ("b") is rendered.
Shows how the "b" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -24,7 +24,7 @@ class SmallDashNestBumperBGame extends BallGame {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(DashNestBumper.b()..priority = 1);
await add(DashBumper.b()..priority = 1);
await traceAllBodies();
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class BigDashNestBumperGame extends BallGame {
BigDashNestBumperGame()
class DashBumperMainGame extends BallGame {
DashBumperMainGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.main.active.keyName,
@ -14,7 +14,7 @@ class BigDashNestBumperGame extends BallGame {
);
static const description = '''
Shows how a BigDashNestBumper is rendered.
Shows how the "main" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -25,7 +25,7 @@ class BigDashNestBumperGame extends BallGame {
camera.followVector2(Vector2.zero());
await add(
DashNestBumper.main()..priority = 1,
DashBumper.main()..priority = 1,
);
await traceAllBodies();
}

@ -1,9 +1,9 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_b_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_main_game.dart';
import 'package:sandbox/stories/flutter_forest/signpost_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart';
void addFlutterForestStories(Dashbook dashbook) {
dashbook.storiesOf('Flutter Forest')
@ -13,18 +13,18 @@ void addFlutterForestStories(Dashbook dashbook) {
gameBuilder: (_) => SignpostGame(),
)
..addGame(
title: 'Big Dash Nest Bumper',
description: BigDashNestBumperGame.description,
gameBuilder: (_) => BigDashNestBumperGame(),
title: 'Main Dash Bumper',
description: DashBumperMainGame.description,
gameBuilder: (_) => DashBumperMainGame(),
)
..addGame(
title: 'Small Dash Nest Bumper A',
description: SmallDashNestBumperAGame.description,
gameBuilder: (_) => SmallDashNestBumperAGame(),
title: 'Dash Bumper A',
description: DashBumperAGame.description,
gameBuilder: (_) => DashBumperAGame(),
)
..addGame(
title: 'Small Dash Nest Bumper B',
description: SmallDashNestBumperBGame.description,
gameBuilder: (_) => SmallDashNestBumperBGame(),
title: 'Dash Bumper B',
description: DashBumperBGame.description,
gameBuilder: (_) => DashBumperBGame(),
);
}

@ -1,7 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -21,9 +21,18 @@ void main() {
Assets.images.android.spaceship.lightBeam.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
late AndroidSpaceshipCubit bloc;
setUp(() {
bloc = _MockAndroidSpaceshipCubit();
});
flameTester.test('loads correctly', (game) async {
final component = AndroidSpaceship(position: Vector2.zero());
final component =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [AndroidSpaceship(position: Vector2.zero())],
);
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
@ -33,7 +42,13 @@ void main() {
setUp: (game, tester) async {
await game.images.loadAll(assets);
final canvas = ZCanvasComponent(
children: [AndroidSpaceship(position: Vector2.zero())],
children: [
FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value(
value: bloc,
children: [AndroidSpaceship(position: Vector2.zero())],
),
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
@ -70,28 +85,16 @@ void main() {
},
);
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 provider =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [androidSpaceship],
);
await game.ensureAdd(provider);
final androidSpaceshipEntrance =
androidSpaceship.firstChild<AndroidSpaceshipEntrance>();

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
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';
@ -43,16 +44,19 @@ void main() {
);
final entrance = AndroidSpaceshipEntrance();
final androidSpaceship = AndroidSpaceship.test(
bloc: bloc,
children: [entrance],
final androidSpaceship = FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value(
value: bloc,
children: [
AndroidSpaceship.test(children: [entrance])
],
);
await entrance.add(behavior);
await game.ensureAdd(androidSpaceship);
behavior.beginContact(_MockBall(), _MockContact());
verify(androidSpaceship.bloc.onBallEntered).called(1);
verify(bloc.onBallEntered).called(1);
},
);
},

@ -10,8 +10,9 @@ import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
final flameTester = FlameTester(
() => TestGame([theme.Assets.images.dash.ball.keyName]),
);
group('BallScalingBehavior', () {
test('can be instantiated', () {

@ -6,11 +6,11 @@ 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/dash_nest_bumper/behaviors/behaviors.dart';
import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class _MockDashBumperCubit extends Mock implements DashBumperCubit {}
class _MockBall extends Mock implements Ball {}
@ -21,33 +21,33 @@ void main() {
final flameTester = FlameTester(TestGame.new);
group(
'DashNestBumperBallContactBehavior',
'DashBumperBallContactBehavior',
() {
test('can be instantiated', () {
expect(
DashNestBumperBallContactBehavior(),
isA<DashNestBumperBallContactBehavior>(),
DashBumperBallContactBehavior(),
isA<DashBumperBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = DashNestBumperBallContactBehavior();
final bloc = _MockDashNestBumperCubit();
final behavior = DashBumperBallContactBehavior();
final bloc = _MockDashBumperCubit();
whenListen(
bloc,
const Stream<DashNestBumperState>.empty(),
initialState: DashNestBumperState.active,
const Stream<DashBumperState>.empty(),
initialState: DashBumperState.active,
);
final dashNestBumper = DashNestBumper.test(bloc: bloc);
await dashNestBumper.add(behavior);
await game.ensureAdd(dashNestBumper);
final bumper = DashBumper.test(bloc: bloc);
await bumper.add(behavior);
await game.ensureAdd(bumper);
behavior.beginContact(_MockBall(), _MockContact());
verify(dashNestBumper.bloc.onBallContacted).called(1);
verify(bumper.bloc.onBallContacted).called(1);
},
);
},

@ -4,20 +4,20 @@ import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'DashNestBumperCubit',
'DashBumperCubit',
() {
blocTest<DashNestBumperCubit, DashNestBumperState>(
blocTest<DashBumperCubit, DashBumperState>(
'onBallContacted emits active',
build: DashNestBumperCubit.new,
build: DashBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [DashNestBumperState.active],
expect: () => [DashBumperState.active],
);
blocTest<DashNestBumperCubit, DashNestBumperState>(
blocTest<DashBumperCubit, DashBumperState>(
'onReset emits inactive',
build: DashNestBumperCubit.new,
build: DashBumperCubit.new,
act: (bloc) => bloc.onReset(),
expect: () => [DashNestBumperState.inactive],
expect: () => [DashBumperState.inactive],
);
},
);

@ -0,0 +1,139 @@
// 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/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockDashBumperCubit extends Mock implements DashBumperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DashBumper', () {
final flameTester = FlameTester(
() => TestGame(
[
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
],
),
);
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockDashBumperCubit();
whenListen(
bloc,
const Stream<DashBumperState>.empty(),
initialState: DashBumperState.inactive,
);
when(bloc.close).thenAnswer((_) async {});
final bumper = DashBumper.test(bloc: bloc);
await game.ensureAdd(bumper);
game.remove(bumper);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test('adds a bumperBallContactBehavior', (game) async {
final bumper = DashBumper.a();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<DashBumperBallContactBehavior>().single,
isNotNull,
);
});
group("'main' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final bumper = DashBumper.main(
children: [component],
);
await game.ensureAdd(bumper);
expect(bumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final bumper = DashBumper.main();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'a' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final bumper = DashBumper.a(
children: [component],
);
await game.ensureAdd(bumper);
expect(bumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final bumper = DashBumper.a();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'b' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final bumper = DashBumper.b(
children: [component],
);
await game.ensureAdd(bumper);
expect(bumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final bumper = DashBumper.b();
await game.ensureAdd(bumper);
expect(
bumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -1,137 +0,0 @@
// 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/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DashNestBumper', () {
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockDashNestBumperCubit();
whenListen(
bloc,
const Stream<DashNestBumperState>.empty(),
initialState: DashNestBumperState.inactive,
);
when(bloc.close).thenAnswer((_) async {});
final dashNestBumper = DashNestBumper.test(bloc: bloc);
await game.ensureAdd(dashNestBumper);
game.remove(dashNestBumper);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test('adds a DashNestBumperBallContactBehavior', (game) async {
final dashNestBumper = DashNestBumper.a();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children
.whereType<DashNestBumperBallContactBehavior>()
.single,
isNotNull,
);
});
group("'main' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.main(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final dashNestBumper = DashNestBumper.main();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'a' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.a(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final dashNestBumper = DashNestBumper.a();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
group("'b' adds", () {
flameTester.test('new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.b(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a BumpingBehavior', (game) async {
final dashNestBumper = DashNestBumper.b();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children.whereType<BumpingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -0,0 +1,74 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/effects.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/score_component/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
group('ScoreComponentScalingBehavior', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(
() => TestGame([
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
]),
);
test('can be instantiated', () {
expect(
ScoreComponentScalingBehavior(),
isA<ScoreComponentScalingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final parent = ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
);
final behavior = ScoreComponentScalingBehavior();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
expect(parent.children, contains(behavior));
});
flameTester.test(
'scales the sprite',
(game) async {
final parent1 = ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2(0, 10),
effectController: EffectController(duration: 1),
);
final parent2 = ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2(0, -10),
effectController: EffectController(duration: 1),
);
await game.ensureAddAll([parent1, parent2]);
await parent1.ensureAdd(ScoreComponentScalingBehavior());
await parent2.ensureAdd(ScoreComponentScalingBehavior());
game.update(1);
expect(
parent1.scale.x,
greaterThan(parent2.scale.x),
);
expect(
parent1.scale.y,
greaterThan(parent2.scale.y),
);
},
);
});
}

@ -5,24 +5,37 @@ import 'package:flame/effects.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/score_component/behaviors/behaviors.dart';
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
final flameTester = FlameTester(
() => TestGame([
Assets.images.score.fiveThousand.keyName,
Assets.images.score.twentyThousand.keyName,
Assets.images.score.twoHundredThousand.keyName,
Assets.images.score.oneMillion.keyName,
]),
);
group('ScoreComponent', () {
test('can be instantiated', () {
expect(
ScoreComponent(
points: Points.fiveThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
),
isA<ScoreComponent>(),
);
});
flameTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
@ -38,13 +51,32 @@ void main() {
},
);
flameTester.test(
'adds a ScoreComponentScalingBehavior',
(game) async {
await game.onLoad();
game.camera.followVector2(Vector2.zero());
final component = ScoreComponent(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
);
await game.ensureAdd(component);
expect(
component.children.whereType<ScoreComponentScalingBehavior>().length,
equals(1),
);
},
);
flameTester.testGameWidget(
'has a movement effect',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -63,10 +95,10 @@ void main() {
flameTester.testGameWidget(
'is removed once finished',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -83,12 +115,14 @@ void main() {
);
group('renders correctly', () {
const goldensPath = '../golden/score/';
flameTester.testGameWidget(
'5000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.fiveThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -104,7 +138,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/5k.png'),
matchesGoldenFile('${goldensPath}5k.png'),
);
},
);
@ -112,9 +146,9 @@ void main() {
flameTester.testGameWidget(
'20000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.twentyThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -130,7 +164,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/20k.png'),
matchesGoldenFile('${goldensPath}20k.png'),
);
},
);
@ -138,9 +172,9 @@ void main() {
flameTester.testGameWidget(
'200000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.twoHundredThousand,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -156,7 +190,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/200k.png'),
matchesGoldenFile('${goldensPath}200k.png'),
);
},
);
@ -164,9 +198,9 @@ void main() {
flameTester.testGameWidget(
'1000000 points',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.onLoad();
await game.ensureAdd(
ScoreComponent(
ScoreComponent.test(
points: Points.oneMillion,
position: Vector2.zero(),
effectController: EffectController(duration: 1),
@ -182,7 +216,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/score/1m.png'),
matchesGoldenFile('${goldensPath}1m.png'),
);
},
);

@ -62,6 +62,7 @@ flutter:
- assets/images/bonus_animation/
- assets/images/score/
- assets/images/link_box/
- assets/images/loading_game/
flutter_gen:
line_length: 80

@ -35,6 +35,7 @@ void main() {
pinballPlayer: pinballPlayer,
),
);
await tester.pump(const Duration(milliseconds: 400));
expect(find.byType(PinballGamePage), findsOneWidget);
});
});

@ -1,35 +0,0 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
void main() {
group('AssetsManagerCubit', () {
final completer1 = Completer<void>();
final completer2 = Completer<void>();
final future1 = completer1.future;
final future2 = completer2.future;
blocTest<AssetsManagerCubit, AssetsManagerState>(
'emits the loaded on the order that they load',
build: () => AssetsManagerCubit([future1, future2]),
act: (cubit) {
cubit.load();
completer2.complete();
completer1.complete();
},
expect: () => [
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2],
),
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2, future1],
),
],
);
});
}

@ -13,12 +13,11 @@ void main() {
});
test('has the correct initial state', () {
final future = Future<void>.value();
expect(
AssetsManagerState.initial(loadables: [future]),
AssetsManagerState.initial(),
equals(
AssetsManagerState(
loadables: [future],
loadables: const [],
loaded: const [],
),
),

@ -131,11 +131,28 @@ void main() {
);
});
flameTester.test('adds a FlameBlocProvider', (game) async {
final androidAcres = AndroidAcres();
await game.pump(androidAcres);
expect(
androidAcres.children
.whereType<
FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>>()
.single,
isNotNull,
);
});
flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async {
final androidAcres = AndroidAcres();
await game.pump(androidAcres);
final provider = androidAcres.children
.whereType<
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>>()
.single;
expect(
androidAcres.children.whereType<AndroidSpaceshipBonusBehavior>().single,
provider.children.whereType<AndroidSpaceshipBonusBehavior>().single,
isNotNull,
);
});

@ -1,5 +1,8 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
@ -41,13 +44,21 @@ class _TestGame extends Forge2DGame {
Future<void> pump(
AndroidAcres child, {
required GameBloc gameBloc,
required AndroidSpaceshipCubit androidSpaceshipCubit,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: androidSpaceshipCubit,
),
],
children: [child],
),
);
@ -56,6 +67,9 @@ class _TestGame extends Forge2DGame {
class _MockGameBloc extends Mock implements GameBloc {}
class _MockAndroidSpaceshipCubit extends Mock implements AndroidSpaceshipCubit {
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -70,20 +84,30 @@ void main() {
flameTester.testGameWidget(
'adds GameBonus.androidSpaceship to the game '
'when android spacehship has a bonus',
'when android spaceship has a bonus',
setUp: (game, tester) async {
final behavior = AndroidSpaceshipBonusBehavior();
final parent = AndroidAcres.test();
final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
final androidSpaceshipCubit = _MockAndroidSpaceshipCubit();
final streamController = StreamController<AndroidSpaceshipState>();
whenListen(
androidSpaceshipCubit,
streamController.stream,
initialState: AndroidSpaceshipState.withoutBonus,
);
await parent.add(androidSpaceship);
await game.pump(
parent,
androidSpaceshipCubit: androidSpaceshipCubit,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
androidSpaceship.bloc.onBallEntered();
streamController.add(AndroidSpaceshipState.withBonus);
await tester.pump();
verify(

@ -53,8 +53,7 @@ void main() {
final flameTester = FlameTester(_TestGame.new);
void _contactedBumper(DashNestBumper bumper) =>
bumper.bloc.onBallContacted();
void _contactedBumper(DashBumper bumper) => bumper.bloc.onBallContacted();
flameTester.testGameWidget(
'adds GameBonus.dashNest to the game '
@ -64,9 +63,9 @@ void main() {
final behavior = FlutterForestBonusBehavior();
final parent = FlutterForest.test();
final bumpers = [
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
];
final animatronic = DashAnimatronic();
final signpost = Signpost.test(bloc: SignpostCubit());
@ -74,7 +73,7 @@ void main() {
await parent.ensureAddAll([...bumpers, animatronic, signpost]);
await parent.ensureAdd(behavior);
expect(game.descendants().whereType<DashNestBumper>(), equals(bumpers));
expect(game.descendants().whereType<DashBumper>(), equals(bumpers));
bumpers.forEach(_contactedBumper);
await tester.pump();
bumpers.forEach(_contactedBumper);
@ -96,9 +95,9 @@ void main() {
final behavior = FlutterForestBonusBehavior();
final parent = FlutterForest.test();
final bumpers = [
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
];
final animatronic = DashAnimatronic();
final signpost = Signpost.test(bloc: SignpostCubit());
@ -106,7 +105,7 @@ void main() {
await parent.ensureAddAll([...bumpers, animatronic, signpost]);
await parent.ensureAdd(behavior);
expect(game.descendants().whereType<DashNestBumper>(), equals(bumpers));
expect(game.descendants().whereType<DashBumper>(), equals(bumpers));
bumpers.forEach(_contactedBumper);
await tester.pump();
bumpers.forEach(_contactedBumper);
@ -130,9 +129,9 @@ void main() {
final behavior = FlutterForestBonusBehavior();
final parent = FlutterForest.test();
final bumpers = [
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
DashBumper.test(bloc: DashBumperCubit()),
];
final animatronic = DashAnimatronic();
final signpost = Signpost.test(bloc: SignpostCubit());
@ -140,7 +139,7 @@ void main() {
await parent.ensureAddAll([...bumpers, animatronic, signpost]);
await parent.ensureAdd(behavior);
expect(game.descendants().whereType<DashNestBumper>(), equals(bumpers));
expect(game.descendants().whereType<DashBumper>(), equals(bumpers));
bumpers.forEach(_contactedBumper);
await tester.pump();

@ -91,23 +91,23 @@ void main() {
);
flameTester.test(
'three DashNestBumper',
'three DashBumper',
(game) async {
final component = FlutterForest();
await game.pump(component);
expect(
game.descendants().whereType<DashNestBumper>().length,
game.descendants().whereType<DashBumper>().length,
equals(3),
);
},
);
flameTester.test(
'three DashNestBumpers with BumperNoiseBehavior',
'three DashBumpers with BumperNoiseBehavior',
(game) async {
final component = FlutterForest();
await game.pump(component);
final bumpers = game.descendants().whereType<DashNestBumper>();
final bumpers = game.descendants().whereType<DashBumper>();
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoiseBehavior>(),

@ -82,14 +82,26 @@ void main() {
);
});
testWidgets('renders PinballGameView', (tester) async {
await tester.pumpApp(
PinballGamePage(),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
group('renders PinballGameView', () {
testWidgets('with debug mode turned on', (tester) async {
await tester.pumpApp(
PinballGamePage(),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
expect(find.byType(PinballGameView), findsOneWidget);
});
expect(find.byType(PinballGameView), findsOneWidget);
testWidgets('with debug mode turned off', (tester) async {
await tester.pumpApp(
PinballGamePage(isDebugMode: false),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
expect(find.byType(PinballGameView), findsOneWidget);
});
});
testWidgets(
@ -106,9 +118,7 @@ void main() {
initialState: initialAssetsState,
);
await tester.pumpApp(
PinballGameView(
game: game,
),
PinballGameView(game),
assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit,
);
@ -138,9 +148,7 @@ void main() {
);
await tester.pumpApp(
PinballGameView(
game: game,
),
PinballGameView(game),
assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
@ -151,61 +159,6 @@ void main() {
expect(find.byType(PinballGameLoadedView), findsOneWidget);
});
group('route', () {
Future<void> pumpRoute({
required WidgetTester tester,
required bool isDebugMode,
}) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
PinballGamePage.route(
isDebugMode: isDebugMode,
),
);
},
child: const Text('Tap me'),
);
},
),
),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
await tester.tap(find.text('Tap me'));
// We can't use pumpAndSettle here because the page renders a Flame game
// which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation
}
testWidgets('route creates the correct non debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: false);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('route creates the correct debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: true);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
});
});
group('PinballGameView', () {
@ -230,7 +183,7 @@ void main() {
testWidgets('renders game', (tester) async {
await tester.pumpApp(
PinballGameView(game: game),
PinballGameView(game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
@ -258,7 +211,7 @@ void main() {
);
await tester.pumpApp(
PinballGameView(game: game),
PinballGameView(game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
@ -276,7 +229,6 @@ void main() {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
startGameBloc,
Stream.value(startGameState),
@ -287,17 +239,12 @@ void main() {
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(
find.byType(GameHud),
findsNothing,
);
expect(find.byType(GameHud), findsNothing);
});
testWidgets('keep focus on game when mouse hovers over it', (tester) async {
@ -307,7 +254,6 @@ void main() {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
startGameBloc,
Stream.value(startGameState),
@ -319,28 +265,24 @@ void main() {
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
game.focusNode.unfocus();
await tester.pump();
expect(game.focusNode.hasFocus, isFalse);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo((game.size / 2).toOffset());
await tester.pump();
expect(game.focusNode.hasFocus, isTrue);
});
testWidgets('mobile controls when the overlay is added', (tester) async {
await tester.pumpApp(
PinballGameView(game: game),
PinballGameView(game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
@ -357,23 +299,17 @@ void main() {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(
find.image(Assets.images.linkBox.infoIcon),
findsOneWidget,
);
expect(find.image(Assets.images.linkBox.infoIcon), findsOneWidget);
});
testWidgets('opens MoreInformationDialog when tapped', (tester) async {
@ -386,16 +322,13 @@ void main() {
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
await tester.tap(find.byType(IconButton));
await tester.pump();
expect(
find.byType(MoreInformationDialog),
findsOneWidget,
);
expect(find.byType(MoreInformationDialog), findsOneWidget);
});
});
});

@ -76,6 +76,10 @@
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
fetch("assets/assets/images/loading_game/io_pinball.png").catch(function (e) {
console.warn(e);
});
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {

Loading…
Cancel
Save