feat: implemented DashBumper assets (#119)

* chore: rebase

* chore: rebase

* chore: rebase

* docs: removed extra paragraph

* fix: removed checks

* refactor: removed ephemeral state

* refactor: uncommented code

* feat: automatically sized Sprites

* docs: included TODO comment

* refactor: corrected typo

* docs: updated doc comment

* feat: adjusted tests

* chore: rebase

* feat: included mustCallSuper

* feat: implemented FlutterForest controllers

* feat: improved tests

* fix: analyzer

* refactor: removed unneccessary mock

* feat: include DashNestBumper tests

* docs: improved doc comment

* refactor: fixed test name grammar

* fix: fixed test
pull/133/head
Alejandro Santiago 4 years ago committed by GitHub
parent 48f831264e
commit e5c3708952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/foundation.dart';
/// {@template component_controller} /// {@template component_controller}
/// A [ComponentController] is a [Component] in charge of handling the logic /// A [ComponentController] is a [Component] in charge of handling the logic
@ -30,6 +31,7 @@ mixin Controls<T extends ComponentController> on Component {
late final T controller; late final T controller;
@override @override
@mustCallSuper
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await add(controller); await add(controller);

@ -1,11 +1,10 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -16,42 +15,28 @@ import 'package:pinball_components/pinball_components.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] /// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [BigDashNestBumper] releases a new [Ball]. /// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// {@endtemplate} /// {@endtemplate}
// TODO(alestiago): Make a [Blueprint] once nesting [Blueprint] is implemented. // TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from
class FlutterForest extends Component // [Component].
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> { class FlutterForest extends Component with Controls<_FlutterForestController> {
/// {@macro flutter_forest} /// {@macro flutter_forest}
FlutterForest() {
@override controller = _FlutterForestController(this);
bool listenWhen(GameState? previousState, GameState newState) {
return (previousState?.bonusHistory.length ?? 0) <
newState.bonusHistory.length &&
newState.bonusHistory.last == GameBonus.dashNest;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
add(
ControlledBall.bonus(
theme: gameRef.theme,
)..initialPosition = Vector2(17.2, 52.7),
);
} }
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
gameRef.addContactCallback(DashNestBumperBallContactCallback()); await super.onLoad();
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3);
// TODO(alestiago): adjust positioning once sprites are added. final bigNest = _ControlledBigDashNestBumper(
final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') id: 'big_nest_bumper',
..initialPosition = Vector2(8.95, 51.95); )..initialPosition = Vector2(18.55, 59.35);
final smallRightNest = SmallDashNestBumper(id: 'small_right_nest') final smallLeftNest = _ControlledSmallDashNestBumper.a(
..initialPosition = Vector2(23.3, 46.75); id: 'small_nest_bumper_a',
final bigNest = BigDashNestBumper(id: 'big_nest') )..initialPosition = Vector2(8.95, 51.95);
..initialPosition = Vector2(18.55, 59.35); final smallRightNest = _ControlledSmallDashNestBumper.b(
id: 'small_nest_bumper_b',
)..initialPosition = Vector2(23.3, 46.75);
await addAll([ await addAll([
signPost, signPost,
@ -62,87 +47,111 @@ class FlutterForest extends Component
} }
} }
/// {@template dash_nest_bumper} class _FlutterForestController extends ComponentController<FlutterForest>
/// Bumper located in the [FlutterForest]. with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@endtemplate} _FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
@visibleForTesting
abstract class DashNestBumper extends BodyComponent<PinballGame> @override
with ScorePoints, InitialPosition { Future<void> onLoad() async {
/// {@macro dash_nest_bumper} await super.onLoad();
DashNestBumper({required this.id}) { gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback());
paint = Paint()
..color = Colors.blue.withOpacity(0.5)
..style = PaintingStyle.fill;
} }
/// Unique identifier for this [DashNestBumper]. @override
/// bool listenWhen(GameState? previousState, GameState newState) {
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. return (previousState?.bonusHistory.length ?? 0) <
final String id; newState.bonusHistory.length &&
newState.bonusHistory.last == GameBonus.dashNest;
} }
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
@visibleForTesting
class DashNestBumperBallContactCallback
extends ContactCallback<DashNestBumper, Ball> {
@override @override
void begin(DashNestBumper dashNestBumper, Ball ball, Contact _) { void onNewState(GameState state) {
dashNestBumper.gameRef.read<GameBloc>().add( super.onNewState(state);
DashNestActivated(dashNestBumper.id),
component.add(
ControlledBall.bonus(theme: gameRef.theme)
..initialPosition = Vector2(17.2, 52.7),
); );
} }
} }
/// {@macro dash_nest_bumper} class _ControlledBigDashNestBumper extends BigDashNestBumper
@visibleForTesting with Controls<DashNestBumperController>, ScorePoints {
class BigDashNestBumper extends DashNestBumper { _ControlledBigDashNestBumper({required String id}) : super() {
/// {@macro dash_nest_bumper} controller = DashNestBumperController(this, id: id);
BigDashNestBumper({required String id}) : super(id: id); }
@override @override
int get points => 20; int get points => 20;
}
@override class _ControlledSmallDashNestBumper extends SmallDashNestBumper
Body createBody() { with Controls<DashNestBumperController>, ScorePoints {
final shape = EllipseShape( _ControlledSmallDashNestBumper.a({required String id}) : super.a() {
center: Vector2.zero(), controller = DashNestBumperController(this, id: id);
majorRadius: 4.85, }
minorRadius: 3.95,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef); _ControlledSmallDashNestBumper.b({required String id}) : super.b() {
controller = DashNestBumperController(this, id: id);
} }
@override
int get points => 10;
} }
/// {@macro dash_nest_bumper} /// {@template dash_nest_bumper_controller}
/// Controls a [DashNestBumper].
/// {@endtemplate}
@visibleForTesting @visibleForTesting
class SmallDashNestBumper extends DashNestBumper { class DashNestBumperController extends ComponentController<DashNestBumper>
/// {@macro dash_nest_bumper} with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
SmallDashNestBumper({required String id}) : super(id: id); /// {@macro dash_nest_bumper_controller}
DashNestBumperController(
DashNestBumper dashNestBumper, {
required this.id,
}) : super(dashNestBumper);
/// Unique identifier for the controlled [DashNestBumper].
///
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests].
final String id;
@override @override
int get points => 10; bool listenWhen(GameState? previousState, GameState newState) {
final wasActive = previousState?.activatedDashNests.contains(id) ?? false;
final isActive = newState.activatedDashNests.contains(id);
return wasActive != isActive;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
if (state.activatedDashNests.contains(id)) {
component.activate();
} else {
component.deactivate();
}
}
/// Registers when a [DashNestBumper] is hit by a [Ball].
///
/// Triggered by [_ControlledDashNestBumperBallContactCallback].
void hit() {
gameRef.read<GameBloc>().add(DashNestActivated(id));
}
}
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
class _ControlledDashNestBumperBallContactCallback
extends ContactCallback<Controls<DashNestBumperController>, Ball> {
@override @override
Body createBody() { void begin(
final shape = EllipseShape( Controls<DashNestBumperController> controlledDashNestBumper,
center: Vector2.zero(), Ball _,
majorRadius: 3, Contact __,
minorRadius: 2.25, ) {
)..rotate(math.pi / 2); controlledDashNestBumper.controller.hit();
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
} }

@ -15,6 +15,12 @@ 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.dino.dinoLandTop.keyName), images.load(components.Assets.images.dino.dinoLandTop.keyName),
images.load(components.Assets.images.dino.dinoLandBottom.keyName), images.load(components.Assets.images.dino.dinoLandBottom.keyName),
images.load(components.Assets.images.dashBumper.a.active.keyName),
images.load(components.Assets.images.dashBumper.a.inactive.keyName),
images.load(components.Assets.images.dashBumper.b.active.keyName),
images.load(components.Assets.images.dashBumper.b.inactive.keyName),
images.load(components.Assets.images.dashBumper.main.active.keyName),
images.load(components.Assets.images.dashBumper.main.inactive.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
]); ]);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@ -14,6 +14,8 @@ class $AssetsImagesGen {
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesDashBumperGen get dashBumper =>
const $AssetsImagesDashBumperGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
@ -42,6 +44,15 @@ class $AssetsImagesBaseboardGen {
const AssetGenImage('assets/images/baseboard/right.png'); const AssetGenImage('assets/images/baseboard/right.png');
} }
class $AssetsImagesDashBumperGen {
const $AssetsImagesDashBumperGen();
$AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen();
$AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen();
$AssetsImagesDashBumperMainGen get main =>
const $AssetsImagesDashBumperMainGen();
}
class $AssetsImagesDinoGen { class $AssetsImagesDinoGen {
const $AssetsImagesDinoGen(); const $AssetsImagesDinoGen();
@ -66,6 +77,42 @@ class $AssetsImagesFlipperGen {
const AssetGenImage('assets/images/flipper/right.png'); const AssetGenImage('assets/images/flipper/right.png');
} }
class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen();
/// File path: assets/images/dash_bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/a/active.png');
/// File path: assets/images/dash_bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/a/inactive.png');
}
class $AssetsImagesDashBumperBGen {
const $AssetsImagesDashBumperBGen();
/// File path: assets/images/dash_bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/b/active.png');
/// File path: assets/images/dash_bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/b/inactive.png');
}
class $AssetsImagesDashBumperMainGen {
const $AssetsImagesDashBumperMainGen();
/// File path: assets/images/dash_bumper/main/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/main/active.png');
/// File path: assets/images/dash_bumper/main/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/main/inactive.png');
}
class Assets { class Assets {
Assets._(); Assets._();

@ -2,6 +2,7 @@ export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_dimensions.dart'; export 'board_dimensions.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'dash_nest_bumper.dart';
export 'dino_walls.dart'; export 'dino_walls.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';

@ -0,0 +1,142 @@
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 dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@endtemplate}
abstract class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
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;
}
@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);
}
}
/// {@macro dash_nest_bumper}
class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
BigDashNestBumper()
: super._(
activeAssetPath: Assets.images.dashBumper.main.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
),
);
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 4.85,
minorRadius: 3.95,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@macro dash_nest_bumper}
class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
SmallDashNestBumper._({
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : super._(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
spriteComponent: spriteComponent,
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.a()
: this._(
activeAssetPath: Assets.images.dashBumper.a.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.b()
: this._(
activeAssetPath: Assets.images.dashBumper.b.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
);
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3,
minorRadius: 2.25,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -29,6 +29,9 @@ flutter:
- assets/images/baseboard/ - assets/images/baseboard/
- assets/images/dino/ - assets/images/dino/
- assets/images/flipper/ - assets/images/flipper/
- assets/images/dash_bumper/a/
- assets/images/dash_bumper/b/
- assets/images/dash_bumper/main/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -0,0 +1,116 @@
// 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('BigDashNestBumper', () {
flameTester.test('loads correctly', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = BigDashNestBumper();
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)),
);
});
});
group('SmallDashNestBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = SmallDashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = SmallDashNestBumper.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)),
);
});
});
}

@ -48,8 +48,9 @@ void main() {
final canvas = MockCanvas(); final canvas = MockCanvas();
effect.render(canvas); effect.render(canvas);
verify(() => canvas.drawCircle(any(), any(), any())) verify(() => canvas.drawCircle(any(), any(), any())).called(
.called(greaterThan(0)); greaterThan(0),
);
}); });
}); });
} }

@ -1,7 +1,9 @@
// 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_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -9,6 +11,18 @@ 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(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
@ -30,33 +44,46 @@ void main() {
'a FlutterSignPost', 'a FlutterSignPost',
(game) async { (game) async {
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect( expect(
game.descendants().whereType<FlutterSignPost>().length, flutterForest.descendants().whereType<FlutterSignPost>().length,
equals(1), equals(1),
); );
}, },
); );
});
flameTester.test( flameTester.test(
'onNewState adds a new ball', 'a BigDashNestBumper',
(game) async { (game) async {
final flutterForest = FlutterForest();
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length; expect(
flutterForest.onNewState(MockGameState()); flutterForest.descendants().whereType<BigDashNestBumper>().length,
equals(1),
);
},
);
flameTester.test(
'two SmallDashNestBumper',
(game) async {
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect( expect(
game.descendants().whereType<Ball>().length, flutterForest.descendants().whereType<SmallDashNestBumper>().length,
greaterThan(previousBalls), equals(2),
); );
}, },
); );
});
group('controller', () {
group('listenWhen', () { group('listenWhen', () {
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
final tester = flameBlocTester( final tester = flameBlocTester(
@ -64,14 +91,6 @@ void main() {
gameBloc: () => gameBloc, gameBloc: () => gameBloc,
); );
setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
tester.testGameWidget( tester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest is added',
verify: (game, tester) async { verify: (game, tester) async {
@ -85,7 +104,8 @@ void main() {
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
); );
expect( expect(
flutterForest.listenWhen(const GameState.initial(), state), flutterForest.controller
.listenWhen(const GameState.initial(), state),
isTrue, isTrue,
); );
}, },
@ -93,15 +113,31 @@ void main() {
}); });
}); });
group('DashNestBumperBallContactCallback', () { flameTester.test(
final gameBloc = MockGameBloc(); 'onNewState adds a new ball',
final tester = flameBlocTester( (game) async {
// TODO(alestiago): Use TestGame.new once a controller is implemented. final flutterForest = FlutterForest();
game: PinballGameTest.create, await game.ready();
gameBloc: () => gameBloc, await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length;
flutterForest.controller.onNewState(MockGameState());
await game.ready();
expect(
game.descendants().whereType<Ball>().length,
greaterThan(previousBalls),
);
},
); );
group('bumpers', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() { setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -109,36 +145,167 @@ void main() {
); );
}); });
final dashNestBumper = MockDashNestBumper(); final tester = flameBlocTester<PinballGame>(
game: PinballGameTest.create,
gameBloc: () => gameBloc,
);
tester.testGameWidget( tester.testGameWidget(
'adds a DashNestActivated event with DashNestBumper.id', 'add DashNestActivated event',
setUp: (game, tester) async { setUp: (game, tester) async {
const id = '0'; await game.ready();
when(() => dashNestBumper.id).thenReturn(id); final flutterForest =
when(() => dashNestBumper.gameRef).thenReturn(game); game.descendants().whereType<FlutterForest>().first;
await game.ensureAdd(ball);
final bumpers =
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
final controller = bumper.firstChild<DashNestBumperController>()!;
verify(
() => gameBloc.add(DashNestActivated(controller.id)),
).called(1);
}
}, },
verify: (game, tester) async { );
final contactCallback = DashNestBumperBallContactCallback();
contactCallback.begin(dashNestBumper, MockBall(), MockContact());
tester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
await game.ensureAdd(ball);
final bumpers =
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
final points = (bumper as ScorePoints).points;
verify( verify(
() => gameBloc.add(DashNestActivated(dashNestBumper.id)), () => gameBloc.add(Scored(points: points)),
).called(1); ).called(1);
}
}, },
); );
}); });
});
group('DashNestBumperController', () {
late DashNestBumper dashNestBumper;
group('BigDashNestBumper', () { setUp(() {
test('has points', () { dashNestBumper = MockDashNestBumper();
final dashNestBumper = BigDashNestBumper(id: '');
expect(dashNestBumper.points, greaterThan(0));
}); });
group(
'listensWhen',
() {
late GameState previousState;
late GameState newState;
setUp(
() {
previousState = MockGameState();
newState = MockGameState();
},
);
test('listens when the id is added to activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isTrue);
}); });
group('SmallDashNestBumper', () { test('listens when the id is removed from activatedDashNests', () {
test('has points', () { const id = '';
final dashNestBumper = SmallDashNestBumper(id: ''); final controller = DashNestBumperController(
expect(dashNestBumper.points, greaterThan(0)); dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({});
expect(controller.listenWhen(previousState, newState), isTrue);
}); });
test("doesn't listen when the id is never in activatedDashNests", () {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
when(() => previousState.activatedDashNests).thenReturn({});
when(() => newState.activatedDashNests).thenReturn({});
expect(controller.listenWhen(previousState, newState), isFalse);
});
test("doesn't listen when the id still in activatedDashNests", () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isFalse);
});
},
);
group(
'onNewState',
() {
late GameState state;
setUp(() {
state = MockGameState();
});
test(
'activates the bumper when id in activatedDashNests',
() {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => state.activatedDashNests).thenReturn({id});
controller.onNewState(state);
verify(() => dashNestBumper.activate()).called(1);
},
);
test(
'deactivates the bumper when id not in activatedDashNests',
() {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
when(() => state.activatedDashNests).thenReturn({});
controller.onNewState(state);
verify(() => dashNestBumper.deactivate()).called(1);
},
);
},
);
}); });
} }

Loading…
Cancel
Save