mirror of https://github.com/flutter/pinball.git
refactor: removed `GameBallsController` (#349)
parent
28606241df
commit
ace61193fb
@ -0,0 +1,33 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_bloc/flame_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Spawns a new [Ball] into the game when all balls are lost and still
|
||||
/// [GameStatus.playing].
|
||||
class BallSpawningBehavior extends Component
|
||||
with ParentIsA<PinballGame>, BlocComponent<GameBloc, GameState> {
|
||||
@override
|
||||
bool listenWhen(GameState? previousState, GameState newState) {
|
||||
if (!newState.status.isPlaying) return false;
|
||||
|
||||
final startedGame = previousState?.status.isWaiting ?? true;
|
||||
final lostRound =
|
||||
(previousState?.rounds ?? newState.rounds + 1) > newState.rounds;
|
||||
return startedGame || lostRound;
|
||||
}
|
||||
|
||||
@override
|
||||
void onNewState(GameState state) {
|
||||
final plunger = parent.descendants().whereType<Plunger>().single;
|
||||
final canvas = parent.descendants().whereType<ZCanvasComponent>().single;
|
||||
final ball = ControlledBall.launch(characterTheme: parent.characterTheme)
|
||||
..initialPosition = Vector2(
|
||||
plunger.body.position.x,
|
||||
plunger.body.position.y - Ball.size.y,
|
||||
);
|
||||
|
||||
canvas.add(ball);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export 'ball_spawning_behavior.dart';
|
||||
export 'bumper_noisy_behavior.dart';
|
||||
export 'camera_focusing_behavior.dart';
|
||||
export 'scoring_behavior.dart';
|
||||
|
@ -1,34 +0,0 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template drain}
|
||||
/// Area located at the bottom of the board to detect when a [Ball] is lost.
|
||||
/// {@endtemplate}
|
||||
// TODO(allisonryan0002): move to components package when possible.
|
||||
class Drain extends BodyComponent with ContactCallbacks {
|
||||
/// {@macro drain}
|
||||
Drain() : super(renderBody: false);
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = EdgeShape()
|
||||
..set(
|
||||
BoardDimensions.bounds.bottomLeft.toVector2(),
|
||||
BoardDimensions.bounds.bottomRight.toVector2(),
|
||||
);
|
||||
final fixtureDef = FixtureDef(shape, isSensor: true);
|
||||
final bodyDef = BodyDef(userData: this);
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
|
||||
// TODO(allisonryan0002): move this to ball.dart when BallLost is removed.
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! ControlledBall) return;
|
||||
other.controller.lost();
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'draining_behavior.dart';
|
@ -0,0 +1,21 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Handles removing a [Ball] from the game.
|
||||
class DrainingBehavior extends ContactBehavior<Drain>
|
||||
with HasGameRef<PinballGame> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
other.removeFromParent();
|
||||
final ballsLeft = gameRef.descendants().whereType<Ball>().length;
|
||||
if (ballsLeft - 1 == 0) {
|
||||
gameRef.read<GameBloc>().add(const RoundLost());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:pinball/game/components/drain/behaviors/behaviors.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template drain}
|
||||
/// Area located at the bottom of the board.
|
||||
///
|
||||
/// Its [DrainingBehavior] handles removing a [Ball] from the game.
|
||||
/// {@endtemplate}
|
||||
class Drain extends BodyComponent with ContactCallbacks {
|
||||
/// {@macro drain}
|
||||
Drain()
|
||||
: super(
|
||||
renderBody: false,
|
||||
children: [DrainingBehavior()],
|
||||
);
|
||||
|
||||
/// Creates a [Drain] without any children.
|
||||
///
|
||||
/// This can be used for testing a [Drain]'s behaviors in isolation.
|
||||
@visibleForTesting
|
||||
Drain.test();
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = EdgeShape()
|
||||
..set(
|
||||
BoardDimensions.bounds.bottomLeft.toVector2(),
|
||||
BoardDimensions.bounds.bottomRight.toVector2(),
|
||||
);
|
||||
final fixtureDef = FixtureDef(shape, isSensor: true);
|
||||
return world.createBody(BodyDef())..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/behaviors/ball_spawning_behavior.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart' as theme;
|
||||
|
||||
import '../../../../helpers/test_games.dart';
|
||||
|
||||
class _MockGameState extends Mock implements GameState {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group(
|
||||
'BallSpawningBehavior',
|
||||
() {
|
||||
final flameTester = FlameTester(EmptyPinballTestGame.new);
|
||||
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
BallSpawningBehavior(),
|
||||
isA<BallSpawningBehavior>(),
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'loads',
|
||||
(game) async {
|
||||
final behavior = BallSpawningBehavior();
|
||||
await game.ensureAdd(behavior);
|
||||
expect(game.contains(behavior), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('listenWhen', () {
|
||||
test(
|
||||
'never listens when new state not playing',
|
||||
() {
|
||||
final waiting = const GameState.initial()
|
||||
..copyWith(status: GameStatus.waiting);
|
||||
final gameOver = const GameState.initial()
|
||||
..copyWith(status: GameStatus.gameOver);
|
||||
|
||||
final behavior = BallSpawningBehavior();
|
||||
expect(behavior.listenWhen(_MockGameState(), waiting), isFalse);
|
||||
expect(behavior.listenWhen(_MockGameState(), gameOver), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'listens when started playing',
|
||||
() {
|
||||
final waiting =
|
||||
const GameState.initial().copyWith(status: GameStatus.waiting);
|
||||
final playing =
|
||||
const GameState.initial().copyWith(status: GameStatus.playing);
|
||||
|
||||
final behavior = BallSpawningBehavior();
|
||||
expect(behavior.listenWhen(waiting, playing), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'listens when lost rounds',
|
||||
() {
|
||||
final playing1 = const GameState.initial().copyWith(
|
||||
status: GameStatus.playing,
|
||||
rounds: 2,
|
||||
);
|
||||
final playing2 = const GameState.initial().copyWith(
|
||||
status: GameStatus.playing,
|
||||
rounds: 1,
|
||||
);
|
||||
|
||||
final behavior = BallSpawningBehavior();
|
||||
expect(behavior.listenWhen(playing1, playing2), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"doesn't listen when didn't lose any rounds",
|
||||
() {
|
||||
final playing = const GameState.initial().copyWith(
|
||||
status: GameStatus.playing,
|
||||
rounds: 2,
|
||||
);
|
||||
|
||||
final behavior = BallSpawningBehavior();
|
||||
expect(behavior.listenWhen(playing, playing), isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'onNewState adds a ball',
|
||||
(game) async {
|
||||
await game.images.load(theme.Assets.images.dash.ball.keyName);
|
||||
final behavior = BallSpawningBehavior();
|
||||
await game.ensureAddAll([
|
||||
behavior,
|
||||
ZCanvasComponent(),
|
||||
Plunger.test(compressionDistance: 10),
|
||||
]);
|
||||
expect(game.descendants().whereType<Ball>(), isEmpty);
|
||||
|
||||
behavior.onNewState(_MockGameState());
|
||||
await game.ready();
|
||||
|
||||
expect(game.descendants().whereType<Ball>(), isNotEmpty);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
// 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/components/drain/behaviors/behaviors.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart' as theme;
|
||||
|
||||
import '../../../../helpers/helpers.dart';
|
||||
|
||||
class _MockGameBloc extends Mock implements GameBloc {}
|
||||
|
||||
class _MockContact extends Mock implements Contact {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group(
|
||||
'DrainingBehavior',
|
||||
() {
|
||||
final flameTester = FlameTester(Forge2DGame.new);
|
||||
|
||||
test('can be instantiated', () {
|
||||
expect(DrainingBehavior(), isA<DrainingBehavior>());
|
||||
});
|
||||
|
||||
flameTester.test(
|
||||
'loads',
|
||||
(game) async {
|
||||
final parent = Drain.test();
|
||||
final behavior = DrainingBehavior();
|
||||
await parent.add(behavior);
|
||||
await game.ensureAdd(parent);
|
||||
expect(parent.contains(behavior), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('beginContact', () {
|
||||
final asset = theme.Assets.images.dash.ball.keyName;
|
||||
late GameBloc gameBloc;
|
||||
|
||||
setUp(() {
|
||||
gameBloc = _MockGameBloc();
|
||||
whenListen(
|
||||
gameBloc,
|
||||
const Stream<GameState>.empty(),
|
||||
);
|
||||
});
|
||||
|
||||
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
||||
gameBuilder: EmptyPinballTestGame.new,
|
||||
blocBuilder: () => gameBloc,
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'adds RoundLost when no balls left',
|
||||
setUp: (game, tester) async {
|
||||
await game.images.load(asset);
|
||||
|
||||
final drain = Drain.test();
|
||||
final behavior = DrainingBehavior();
|
||||
final ball = Ball.test();
|
||||
await drain.add(behavior);
|
||||
await game.ensureAddAll([drain, ball]);
|
||||
|
||||
behavior.beginContact(ball, _MockContact());
|
||||
await game.ready();
|
||||
|
||||
expect(game.descendants().whereType<Ball>(), isEmpty);
|
||||
verify(() => gameBloc.add(const RoundLost())).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
"doesn't add RoundLost when there are balls left",
|
||||
setUp: (game, tester) async {
|
||||
await game.images.load(asset);
|
||||
|
||||
final drain = Drain.test();
|
||||
final behavior = DrainingBehavior();
|
||||
final ball1 = Ball.test();
|
||||
final ball2 = Ball.test();
|
||||
await drain.add(behavior);
|
||||
await game.ensureAddAll([
|
||||
drain,
|
||||
ball1,
|
||||
ball2,
|
||||
]);
|
||||
|
||||
behavior.beginContact(ball1, _MockContact());
|
||||
await game.ready();
|
||||
|
||||
expect(game.descendants().whereType<Ball>(), isNotEmpty);
|
||||
verifyNever(() => gameBloc.add(const RoundLost()));
|
||||
},
|
||||
);
|
||||
|
||||
flameBlocTester.testGameWidget(
|
||||
'removes the Ball',
|
||||
setUp: (game, tester) async {
|
||||
await game.images.load(asset);
|
||||
final drain = Drain.test();
|
||||
final behavior = DrainingBehavior();
|
||||
final ball = Ball.test();
|
||||
await drain.add(behavior);
|
||||
await game.ensureAddAll([drain, ball]);
|
||||
|
||||
behavior.beginContact(ball, _MockContact());
|
||||
await game.ready();
|
||||
|
||||
expect(game.descendants().whereType<Ball>(), isEmpty);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
Loading…
Reference in new issue