Merge branch 'main' into feat/add-slingshots

pull/148/head
Allison Ryan 4 years ago
commit c3a36878d7

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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

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

@ -5,11 +5,10 @@ part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus {
/// 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,
/// Bonus achieved when the user activates all of the Dash
/// nests on the board, adding a new ball to the board.
/// Bonus achieved when the user activates all dash nest bumpers.
dashNest,
}

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

@ -1,8 +1,8 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the
@ -14,10 +14,9 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
/// {@macro plunger}
Plunger({
required this.compressionDistance,
}) : super(
// TODO(allisonryan0002): remove paint after asset is added.
paint: Paint()..color = const Color.fromARGB(255, 241, 8, 8),
);
// TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities
// are fixed.
}) : super(priority: 0);
/// Distance the plunger can lower.
final double compressionDistance;
@ -88,13 +87,36 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
plunger: this,
anchor: anchor,
);
world.createJoint(PrismaticJoint(jointDef));
world.createJoint(
PrismaticJoint(jointDef)..setLimits(-compressionDistance, 0),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
renderBody = false;
await _loadSprite();
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.components.plunger.path,
);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(5.5, 40),
anchor: Anchor.center,
position: Vector2(2, 19),
angle: -0.033,
),
);
}
}
@ -111,6 +133,16 @@ class PlungerAnchor extends JointAnchor {
plunger.body.position.y - plunger.compressionDistance,
);
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.5;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template plunger_anchor_prismatic_joint_def}

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

@ -2,10 +2,10 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -13,29 +13,38 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents {
PinballGame({required this.theme, required this.audio}) {
with
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController> {
PinballGame({
required this.theme,
required this.audio,
}) {
images.prefix = '';
controller = _GameBallsController(this);
}
final PinballTheme theme;
final PinballAudio audio;
@override
void onAttach() {
super.onAttach();
spawnBall();
}
@override
Future<void> onLoad() async {
_addContactCallbacks();
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
unawaited(_addPlunger());
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
unawaited(add(Board()));
unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls()));
@ -54,10 +63,8 @@ class PinballGame extends Forge2DGame
),
);
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
controller.attachTo(plunger);
await super.onLoad();
}
void _addContactCallbacks() {
@ -71,13 +78,6 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add);
}
Future<void> _addPlunger() async {
final plunger = Plunger(compressionDistance: 29)
..initialPosition =
BoardDimensions.bounds.center.toVector2() + Vector2(41.5, -49);
await add(plunger);
}
Future<void> _addBonusWord() async {
await add(
BonusWord(
@ -88,21 +88,49 @@ class PinballGame extends Forge2DGame
),
);
}
}
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
_GameBallsController(PinballGame game) : super(game);
Future<void> spawnBall() async {
// TODO(alestiago): Remove once this logic is moved to controller.
var plunger = firstChild<Plunger>();
if (plunger == null) {
await add(plunger = Plunger(compressionDistance: 1));
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(
theme: theme,
theme: gameRef.theme,
)..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y + Ball.size.y,
_plunger.body.position.x,
_plunger.body.position.y + Ball.size.y,
);
await add(ball);
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;
}
}
@ -113,7 +141,9 @@ class DebugPinballGame extends PinballGame with TapDetector {
}) : super(
theme: theme,
audio: audio,
);
) {
controller = _DebugGameBallsController(this);
}
@override
Future<void> onLoad() async {
@ -145,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;
}
}

@ -17,6 +17,10 @@ class $AssetsImagesComponentsGen {
AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png');
/// File path: assets/images/components/plunger.png
AssetGenImage get plunger =>
const AssetGenImage('assets/images/components/plunger.png');
}
class Assets {

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

@ -31,6 +31,8 @@ class $AssetsImagesGen {
const $AssetsImagesLaunchRampGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyBumperGen get sparkyBumper =>
const $AssetsImagesSparkyBumperGen();
}
class $AssetsImagesBaseboardGen {
@ -163,6 +165,14 @@ class $AssetsImagesSpaceshipGen {
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 {
const $AssetsImagesDashBumperAGen();
@ -227,6 +237,42 @@ class $AssetsImagesSpaceshipRampGen {
'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 {
Assets._();

@ -20,3 +20,4 @@ export 'slingshot.dart';
export 'spaceship.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp.dart';
export 'sparky_bumper.dart';

@ -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,9 @@ flutter:
- assets/images/chrome_dino/
- assets/images/kicker/
- assets/images/slingshot/
- assets/images/sparky_bumper/a/
- assets/images/sparky_bumper/b/
- assets/images/sparky_bumper/c/
flutter_gen:
line_length: 80

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

@ -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,
);
}

@ -7,3 +7,4 @@ export 'flipper/stories.dart';
export 'layer/stories.dart';
export 'slingshot/stories.dart';
export 'spaceship/stories.dart';
export 'sparky_bumper/stories.dart';

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

@ -13,42 +13,12 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusBallController', () {
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', () {
group('BallController', () {
test('can be instantiated', () {
expect(
LaunchedBallController(MockBall()),
isA<LaunchedBallController>(),
BallController(MockBall()),
isA<BallController>(),
);
});
@ -74,7 +44,7 @@ void main() {
flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
@ -84,114 +54,6 @@ void main() {
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
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -11,18 +10,6 @@ import 'package:pinball_components/pinball_components.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() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
@ -92,7 +79,7 @@ void main() {
);
flameBlocTester.testGameWidget(
'listens when a Bonus.dashNest is added',
'listens when a Bonus.dashNest and a bonusBall is added',
verify: (game, tester) async {
final flutterForest = FlutterForest();
@ -103,6 +90,7 @@ void main() {
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
);
expect(
flutterForest.controller
.listenWhen(const GameState.initial(), state),

@ -12,11 +12,32 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
final flameTester = FlameTester(TestGame.new);
group('Plunger', () {
const compressionDistance = 0.0;
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.add(
Plunger(
compressionDistance: compressionDistance,
),
);
await game.ready();
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1;
},
// TODO(ruimiguel): enable test when workflows are fixed.
// verify: (game, tester) async {
// await expectLater(
// find.byGame<Forge2DGame>(),
// matchesGoldenFile('golden/plunger.png'),
// );
// },
);
flameTester.test(
'loads correctly',
(game) async {

@ -3,40 +3,15 @@
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/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
final flameTester = FlameTester(EmptyPinballGameTest.new);
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(
'loads correctly',
(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
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -10,11 +11,11 @@ import 'package:pinball_components/pinball_components.dart';
import '../helpers/helpers.dart';
void main() {
group('PinballGame', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.new);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416
@ -60,8 +61,106 @@ void main() {
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 {
await game.ready();
@ -71,12 +170,46 @@ void main() {
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onTapUp(tapUpEvent);
await game.ready();
expect(
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_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 'extensions.dart';
export 'fakes.dart';
export 'forge2d.dart';
export 'key_testers.dart';
export 'mocks.dart';
export 'navigator.dart';

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

Loading…
Cancel
Save