mirror of https://github.com/flutter/pinball.git
commit
9e01799203
@ -0,0 +1,91 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
/// {@template baseboard}
|
||||||
|
/// Straight, angled board piece to corral the [Ball] towards the [Flipper]s.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Baseboard extends BodyComponent {
|
||||||
|
/// {@macro baseboard}
|
||||||
|
Baseboard._({
|
||||||
|
required Vector2 position,
|
||||||
|
required BoardSide side,
|
||||||
|
}) : _position = position,
|
||||||
|
_side = side;
|
||||||
|
|
||||||
|
/// A left positioned [Baseboard].
|
||||||
|
Baseboard.left({
|
||||||
|
required Vector2 position,
|
||||||
|
}) : this._(
|
||||||
|
position: position,
|
||||||
|
side: BoardSide.left,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A right positioned [Baseboard].
|
||||||
|
Baseboard.right({
|
||||||
|
required Vector2 position,
|
||||||
|
}) : this._(
|
||||||
|
position: position,
|
||||||
|
side: BoardSide.right,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The width of the [Baseboard].
|
||||||
|
static const width = 10.0;
|
||||||
|
|
||||||
|
/// The height of the [Baseboard].
|
||||||
|
static const height = 2.0;
|
||||||
|
|
||||||
|
/// The position of the [Baseboard] body.
|
||||||
|
final Vector2 _position;
|
||||||
|
|
||||||
|
/// Whether the [Baseboard] is on the left or right side of the board.
|
||||||
|
final BoardSide _side;
|
||||||
|
|
||||||
|
List<FixtureDef> _createFixtureDefs() {
|
||||||
|
final fixtures = <FixtureDef>[];
|
||||||
|
|
||||||
|
final circleShape1 = CircleShape()..radius = Baseboard.height / 2;
|
||||||
|
circleShape1.position.setValues(
|
||||||
|
-(Baseboard.width / 2) + circleShape1.radius,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
final circle1FixtureDef = FixtureDef(circleShape1);
|
||||||
|
fixtures.add(circle1FixtureDef);
|
||||||
|
|
||||||
|
final circleShape2 = CircleShape()..radius = Baseboard.height / 2;
|
||||||
|
circleShape2.position.setValues(
|
||||||
|
(Baseboard.width / 2) - circleShape2.radius,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
final circle2FixtureDef = FixtureDef(circleShape2);
|
||||||
|
fixtures.add(circle2FixtureDef);
|
||||||
|
|
||||||
|
final rectangle = PolygonShape()
|
||||||
|
..setAsBoxXY(
|
||||||
|
(Baseboard.width - Baseboard.height) / 2,
|
||||||
|
Baseboard.height / 2,
|
||||||
|
);
|
||||||
|
final rectangleFixtureDef = FixtureDef(rectangle);
|
||||||
|
fixtures.add(rectangleFixtureDef);
|
||||||
|
|
||||||
|
return fixtures;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
// TODO(allisonryan0002): share sweeping angle with flipper when components
|
||||||
|
// are grouped.
|
||||||
|
const angle = math.pi / 7;
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()
|
||||||
|
..position = _position
|
||||||
|
..type = BodyType.static
|
||||||
|
..angle = _side.isLeft ? -angle : angle;
|
||||||
|
|
||||||
|
final body = world.createBody(bodyDef);
|
||||||
|
_createFixtureDefs().forEach(body.createFixture);
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/effects.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
/// {@template bonus_word}
|
||||||
|
/// Loads all [BonusLetter]s to compose a [BonusWord].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class BonusWord extends Component {
|
||||||
|
/// {@macro bonus_word}
|
||||||
|
BonusWord({required Vector2 position}) : _position = position;
|
||||||
|
|
||||||
|
final Vector2 _position;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final letters = GameBloc.bonusWord.split('');
|
||||||
|
|
||||||
|
for (var i = 0; i < letters.length; i++) {
|
||||||
|
unawaited(
|
||||||
|
add(
|
||||||
|
BonusLetter(
|
||||||
|
position: _position - Vector2(16 - (i * 6), -30),
|
||||||
|
letter: letters[i],
|
||||||
|
index: i,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template bonus_letter}
|
||||||
|
/// [BodyType.static] sensor component, part of a word bonus,
|
||||||
|
/// which will activate its letter after contact with a [Ball].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class BonusLetter extends BodyComponent<PinballGame>
|
||||||
|
with BlocComponent<GameBloc, GameState> {
|
||||||
|
/// {@macro bonus_letter}
|
||||||
|
BonusLetter({
|
||||||
|
required Vector2 position,
|
||||||
|
required String letter,
|
||||||
|
required int index,
|
||||||
|
}) : _position = position,
|
||||||
|
_letter = letter,
|
||||||
|
_index = index {
|
||||||
|
paint = Paint()..color = _disableColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The area size of this [BonusLetter].
|
||||||
|
static final areaSize = Vector2.all(4);
|
||||||
|
|
||||||
|
static const _activeColor = Colors.green;
|
||||||
|
static const _disableColor = Colors.red;
|
||||||
|
|
||||||
|
final Vector2 _position;
|
||||||
|
final String _letter;
|
||||||
|
final int _index;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
await add(
|
||||||
|
TextComponent(
|
||||||
|
position: Vector2(-1, -1),
|
||||||
|
text: _letter,
|
||||||
|
textRenderer: TextPaint(
|
||||||
|
style: const TextStyle(fontSize: 2, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = CircleShape()..radius = areaSize.x / 2;
|
||||||
|
|
||||||
|
final fixtureDef = FixtureDef(shape)..isSensor = true;
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()
|
||||||
|
..userData = this
|
||||||
|
..position = _position
|
||||||
|
..type = BodyType.static;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool listenWhen(GameState? previousState, GameState newState) {
|
||||||
|
final wasActive = previousState?.isLetterActivated(_index) ?? false;
|
||||||
|
final isActive = newState.isLetterActivated(_index);
|
||||||
|
|
||||||
|
return wasActive != isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNewState(GameState state) {
|
||||||
|
final isActive = state.isLetterActivated(_index);
|
||||||
|
|
||||||
|
add(
|
||||||
|
ColorEffect(
|
||||||
|
isActive ? _activeColor : _disableColor,
|
||||||
|
const Offset(0, 1),
|
||||||
|
EffectController(duration: 0.25),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activates this [BonusLetter], if it's not already activated.
|
||||||
|
void activate() {
|
||||||
|
final isActive = state?.isLetterActivated(_index) ?? false;
|
||||||
|
if (!isActive) {
|
||||||
|
gameRef.read<GameBloc>().add(BonusLetterActivated(_index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggers [BonusLetter.activate] method when a [BonusLetter] and a [Ball]
|
||||||
|
/// come in contact.
|
||||||
|
class BonusLetterBallContactCallback
|
||||||
|
extends ContactCallback<Ball, BonusLetter> {
|
||||||
|
@override
|
||||||
|
void begin(Ball ball, BonusLetter bonusLetter, Contact contact) {
|
||||||
|
bonusLetter.activate();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Baseboard', () {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(PinballGameTest.create);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
await game.ready();
|
||||||
|
final leftBaseboard = Baseboard.left(position: Vector2.zero());
|
||||||
|
final rightBaseboard = Baseboard.right(position: Vector2.zero());
|
||||||
|
await game.ensureAddAll([leftBaseboard, rightBaseboard]);
|
||||||
|
|
||||||
|
expect(game.contains(leftBaseboard), isTrue);
|
||||||
|
expect(game.contains(rightBaseboard), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('body', () {
|
||||||
|
flameTester.test(
|
||||||
|
'positions correctly',
|
||||||
|
(game) async {
|
||||||
|
final position = Vector2.all(10);
|
||||||
|
final baseboard = Baseboard.left(position: position);
|
||||||
|
await game.ensureAdd(baseboard);
|
||||||
|
game.contains(baseboard);
|
||||||
|
|
||||||
|
expect(baseboard.body.position, position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is static',
|
||||||
|
(game) async {
|
||||||
|
final baseboard = Baseboard.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(baseboard);
|
||||||
|
|
||||||
|
expect(baseboard.body.bodyType, equals(BodyType.static));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is at an angle',
|
||||||
|
(game) async {
|
||||||
|
final leftBaseboard = Baseboard.left(position: Vector2.zero());
|
||||||
|
final rightBaseboard = Baseboard.right(position: Vector2.zero());
|
||||||
|
await game.ensureAddAll([leftBaseboard, rightBaseboard]);
|
||||||
|
|
||||||
|
expect(leftBaseboard.body.angle, isNegative);
|
||||||
|
expect(rightBaseboard.body.angle, isPositive);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fixtures', () {
|
||||||
|
flameTester.test(
|
||||||
|
'has three',
|
||||||
|
(game) async {
|
||||||
|
final baseboard = Baseboard.left(position: Vector2.zero());
|
||||||
|
await game.ensureAdd(baseboard);
|
||||||
|
|
||||||
|
expect(baseboard.body.fixtures.length, equals(3));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,244 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flame/effects.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 '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('BonusWord', () {
|
||||||
|
final flameTester = FlameTester(PinballGameTest.create);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'loads the letters correctly',
|
||||||
|
(game) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final bonusWord = game.children.whereType<BonusWord>().first;
|
||||||
|
final letters = bonusWord.children.whereType<BonusLetter>();
|
||||||
|
expect(letters.length, equals(GameBloc.bonusWord.length));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('BonusLetter', () {
|
||||||
|
final flameTester = FlameTester(PinballGameTest.create);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
final bonusLetter = BonusLetter(
|
||||||
|
position: Vector2.zero(),
|
||||||
|
letter: 'G',
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(bonusLetter);
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(game.contains(bonusLetter), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('body', () {
|
||||||
|
flameTester.test(
|
||||||
|
'positions correctly',
|
||||||
|
(game) async {
|
||||||
|
final position = Vector2.all(10);
|
||||||
|
final bonusLetter = BonusLetter(
|
||||||
|
position: position,
|
||||||
|
letter: 'G',
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(bonusLetter);
|
||||||
|
game.contains(bonusLetter);
|
||||||
|
|
||||||
|
expect(bonusLetter.body.position, position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is static',
|
||||||
|
(game) async {
|
||||||
|
final bonusLetter = BonusLetter(
|
||||||
|
position: Vector2.zero(),
|
||||||
|
letter: 'G',
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(bonusLetter);
|
||||||
|
|
||||||
|
expect(bonusLetter.body.bodyType, equals(BodyType.static));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fixture', () {
|
||||||
|
flameTester.test(
|
||||||
|
'exists',
|
||||||
|
(game) async {
|
||||||
|
final bonusLetter = BonusLetter(
|
||||||
|
position: Vector2.zero(),
|
||||||
|
letter: 'G',
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(bonusLetter);
|
||||||
|
|
||||||
|
expect(bonusLetter.body.fixtures[0], isA<Fixture>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is sensor',
|
||||||
|
(game) async {
|
||||||
|
final bonusLetter = BonusLetter(
|
||||||
|
position: Vector2.zero(),
|
||||||
|
letter: 'G',
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(bonusLetter);
|
||||||
|
|
||||||
|
final fixture = bonusLetter.body.fixtures[0];
|
||||||
|
expect(fixture.isSensor, isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'shape is circular',
|
||||||
|
(game) async {
|
||||||
|
final bonusLetter = BonusLetter(
|
||||||
|
position: Vector2.zero(),
|
||||||
|
letter: 'G',
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(bonusLetter);
|
||||||
|
|
||||||
|
final fixture = bonusLetter.body.fixtures[0];
|
||||||
|
expect(fixture.shape.shapeType, equals(ShapeType.circle));
|
||||||
|
expect(fixture.shape.radius, equals(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('bonus letter activation', () {
|
||||||
|
final gameBloc = MockGameBloc();
|
||||||
|
|
||||||
|
BonusLetter _getBonusLetter(PinballGame game) {
|
||||||
|
return game.children
|
||||||
|
.whereType<BonusWord>()
|
||||||
|
.first
|
||||||
|
.children
|
||||||
|
.whereType<BonusLetter>()
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
const Stream<GameState>.empty(),
|
||||||
|
initialState: const GameState.initial(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final tester = flameBlocTester(gameBloc: () => gameBloc);
|
||||||
|
|
||||||
|
tester.widgetTest(
|
||||||
|
'adds BonusLetterActivated to GameBloc when not activated',
|
||||||
|
(game, tester) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
_getBonusLetter(game).activate();
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.widgetTest(
|
||||||
|
"doesn't add BonusLetterActivated to GameBloc when already activated",
|
||||||
|
(game, tester) async {
|
||||||
|
const state = GameState(
|
||||||
|
score: 0,
|
||||||
|
balls: 2,
|
||||||
|
activatedBonusLetters: [0],
|
||||||
|
bonusHistory: [],
|
||||||
|
);
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: state,
|
||||||
|
);
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
_getBonusLetter(game).activate();
|
||||||
|
await game.ready(); // Making sure that all additions are done
|
||||||
|
|
||||||
|
verifyNever(() => gameBloc.add(const BonusLetterActivated(0)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.widgetTest(
|
||||||
|
'adds a ColorEffect',
|
||||||
|
(game, tester) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
const state = GameState(
|
||||||
|
score: 0,
|
||||||
|
balls: 2,
|
||||||
|
activatedBonusLetters: [0],
|
||||||
|
bonusHistory: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final bonusLetter = _getBonusLetter(game);
|
||||||
|
|
||||||
|
bonusLetter.onNewState(state);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
bonusLetter.children.whereType<ColorEffect>().length,
|
||||||
|
equals(1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.widgetTest(
|
||||||
|
'only listens when there is a change on the letter status',
|
||||||
|
(game, tester) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
const state = GameState(
|
||||||
|
score: 0,
|
||||||
|
balls: 2,
|
||||||
|
activatedBonusLetters: [0],
|
||||||
|
bonusHistory: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final bonusLetter = _getBonusLetter(game);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
bonusLetter.listenWhen(const GameState.initial(), state),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('BonusLetterBallContactCallback', () {
|
||||||
|
test('calls ball.activate', () {
|
||||||
|
final ball = MockBall();
|
||||||
|
final bonusLetter = MockBonusLetter();
|
||||||
|
|
||||||
|
final contactCallback = BonusLetterBallContactCallback();
|
||||||
|
contactCallback.begin(ball, bonusLetter, MockContact());
|
||||||
|
|
||||||
|
verify(bonusLetter.activate).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue