Merge branch 'main' into feat/allow-stacking-contact-callbacks-in-fixtures

pull/263/head
Jorge Coca 3 years ago committed by GitHub
commit 75d31ff2e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,8 +6,8 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_acres} /// {@template android_acres}
/// Area positioned on the left side of the board containing the [Spaceship], /// Area positioned on the left side of the board containing the
/// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. /// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
/// {@endtemplate} /// {@endtemplate}
class AndroidAcres extends Blueprint { class AndroidAcres extends Blueprint {
/// {@macro android_acres} /// {@macro android_acres}
@ -18,16 +18,21 @@ class AndroidAcres extends Blueprint {
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-32.52, -9.1), )..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-22.89, -17.35), )..initialPosition = Vector2(-32.6, -9.2),
AndroidBumper.cow(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-20.5, -13.8),
], ],
blueprints: [ blueprints: [
SpaceshipRamp(), SpaceshipRamp(),
Spaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
SpaceshipRail(), SpaceshipRail(),
], ],
); );

@ -16,6 +16,7 @@ class BottomGroup extends Component {
_BottomGroupSide(side: BoardSide.right), _BottomGroupSide(side: BoardSide.right),
_BottomGroupSide(side: BoardSide.left), _BottomGroupSide(side: BoardSide.left),
], ],
priority: RenderPriority.bottomGroup,
); );
} }
@ -28,8 +29,7 @@ class _BottomGroupSide extends Component {
/// {@macro bottom_group_side} /// {@macro bottom_group_side}
_BottomGroupSide({ _BottomGroupSide({
required BoardSide side, required BoardSide side,
}) : _side = side, }) : _side = side;
super(priority: RenderPriority.bottomGroup);
final BoardSide _side; final BoardSide _side;

@ -10,5 +10,6 @@ export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word/google_word.dart'; export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';
export 'sparky_fire_zone.dart'; export 'sparky_fire_zone.dart';

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

@ -0,0 +1,25 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiplier] when GameState.multiplier changes.
class MultipliersBehavior extends Component
with
HasGameRef<PinballGame>,
ParentIsA<Multipliers>,
BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.multiplier != newState.multiplier;
}
@override
void onNewState(GameState state) {
final multipliers = parent.children.whereType<Multiplier>();
for (final multiplier in multipliers) {
multiplier.bloc.next(state.multiplier);
}
}
}

@ -0,0 +1,44 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template multipliers}
/// A group for the multipliers on the board.
/// {@endtemplate}
class Multipliers extends Component {
/// {@macro multipliers}
Multipliers()
: super(
children: [
Multiplier.x2(
position: Vector2(-19.5, -2),
angle: -15 * math.pi / 180,
),
Multiplier.x3(
position: Vector2(13, -9.4),
angle: 15 * math.pi / 180,
),
Multiplier.x4(
position: Vector2(0, -21.2),
angle: 0,
),
Multiplier.x5(
position: Vector2(-8.5, -28),
angle: -3 * math.pi / 180,
),
Multiplier.x6(
position: Vector2(10, -30.7),
angle: 8 * math.pi / 180,
),
MultipliersBehavior(),
],
);
/// Creates [Multipliers] without any children.
///
/// This can be used for testing [Multipliers]'s behaviors in isolation.
@visibleForTesting
Multipliers.test();
}

@ -50,39 +50,42 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.bottom.keyName),
images.load(components.Assets.images.boundary.outer.keyName), images.load(components.Assets.images.boundary.outer.keyName),
images.load(components.Assets.images.boundary.outerBottom.keyName), images.load(components.Assets.images.boundary.outerBottom.keyName),
images.load(components.Assets.images.spaceship.saucer.keyName), images.load(components.Assets.images.android.spaceship.saucer.keyName),
images.load(components.Assets.images.spaceship.bridge.keyName), images
images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), .load(components.Assets.images.android.spaceship.animatronic.keyName),
images.load(components.Assets.images.android.spaceship.lightBeam.keyName),
images.load(components.Assets.images.android.ramp.boardOpening.keyName),
images.load( images.load(
components.Assets.images.spaceship.ramp.railingForeground.keyName, components.Assets.images.android.ramp.railingForeground.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.railingBackground.keyName, components.Assets.images.android.ramp.railingBackground.keyName,
), ),
images.load(components.Assets.images.spaceship.ramp.main.keyName), images.load(components.Assets.images.android.ramp.main.keyName),
images images.load(components.Assets.images.android.ramp.arrow.inactive.keyName),
.load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active1.keyName, components.Assets.images.android.ramp.arrow.active1.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active2.keyName, components.Assets.images.android.ramp.arrow.active2.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active3.keyName, components.Assets.images.android.ramp.arrow.active3.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active4.keyName, components.Assets.images.android.ramp.arrow.active4.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active5.keyName, components.Assets.images.android.ramp.arrow.active5.keyName,
), ),
images.load(components.Assets.images.spaceship.rail.main.keyName), images.load(components.Assets.images.android.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.exit.keyName), images.load(components.Assets.images.android.rail.exit.keyName),
images.load(components.Assets.images.androidBumper.a.lit.keyName), images.load(components.Assets.images.android.bumper.a.lit.keyName),
images.load(components.Assets.images.androidBumper.a.dimmed.keyName), images.load(components.Assets.images.android.bumper.a.dimmed.keyName),
images.load(components.Assets.images.androidBumper.b.lit.keyName), images.load(components.Assets.images.android.bumper.b.lit.keyName),
images.load(components.Assets.images.androidBumper.b.dimmed.keyName), images.load(components.Assets.images.android.bumper.b.dimmed.keyName),
images.load(components.Assets.images.android.bumper.cow.lit.keyName),
images.load(components.Assets.images.android.bumper.cow.dimmed.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.animatronic.keyName), images.load(components.Assets.images.sparky.animatronic.keyName),
@ -101,6 +104,16 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter5.keyName), images.load(components.Assets.images.googleWord.letter5.keyName),
images.load(components.Assets.images.googleWord.letter6.keyName), images.load(components.Assets.images.googleWord.letter6.keyName),
images.load(components.Assets.images.backboard.display.keyName), images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.lit.keyName),
images.load(components.Assets.images.multiplier.x3.dimmed.keyName),
images.load(components.Assets.images.multiplier.x4.lit.keyName),
images.load(components.Assets.images.multiplier.x4.dimmed.keyName),
images.load(components.Assets.images.multiplier.x5.lit.keyName),
images.load(components.Assets.images.multiplier.x5.dimmed.keyName),
images.load(components.Assets.images.multiplier.x6.lit.keyName),
images.load(components.Assets.images.multiplier.x6.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName), images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),

@ -49,10 +49,10 @@ class PinballGame extends Forge2DGame
await add(Drain()); await add(Drain());
await add(BottomGroup()); await add(BottomGroup());
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
final launcher = Launcher(); final launcher = Launcher();
unawaited(addFromBlueprint(launcher)); unawaited(addFromBlueprint(launcher));
await add(Multipliers());
await add(FlutterForest()); await add(FlutterForest());
await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(SparkyFireZone());
await addFromBlueprint(AndroidAcres()); await addFromBlueprint(AndroidAcres());
@ -67,7 +67,7 @@ class PinballGame extends Forge2DGame
), ),
); );
controller.attachTo(launcher.components.whereType<Plunger>().first); controller.attachTo(launcher.components.whereType<Plunger>().single);
await super.onLoad(); await super.onLoad();
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

@ -10,8 +10,7 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAndroidBumperGen get androidBumper => $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
const $AssetsImagesAndroidBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -24,20 +23,23 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesMultiplierGen get multiplier =>
const $AssetsImagesMultiplierGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
} }
class $AssetsImagesAndroidBumperGen { class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidBumperGen(); const $AssetsImagesAndroidGen();
$AssetsImagesAndroidBumperAGen get a => $AssetsImagesAndroidBumperGen get bumper =>
const $AssetsImagesAndroidBumperAGen(); const $AssetsImagesAndroidBumperGen();
$AssetsImagesAndroidBumperBGen get b => $AssetsImagesAndroidRailGen get rail => const $AssetsImagesAndroidRailGen();
const $AssetsImagesAndroidBumperBGen(); $AssetsImagesAndroidRampGen get ramp => const $AssetsImagesAndroidRampGen();
$AssetsImagesAndroidSpaceshipGen get spaceship =>
const $AssetsImagesAndroidSpaceshipGen();
} }
class $AssetsImagesBackboardGen { class $AssetsImagesBackboardGen {
@ -188,6 +190,16 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png'); const AssetGenImage('assets/images/launch_ramp/ramp.png');
} }
class $AssetsImagesMultiplierGen {
const $AssetsImagesMultiplierGen();
$AssetsImagesMultiplierX2Gen get x2 => const $AssetsImagesMultiplierX2Gen();
$AssetsImagesMultiplierX3Gen get x3 => const $AssetsImagesMultiplierX3Gen();
$AssetsImagesMultiplierX4Gen get x4 => const $AssetsImagesMultiplierX4Gen();
$AssetsImagesMultiplierX5Gen get x5 => const $AssetsImagesMultiplierX5Gen();
$AssetsImagesMultiplierX6Gen get x6 => const $AssetsImagesMultiplierX6Gen();
}
class $AssetsImagesPlungerGen { class $AssetsImagesPlungerGen {
const $AssetsImagesPlungerGen(); const $AssetsImagesPlungerGen();
@ -232,23 +244,6 @@ class $AssetsImagesSlingshotGen {
const AssetGenImage('assets/images/slingshot/upper.png'); const AssetGenImage('assets/images/slingshot/upper.png');
} }
class $AssetsImagesSpaceshipGen {
const $AssetsImagesSpaceshipGen();
/// File path: assets/images/spaceship/bridge.png
AssetGenImage get bridge =>
const AssetGenImage('assets/images/spaceship/bridge.png');
$AssetsImagesSpaceshipRailGen get rail =>
const $AssetsImagesSpaceshipRailGen();
$AssetsImagesSpaceshipRampGen get ramp =>
const $AssetsImagesSpaceshipRampGen();
/// File path: assets/images/spaceship/saucer.png
AssetGenImage get saucer =>
const AssetGenImage('assets/images/spaceship/saucer.png');
}
class $AssetsImagesSparkyGen { class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyGen(); const $AssetsImagesSparkyGen();
@ -262,28 +257,66 @@ class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
} }
class $AssetsImagesAndroidBumperAGen { class $AssetsImagesAndroidBumperGen {
const $AssetsImagesAndroidBumperAGen(); const $AssetsImagesAndroidBumperGen();
/// File path: assets/images/android_bumper/a/dimmed.png $AssetsImagesAndroidBumperAGen get a =>
AssetGenImage get dimmed => const $AssetsImagesAndroidBumperAGen();
const AssetGenImage('assets/images/android_bumper/a/dimmed.png'); $AssetsImagesAndroidBumperBGen get b =>
const $AssetsImagesAndroidBumperBGen();
$AssetsImagesAndroidBumperCowGen get cow =>
const $AssetsImagesAndroidBumperCowGen();
}
/// File path: assets/images/android_bumper/a/lit.png class $AssetsImagesAndroidRailGen {
AssetGenImage get lit => const $AssetsImagesAndroidRailGen();
const AssetGenImage('assets/images/android_bumper/a/lit.png');
/// File path: assets/images/android/rail/exit.png
AssetGenImage get exit =>
const AssetGenImage('assets/images/android/rail/exit.png');
/// File path: assets/images/android/rail/main.png
AssetGenImage get main =>
const AssetGenImage('assets/images/android/rail/main.png');
} }
class $AssetsImagesAndroidBumperBGen { class $AssetsImagesAndroidRampGen {
const $AssetsImagesAndroidBumperBGen(); const $AssetsImagesAndroidRampGen();
/// File path: assets/images/android_bumper/b/dimmed.png $AssetsImagesAndroidRampArrowGen get arrow =>
AssetGenImage get dimmed => const $AssetsImagesAndroidRampArrowGen();
const AssetGenImage('assets/images/android_bumper/b/dimmed.png');
/// File path: assets/images/android_bumper/b/lit.png /// File path: assets/images/android/ramp/board-opening.png
AssetGenImage get lit => AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/android_bumper/b/lit.png'); const AssetGenImage('assets/images/android/ramp/board-opening.png');
/// File path: assets/images/android/ramp/main.png
AssetGenImage get main =>
const AssetGenImage('assets/images/android/ramp/main.png');
/// File path: assets/images/android/ramp/railing-background.png
AssetGenImage get railingBackground =>
const AssetGenImage('assets/images/android/ramp/railing-background.png');
/// File path: assets/images/android/ramp/railing-foreground.png
AssetGenImage get railingForeground =>
const AssetGenImage('assets/images/android/ramp/railing-foreground.png');
}
class $AssetsImagesAndroidSpaceshipGen {
const $AssetsImagesAndroidSpaceshipGen();
/// File path: assets/images/android/spaceship/animatronic.png
AssetGenImage get animatronic =>
const AssetGenImage('assets/images/android/spaceship/animatronic.png');
/// File path: assets/images/android/spaceship/light-beam.png
AssetGenImage get lightBeam =>
const AssetGenImage('assets/images/android/spaceship/light-beam.png');
/// File path: assets/images/android/spaceship/saucer.png
AssetGenImage get saucer =>
const AssetGenImage('assets/images/android/spaceship/saucer.png');
} }
class $AssetsImagesDashBumperGen { class $AssetsImagesDashBumperGen {
@ -307,39 +340,64 @@ class $AssetsImagesDinoAnimatronicGen {
const AssetGenImage('assets/images/dino/animatronic/mouth.png'); const AssetGenImage('assets/images/dino/animatronic/mouth.png');
} }
class $AssetsImagesSpaceshipRailGen { class $AssetsImagesMultiplierX2Gen {
const $AssetsImagesSpaceshipRailGen(); const $AssetsImagesMultiplierX2Gen();
/// File path: assets/images/spaceship/rail/exit.png /// File path: assets/images/multiplier/x2/dimmed.png
AssetGenImage get exit => AssetGenImage get dimmed =>
const AssetGenImage('assets/images/spaceship/rail/exit.png'); const AssetGenImage('assets/images/multiplier/x2/dimmed.png');
/// File path: assets/images/spaceship/rail/main.png /// File path: assets/images/multiplier/x2/lit.png
AssetGenImage get main => AssetGenImage get lit =>
const AssetGenImage('assets/images/spaceship/rail/main.png'); const AssetGenImage('assets/images/multiplier/x2/lit.png');
} }
class $AssetsImagesSpaceshipRampGen { class $AssetsImagesMultiplierX3Gen {
const $AssetsImagesSpaceshipRampGen(); const $AssetsImagesMultiplierX3Gen();
$AssetsImagesSpaceshipRampArrowGen get arrow => /// File path: assets/images/multiplier/x3/dimmed.png
const $AssetsImagesSpaceshipRampArrowGen(); AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x3/dimmed.png');
/// File path: assets/images/spaceship/ramp/board-opening.png /// File path: assets/images/multiplier/x3/lit.png
AssetGenImage get boardOpening => AssetGenImage get lit =>
const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); const AssetGenImage('assets/images/multiplier/x3/lit.png');
}
/// File path: assets/images/spaceship/ramp/main.png class $AssetsImagesMultiplierX4Gen {
AssetGenImage get main => const $AssetsImagesMultiplierX4Gen();
const AssetGenImage('assets/images/spaceship/ramp/main.png');
/// File path: assets/images/spaceship/ramp/railing-background.png /// File path: assets/images/multiplier/x4/dimmed.png
AssetGenImage get railingBackground => const AssetGenImage( AssetGenImage get dimmed =>
'assets/images/spaceship/ramp/railing-background.png'); const AssetGenImage('assets/images/multiplier/x4/dimmed.png');
/// File path: assets/images/spaceship/ramp/railing-foreground.png /// File path: assets/images/multiplier/x4/lit.png
AssetGenImage get railingForeground => const AssetGenImage( AssetGenImage get lit =>
'assets/images/spaceship/ramp/railing-foreground.png'); const AssetGenImage('assets/images/multiplier/x4/lit.png');
}
class $AssetsImagesMultiplierX5Gen {
const $AssetsImagesMultiplierX5Gen();
/// File path: assets/images/multiplier/x5/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x5/dimmed.png');
/// File path: assets/images/multiplier/x5/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x5/lit.png');
}
class $AssetsImagesMultiplierX6Gen {
const $AssetsImagesMultiplierX6Gen();
/// File path: assets/images/multiplier/x6/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x6/dimmed.png');
/// File path: assets/images/multiplier/x6/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x6/lit.png');
} }
class $AssetsImagesSparkyBumperGen { class $AssetsImagesSparkyBumperGen {
@ -362,6 +420,70 @@ class $AssetsImagesSparkyComputerGen {
const AssetGenImage('assets/images/sparky/computer/top.png'); const AssetGenImage('assets/images/sparky/computer/top.png');
} }
class $AssetsImagesAndroidBumperAGen {
const $AssetsImagesAndroidBumperAGen();
/// File path: assets/images/android/bumper/a/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android/bumper/a/dimmed.png');
/// File path: assets/images/android/bumper/a/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android/bumper/a/lit.png');
}
class $AssetsImagesAndroidBumperBGen {
const $AssetsImagesAndroidBumperBGen();
/// File path: assets/images/android/bumper/b/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android/bumper/b/dimmed.png');
/// File path: assets/images/android/bumper/b/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android/bumper/b/lit.png');
}
class $AssetsImagesAndroidBumperCowGen {
const $AssetsImagesAndroidBumperCowGen();
/// File path: assets/images/android/bumper/cow/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android/bumper/cow/dimmed.png');
/// File path: assets/images/android/bumper/cow/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android/bumper/cow/lit.png');
}
class $AssetsImagesAndroidRampArrowGen {
const $AssetsImagesAndroidRampArrowGen();
/// File path: assets/images/android/ramp/arrow/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/android/ramp/arrow/active1.png');
/// File path: assets/images/android/ramp/arrow/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/android/ramp/arrow/active2.png');
/// File path: assets/images/android/ramp/arrow/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/android/ramp/arrow/active3.png');
/// File path: assets/images/android/ramp/arrow/active4.png
AssetGenImage get active4 =>
const AssetGenImage('assets/images/android/ramp/arrow/active4.png');
/// File path: assets/images/android/ramp/arrow/active5.png
AssetGenImage get active5 =>
const AssetGenImage('assets/images/android/ramp/arrow/active5.png');
/// File path: assets/images/android/ramp/arrow/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/android/ramp/arrow/inactive.png');
}
class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen(); const $AssetsImagesDashBumperAGen();
@ -398,34 +520,6 @@ class $AssetsImagesDashBumperMainGen {
const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); const AssetGenImage('assets/images/dash/bumper/main/inactive.png');
} }
class $AssetsImagesSpaceshipRampArrowGen {
const $AssetsImagesSpaceshipRampArrowGen();
/// File path: assets/images/spaceship/ramp/arrow/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active1.png');
/// File path: assets/images/spaceship/ramp/arrow/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active2.png');
/// File path: assets/images/spaceship/ramp/arrow/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active3.png');
/// File path: assets/images/spaceship/ramp/arrow/active4.png
AssetGenImage get active4 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active4.png');
/// File path: assets/images/spaceship/ramp/arrow/active5.png
AssetGenImage get active5 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active5.png');
/// File path: assets/images/spaceship/ramp/arrow/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/inactive.png');
}
class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();

@ -10,7 +10,7 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_bumper_cubit.dart'; export 'cubit/android_bumper_cubit.dart';
/// {@template android_bumper} /// {@template android_bumper}
/// Bumper for area under the [Spaceship]. /// Bumper for area under the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class AndroidBumper extends BodyComponent with InitialPosition { class AndroidBumper extends BodyComponent with InitialPosition {
/// {@macro android_bumper} /// {@macro android_bumper}
@ -19,6 +19,7 @@ class AndroidBumper extends BodyComponent with InitialPosition {
required double minorRadius, required double minorRadius,
required String litAssetPath, required String litAssetPath,
required String dimmedAssetPath, required String dimmedAssetPath,
required Vector2 spritePosition,
Iterable<Component>? children, Iterable<Component>? children,
required this.bloc, required this.bloc,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
@ -32,6 +33,7 @@ class AndroidBumper extends BodyComponent with InitialPosition {
_AndroidBumperSpriteGroupComponent( _AndroidBumperSpriteGroupComponent(
dimmedAssetPath: dimmedAssetPath, dimmedAssetPath: dimmedAssetPath,
litAssetPath: litAssetPath, litAssetPath: litAssetPath,
position: spritePosition,
state: bloc.state, state: bloc.state,
), ),
...?children, ...?children,
@ -44,8 +46,9 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.52, majorRadius: 3.52,
minorRadius: 2.97, minorRadius: 2.97,
litAssetPath: Assets.images.androidBumper.a.lit.keyName, litAssetPath: Assets.images.android.bumper.a.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.a.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.a.dimmed.keyName,
spritePosition: Vector2(0, -0.1),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
); );
@ -56,8 +59,22 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.19, majorRadius: 3.19,
minorRadius: 2.79, minorRadius: 2.79,
litAssetPath: Assets.images.androidBumper.b.lit.keyName, litAssetPath: Assets.images.android.bumper.b.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.b.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.b.dimmed.keyName,
spritePosition: Vector2(0, -0.1),
bloc: AndroidBumperCubit(),
children: children,
);
/// {@macro android_bumper}
AndroidBumper.cow({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.4,
minorRadius: 2.9,
litAssetPath: Assets.images.android.bumper.cow.lit.keyName,
dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName,
spritePosition: Vector2(0, -0.68),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
); );
@ -113,12 +130,13 @@ class _AndroidBumperSpriteGroupComponent
_AndroidBumperSpriteGroupComponent({ _AndroidBumperSpriteGroupComponent({
required String litAssetPath, required String litAssetPath,
required String dimmedAssetPath, required String dimmedAssetPath,
required Vector2 position,
required AndroidBumperState state, required AndroidBumperState state,
}) : _litAssetPath = litAssetPath, }) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath, _dimmedAssetPath = dimmedAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -0.1), position: position,
current: state, current: state,
); );

@ -5,7 +5,7 @@ import 'package:bloc/bloc.dart';
part 'android_bumper_state.dart'; part 'android_bumper_state.dart';
class AndroidBumperCubit extends Cubit<AndroidBumperState> { class AndroidBumperCubit extends Cubit<AndroidBumperState> {
AndroidBumperCubit() : super(AndroidBumperState.dimmed); AndroidBumperCubit() : super(AndroidBumperState.lit);
void onBallContacted() { void onBallContacted() {
emit(AndroidBumperState.dimmed); emit(AndroidBumperState.dimmed);

@ -0,0 +1,209 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
class AndroidSpaceship extends Blueprint {
AndroidSpaceship({required Vector2 position})
: super(
components: [
_SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25),
_SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: RenderPriority.ballOnSpaceshipRail,
)..initialPosition = position - Vector2(5.3, -5.4),
_SpaceshipHole(
outsideLayer: Layer.board,
outsidePriority: RenderPriority.ballOnBoard,
)..initialPosition = position - Vector2(-7.5, -1.1),
],
);
}
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
_SpaceshipSaucer() : super(renderBody: false) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = _SpaceshipSaucerShape();
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
angle: -1.7,
);
return world.createBody(bodyDef)..createFixtureFromShape(shape);
}
}
class _SpaceshipSaucerShape extends ChainShape {
_SpaceshipSaucerShape() {
const minorRadius = 9.75;
const majorRadius = 11.9;
createChain(
[
for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180)
Vector2(
minorRadius * math.cos(angle),
majorRadius * math.sin(angle),
),
],
);
}
}
class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_SpaceshipSaucerSpriteAnimationComponent()
: super(
anchor: Anchor.center,
priority: RenderPriority.spaceshipSaucer,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.saucer.keyName,
);
const amountPerRow = 5;
const amountPerColumn = 3;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
}
}
// TODO(allisonryan0002): add pulsing behavior.
class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef {
_LightBeamSpriteComponent()
: super(
anchor: Anchor.center,
priority: RenderPriority.spaceshipLightBeam,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.android.spaceship.lightBeam.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _AndroidHead extends BodyComponent with InitialPosition, Layered {
_AndroidHead()
: super(
priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimationComponent()],
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3.1,
minorRadius: 2,
)..rotate(1.4);
// TODO(allisonryan0002): use bumping behavior.
final fixtureDef = FixtureDef(
shape,
restitution: 0.1,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_AndroidHeadSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-0.24, -2.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
);
const amountPerRow = 18;
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
}
}
class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super(
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down,
insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority,
) {
layer = Layer.spaceship;
}
@override
Shape get shape {
return ArcShape(
center: Vector2(0, -3.2),
arcRadius: 5,
angle: 1,
rotation: -2,
);
}
}

@ -1,4 +1,5 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template bumping_behavior} /// {@template bumping_behavior}
@ -11,15 +12,22 @@ class BumpingBehavior extends ContactBehavior {
/// Determines how strong the bump is. /// Determines how strong the bump is.
final double _strength; final double _strength;
/// This is used to recoginze the current state of a contact manifold in world
/// coordinates.
@visibleForTesting
final WorldManifold worldManifold = WorldManifold();
@override @override
void postSolve(Object other, Contact contact, ContactImpulse impulse) { void postSolve(Object other, Contact contact, ContactImpulse impulse) {
super.postSolve(other, contact, impulse); super.postSolve(other, contact, impulse);
if (other is! BodyComponent) return; if (other is! BodyComponent) return;
contact.getWorldManifold(worldManifold);
other.body.applyLinearImpulse( other.body.applyLinearImpulse(
contact.manifold.localPoint worldManifold.normal
..normalize() ..multiply(
..multiply(Vector2.all(other.body.mass * _strength)), Vector2.all(other.body.mass * _strength),
),
); );
} }
} }

@ -1,4 +1,5 @@
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
@ -19,6 +20,7 @@ export 'kicker.dart';
export 'launch_ramp.dart'; export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'layer_sensor.dart'; export 'layer_sensor.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'render_priority.dart'; export 'render_priority.dart';
export 'rocket.dart'; export 'rocket.dart';
@ -26,7 +28,6 @@ export 'score_text.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart'; export 'signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';

@ -0,0 +1,25 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_components/pinball_components.dart';
part 'multiplier_state.dart';
class MultiplierCubit extends Cubit<MultiplierState> {
MultiplierCubit(MultiplierValue multiplierValue)
: super(MultiplierState.initial(multiplierValue));
/// Event added when the game's current multiplier changes.
void next(int multiplier) {
if (state.value.equals(multiplier)) {
if (state.spriteState == MultiplierSpriteState.dimmed) {
emit(state.copyWith(spriteState: MultiplierSpriteState.lit));
}
} else {
if (state.spriteState == MultiplierSpriteState.lit) {
emit(state.copyWith(spriteState: MultiplierSpriteState.dimmed));
}
}
}
}

@ -0,0 +1,56 @@
// ignore_for_file: public_member_api_docs
part of 'multiplier_cubit.dart';
enum MultiplierSpriteState {
lit,
dimmed,
}
class MultiplierState extends Equatable {
const MultiplierState({
required this.value,
required this.spriteState,
});
const MultiplierState.initial(MultiplierValue multiplierValue)
: this(
value: multiplierValue,
spriteState: MultiplierSpriteState.dimmed,
);
/// Current value for the [Multiplier]
final MultiplierValue value;
/// The [MultiplierSpriteGroupComponent] current sprite state
final MultiplierSpriteState spriteState;
MultiplierState copyWith({
MultiplierSpriteState? spriteState,
}) {
return MultiplierState(
value: value,
spriteState: spriteState ?? this.spriteState,
);
}
@override
List<Object> get props => [value, spriteState];
}
extension MultiplierValueX on MultiplierValue {
bool equals(int value) {
switch (this) {
case MultiplierValue.x2:
return value == 2;
case MultiplierValue.x3:
return value == 3;
case MultiplierValue.x4:
return value == 4;
case MultiplierValue.x5:
return value == 5;
case MultiplierValue.x6:
return value == 6;
}
}
}

@ -0,0 +1,204 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/src/components/multiplier/cubit/multiplier_cubit.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/multiplier_cubit.dart';
/// {@template multiplier}
/// Backlit multiplier decal displayed on the board.
/// {@endtemplate}
class Multiplier extends Component {
/// {@macro multiplier}
Multiplier._({
required MultiplierValue value,
required Vector2 position,
required double angle,
required this.bloc,
}) : _value = value,
_position = position,
_angle = angle,
super();
/// {@macro multiplier}
Multiplier.x2({
required Vector2 position,
required double angle,
}) : this._(
value: MultiplierValue.x2,
position: position,
angle: angle,
bloc: MultiplierCubit(MultiplierValue.x2),
);
/// {@macro multiplier}
Multiplier.x3({
required Vector2 position,
required double angle,
}) : this._(
value: MultiplierValue.x3,
position: position,
angle: angle,
bloc: MultiplierCubit(MultiplierValue.x3),
);
/// {@macro multiplier}
Multiplier.x4({
required Vector2 position,
required double angle,
}) : this._(
value: MultiplierValue.x4,
position: position,
angle: angle,
bloc: MultiplierCubit(MultiplierValue.x4),
);
/// {@macro multiplier}
Multiplier.x5({
required Vector2 position,
required double angle,
}) : this._(
value: MultiplierValue.x5,
position: position,
angle: angle,
bloc: MultiplierCubit(MultiplierValue.x5),
);
/// {@macro multiplier}
Multiplier.x6({
required Vector2 position,
required double angle,
}) : this._(
value: MultiplierValue.x6,
position: position,
angle: angle,
bloc: MultiplierCubit(MultiplierValue.x6),
);
/// Creates a [Multiplier] without any children.
///
/// This can be used for testing [Multiplier]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
Multiplier.test({
required MultiplierValue value,
required this.bloc,
}) : _value = value,
_position = Vector2.zero(),
_angle = 0;
// TODO(ruimiguel): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final MultiplierCubit bloc;
final MultiplierValue _value;
final Vector2 _position;
final double _angle;
late final MultiplierSpriteGroupComponent _sprite;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Future<void> onLoad() async {
await super.onLoad();
_sprite = MultiplierSpriteGroupComponent(
position: _position,
litAssetPath: _value.litAssetPath,
dimmedAssetPath: _value.dimmedAssetPath,
angle: _angle,
current: bloc.state,
);
await add(_sprite);
}
}
/// Available multiplier values.
enum MultiplierValue {
x2,
x3,
x4,
x5,
x6,
}
extension on MultiplierValue {
String get litAssetPath {
switch (this) {
case MultiplierValue.x2:
return Assets.images.multiplier.x2.lit.keyName;
case MultiplierValue.x3:
return Assets.images.multiplier.x3.lit.keyName;
case MultiplierValue.x4:
return Assets.images.multiplier.x4.lit.keyName;
case MultiplierValue.x5:
return Assets.images.multiplier.x5.lit.keyName;
case MultiplierValue.x6:
return Assets.images.multiplier.x6.lit.keyName;
}
}
String get dimmedAssetPath {
switch (this) {
case MultiplierValue.x2:
return Assets.images.multiplier.x2.dimmed.keyName;
case MultiplierValue.x3:
return Assets.images.multiplier.x3.dimmed.keyName;
case MultiplierValue.x4:
return Assets.images.multiplier.x4.dimmed.keyName;
case MultiplierValue.x5:
return Assets.images.multiplier.x5.dimmed.keyName;
case MultiplierValue.x6:
return Assets.images.multiplier.x6.dimmed.keyName;
}
}
}
/// {@template multiplier_sprite_group_component}
/// A [SpriteGroupComponent] for a [Multiplier] with lit and dimmed states.
/// {@endtemplate}
@visibleForTesting
class MultiplierSpriteGroupComponent
extends SpriteGroupComponent<MultiplierSpriteState>
with HasGameRef, ParentIsA<Multiplier> {
/// {@macro multiplier_sprite_group_component}
MultiplierSpriteGroupComponent({
required Vector2 position,
required String litAssetPath,
required String dimmedAssetPath,
required double angle,
required MultiplierState current,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
anchor: Anchor.center,
position: position,
angle: angle,
current: current.spriteState,
);
final String _litAssetPath;
final String _dimmedAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.spriteState);
final sprites = {
MultiplierSpriteState.lit:
Sprite(gameRef.images.fromCache(_litAssetPath)),
MultiplierSpriteState.dimmed:
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -20,7 +20,7 @@ abstract class RenderPriority {
static const int ballOnSpaceshipRamp = static const int ballOnSpaceshipRamp =
_above + spaceshipRampBackgroundRailing; _above + spaceshipRampBackgroundRailing;
/// Render priority for the [Ball] while it's on the [Spaceship]. /// Render priority for the [Ball] while it's on the [AndroidSpaceship].
static const int ballOnSpaceship = _above + spaceshipSaucer; static const int ballOnSpaceship = _above + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [SpaceshipRail]. /// Render priority for the [Ball] while it's on the [SpaceshipRail].
@ -91,7 +91,7 @@ abstract class RenderPriority {
static const int spaceshipSaucer = _above + ballOnSpaceshipRail; static const int spaceshipSaucer = _above + ballOnSpaceshipRail;
static const int spaceshipSaucerWall = _above + spaceshipSaucer; static const int spaceshipLightBeam = _below + spaceshipSaucer;
static const int androidHead = _above + spaceshipSaucer; static const int androidHead = _above + spaceshipSaucer;

@ -1,246 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship}
/// A [Blueprint] which creates the spaceship feature.
/// {@endtemplate}
class Spaceship extends Blueprint {
/// {@macro spaceship}
Spaceship({required Vector2 position})
: super(
components: [
SpaceshipSaucer()..initialPosition = position,
_SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position,
_SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: RenderPriority.ballOnSpaceshipRail,
)..initialPosition = position - Vector2(5.2, -4.8),
_SpaceshipHole(
outsideLayer: Layer.board,
outsidePriority: RenderPriority.ballOnBoard,
)..initialPosition = position - Vector2(-7.2, -0.8),
SpaceshipWall()..initialPosition = position,
],
);
/// Total size of the spaceship.
static final size = Vector2(25, 19);
}
/// {@template spaceship_saucer}
/// A [BodyComponent] for the base, or the saucer of the spaceship
/// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer}
SpaceshipSaucer()
: super(
priority: RenderPriority.spaceshipSaucer,
renderBody: false,
children: [
_SpaceshipSaucerSpriteComponent(),
],
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 3;
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef {
_SpaceshipSaucerSpriteComponent()
: super(
anchor: Anchor.center,
// TODO(alestiago): Refactor to use sprite orignial size instead.
size: Spaceship.size,
);
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Use cached sprite.
sprite = await gameRef.loadSprite(
Assets.images.spaceship.saucer.keyName,
);
}
}
/// {@template spaceship_bridge}
/// A [BodyComponent] that provides both the collision and the rotation
/// animation for the bridge.
/// {@endtemplate}
class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge}
AndroidHead()
: super(
priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimation()],
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final circleShape = CircleShape()..radius = 2;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)
..createFixture(
FixtureDef(circleShape)..restitution = 0.4,
);
}
}
class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final image = await gameRef.images.load(
Assets.images.spaceship.bridge.keyName,
);
size = Vector2(8.2, 10);
position = Vector2(0, -2);
anchor = Anchor.center;
final data = SpriteAnimationData.sequenced(
amount: 72,
amountPerRow: 24,
stepTime: 0.05,
textureSize: size * 10,
);
animation = SpriteAnimation.fromFrameData(image, data);
}
}
class _SpaceshipEntrance extends LayerSensor {
_SpaceshipEntrance()
: super(
insideLayer: Layer.spaceship,
orientation: LayerEntranceOrientation.up,
insidePriority: RenderPriority.ballOnSpaceship,
) {
layer = Layer.spaceship;
}
@override
Shape get shape {
final radius = Spaceship.size.y / 2;
return PolygonShape()
..setAsEdge(
Vector2(
radius * cos(20 * pi / 180),
radius * sin(20 * pi / 180),
)..rotate(90 * pi / 180),
Vector2(
radius * cos(340 * pi / 180),
radius * sin(340 * pi / 180),
)..rotate(90 * pi / 180),
);
}
}
class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super(
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down,
insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority,
) {
layer = Layer.spaceship;
}
@override
Shape get shape {
return ArcShape(
center: Vector2(0, -3.2),
arcRadius: 5,
angle: 1,
rotation: -2,
);
}
}
/// {@template spaceship_wall_shape}
/// The [ChainShape] that defines the shape of the [SpaceshipWall].
/// {@endtemplate}
class _SpaceshipWallShape extends ChainShape {
/// {@macro spaceship_wall_shape}
_SpaceshipWallShape() {
final minorRadius = (Spaceship.size.y - 2) / 2;
final majorRadius = (Spaceship.size.x - 2) / 2;
createChain(
[
// TODO(alestiago): Try converting this logic to radian.
for (var angle = 20; angle <= 340; angle++)
Vector2(
minorRadius * cos(angle * pi / 180),
majorRadius * sin(angle * pi / 180),
),
],
);
}
}
/// {@template spaceship_wall}
/// A [BodyComponent] that provides the collision for the wall
/// surrounding the spaceship.
///
/// It has a small opening to allow the [Ball] to get inside the spaceship
/// saucer.
///
/// It also contains the [SpriteComponent] for the lower wall
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall}
SpaceshipWall()
: super(
priority: RenderPriority.spaceshipSaucerWall,
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = _SpaceshipWallShape();
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
angle: -1.7,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail} /// {@template spaceship_rail}
/// A [Blueprint] for the rail exiting the [Spaceship]. /// A [Blueprint] for the rail exiting the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRail extends Blueprint { class SpaceshipRail extends Blueprint {
/// {@macro spaceship_rail} /// {@macro spaceship_rail}
@ -116,7 +116,7 @@ class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef {
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -139,7 +139,7 @@ class _SpaceshipRailExitSpriteComponent extends SpriteComponent
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;

@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// A [Blueprint] which creates the ramp leading into the [Spaceship]. /// A [Blueprint] which creates the ramp leading into the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRamp extends Blueprint { class SpaceshipRamp extends Blueprint {
/// {@macro spaceship_ramp} /// {@macro spaceship_ramp}
@ -73,17 +73,17 @@ extension on SpaceshipRampArrowSpriteState {
String get path { String get path {
switch (this) { switch (this) {
case SpaceshipRampArrowSpriteState.inactive: case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.spaceship.ramp.arrow.inactive.keyName; return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1: case SpaceshipRampArrowSpriteState.active1:
return Assets.images.spaceship.ramp.arrow.active1.keyName; return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2: case SpaceshipRampArrowSpriteState.active2:
return Assets.images.spaceship.ramp.arrow.active2.keyName; return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3: case SpaceshipRampArrowSpriteState.active3:
return Assets.images.spaceship.ramp.arrow.active3.keyName; return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4: case SpaceshipRampArrowSpriteState.active4:
return Assets.images.spaceship.ramp.arrow.active4.keyName; return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5: case SpaceshipRampArrowSpriteState.active5:
return Assets.images.spaceship.ramp.arrow.active5.keyName; return Assets.images.android.ramp.arrow.active5.keyName;
} }
} }
@ -161,7 +161,7 @@ class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -182,7 +182,7 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -234,7 +234,7 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -304,7 +304,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;

@ -45,7 +45,6 @@ flutter:
- asset: fonts/PixeloidMono-1G8ae.ttf - asset: fonts/PixeloidMono-1G8ae.ttf
assets: assets:
- assets/images/
- assets/images/ball/ - assets/images/ball/
- assets/images/baseboard/ - assets/images/baseboard/
- assets/images/boundary/ - assets/images/boundary/
@ -57,15 +56,16 @@ flutter:
- assets/images/dash/bumper/a/ - assets/images/dash/bumper/a/
- assets/images/dash/bumper/b/ - assets/images/dash/bumper/b/
- assets/images/dash/bumper/main/ - assets/images/dash/bumper/main/
- assets/images/spaceship/ - assets/images/android/spaceship/
- assets/images/spaceship/rail/ - assets/images/android/rail/
- assets/images/spaceship/ramp/ - assets/images/android/ramp/
- assets/images/spaceship/ramp/arrow/ - assets/images/android/ramp/arrow/
- assets/images/android/bumper/a/
- assets/images/android/bumper/b/
- assets/images/android/bumper/cow/
- assets/images/kicker/ - assets/images/kicker/
- assets/images/plunger/ - assets/images/plunger/
- assets/images/slingshot/ - assets/images/slingshot/
- assets/images/android_bumper/a/
- assets/images/android_bumper/b/
- assets/images/sparky/ - assets/images/sparky/
- assets/images/sparky/computer/ - assets/images/sparky/computer/
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/
@ -74,6 +74,11 @@ flutter:
- assets/images/backboard/ - assets/images/backboard/
- assets/images/google_word/ - assets/images/google_word/
- assets/images/signpost/ - assets/images/signpost/
- assets/images/multiplier/x2/
- assets/images/multiplier/x3/
- assets/images/multiplier/x4/
- assets/images/multiplier/x5/
- assets/images/multiplier/x6/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -27,6 +27,7 @@ void main() {
addScoreTextStories(dashbook); addScoreTextStories(dashbook);
addBackboardStories(dashbook); addBackboardStories(dashbook);
addDinoWallStories(dashbook); addDinoWallStories(dashbook);
addMultipliersStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -9,8 +9,8 @@ class AndroidBumperAGame extends BallGame {
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,
], ],
); );

@ -9,8 +9,8 @@ class AndroidBumperBGame extends BallGame {
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
], ],
); );

@ -0,0 +1,33 @@
import 'dart:async';
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidBumperCowGame extends BallGame {
AndroidBumperCowGame()
: super(
imagesFileNames: [
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
],
);
static const description = '''
Shows how a AndroidBumper.cow is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(
AndroidBumper.cow()..priority = 1,
);
await traceAllBodies();
}
}

@ -0,0 +1,38 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidSpaceshipGame extends BallGame {
AndroidSpaceshipGame()
: super(
ballPriority: RenderPriority.ballOnSpaceship,
ballLayer: Layer.spaceship,
imagesFileNames: [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
],
);
static const description = '''
Shows how the AndroidSpaceship is rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a Ball into the game.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addFromBlueprint(
AndroidSpaceship(position: Vector2.zero()),
);
await traceAllBodies();
}
}

@ -1,35 +0,0 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/common/common.dart';
class SpaceshipGame extends AssetsGame with TapDetector {
static const description = '''
Shows how a Spaceship works.
- Tap anywhere on the screen to spawn a Ball into the game.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addFromBlueprint(
Spaceship(position: Vector2.zero()),
);
await ready();
}
@override
void onTapUp(TapUpInfo info) {
add(
Ball(baseColor: Colors.blue)
..initialPosition = info.eventPosition.game
..layer = Layer.spaceshipEntranceRamp,
);
}
}

@ -13,8 +13,8 @@ class SpaceshipRailGame extends BallGame {
ballPriority: RenderPriority.ballOnSpaceshipRail, ballPriority: RenderPriority.ballOnSpaceshipRail,
ballLayer: Layer.spaceshipExitRail, ballLayer: Layer.spaceshipExitRail,
imagesFileNames: [ imagesFileNames: [
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
], ],
); );

@ -14,16 +14,16 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
ballPriority: RenderPriority.ballOnSpaceshipRamp, ballPriority: RenderPriority.ballOnSpaceshipRamp,
ballLayer: Layer.spaceshipEntranceRamp, ballLayer: Layer.spaceshipEntranceRamp,
imagesFileNames: [ imagesFileNames: [
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName, Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName, Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName, Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.android.ramp.arrow.active5.keyName,
], ],
); );

@ -2,7 +2,8 @@ import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart';
import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_cow_game.dart';
import 'package:sandbox/stories/android_acres/android_spaceship_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart'; import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart'; import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart';
@ -19,9 +20,14 @@ void addAndroidAcresStories(Dashbook dashbook) {
gameBuilder: (_) => AndroidBumperBGame(), gameBuilder: (_) => AndroidBumperBGame(),
) )
..addGame( ..addGame(
title: 'Spaceship', title: 'Android Bumper Cow',
description: SpaceshipGame.description, description: AndroidBumperCowGame.description,
gameBuilder: (_) => SpaceshipGame(), gameBuilder: (_) => AndroidBumperCowGame(),
)
..addGame(
title: 'Android Spaceship',
description: AndroidSpaceshipGame.description,
gameBuilder: (_) => AndroidSpaceshipGame(),
) )
..addGame( ..addGame(
title: 'Spaceship Rail', title: 'Spaceship Rail',

@ -0,0 +1,97 @@
import 'dart:math' as math;
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class MultipliersGame extends BallGame with KeyboardEvents {
MultipliersGame()
: super(
imagesFileNames: [
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
Assets.images.multiplier.x3.dimmed.keyName,
Assets.images.multiplier.x4.lit.keyName,
Assets.images.multiplier.x4.dimmed.keyName,
Assets.images.multiplier.x5.lit.keyName,
Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.keyName,
Assets.images.multiplier.x6.dimmed.keyName,
],
);
static const description = '''
Shows how the Multipliers are rendered.
- Tap anywhere on the screen to spawn a ball into the game.
- Press digits 2 to 6 for toggle state multipliers 2 to 6.
''';
final List<Multiplier> multipliers = [
Multiplier.x2(
position: Vector2(-20, 0),
angle: -15 * math.pi / 180,
),
Multiplier.x3(
position: Vector2(20, -5),
angle: 15 * math.pi / 180,
),
Multiplier.x4(
position: Vector2(0, -15),
angle: 0,
),
Multiplier.x5(
position: Vector2(-10, -25),
angle: -3 * math.pi / 180,
),
Multiplier.x6(
position: Vector2(10, -35),
angle: 8 * math.pi / 180,
),
];
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addAll(multipliers);
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent) {
var currentMultiplier = 1;
if (event.logicalKey == LogicalKeyboardKey.digit2) {
currentMultiplier = 2;
}
if (event.logicalKey == LogicalKeyboardKey.digit3) {
currentMultiplier = 3;
}
if (event.logicalKey == LogicalKeyboardKey.digit4) {
currentMultiplier = 4;
}
if (event.logicalKey == LogicalKeyboardKey.digit5) {
currentMultiplier = 5;
}
if (event.logicalKey == LogicalKeyboardKey.digit6) {
currentMultiplier = 6;
}
for (final multiplier in multipliers) {
multiplier.bloc.next(currentMultiplier);
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}

@ -0,0 +1,11 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/multipliers/multipliers_game.dart';
void addMultipliersStories(Dashbook dashbook) {
dashbook.storiesOf('Multipliers').addGame(
title: 'Multipliers',
description: MultipliersGame.description,
gameBuilder: (_) => MultipliersGame(),
);
}

@ -10,6 +10,7 @@ export 'flutter_forest/stories.dart';
export 'google_word/stories.dart'; export 'google_word/stories.dart';
export 'launch_ramp/stories.dart'; export 'launch_ramp/stories.dart';
export 'layer/stories.dart'; export 'layer/stories.dart';
export 'multipliers/stories.dart';
export 'plunger/stories.dart'; export 'plunger/stories.dart';
export 'score_text/stories.dart'; export 'score_text/stories.dart';
export 'slingshot/stories.dart'; export 'slingshot/stories.dart';

@ -15,6 +15,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.8.2"
bloc:
dependency: transitive
description:
name: bloc
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.3"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -171,7 +178,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.4" version: "0.6.3"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -199,7 +206,7 @@ packages:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.4" version: "0.1.3"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -220,7 +227,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -351,7 +358,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -386,7 +393,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.9" version: "0.4.8"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -456,7 +463,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.1"
very_good_analysis: very_good_analysis:
dependency: "direct dev" dependency: "direct dev"
description: description:

@ -24,3 +24,5 @@ class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}

@ -13,10 +13,12 @@ import '../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
@ -33,6 +35,12 @@ void main() {
expect(game.contains(androidBumper), isTrue); expect(game.contains(androidBumper), isTrue);
}); });
flameTester.test('"cow" loads correctly', (game) async {
final androidBumper = AndroidBumper.cow();
await game.ensureAdd(androidBumper);
expect(game.contains(androidBumper), isTrue);
});
// TODO(alestiago): Consider refactoring once the following is merged: // TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538 // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs

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

@ -7,22 +7,16 @@ import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart'; import 'package:pinball_components/src/components/bumping_behavior.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
import 'layer_test.dart';
class MockContactImpulse extends Mock implements ContactImpulse {} class _MockContact extends Mock implements Contact {}
class MockManifold extends Mock implements Manifold {} class _MockContactImpulse extends Mock implements ContactImpulse {}
class TestHeavyBodyComponent extends BodyComponent { class _TestBodyComponent extends BodyComponent {
@override @override
Body createBody() { Body createBody() => world.createBody(
final shape = CircleShape(); BodyDef(type: BodyType.dynamic),
return world.createBody( )..createFixtureFromShape(CircleShape(), 1);
BodyDef(
type: BodyType.dynamic,
),
)..createFixtureFromShape(shape, 20);
}
} }
void main() { void main() {
@ -32,7 +26,7 @@ void main() {
group('BumpingBehavior', () { group('BumpingBehavior', () {
flameTester.test('can be added', (game) async { flameTester.test('can be added', (game) async {
final behavior = BumpingBehavior(strength: 0); final behavior = BumpingBehavior(strength: 0);
final component = TestBodyComponent(); final component = _TestBodyComponent();
await component.add(behavior); await component.add(behavior);
await game.ensureAdd(component); await game.ensureAdd(component);
}); });
@ -40,16 +34,18 @@ void main() {
flameTester.testGameWidget( flameTester.testGameWidget(
'the bump is greater when the strengh is greater', 'the bump is greater when the strengh is greater',
setUp: (game, tester) async { setUp: (game, tester) async {
final component1 = TestBodyComponent(); final component1 = _TestBodyComponent();
final behavior1 = BumpingBehavior(strength: 1); final behavior1 = BumpingBehavior(strength: 1)
..worldManifold.normal.setFrom(Vector2.all(1));
await component1.add(behavior1); await component1.add(behavior1);
final component2 = TestBodyComponent(); final component2 = _TestBodyComponent();
final behavior2 = BumpingBehavior(strength: 2); final behavior2 = BumpingBehavior(strength: 2)
..worldManifold.normal.setFrom(Vector2.all(1));
await component2.add(behavior2); await component2.add(behavior2);
final dummy1 = TestHeavyBodyComponent(); final dummy1 = _TestBodyComponent();
final dummy2 = TestHeavyBodyComponent(); final dummy2 = _TestBodyComponent();
await game.ensureAddAll([ await game.ensureAddAll([
component1, component1,
@ -58,14 +54,8 @@ void main() {
dummy2, dummy2,
]); ]);
expect(dummy1.body.inverseMass, greaterThan(0)); final contact = _MockContact();
expect(dummy2.body.inverseMass, greaterThan(0)); final contactImpulse = _MockContactImpulse();
final contact = MockContact();
final manifold = MockManifold();
final contactImpulse = MockContactImpulse();
when(() => manifold.localPoint).thenReturn(Vector2.all(1));
when(() => contact.manifold).thenReturn(manifold);
behavior1.postSolve(dummy1, contact, contactImpulse); behavior1.postSolve(dummy1, contact, contactImpulse);
behavior2.postSolve(dummy2, contact, contactImpulse); behavior2.postSolve(dummy2, contact, contactImpulse);

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

@ -0,0 +1,118 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'MultiplierCubit',
() {
blocTest<MultiplierCubit, MultiplierState>(
"emits [lit] when 'next' on x2 dimmed with x2 multiplier value",
build: () => MultiplierCubit(MultiplierValue.x2),
act: (bloc) => bloc.next(2),
expect: () => [
isA<MultiplierState>()
..having(
(state) => state.spriteState,
'spriteState',
MultiplierSpriteState.lit,
),
],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits [lit] when 'next' on x3 dimmed with x3 multiplier value",
build: () => MultiplierCubit(MultiplierValue.x3),
act: (bloc) => bloc.next(3),
expect: () => [
isA<MultiplierState>()
..having(
(state) => state.spriteState,
'spriteState',
MultiplierSpriteState.lit,
),
],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits [lit] when 'next' on x4 dimmed with x4 multiplier value",
build: () => MultiplierCubit(MultiplierValue.x4),
act: (bloc) => bloc.next(4),
expect: () => [
isA<MultiplierState>()
..having(
(state) => state.spriteState,
'spriteState',
MultiplierSpriteState.lit,
),
],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits [lit] when 'next' on x5 dimmed with x5 multiplier value",
build: () => MultiplierCubit(MultiplierValue.x5),
act: (bloc) => bloc.next(5),
expect: () => [
isA<MultiplierState>()
..having(
(state) => state.spriteState,
'spriteState',
MultiplierSpriteState.lit,
),
],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits [lit] when 'next' on x6 dimmed with x6 multiplier value",
build: () => MultiplierCubit(MultiplierValue.x6),
act: (bloc) => bloc.next(6),
expect: () => [
isA<MultiplierState>()
..having(
(state) => state.spriteState,
'spriteState',
MultiplierSpriteState.lit,
),
],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits [dimmed] when 'next' on lit with different multiplier value",
build: () => MultiplierCubit(MultiplierValue.x2),
seed: () => MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
),
act: (bloc) => bloc.next(3),
expect: () => [
isA<MultiplierState>()
..having(
(state) => state.spriteState,
'spriteState',
MultiplierSpriteState.dimmed,
),
],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits nothing when 'next' on lit with same multiplier value",
build: () => MultiplierCubit(MultiplierValue.x2),
seed: () => MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
),
act: (bloc) => bloc.next(2),
expect: () => <MultiplierState>[],
);
blocTest<MultiplierCubit, MultiplierState>(
"emits nothing when 'next' on dimmed with different multiplier value",
build: () => MultiplierCubit(MultiplierValue.x2),
act: (bloc) => bloc.next(3),
expect: () => <MultiplierState>[],
);
},
);
}

@ -0,0 +1,75 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/pinball_components.dart';
void main() {
group('MultiplierState', () {
test('supports value equality', () {
expect(
MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
),
equals(
MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
),
isNotNull,
);
});
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified',
() {
const multiplierState = MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
);
expect(
multiplierState.copyWith(),
equals(multiplierState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
const multiplierState = MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.lit,
);
final otherMultiplierState = MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.dimmed,
);
expect(multiplierState, isNot(equals(otherMultiplierState)));
expect(
multiplierState.copyWith(
spriteState: MultiplierSpriteState.dimmed,
),
equals(otherMultiplierState),
);
},
);
});
});
}

@ -0,0 +1,517 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
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 '../../../helpers/helpers.dart';
void main() {
final bloc = MockMultiplierCubit();
group('Multiplier', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
Assets.images.multiplier.x3.dimmed.keyName,
Assets.images.multiplier.x4.lit.keyName,
Assets.images.multiplier.x4.dimmed.keyName,
Assets.images.multiplier.x5.lit.keyName,
Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.keyName,
Assets.images.multiplier.x6.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('"x2" loads correctly', (game) async {
final multiplier = Multiplier.x2(
position: Vector2.zero(),
angle: 0,
);
await game.ensureAdd(multiplier);
expect(game.contains(multiplier), isTrue);
});
flameTester.test('"x3" loads correctly', (game) async {
final multiplier = Multiplier.x3(
position: Vector2.zero(),
angle: 0,
);
await game.ensureAdd(multiplier);
expect(game.contains(multiplier), isTrue);
});
flameTester.test('"x4" loads correctly', (game) async {
final multiplier = Multiplier.x4(
position: Vector2.zero(),
angle: 0,
);
await game.ensureAdd(multiplier);
expect(game.contains(multiplier), isTrue);
});
flameTester.test('"x5" loads correctly', (game) async {
final multiplier = Multiplier.x5(
position: Vector2.zero(),
angle: 0,
);
await game.ensureAdd(multiplier);
expect(game.contains(multiplier), isTrue);
});
flameTester.test('"x6" loads correctly', (game) async {
final multiplier = Multiplier.x6(
position: Vector2.zero(),
angle: 0,
);
await game.ensureAdd(multiplier);
expect(game.contains(multiplier), isTrue);
});
group('renders correctly', () {
group('x2', () {
const multiplierValue = MultiplierValue.x2;
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.lit,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.lit,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x2-lit.png'),
);
},
);
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.dimmed,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.dimmed,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x2-dimmed.png'),
);
},
);
});
group('x3', () {
const multiplierValue = MultiplierValue.x3;
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.lit,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.lit,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x3-lit.png'),
);
},
);
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.dimmed,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.dimmed,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x3-dimmed.png'),
);
},
);
});
group('x4', () {
const multiplierValue = MultiplierValue.x4;
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.lit,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.lit,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x4-lit.png'),
);
},
);
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.dimmed,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.dimmed,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x4-dimmed.png'),
);
},
);
});
group('x5', () {
const multiplierValue = MultiplierValue.x5;
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.lit,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.lit,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x5-lit.png'),
);
},
);
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.dimmed,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.dimmed,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x5-dimmed.png'),
);
},
);
});
group('x6', () {
const multiplierValue = MultiplierValue.x6;
flameTester.testGameWidget(
'lit when bloc state is lit',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.lit,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.lit,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x6-lit.png'),
);
},
);
flameTester.testGameWidget(
'dimmed when bloc state is dimmed',
setUp: (game, tester) async {
await game.images.loadAll(assets);
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: multiplierValue,
spriteState: MultiplierSpriteState.dimmed,
),
);
final multiplier = Multiplier.test(
value: multiplierValue,
bloc: bloc,
);
await game.ensureAdd(multiplier);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
expect(
game
.descendants()
.whereType<MultiplierSpriteGroupComponent>()
.first
.current,
MultiplierSpriteState.dimmed,
);
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/multipliers/x6-dimmed.png'),
);
},
);
});
});
flameTester.test('closes bloc when removed', (game) async {
whenListen(
bloc,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState(
value: MultiplierValue.x2,
spriteState: MultiplierSpriteState.dimmed,
),
);
when(bloc.close).thenAnswer((_) async {});
final multiplier = Multiplier.test(value: MultiplierValue.x2, bloc: bloc);
await game.ensureAdd(multiplier);
game.remove(multiplier);
await game.ready();
verify(bloc.close).called(1);
});
});
}

@ -12,8 +12,8 @@ void main() {
group('SpaceshipRail', () { group('SpaceshipRail', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));

@ -11,16 +11,16 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName, Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName, Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName, Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.android.ramp.arrow.active5.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));

@ -1,56 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
void main() {
group('Spaceship', () {
late Filter filterData;
late Fixture fixture;
late Body body;
late Ball ball;
late Forge2DGame game;
setUp(() {
filterData = MockFilter();
fixture = MockFixture();
when(() => fixture.filterData).thenReturn(filterData);
body = MockBody();
when(() => body.fixtures).thenReturn([fixture]);
game = MockGame();
ball = MockBall();
when(() => ball.gameRef).thenReturn(game);
when(() => ball.body).thenReturn(body);
});
group('Spaceship', () {
final tester = FlameTester(TestGame.new);
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
final position = Vector2(30, -30);
await game.addFromBlueprint(Spaceship(position: position));
game.camera.followVector2(position);
await game.ready();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship.png'),
);
},
);
});
});
}

@ -11,22 +11,27 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.spaceship.saucer.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.spaceship.animatronic.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.spaceship.lightBeam.keyName,
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName, Assets.images.android.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName, Assets.images.android.ramp.main.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.spaceship.rail.main.keyName, Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.rail.main.keyName,
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.rail.exit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
]; ];
final flameTester = FlameTester( final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets), () => EmptyPinballTestGame(assets: assets),
@ -46,7 +51,7 @@ void main() {
'a Spaceship', 'a Spaceship',
(game) async { (game) async {
expect( expect(
AndroidAcres().blueprints.whereType<Spaceship>().single, AndroidAcres().blueprints.whereType<AndroidSpaceship>().single,
isNotNull, isNotNull,
); );
}, },
@ -73,7 +78,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'two AndroidBumper', 'three AndroidBumper',
(game) async { (game) async {
final androidZone = AndroidAcres(); final androidZone = AndroidAcres();
await game.addFromBlueprint(androidZone); await game.addFromBlueprint(androidZone);
@ -81,7 +86,7 @@ void main() {
expect( expect(
game.descendants().whereType<AndroidBumper>().length, game.descendants().whereType<AndroidBumper>().length,
equals(2), equals(3),
); );
}, },
); );

@ -0,0 +1,133 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
Assets.images.multiplier.x3.dimmed.keyName,
Assets.images.multiplier.x4.lit.keyName,
Assets.images.multiplier.x4.dimmed.keyName,
Assets.images.multiplier.x5.lit.keyName,
Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.keyName,
Assets.images.multiplier.x6.dimmed.keyName,
];
group('MultipliersBehavior', () {
late GameBloc gameBloc;
setUp(() {
registerFallbackValue(MockComponent());
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('listenWhen', () {
test('is true when the multiplier has changed', () {
final state = GameState(
score: 10,
multiplier: 2,
rounds: 0,
bonusHistory: const [],
);
final previous = GameState.initial();
expect(
MultipliersBehavior().listenWhen(previous, state),
isTrue,
);
});
test('is false when the multiplier state is the same', () {
final state = GameState(
score: 10,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
);
final previous = GameState.initial();
expect(
MultipliersBehavior().listenWhen(previous, state),
isFalse,
);
});
});
group('onNewState', () {
flameBlocTester.testGameWidget(
"calls 'next' once per each multiplier when GameBloc emit state",
setUp: (game, tester) async {
final behavior = MultipliersBehavior();
final parent = Multipliers.test();
final multiplierX2Cubit = MockMultiplierCubit();
final multiplierX3Cubit = MockMultiplierCubit();
final multipliers = [
Multiplier.test(
value: MultiplierValue.x2,
bloc: multiplierX2Cubit,
),
Multiplier.test(
value: MultiplierValue.x3,
bloc: multiplierX3Cubit,
),
];
whenListen(
multiplierX2Cubit,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState.initial(MultiplierValue.x2),
);
when(() => multiplierX2Cubit.next(any())).thenAnswer((_) async {});
whenListen(
multiplierX3Cubit,
const Stream<MultiplierState>.empty(),
initialState: MultiplierState.initial(MultiplierValue.x2),
);
when(() => multiplierX3Cubit.next(any())).thenAnswer((_) async {});
await parent.addAll(multipliers);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
await tester.pump();
behavior.onNewState(
GameState.initial().copyWith(multiplier: 2),
);
for (final multiplier in multipliers) {
verify(
() => multiplier.bloc.next(any()),
).called(1);
}
},
);
});
});
}

@ -0,0 +1,63 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
Assets.images.multiplier.x3.dimmed.keyName,
Assets.images.multiplier.x4.lit.keyName,
Assets.images.multiplier.x4.dimmed.keyName,
Assets.images.multiplier.x5.lit.keyName,
Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.keyName,
Assets.images.multiplier.x6.dimmed.keyName,
];
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('Multipliers', () {
flameBlocTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
final multipliersGroup = Multipliers();
await game.ensureAdd(multipliersGroup);
expect(game.contains(multipliersGroup), isTrue);
},
);
group('loads', () {
flameBlocTester.testGameWidget(
'five Multiplier',
setUp: (game, tester) async {
final multipliersGroup = Multipliers();
await game.ensureAdd(multipliersGroup);
expect(
multipliersGroup.descendants().whereType<Multiplier>().length,
equals(5),
);
},
);
});
});
}

@ -1,5 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
@ -14,10 +15,12 @@ import '../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
Assets.images.backboard.backboardScores.keyName, Assets.images.backboard.backboardScores.keyName,
Assets.images.backboard.backboardGameOver.keyName, Assets.images.backboard.backboardGameOver.keyName,
Assets.images.backboard.display.keyName, Assets.images.backboard.display.keyName,
@ -52,6 +55,16 @@ void main() {
Assets.images.launchRamp.ramp.keyName, Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.foregroundRailing.keyName, Assets.images.launchRamp.foregroundRailing.keyName,
Assets.images.launchRamp.backgroundRailing.keyName, Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
Assets.images.multiplier.x3.dimmed.keyName,
Assets.images.multiplier.x4.lit.keyName,
Assets.images.multiplier.x4.dimmed.keyName,
Assets.images.multiplier.x5.lit.keyName,
Assets.images.multiplier.x5.dimmed.keyName,
Assets.images.multiplier.x6.lit.keyName,
Assets.images.multiplier.x6.dimmed.keyName,
Assets.images.plunger.plunger.keyName, Assets.images.plunger.plunger.keyName,
Assets.images.plunger.rocket.keyName, Assets.images.plunger.rocket.keyName,
Assets.images.signpost.inactive.keyName, Assets.images.signpost.inactive.keyName,
@ -60,20 +73,21 @@ void main() {
Assets.images.signpost.active3.keyName, Assets.images.signpost.active3.keyName,
Assets.images.slingshot.upper.keyName, Assets.images.slingshot.upper.keyName,
Assets.images.slingshot.lower.keyName, Assets.images.slingshot.lower.keyName,
Assets.images.spaceship.saucer.keyName, Assets.images.android.spaceship.saucer.keyName,
Assets.images.spaceship.bridge.keyName, Assets.images.android.spaceship.animatronic.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.spaceship.lightBeam.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName, Assets.images.android.ramp.main.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName, Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName, Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.spaceship.rail.main.keyName, Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.sparky.bumper.a.active.keyName, Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName, Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName, Assets.images.sparky.bumper.b.active.keyName,
@ -92,6 +106,17 @@ void main() {
Assets.images.sparky.bumper.c.inactive.keyName, Assets.images.sparky.bumper.c.inactive.keyName,
]; ];
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameTester = FlameTester( final flameTester = FlameTester(
() => PinballTestGame(assets: assets), () => PinballTestGame(assets: assets),
); );
@ -99,11 +124,16 @@ void main() {
() => DebugPinballTestGame(assets: assets), () => DebugPinballTestGame(assets: assets),
); );
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: () => PinballTestGame(assets: assets),
blocBuilder: () => gameBloc,
);
group('PinballGame', () { group('PinballGame', () {
group('components', () { group('components', () {
// TODO(alestiago): tests that Blueprints get added once the Blueprint // TODO(alestiago): tests that Blueprints get added once the Blueprint
// class is removed. // class is removed.
flameTester.test( flameBlocTester.test(
'has only one Drain', 'has only one Drain',
(game) async { (game) async {
await game.ready(); await game.ready();
@ -114,11 +144,10 @@ void main() {
}, },
); );
flameTester.test( flameBlocTester.test(
'has only one BottomGroup', 'has only one BottomGroup',
(game) async { (game) async {
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<BottomGroup>().length, game.children.whereType<BottomGroup>().length,
equals(1), equals(1),
@ -126,7 +155,7 @@ void main() {
}, },
); );
flameTester.test( flameBlocTester.test(
'has only one Plunger', 'has only one Plunger',
(game) async { (game) async {
await game.ready(); await game.ready();
@ -137,7 +166,7 @@ void main() {
}, },
); );
flameTester.test('has one FlutterForest', (game) async { flameBlocTester.test('has one FlutterForest', (game) async {
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<FlutterForest>().length, game.children.whereType<FlutterForest>().length,
@ -145,7 +174,7 @@ void main() {
); );
}); });
flameTester.test( flameBlocTester.test(
'one GoogleWord', 'one GoogleWord',
(game) async { (game) async {
await game.ready(); await game.ready();

@ -64,6 +64,8 @@ class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {} class MockFixture extends Mock implements Fixture {}
class MockComponent extends Mock implements Component {}
class MockComponentSet extends Mock implements ComponentSet {} class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {} class MockDashNestBumper extends Mock implements DashNestBumper {}
@ -86,3 +88,9 @@ class MockGameFlowController extends Mock implements GameFlowController {}
class MockAndroidBumper extends Mock implements AndroidBumper {} class MockAndroidBumper extends Mock implements AndroidBumper {}
class MockSparkyBumper extends Mock implements SparkyBumper {} class MockSparkyBumper extends Mock implements SparkyBumper {}
class MockMultiplier extends Mock implements Multiplier {}
class MockMultipliersGroup extends Mock implements Multipliers {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}

Loading…
Cancel
Save