Merge branch 'main' into feat/composable-blueprints

pull/92/head
Erick 4 years ago committed by GitHub
commit 0433c20567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

@ -5,7 +5,7 @@ import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template baseboard}
/// Straight, angled board piece to corral the [Ball] towards the [Flipper]s.
/// Wing-shaped board piece to corral the [Ball] towards the [Flipper]s.
/// {@endtemplate}
class Baseboard extends BodyComponent with InitialPosition {
/// {@macro baseboard}
@ -13,41 +13,68 @@ class Baseboard extends BodyComponent with InitialPosition {
required BoardSide side,
}) : _side = side;
/// The width of the [Baseboard].
static const width = 10.0;
/// The height of the [Baseboard].
static const height = 2.0;
/// The size of the [Baseboard].
static final size = Vector2(24.2, 13.5);
/// Whether the [Baseboard] is on the left or right side of the board.
final BoardSide _side;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final direction = _side.direction;
final arcsAngle = -1.11 * direction;
const arcsRotation = math.pi / 2.08;
final topCircleShape = CircleShape()..radius = 0.7;
topCircleShape.position.setValues(11.39 * direction, 6.05);
final topCircleFixtureDef = FixtureDef(topCircleShape);
fixturesDef.add(topCircleFixtureDef);
final innerEdgeShape = EdgeShape()
..set(
Vector2(10.86 * direction, 6.45),
Vector2(6.96 * direction, 0.25),
);
final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape);
fixturesDef.add(innerEdgeShapeFixtureDef);
final outerEdgeShape = EdgeShape()
..set(
Vector2(11.96 * direction, 5.85),
Vector2(5.48 * direction, -4.85),
);
final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape);
fixturesDef.add(outerEdgeShapeFixtureDef);
final upperArcFixtureDefs = Pathway.arc(
center: Vector2(1.76 * direction, 3.25),
width: 0,
radius: 6.1,
angle: arcsAngle,
rotation: arcsRotation,
singleWall: true,
).createFixtureDefs();
fixturesDef.addAll(upperArcFixtureDefs);
final lowerArcFixtureDefs = Pathway.arc(
center: Vector2(1.85 * direction, -2.15),
width: 0,
radius: 4.5,
angle: arcsAngle,
rotation: arcsRotation,
singleWall: true,
).createFixtureDefs();
fixturesDef.addAll(lowerArcFixtureDefs);
final circleShape1 = CircleShape()..radius = Baseboard.height / 2;
circleShape1.position.setValues(
-(Baseboard.width / 2) + circleShape1.radius,
0,
);
final circle1FixtureDef = FixtureDef(circleShape1);
fixturesDef.add(circle1FixtureDef);
final circleShape2 = CircleShape()..radius = Baseboard.height / 2;
circleShape2.position.setValues(
(Baseboard.width / 2) - circleShape2.radius,
0,
);
final circle2FixtureDef = FixtureDef(circleShape2);
fixturesDef.add(circle2FixtureDef);
final rectangle = PolygonShape()
..setAsBoxXY(
(Baseboard.width - Baseboard.height) / 2,
Baseboard.height / 2,
final bottomRectangle = PolygonShape()
..setAsBox(
7,
2,
Vector2(-5.14 * direction, -4.75),
0,
);
final rectangleFixtureDef = FixtureDef(rectangle);
fixturesDef.add(rectangleFixtureDef);
final bottomRectangleFixtureDef = FixtureDef(bottomRectangle);
fixturesDef.add(bottomRectangleFixtureDef);
return fixturesDef;
}
@ -56,7 +83,7 @@ class Baseboard extends BodyComponent with InitialPosition {
Body createBody() {
// TODO(allisonryan0002): share sweeping angle with flipper when components
// are grouped.
const angle = math.pi / 7;
const angle = math.pi / 5;
final bodyDef = BodyDef()
..position = initialPosition

@ -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,
]);
}
}
@ -134,8 +95,8 @@ class _BottomGroupSide extends Component {
final baseboard = Baseboard(side: _side)
..initialPosition = _position +
Vector2(
(Flipper.size.x * direction) - direction,
Flipper.size.y,
(Baseboard.size.x / 1.6 * direction),
Baseboard.size.y - 2,
);
final kicker = Kicker(
side: _side,

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

@ -17,8 +17,6 @@ class $AssetsImagesComponentsGen {
AssetGenImage get flipper =>
const AssetGenImage('assets/images/components/flipper.png');
AssetGenImage get sauce =>
const AssetGenImage('assets/images/components/sauce.png');
$AssetsImagesComponentsSpaceshipGen get spaceship =>
const $AssetsImagesComponentsSpaceshipGen();
}

@ -61,14 +61,14 @@ void main() {
group('fixtures', () {
flameTester.test(
'has three',
'has six',
(game) async {
final baseboard = Baseboard(
side: BoardSide.left,
);
await game.ensureAdd(baseboard);
expect(baseboard.body.fixtures.length, equals(3));
expect(baseboard.body.fixtures.length, equals(6));
},
);
});

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