feat: bumping all `DashNestBumper`s summons a new `Ball` (#89)

* refactor: rename RoundBumper to DashNestBumper

* feat: implemented DashNestBumper

* refactor: renamed files and tests

* chore: removed unused import

* refactor: renamed file to be a test file

* feat: added a new Ball on new state

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/100/head
Alejandro Santiago 4 years ago committed by GitHub
parent e2c67571d5
commit cfd4e790fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,11 +1,9 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template board}
/// The main flat surface of the [PinballGame], where the [Flipper]s,
/// [RoundBumper]s, [Kicker]s are arranged.
/// {entemplate}
/// The main flat surface of the [PinballGame].
/// {endtemplate}
class Board extends Component {
/// {@macro board}
Board();
@ -21,7 +19,7 @@ class Board extends Component {
spacing: 2,
);
final dashForest = _FlutterForest(
final flutterForest = FlutterForest(
position: Vector2(
PinballGame.boardBounds.center.dx + 20,
PinballGame.boardBounds.center.dy + 48,
@ -30,44 +28,7 @@ class Board extends Component {
await addAll([
bottomGroup,
dashForest,
]);
}
}
/// {@template flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball]
/// can bounce off [RoundBumper]s.
/// {@endtemplate}
class _FlutterForest extends Component {
/// {@macro flutter_forest}
_FlutterForest({
required this.position,
});
final Vector2 position;
@override
Future<void> onLoad() async {
// TODO(alestiago): adjust positioning once sprites are added.
// TODO(alestiago): Use [NestBumper] instead of [RoundBumper] once provided.
final smallLeftNest = RoundBumper(
radius: 1,
points: 10,
)..initialPosition = position + Vector2(-4.8, 2.8);
final smallRightNest = RoundBumper(
radius: 1,
points: 10,
)..initialPosition = position + Vector2(0.5, -5.5);
final bigNest = RoundBumper(
radius: 2,
points: 20,
)..initialPosition = position;
await addAll([
smallLeftNest,
smallRightNest,
bigNest,
flutterForest,
]);
}
}

@ -21,13 +21,9 @@ class BonusWord extends Component with BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
if ((previousState?.bonusHistory.length ?? 0) <
return (previousState?.bonusHistory.length ?? 0) <
newState.bonusHistory.length &&
newState.bonusHistory.last == GameBonus.word) {
return true;
}
return false;
newState.bonusHistory.last == GameBonus.word;
}
@override

@ -4,6 +4,7 @@ export 'board.dart';
export 'board_side.dart';
export 'bonus_word.dart';
export 'flipper.dart';
export 'flutter_forest.dart';
export 'jetpack_ramp.dart';
export 'joint_anchor.dart';
export 'kicker.dart';
@ -11,7 +12,6 @@ export 'launcher_ramp.dart';
export 'pathway.dart';
export 'plunger.dart';
export 'ramp_opening.dart';
export 'round_bumper.dart';
export 'score_points.dart';
export 'spaceship.dart';
export 'wall.dart';

@ -0,0 +1,131 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball]
/// can bounce off [DashNestBumper]s.
///
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// {@endtemplate}
// TODO(alestiago): Make a [Blueprint] once nesting [Blueprint] is implemented.
class FlutterForest extends Component
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
/// {@macro flutter_forest}
FlutterForest({
required this.position,
});
/// The position of the [FlutterForest] on the [Board].
final Vector2 position;
@override
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);
gameRef.addFromBlueprint(BallBlueprint(position: position));
}
@override
Future<void> onLoad() async {
gameRef.addContactCallback(DashNestBumperBallContactCallback());
// TODO(alestiago): adjust positioning once sprites are added.
final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest')
..initialPosition = position + Vector2(-4.8, 2.8);
final smallRightNest = SmallDashNestBumper(id: 'small_right_nest')
..initialPosition = position + Vector2(0.5, -5.5);
final bigNest = BigDashNestBumper(id: 'big_nest')
..initialPosition = position;
await addAll([
smallLeftNest,
smallRightNest,
bigNest,
]);
}
}
/// {@template dash_nest_bumper}
/// Bumper located in the [FlutterForest].
/// {@endtemplate}
@visibleForTesting
abstract class DashNestBumper extends BodyComponent<PinballGame>
with ScorePoints, InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper({required this.id});
/// Unique identifier for this [DashNestBumper].
///
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests].
final String id;
}
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
@visibleForTesting
class DashNestBumperBallContactCallback
extends ContactCallback<DashNestBumper, Ball> {
@override
void begin(DashNestBumper dashNestBumper, Ball ball, Contact _) {
dashNestBumper.gameRef.read<GameBloc>().add(
DashNestActivated(dashNestBumper.id),
);
}
}
/// {@macro dash_nest_bumper}
@visibleForTesting
class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
BigDashNestBumper({required String id}) : super(id: id);
@override
int get points => 20;
@override
Body createBody() {
final shape = CircleShape()..radius = 2.5;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@macro dash_nest_bumper}
@visibleForTesting
class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
SmallDashNestBumper({required String id}) : super(id: id);
@override
int get points => 10;
@override
Body createBody() {
final shape = CircleShape()..radius = 1;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -1,35 +0,0 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template round_bumper}
/// Circular body that repels a [Ball] on contact, increasing the score.
/// {@endtemplate}
class RoundBumper extends BodyComponent with ScorePoints, InitialPosition {
/// {@macro round_bumper}
RoundBumper({
required double radius,
required int points,
}) : _radius = radius,
_points = points;
/// The radius of the [RoundBumper].
final double _radius;
/// Points awarded from hitting this [RoundBumper].
final int _points;
@override
int get points => _points;
@override
Body createBody() {
final shape = CircleShape()..radius = _radius;
final fixtureDef = FixtureDef(shape)..restitution = 1;
final bodyDef = BodyDef()..position = initialPosition;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -75,15 +75,15 @@ void main() {
);
flameTester.test(
'has three RoundBumpers',
'has one FlutterForest',
(game) async {
// TODO(alestiago): change to [NestBumpers] once provided.
final board = Board();
await game.ready();
await game.ensureAdd(board);
final roundBumpers = board.descendants().whereType<RoundBumper>();
expect(roundBumpers.length, equals(3));
final flutterForest = board.descendants().whereType<FlutterForest>();
expect(flutterForest.length, equals(1));
},
);
});

@ -194,6 +194,7 @@ void main() {
group('bonus letter activation', () {
final gameBloc = MockGameBloc();
final tester = flameBlocTester(gameBloc: () => gameBloc);
BonusLetter _getBonusLetter(PinballGame game) {
return game.children
@ -212,8 +213,6 @@ void main() {
);
});
final tester = flameBlocTester(gameBloc: () => gameBloc);
tester.widgetTest(
'adds BonusLetterActivated to GameBloc when not activated',
(game, tester) async {

@ -0,0 +1,125 @@
// 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_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('FlutterForest', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final flutterForest = FlutterForest(position: Vector2(0, 0));
await game.ensureAdd(flutterForest);
expect(game.contains(flutterForest), isTrue);
},
);
flameTester.test(
'onNewState adds a new ball',
(game) async {
final flutterForest = FlutterForest(position: Vector2(0, 0));
await game.ready();
await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length;
flutterForest.onNewState(MockGameState());
await game.ready();
expect(
game.descendants().whereType<Ball>().length,
greaterThan(previousBalls),
);
},
);
group('listenWhen', () {
final gameBloc = MockGameBloc();
final tester = flameBlocTester(gameBloc: () => gameBloc);
setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
tester.widgetTest(
'listens when a Bonus.dashNest is added',
(game, tester) async {
await game.ready();
final flutterForest =
game.descendants().whereType<FlutterForest>().first;
const state = GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
);
expect(
flutterForest.listenWhen(const GameState.initial(), state),
isTrue,
);
},
);
});
});
group('DashNestBumperBallContactCallback', () {
final gameBloc = MockGameBloc();
final tester = flameBlocTester(gameBloc: () => gameBloc);
setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
tester.widgetTest(
'adds a DashNestActivated event with DashNestBumper.id',
(game, tester) async {
final contactCallback = DashNestBumperBallContactCallback();
const id = '0';
final dashNestBumper = MockDashNestBumper();
when(() => dashNestBumper.id).thenReturn(id);
when(() => dashNestBumper.gameRef).thenReturn(game);
contactCallback.begin(dashNestBumper, MockBall(), MockContact());
verify(() => gameBloc.add(DashNestActivated(dashNestBumper.id)))
.called(1);
},
);
});
group('BigDashNestBumper', () {
test('has points', () {
final dashNestBumper = BigDashNestBumper(id: '');
expect(dashNestBumper.points, greaterThan(0));
});
});
group('SmallDashNestBumper', () {
test('has points', () {
final dashNestBumper = SmallDashNestBumper(id: '');
expect(dashNestBumper.points, greaterThan(0));
});
});
}

@ -1,102 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RoundBumper', () {
final flameTester = FlameTester(Forge2DGame.new);
const radius = 1.0;
const points = 1;
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final roundBumper = RoundBumper(
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(game.contains(roundBumper), isTrue);
},
);
flameTester.test(
'has points',
(game) async {
final roundBumper = RoundBumper(
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(roundBumper.points, equals(points));
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final roundBumper = RoundBumper(
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(roundBumper.body.bodyType, equals(BodyType.static));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final roundBumper = RoundBumper(
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(roundBumper.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'has restitution',
(game) async {
final roundBumper = RoundBumper(
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
final fixture = roundBumper.body.fixtures[0];
expect(fixture.restitution, greaterThan(0));
},
);
flameTester.test(
'shape is circular',
(game) async {
final roundBumper = RoundBumper(
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
final fixture = roundBumper.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1));
},
);
});
});
}

@ -68,3 +68,5 @@ class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {}

Loading…
Cancel
Save