fix: fixed merge conflicts on gamebloc tests

pull/154/head
RuiAlonso 4 years ago
commit 4aa67f62d5

@ -13,7 +13,7 @@ on:
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@b075749771679a5baa4c90d36ad2e8580bbf273b
with:
working_directory: packages/pinball_components
coverage_excludes: "lib/gen/*.dart"

@ -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

@ -20,10 +20,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) {
@ -37,7 +35,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: [],
@ -56,15 +55,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() {}
}

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

@ -15,6 +15,10 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.baseboard.right.keyName),
images.load(components.Assets.images.kicker.left.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.foregroundRailing.keyName,

@ -5,6 +5,7 @@ import 'package:flame/components.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';
@ -12,30 +13,40 @@ 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()));
unawaited(_addBonusWord());
unawaited(addFromBlueprint(SpaceshipRamp()));
@ -52,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() {
@ -69,12 +78,6 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add);
}
Future<void> _addPlunger() async {
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
}
Future<void> _addBonusWord() async {
await add(
BonusWord(
@ -85,13 +88,49 @@ class PinballGame extends Forge2DGame
),
);
}
}
class _GameBallsController extends ComponentController<PinballGame>
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();
}
Future<void> spawnBall() async {
// TODO(alestiago): Remove once this logic is moved to controller.
void _spawnBall() {
final ball = ControlledBall.launch(
theme: theme,
)..initialPosition = Vector2(38, -19 + Ball.size.y);
await add(ball);
theme: gameRef.theme,
)..initialPosition = Vector2(
_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(
theme: theme,
audio: audio,
);
) {
controller = _DebugGameBallsController(this);
}
@override
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

@ -29,6 +29,7 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyBumperGen get sparkyBumper =>
const $AssetsImagesSparkyBumperGen();
@ -127,6 +128,26 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png');
}
class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen();
/// File path: assets/images/slingshot/left_lower.png
AssetGenImage get leftLower =>
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 {
const $AssetsImagesSpaceshipGen();

@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
/// {@template camera_zoom}
/// Applies zoom to the camera of the game where this is added to
/// {@endtemplate}
class CameraZoom extends Effect with HasGameRef {
/// {@macro camera_zoom}
CameraZoom({
required this.value,
}) : super(
EffectController(
duration: 0.4,
curve: Curves.easeOut,
),
);
/// The total zoom value to be applied to the camera
final double value;
late final Tween<double> _tween;
final Completer<void> _completer = Completer();
@override
Future<void> onLoad() async {
_tween = Tween(
begin: gameRef.camera.zoom,
end: value,
);
}
@override
void apply(double progress) {
gameRef.camera.zoom = _tween.transform(progress);
}
/// Returns a [Future] that completes once the zoom is finished
Future<void> get completed {
if (controller.completed) {
return Future.value();
}
return _completer.future;
}
@override
void onRemove() {
_completer.complete();
super.onRemove();
}
}

@ -3,6 +3,7 @@ export 'baseboard.dart';
export 'board_dimensions.dart';
export 'board_side.dart';
export 'boundaries.dart';
export 'camera_zoom.dart';
export 'chrome_dino.dart';
export 'dash_nest_bumper.dart';
export 'dino_walls.dart';
@ -16,6 +17,7 @@ export 'launch_ramp.dart';
export 'layer.dart';
export 'ramp_opening.dart';
export 'shapes/shapes.dart';
export 'slingshot.dart';
export 'spaceship.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp.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,
),
);
}
}

@ -39,6 +39,7 @@ flutter:
- assets/images/spaceship/ramp/
- 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/

@ -21,6 +21,8 @@ void main() {
addChromeDinoStories(dashbook);
addDashNestBumperStories(dashbook);
addKickerStories(dashbook);
addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook);
addZoomStories(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,
);
}

@ -5,5 +5,7 @@ export 'dash_nest_bumper/stories.dart';
export 'effects/stories.dart';
export 'flipper/stories.dart';
export 'layer/stories.dart';
export 'slingshot/stories.dart';
export 'spaceship/stories.dart';
export 'sparky_bumper/stories.dart';
export 'zoom/stories.dart';

@ -0,0 +1,37 @@
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BasicCameraZoomGame extends BasicGame with TapDetector {
static const info = '''
Simple game to demonstrate how the CameraZoom can be used.
Tap to zoom in/out
''';
bool zoomApplied = false;
@override
Future<void> onLoad() async {
final sprite = await loadSprite(Assets.images.flutterSignPost.keyName);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(4, 8),
anchor: Anchor.center,
),
);
camera.followVector2(Vector2.zero());
}
@override
void onTap() {
if (firstChild<CameraZoom>() == null) {
final zoom = CameraZoom(value: zoomApplied ? 30 : 10);
add(zoom);
zoomApplied = !zoomApplied;
}
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/zoom/basic_zoom_game.dart';
void addZoomStories(Dashbook dashbook) {
dashbook.storiesOf('CameraZoom').add(
'Basic',
(context) => GameWidget(
game: BasicCameraZoomGame(),
),
codeLink: buildSourceLink('zoom/basic_zoom_game.dart'),
info: BasicCameraZoomGame.info,
);
}

@ -0,0 +1,85 @@
// 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() {
group('CameraZoom', () {
final tester = FlameTester(TestGame.new);
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 10;
final sprite = await game.loadSprite(
Assets.images.flutterSignPost.keyName,
);
await game.add(
SpriteComponent(
sprite: sprite,
size: Vector2(4, 8),
anchor: Anchor.center,
),
);
await game.add(CameraZoom(value: 40));
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/camera_zoom/no_zoom.png'),
);
game.update(0.2);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/camera_zoom/in_between.png'),
);
game.update(0.4);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/camera_zoom/finished.png'),
);
game.update(0.1);
await tester.pump();
expect(game.firstChild<CameraZoom>(), isNull);
},
);
tester.test(
'completes when checked after it is finished',
(game) async {
await game.add(CameraZoom(value: 40));
game.update(10);
final cameraZoom = game.firstChild<CameraZoom>();
final future = cameraZoom!.completed;
expect(future, completes);
},
);
tester.test(
'completes when checked before it is finished',
(game) async {
final zoom = CameraZoom(value: 40);
final future = zoom.completed;
await game.add(zoom);
game.update(10);
game.update(0);
expect(future, completes);
},
);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

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

@ -14,13 +14,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(
@ -31,22 +28,6 @@ void main() {
activatedSparkyFires: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
activatedSparkyFires: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
activatedSparkyFires: {},
bonusHistory: [],
),
],
);
});
@ -252,7 +233,7 @@ void main() {
),
GameState(
score: 0,
balls: 3,
balls: 4,
activatedBonusLetters: [],
activatedDashNests: {},
activatedSparkyFires: {},

@ -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();
@ -104,6 +91,7 @@ void main() {
activatedSparkyFires: {},
bonusHistory: [GameBonus.dashNest],
);
expect(
flutterForest.controller
.listenWhen(const GameState.initial(), state),

@ -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