fix: fixed merge conflicts with sandbox

pull/152/head
RuiAlonso 4 years ago
commit edbee10f54

@ -33,7 +33,7 @@ abstract class ComponentController<T extends Component> extends Component {
/// Mixin that attaches a single [ComponentController] to a [Component]. /// Mixin that attaches a single [ComponentController] to a [Component].
mixin Controls<T extends ComponentController> on Component { mixin Controls<T extends ComponentController> on Component {
/// The [ComponentController] attached to this [Component]. /// The [ComponentController] attached to this [Component].
late final T controller; late T controller;
@override @override
@mustCallSuper @mustCallSuper

@ -19,9 +19,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
static const bonusWordScore = 10000; static const bonusWordScore = 10000;
void _onBallLost(BallLost event, Emitter emit) { void _onBallLost(BallLost event, Emitter emit) {
if (state.balls > 0) { emit(state.copyWith(balls: state.balls - 1));
emit(state.copyWith(balls: state.balls - 1));
}
} }
void _onScored(Scored event, Emitter emit) { void _onScored(Scored event, Emitter emit) {
@ -36,7 +34,8 @@ class GameBloc extends Bloc<GameEvent, GameState> {
event.letterIndex, event.letterIndex,
]; ];
if (newBonusLetters.length == bonusWord.length) { final achievedBonus = newBonusLetters.length == bonusWord.length;
if (achievedBonus) {
emit( emit(
state.copyWith( state.copyWith(
activatedBonusLetters: [], activatedBonusLetters: [],
@ -55,15 +54,16 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
void _onDashNestActivated(DashNestActivated event, Emitter emit) { void _onDashNestActivated(DashNestActivated event, Emitter emit) {
const nestsRequiredForBonus = 3;
final newNests = { final newNests = {
...state.activatedDashNests, ...state.activatedDashNests,
event.nestId, event.nestId,
}; };
if (newNests.length == nestsRequiredForBonus) {
final achievedBonus = newNests.length == 3;
if (achievedBonus) {
emit( emit(
state.copyWith( state.copyWith(
balls: state.balls + 1,
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [ bonusHistory: [
...state.bonusHistory, ...state.bonusHistory,

@ -5,11 +5,10 @@ part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame. /// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus { enum GameBonus {
/// Bonus achieved when the user activate all of the bonus /// Bonus achieved when the user activate all of the bonus
/// letters on the board, forming the bonus word /// letters on the board, forming the bonus word.
word, word,
/// Bonus achieved when the user activates all of the Dash /// Bonus achieved when the user activates all dash nest bumpers.
/// nests on the board, adding a new ball to the board.
dashNest, dashNest,
} }

@ -1,5 +1,4 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart'; import 'package:pinball/flame/flame.dart';
@ -18,7 +17,7 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.launch({ ControlledBall.launch({
required PinballTheme theme, required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) { }) : super(baseColor: theme.characterTheme.ballColor) {
controller = LaunchedBallController(this); controller = BallController(this);
} }
/// {@template bonus_ball} /// {@template bonus_ball}
@ -29,74 +28,43 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.bonus({ ControlledBall.bonus({
required PinballTheme theme, required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) { }) : super(baseColor: theme.characterTheme.ballColor) {
controller = BonusBallController(this); controller = BallController(this);
} }
/// [Ball] used in [DebugPinballGame]. /// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = BonusBallController(this); controller = DebugBallController(this);
} }
} }
/// {@template ball_controller} /// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic. /// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate} /// {@endtemplate}
abstract class BallController extends ComponentController<Ball> { class BallController extends ComponentController<Ball>
with HasGameRef<PinballGame> {
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); BallController(Ball ball) : super(ball);
/// Removes the [Ball] from a [PinballGame]. /// Removes the [Ball] from a [PinballGame].
/// ///
/// {@template ball_controller_lost}
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall]. /// a [BottomWall].
/// {@endtemplate}
void lost();
}
/// {@template bonus_ball_controller}
/// {@macro ball_controller}
///
/// A [BonusBallController] doesn't change the [GameState.balls] count.
/// {@endtemplate}
class BonusBallController extends BallController {
/// {@macro bonus_ball_controller}
BonusBallController(Ball<Forge2DGame> component) : super(component);
@override
void lost() { void lost() {
component.shouldRemove = true; component.shouldRemove = true;
} }
}
/// {@template launched_ball_controller}
/// {@macro ball_controller}
///
/// A [LaunchedBallController] changes the [GameState.balls] count.
/// {@endtemplate}
class LaunchedBallController extends BallController
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
/// {@macro launched_ball_controller}
LaunchedBallController(Ball<Forge2DGame> ball) : super(ball);
@override @override
bool listenWhen(GameState? previousState, GameState newState) { void onRemove() {
return (previousState?.balls ?? 0) > newState.balls; super.onRemove();
gameRef.read<GameBloc>().add(const BallLost());
} }
}
@override /// {@macro ball_controller}
void onNewState(GameState state) { class DebugBallController extends BallController {
super.onNewState(state); /// {@macro ball_controller}
component.shouldRemove = true; DebugBallController(Ball<Forge2DGame> component) : super(component);
if (state.balls > 0) gameRef.spawnBall();
}
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left.
///
/// {@macro ball_controller_lost}
@override @override
void lost() { void onRemove() {}
gameRef.read<GameBloc>().add(const BallLost());
}
} }

@ -71,12 +71,12 @@ class BottomWall extends Wall {
} }
/// {@template bottom_wall_ball_contact_callback} /// {@template bottom_wall_ball_contact_callback}
/// Listens when a [Ball] falls into a [BottomWall]. /// Listens when a [ControlledBall] falls into a [BottomWall].
/// {@endtemplate} /// {@endtemplate}
class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> { class BottomWallBallContactCallback
extends ContactCallback<ControlledBall, BottomWall> {
@override @override
void begin(Ball ball, BottomWall wall, Contact contact) { void begin(ControlledBall ball, BottomWall wall, Contact contact) {
// TODO(alestiago): replace with .firstChild when available. ball.controller.lost();
ball.children.whereType<BallController>().first.lost();
} }
} }

@ -15,6 +15,10 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.baseboard.right.keyName), images.load(components.Assets.images.baseboard.right.keyName),
images.load(components.Assets.images.kicker.left.keyName), images.load(components.Assets.images.kicker.left.keyName),
images.load(components.Assets.images.kicker.right.keyName), images.load(components.Assets.images.kicker.right.keyName),
images.load(components.Assets.images.slingshot.leftUpper.keyName),
images.load(components.Assets.images.slingshot.leftLower.keyName),
images.load(components.Assets.images.slingshot.rightUpper.keyName),
images.load(components.Assets.images.slingshot.rightLower.keyName),
images.load(components.Assets.images.launchRamp.ramp.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName),
images.load( images.load(
components.Assets.images.launchRamp.foregroundRailing.keyName, components.Assets.images.launchRamp.foregroundRailing.keyName,

@ -5,6 +5,7 @@ import 'package:flame/components.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -12,30 +13,40 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents { with
PinballGame({required this.theme, required this.audio}) { FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController> {
PinballGame({
required this.theme,
required this.audio,
}) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this);
} }
final PinballTheme theme; final PinballTheme theme;
final PinballAudio audio; final PinballAudio audio;
@override
void onAttach() {
super.onAttach();
spawnBall();
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
_addContactCallbacks(); _addContactCallbacks();
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp())); unawaited(addFromBlueprint(LaunchRamp()));
unawaited(_addPlunger());
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
unawaited(add(Board())); unawaited(add(Board()));
unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls())); unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord()); unawaited(_addBonusWord());
unawaited(addFromBlueprint(SpaceshipRamp())); unawaited(addFromBlueprint(SpaceshipRamp()));
@ -52,10 +63,8 @@ class PinballGame extends Forge2DGame
), ),
); );
// Fix camera on the center of the board. controller.attachTo(plunger);
camera await super.onLoad();
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
} }
void _addContactCallbacks() { void _addContactCallbacks() {
@ -69,12 +78,6 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add); createBoundaries(this).forEach(add);
} }
Future<void> _addPlunger() async {
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
}
Future<void> _addBonusWord() async { Future<void> _addBonusWord() async {
await add( await add(
BonusWord( BonusWord(
@ -85,13 +88,49 @@ class PinballGame extends Forge2DGame
), ),
); );
} }
}
Future<void> spawnBall() async { class _GameBallsController extends ComponentController<PinballGame>
// TODO(alestiago): Remove once this logic is moved to controller. with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
_GameBallsController(PinballGame game) : super(game);
late final Plunger _plunger;
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
_spawnBall();
}
@override
Future<void> onLoad() async {
await super.onLoad();
_spawnBall();
}
void _spawnBall() {
final ball = ControlledBall.launch( final ball = ControlledBall.launch(
theme: theme, theme: gameRef.theme,
)..initialPosition = Vector2(38, -19 + Ball.size.y); )..initialPosition = Vector2(
await add(ball); _plunger.body.position.x,
_plunger.body.position.y + Ball.size.y,
);
component.add(ball);
}
/// Attaches the controller to the plunger.
// TODO(alestiago): Remove this method and use onLoad instead.
// ignore: use_setters_to_change_properties
void attachTo(Plunger plunger) {
_plunger = plunger;
} }
} }
@ -102,7 +141,9 @@ class DebugPinballGame extends PinballGame with TapDetector {
}) : super( }) : super(
theme: theme, theme: theme,
audio: audio, audio: audio,
); ) {
controller = _DebugGameBallsController(this);
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -134,3 +175,19 @@ class DebugPinballGame extends PinballGame with TapDetector {
); );
} }
} }
class _DebugGameBallsController extends _GameBallsController {
_DebugGameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component
.descendants()
.whereType<ControlledBall>()
.where((ball) => ball.controller is! DebugBallController)
.isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -29,8 +29,10 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyBumperGen get sparkyBumper =>
const $AssetsImagesSparkyBumperGen();
} }
class $AssetsImagesBaseboardGen { class $AssetsImagesBaseboardGen {
@ -126,12 +128,24 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png'); const AssetGenImage('assets/images/launch_ramp/ramp.png');
} }
class $AssetsImagesPlungerGen { class $AssetsImagesSlingshotGen {
const $AssetsImagesPlungerGen(); const $AssetsImagesSlingshotGen();
/// File path: assets/images/plunger/plunger.png /// File path: assets/images/slingshot/left_lower.png
AssetGenImage get plunger => AssetGenImage get leftLower =>
const AssetGenImage('assets/images/plunger/plunger.png'); const AssetGenImage('assets/images/slingshot/left_lower.png');
/// File path: assets/images/slingshot/left_upper.png
AssetGenImage get leftUpper =>
const AssetGenImage('assets/images/slingshot/left_upper.png');
/// File path: assets/images/slingshot/right_lower.png
AssetGenImage get rightLower =>
const AssetGenImage('assets/images/slingshot/right_lower.png');
/// File path: assets/images/slingshot/right_upper.png
AssetGenImage get rightUpper =>
const AssetGenImage('assets/images/slingshot/right_upper.png');
} }
class $AssetsImagesSpaceshipGen { class $AssetsImagesSpaceshipGen {
@ -151,6 +165,14 @@ class $AssetsImagesSpaceshipGen {
const AssetGenImage('assets/images/spaceship/saucer.png'); const AssetGenImage('assets/images/spaceship/saucer.png');
} }
class $AssetsImagesSparkyBumperGen {
const $AssetsImagesSparkyBumperGen();
$AssetsImagesSparkyBumperAGen get a => const $AssetsImagesSparkyBumperAGen();
$AssetsImagesSparkyBumperBGen get b => const $AssetsImagesSparkyBumperBGen();
$AssetsImagesSparkyBumperCGen get c => const $AssetsImagesSparkyBumperCGen();
}
class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen(); const $AssetsImagesDashBumperAGen();
@ -215,6 +237,42 @@ class $AssetsImagesSpaceshipRampGen {
'assets/images/spaceship/ramp/railing-foreground.png'); 'assets/images/spaceship/ramp/railing-foreground.png');
} }
class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky_bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky_bumper/a/active.png');
/// File path: assets/images/sparky_bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky_bumper/a/inactive.png');
}
class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky_bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky_bumper/b/active.png');
/// File path: assets/images/sparky_bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky_bumper/b/inactive.png');
}
class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky_bumper/c/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky_bumper/c/active.png');
/// File path: assets/images/sparky_bumper/c/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky_bumper/c/inactive.png');
}
class Assets { class Assets {
Assets._(); Assets._();

@ -17,6 +17,8 @@ export 'layer.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'ramp_opening.dart'; export 'ramp_opening.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'slingshot.dart';
export 'spaceship.dart'; export 'spaceship.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp.dart';
export 'sparky_bumper.dart';

@ -0,0 +1,138 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template slingshots}
/// A [Blueprint] which creates the left and right pairs of [Slingshot]s.
/// {@endtemplate}
class Slingshots extends Forge2DBlueprint {
@override
void build(_) {
// TODO(allisonryan0002): use radians values instead of converting degrees.
final leftUpperSlingshot = Slingshot(
length: 5.66,
angle: -1.5 * (math.pi / 180),
spritePath: Assets.images.slingshot.leftUpper.keyName,
)..initialPosition = Vector2(-29, 1.5);
final leftLowerSlingshot = Slingshot(
length: 3.54,
angle: -29.1 * (math.pi / 180),
spritePath: Assets.images.slingshot.leftLower.keyName,
)..initialPosition = Vector2(-31, -6.2);
final rightUpperSlingshot = Slingshot(
length: 5.64,
angle: 1 * (math.pi / 180),
spritePath: Assets.images.slingshot.rightUpper.keyName,
)..initialPosition = Vector2(22.3, 1.58);
final rightLowerSlingshot = Slingshot(
length: 3.46,
angle: 26.8 * (math.pi / 180),
spritePath: Assets.images.slingshot.rightLower.keyName,
)..initialPosition = Vector2(24.7, -6.2);
addAll([
leftUpperSlingshot,
leftLowerSlingshot,
rightUpperSlingshot,
rightLowerSlingshot,
]);
}
}
/// {@template slingshot}
/// Elastic bumper that bounces the [Ball] off of its straight sides.
/// {@endtemplate}
class Slingshot extends BodyComponent with InitialPosition {
/// {@macro slingshot}
Slingshot({
required double length,
required double angle,
required String spritePath,
}) : _length = length,
_angle = angle,
_spritePath = spritePath,
super(priority: 1);
final double _length;
final double _angle;
final String _spritePath;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
const circleRadius = 1.55;
final topCircleShape = CircleShape()..radius = circleRadius;
topCircleShape.position.setValues(0, _length / 2);
final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0;
fixturesDef.add(topCircleFixtureDef);
final bottomCircleShape = CircleShape()..radius = circleRadius;
bottomCircleShape.position.setValues(0, -_length / 2);
final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0;
fixturesDef.add(bottomCircleFixtureDef);
final leftEdgeShape = EdgeShape()
..set(
Vector2(circleRadius, _length / 2),
Vector2(circleRadius, -_length / 2),
);
final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape)
..friction = 0
..restitution = 5;
fixturesDef.add(leftEdgeShapeFixtureDef);
final rightEdgeShape = EdgeShape()
..set(
Vector2(-circleRadius, _length / 2),
Vector2(-circleRadius, -_length / 2),
);
final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape)
..friction = 0
..restitution = 5;
fixturesDef.add(rightEdgeShapeFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..angle = _angle;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(_spritePath);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
angle: _angle,
),
);
}
}

@ -0,0 +1,125 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_bumper}
/// Bumper for Sparky area.
/// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class SparkyBumper extends BodyComponent with InitialPosition {
/// {@macro sparky_bumper}
SparkyBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
/// {@macro sparky_bumper}
SparkyBumper.a()
: this._(
majorRadius: 2.9,
minorRadius: 2.1,
activeAssetPath: Assets.images.sparkyBumper.a.active.keyName,
inactiveAssetPath: Assets.images.sparkyBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.25),
),
);
/// {@macro sparky_bumper}
SparkyBumper.b()
: this._(
majorRadius: 2.85,
minorRadius: 2,
activeAssetPath: Assets.images.sparkyBumper.b.active.keyName,
inactiveAssetPath: Assets.images.sparkyBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.35),
),
);
/// {@macro sparky_bumper}
SparkyBumper.c()
: this._(
majorRadius: 3,
minorRadius: 2.2,
activeAssetPath: Assets.images.sparkyBumper.c.active.keyName,
inactiveAssetPath: Assets.images.sparkyBumper.c.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.4),
),
);
final double _majorRadius;
final double _minorRadius;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
// TODO(erickzanardo): Look into using onNewState instead.
// Currently doing: onNewState(gameRef.read<GameState>()) will throw an
// `Exception: build context is not available yet`
deactivate();
await add(_spriteComponent);
}
@override
Body createBody() {
renderBody = false;
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
}
/// Activates the [DashNestBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
}
}

@ -40,6 +40,10 @@ flutter:
- assets/images/chrome_dino/ - assets/images/chrome_dino/
- assets/images/kicker/ - assets/images/kicker/
- assets/images/plunger/ - assets/images/plunger/
- assets/images/slingshot/
- assets/images/sparky_bumper/a/
- assets/images/sparky_bumper/b/
- assets/images/sparky_bumper/c/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -22,5 +22,7 @@ void main() {
addDashNestBumperStories(dashbook); addDashNestBumperStories(dashbook);
addKickerStories(dashbook); addKickerStories(dashbook);
addPlungerStories(dashbook); addPlungerStories(dashbook);
addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,66 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SlingshotGame extends BasicBallGame {
SlingshotGame({
required this.trace,
}) : super(color: const Color(0xFFFF0000));
static const info = '''
Shows how Slingshots are rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a ball into the game.
''';
final bool trace;
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final leftUpperSlingshot = Slingshot(
length: 5.66,
angle: -1.5 * (math.pi / 180),
spritePath: Assets.images.slingshot.leftUpper.keyName,
)..initialPosition = center + Vector2(-29, 1.5);
final leftLowerSlingshot = Slingshot(
length: 3.54,
angle: -29.1 * (math.pi / 180),
spritePath: Assets.images.slingshot.leftLower.keyName,
)..initialPosition = center + Vector2(-31, -6.2);
final rightUpperSlingshot = Slingshot(
length: 5.64,
angle: 1 * (math.pi / 180),
spritePath: Assets.images.slingshot.rightUpper.keyName,
)..initialPosition = center + Vector2(22.3, 1.58);
final rightLowerSlingshot = Slingshot(
length: 3.46,
angle: 26.8 * (math.pi / 180),
spritePath: Assets.images.slingshot.rightLower.keyName,
)..initialPosition = center + Vector2(24.7, -6.2);
await addAll([
leftUpperSlingshot,
leftLowerSlingshot,
rightUpperSlingshot,
rightLowerSlingshot,
]);
if (trace) {
leftUpperSlingshot.trace();
leftLowerSlingshot.trace();
rightUpperSlingshot.trace();
rightLowerSlingshot.trace();
}
}
}

@ -0,0 +1,17 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/slingshot/slingshot_game.dart';
void addSlingshotStories(Dashbook dashbook) {
dashbook.storiesOf('Slingshots').add(
'Basic',
(context) => GameWidget(
game: SlingshotGame(
trace: context.boolProperty('Trace', true),
),
),
codeLink: buildSourceLink('slingshot_game/basic.dart'),
info: SlingshotGame.info,
);
}

@ -0,0 +1,47 @@
import 'dart:async';
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SparkyBumperGame extends BasicBallGame {
SparkyBumperGame({
required this.trace,
}) : super(color: const Color(0xFF0000FF));
static const info = '''
Shows how a SparkyBumper is rendered.
Activate the "trace" parameter to overlay the body.
''';
final bool trace;
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final sparkyBumperA = SparkyBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)
..priority = 1;
final sparkyBumperB = SparkyBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)
..priority = 1;
final sparkyBumperC = SparkyBumper.c()
..initialPosition = Vector2(center.x + 20, center.y)
..priority = 1;
await addAll([
sparkyBumperA,
sparkyBumperB,
sparkyBumperC,
]);
if (trace) {
sparkyBumperA.trace();
sparkyBumperB.trace();
sparkyBumperC.trace();
}
}
}

@ -0,0 +1,17 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart';
void addSparkyBumperStories(Dashbook dashbook) {
dashbook.storiesOf('Sparky Bumpers').add(
'Basic',
(context) => GameWidget(
game: SparkyBumperGame(
trace: context.boolProperty('Trace', true),
),
),
codeLink: buildSourceLink('sparky_bumper/basic.dart'),
info: SparkyBumperGame.info,
);
}

@ -6,4 +6,6 @@ export 'effects/stories.dart';
export 'flipper/stories.dart'; export 'flipper/stories.dart';
export 'layer/stories.dart'; export 'layer/stories.dart';
export 'plunger/stories.dart'; export 'plunger/stories.dart';
export 'slingshot/stories.dart';
export 'spaceship/stories.dart'; export 'spaceship/stories.dart';
export 'sparky_bumper/stories.dart';

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@ -0,0 +1,97 @@
// 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:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('Slingshot', () {
final flameTester = FlameTester(TestGame.new);
const length = 2.0;
const angle = 0.0;
final spritePath = Assets.images.slingshot.leftUpper.keyName;
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.addFromBlueprint(Slingshots());
await game.ready();
game.camera.followVector2(Vector2.zero());
},
// TODO(allisonryan0002): enable test when workflows are fixed.
// verify: (game, tester) async {
// await expectLater(
// find.byGame<Forge2DGame>(),
// matchesGoldenFile('golden/slingshots.png'),
// );
// },
);
flameTester.test(
'loads correctly',
(game) async {
final slingshot = Slingshot(
length: length,
angle: angle,
spritePath: spritePath,
);
await game.ensureAdd(slingshot);
expect(game.contains(slingshot), isTrue);
},
);
flameTester.test(
'body is static',
(game) async {
final slingshot = Slingshot(
length: length,
angle: angle,
spritePath: spritePath,
);
await game.ensureAdd(slingshot);
expect(slingshot.body.bodyType, equals(BodyType.static));
},
);
flameTester.test(
'has restitution',
(game) async {
final slingshot = Slingshot(
length: length,
angle: angle,
spritePath: spritePath,
);
await game.ensureAdd(slingshot);
final totalRestitution = slingshot.body.fixtures.fold<double>(
0,
(total, fixture) => total + fixture.restitution,
);
expect(totalRestitution, greaterThan(0));
},
);
flameTester.test(
'has no friction',
(game) async {
final slingshot = Slingshot(
length: length,
angle: angle,
spritePath: spritePath,
);
await game.ensureAdd(slingshot);
final totalFriction = slingshot.body.fixtures.fold<double>(
0,
(total, fixture) => total + fixture.friction,
);
expect(totalFriction, equals(0));
},
);
});
}

@ -0,0 +1,74 @@
// 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 '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('SparkyBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = SparkyBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"c" loads correctly', (game) async {
final bumper = SparkyBumper.c();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
);
});
});
}

@ -12,13 +12,10 @@ void main() {
group('LostBall', () { group('LostBall', () {
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
"doesn't decrease ball " 'decreases number of balls',
'when no balls left',
build: GameBloc.new, build: GameBloc.new,
act: (bloc) { act: (bloc) {
for (var i = 0; i <= bloc.state.balls; i++) { bloc.add(const BallLost());
bloc.add(const BallLost());
}
}, },
expect: () => [ expect: () => [
const GameState( const GameState(
@ -28,20 +25,6 @@ void main() {
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
], ],
); );
}); });
@ -230,7 +213,7 @@ void main() {
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 4,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],

@ -13,42 +13,12 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusBallController', () { group('BallController', () {
late Ball ball;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
});
test('can be instantiated', () {
expect(
BonusBallController(ball),
isA<BonusBallController>(),
);
});
flameTester.test(
'lost removes ball',
(game) async {
await game.add(ball);
final controller = BonusBallController(ball);
await ball.ensureAdd(controller);
controller.lost();
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
group('LaunchedBallController', () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
LaunchedBallController(MockBall()), BallController(MockBall()),
isA<LaunchedBallController>(), isA<BallController>(),
); );
}); });
@ -74,7 +44,7 @@ void main() {
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc', 'lost adds BallLost to GameBloc',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = BallController(ball);
await ball.add(controller); await ball.add(controller);
await game.ensureAdd(ball); await game.ensureAdd(ball);
@ -84,114 +54,6 @@ void main() {
verify(() => gameBloc.add(const BallLost())).called(1); verify(() => gameBloc.add(const BallLost())).called(1);
}, },
); );
group('listenWhen', () {
flameBlocTester.testGameWidget(
'listens when a ball has been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(2);
expect(controller.listenWhen(previousState, newState), isTrue);
},
);
flameBlocTester.testGameWidget(
'does not listen when a ball has not been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(3);
expect(controller.listenWhen(previousState, newState), isFalse);
},
);
});
group('onNewState', () {
flameBlocTester.testGameWidget(
'removes ball',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
controller.onNewState(state);
await game.ready();
},
verify: (game, tester) async {
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'spawns a new ball when the ball is not the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>().toList();
expect(currentBalls.contains(ball), isFalse);
expect(currentBalls.length, equals(previousBalls.length));
},
);
flameBlocTester.testGameWidget(
'does not spawn a new ball is the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(0);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(
currentBalls.length,
equals((previousBalls..remove(ball)).length),
);
},
);
});
}); });
}); });
} }

@ -1,7 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -11,18 +10,6 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballGameTest.new);
@ -92,7 +79,7 @@ void main() {
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest and a bonusBall is added',
verify: (game, tester) async { verify: (game, tester) async {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
@ -103,6 +90,7 @@ void main() {
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
); );
expect( expect(
flutterForest.controller flutterForest.controller
.listenWhen(const GameState.initial(), state), .listenWhen(const GameState.initial(), state),

@ -3,40 +3,15 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('Wall', () { group('Wall', () {
group('BottomWallBallContactCallback', () {
test(
'removes the ball on begin contact when the wall is a bottom one',
() {
final wall = MockBottomWall();
final ballController = MockBallController();
final ball = MockBall();
final componentSet = MockComponentSet();
when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
BottomWallBallContactCallback()
// Remove once https://github.com/flame-engine/flame/pull/1415
// is merged
..end(MockBall(), MockBottomWall(), MockContact())
..begin(ball, wall, MockContact());
verify(ballController.lost).called(1);
},
);
});
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
@ -123,4 +98,67 @@ void main() {
); );
}); });
}); });
group(
'BottomWall',
() {
group('removes ball on contact', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'when ball is launch',
setUp: (game, tester) async {
final ball = ControlledBall.launch(theme: game.theme);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'when ball is bonus',
setUp: (game, tester) async {
final ball = ControlledBall.bonus(theme: game.theme);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameTester.test(
'when ball is debug',
(game) async {
final ball = ControlledBall.debug();
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
},
);
} }

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -10,11 +11,11 @@ import 'package:pinball_components/pinball_components.dart';
import '../helpers/helpers.dart'; import '../helpers/helpers.dart';
void main() { void main() {
group('PinballGame', () { TestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.new);
final flameTester = FlameTester(PinballGameTest.new); final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers // TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved: // [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416 // https://github.com/flame-engine/flame/issues/1416
@ -60,8 +61,106 @@ void main() {
equals(1), equals(1),
); );
}); });
group('controller', () {
// TODO(alestiago): Write test to be controller agnostic.
group('listenWhen', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'listens when all balls are lost and there are more than 0 balls',
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(2);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.controller.listenWhen(MockGameState(), newState),
isTrue,
);
},
);
flameTester.test(
"doesn't listen when some balls are left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
expect(
game.descendants().whereType<Ball>().length,
greaterThan(0),
);
expect(
game.controller.listenWhen(MockGameState(), newState),
isFalse,
);
},
);
flameBlocTester.test(
"doesn't listen when no balls left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(0);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.descendants().whereType<Ball>().isEmpty,
isTrue,
);
expect(
game.controller.listenWhen(MockGameState(), newState),
isFalse,
);
},
);
});
group(
'onNewState',
() {
flameTester.test(
'spawns a ball',
(game) async {
await game.ready();
final previousBalls =
game.descendants().whereType<Ball>().toList();
game.controller.onNewState(MockGameState());
await game.ready();
final currentBalls =
game.descendants().whereType<Ball>().toList();
expect(
currentBalls.length,
equals(previousBalls.length + 1),
);
},
);
},
);
});
}); });
});
group('DebugPinballGame', () {
debugModeFlameTester.test('adds a ball on tap up', (game) async { debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready(); await game.ready();
@ -71,12 +170,46 @@ void main() {
final tapUpEvent = MockTapUpInfo(); final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onTapUp(tapUpEvent); game.onTapUp(tapUpEvent);
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<Ball>().length, game.children.whereType<Ball>().length,
equals(1), equals(previousBalls.length + 1),
);
});
group('controller', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final debugModeFlameBlocTester =
FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballGameTest.new,
blocBuilder: () => gameBloc,
);
debugModeFlameBlocTester.testGameWidget(
'ignores debug balls',
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
await game.ready();
game.children.removeWhere((component) => component is Ball);
await game.ready();
await game.ensureAdd(ControlledBall.debug());
expect(
game.controller.listenWhen(MockGameState(), newState),
isTrue,
);
},
); );
}); });
}); });

@ -1,3 +1,5 @@
// ignore_for_file: must_call_super
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';

@ -0,0 +1,13 @@
import 'package:flame_forge2d/flame_forge2d.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}

@ -7,6 +7,7 @@
export 'builders.dart'; export 'builders.dart';
export 'extensions.dart'; export 'extensions.dart';
export 'fakes.dart'; export 'fakes.dart';
export 'forge2d.dart';
export 'key_testers.dart'; export 'key_testers.dart';
export 'mocks.dart'; export 'mocks.dart';
export 'navigator.dart'; export 'navigator.dart';

@ -21,6 +21,8 @@ class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {} class MockBall extends Mock implements Ball {}
class MockControlledBall extends Mock implements ControlledBall {}
class MockBallController extends Mock implements BallController {} class MockBallController extends Mock implements BallController {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}

Loading…
Cancel
Save