mirror of https://github.com/flutter/pinball.git
commit
0a3d4f1f7e
@ -0,0 +1,35 @@
|
|||||||
|
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';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
/// Spawns a new [Ball] into the game when all balls are lost and still
|
||||||
|
/// [GameStatus.playing].
|
||||||
|
class BallSpawningBehavior extends Component
|
||||||
|
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
|
||||||
|
@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 = gameRef.descendants().whereType<Plunger>().single;
|
||||||
|
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
|
||||||
|
final characterTheme = readProvider<CharacterTheme>();
|
||||||
|
final ball = ControlledBall.launch(characterTheme: characterTheme)
|
||||||
|
..initialPosition = Vector2(
|
||||||
|
plunger.body.position.x,
|
||||||
|
plunger.body.position.y - Ball.size.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.add(ball);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export 'bumper_noisy_behavior.dart';
|
export 'ball_spawning_behavior.dart';
|
||||||
|
export 'bumper_noise_behavior.dart';
|
||||||
export 'camera_focusing_behavior.dart';
|
export 'camera_focusing_behavior.dart';
|
||||||
export 'scoring_behavior.dart';
|
export 'scoring_behavior.dart';
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
import 'package:pinball/game/pinball_game.dart';
|
|
||||||
import 'package:pinball_audio/pinball_audio.dart';
|
import 'package:pinball_audio/pinball_audio.dart';
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
|
class BumperNoiseBehavior extends ContactBehavior {
|
||||||
@override
|
@override
|
||||||
void beginContact(Object other, Contact contact) {
|
void beginContact(Object other, Contact contact) {
|
||||||
super.beginContact(other, contact);
|
super.beginContact(other, contact);
|
||||||
gameRef.player.play(PinballAudio.bumper);
|
readProvider<PinballPlayer>().play(PinballAudio.bumper);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,25 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.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 {
|
||||||
|
@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) {
|
||||||
|
ancestors()
|
||||||
|
.whereType<FlameBlocProvider<GameBloc, GameState>>()
|
||||||
|
.first
|
||||||
|
.bloc
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,65 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
|
||||||
|
class FlameProvider<T> extends Component {
|
||||||
|
FlameProvider.value(
|
||||||
|
this.provider, {
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : super(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
final T provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiFlameProvider extends Component {
|
||||||
|
MultiFlameProvider({
|
||||||
|
required List<FlameProvider<dynamic>> providers,
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : _providers = providers,
|
||||||
|
_initialChildren = children,
|
||||||
|
assert(providers.isNotEmpty, 'At least one provider must be given') {
|
||||||
|
_addProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<FlameProvider<dynamic>> _providers;
|
||||||
|
final Iterable<Component>? _initialChildren;
|
||||||
|
FlameProvider<dynamic>? _lastProvider;
|
||||||
|
|
||||||
|
Future<void> _addProviders() async {
|
||||||
|
final _list = [..._providers];
|
||||||
|
|
||||||
|
var current = _list.removeAt(0);
|
||||||
|
while (_list.isNotEmpty) {
|
||||||
|
final provider = _list.removeAt(0);
|
||||||
|
await current.add(provider);
|
||||||
|
current = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
await add(_providers.first);
|
||||||
|
_lastProvider = current;
|
||||||
|
|
||||||
|
_initialChildren?.forEach(add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> add(Component component) async {
|
||||||
|
if (_lastProvider == null) {
|
||||||
|
await super.add(component);
|
||||||
|
}
|
||||||
|
await _lastProvider?.add(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReadFlameProvider on Component {
|
||||||
|
T readProvider<T>() {
|
||||||
|
final providers = ancestors().whereType<FlameProvider<T>>();
|
||||||
|
assert(
|
||||||
|
providers.isNotEmpty,
|
||||||
|
'No FlameProvider<$T> available on the component tree',
|
||||||
|
);
|
||||||
|
|
||||||
|
return providers.first.provider;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(FlameGame.new);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'FlameProvider',
|
||||||
|
() {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
FlameProvider<bool>.value(true),
|
||||||
|
isA<FlameProvider<bool>>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('can be loaded', (game) async {
|
||||||
|
final component = FlameProvider<bool>.value(true);
|
||||||
|
await game.ensureAdd(component);
|
||||||
|
expect(game.children, contains(component));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('adds children', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final provider = FlameProvider<bool>.value(
|
||||||
|
true,
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(provider);
|
||||||
|
expect(provider.children, contains(component));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('MultiFlameProvider', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
MultiFlameProvider(
|
||||||
|
providers: [
|
||||||
|
FlameProvider<bool>.value(true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isA<MultiFlameProvider>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('adds multiple providers', (game) async {
|
||||||
|
final provider1 = FlameProvider<bool>.value(true);
|
||||||
|
final provider2 = FlameProvider<bool>.value(true);
|
||||||
|
final providers = MultiFlameProvider(
|
||||||
|
providers: [provider1, provider2],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(providers);
|
||||||
|
expect(providers.children, contains(provider1));
|
||||||
|
expect(provider1.children, contains(provider2));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('adds children under provider', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final provider = FlameProvider<bool>.value(true);
|
||||||
|
final providers = MultiFlameProvider(
|
||||||
|
providers: [provider],
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(providers);
|
||||||
|
expect(provider.children, contains(component));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group(
|
||||||
|
'ReadFlameProvider',
|
||||||
|
() {
|
||||||
|
flameTester.test('loads provider', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final provider = FlameProvider<bool>.value(
|
||||||
|
true,
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(provider);
|
||||||
|
expect(component.readProvider<bool>(), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'throws assertionError when no provider is found',
|
||||||
|
(game) async {
|
||||||
|
final component = Component();
|
||||||
|
await game.ensureAdd(component);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => component.readProvider<bool>(),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.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/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;
|
||||||
|
|
||||||
|
class _TestGame extends Forge2DGame {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
images.prefix = '';
|
||||||
|
await images.load(theme.Assets.images.dash.ball.keyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pump(
|
||||||
|
Iterable<Component> children, {
|
||||||
|
GameBloc? gameBloc,
|
||||||
|
}) async {
|
||||||
|
await ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: gameBloc ?? GameBloc(),
|
||||||
|
children: [
|
||||||
|
FlameProvider<theme.CharacterTheme>.value(
|
||||||
|
const theme.DashTheme(),
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockGameState extends Mock implements GameState {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group(
|
||||||
|
'BallSpawningBehavior',
|
||||||
|
() {
|
||||||
|
final flameTester = FlameTester(_TestGame.new);
|
||||||
|
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
BallSpawningBehavior(),
|
||||||
|
isA<BallSpawningBehavior>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'loads',
|
||||||
|
(game) async {
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
await game.pump([behavior]);
|
||||||
|
expect(game.descendants(), contains(behavior));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
final behavior = BallSpawningBehavior();
|
||||||
|
await game.pump([
|
||||||
|
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,134 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_bloc/flame_bloc.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;
|
||||||
|
|
||||||
|
class _TestGame extends Forge2DGame {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
images.prefix = '';
|
||||||
|
await images.load(theme.Assets.images.dash.ball.keyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pump(
|
||||||
|
Drain child, {
|
||||||
|
required GameBloc gameBloc,
|
||||||
|
}) async {
|
||||||
|
await ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: gameBloc,
|
||||||
|
children: [child],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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', () {
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = _MockGameBloc();
|
||||||
|
});
|
||||||
|
|
||||||
|
final flameBlocTester = FlameTester(_TestGame.new);
|
||||||
|
|
||||||
|
flameBlocTester.test(
|
||||||
|
'adds RoundLost when no balls left',
|
||||||
|
(game) async {
|
||||||
|
final drain = Drain.test();
|
||||||
|
final behavior = DrainingBehavior();
|
||||||
|
final ball = Ball.test();
|
||||||
|
await drain.add(behavior);
|
||||||
|
await game.pump(
|
||||||
|
drain,
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
behavior.beginContact(ball, _MockContact());
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(game.descendants().whereType<Ball>(), isEmpty);
|
||||||
|
verify(() => gameBloc.add(const RoundLost())).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameBlocTester.test(
|
||||||
|
"doesn't add RoundLost when there are balls left",
|
||||||
|
(game) async {
|
||||||
|
final drain = Drain.test();
|
||||||
|
final behavior = DrainingBehavior();
|
||||||
|
final ball1 = Ball.test();
|
||||||
|
final ball2 = Ball.test();
|
||||||
|
await drain.add(behavior);
|
||||||
|
await game.pump(
|
||||||
|
drain,
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await game.ensureAddAll([ball1, ball2]);
|
||||||
|
|
||||||
|
behavior.beginContact(ball1, _MockContact());
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(game.descendants().whereType<Ball>(), isNotEmpty);
|
||||||
|
verifyNever(() => gameBloc.add(const RoundLost()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameBlocTester.test(
|
||||||
|
'removes the Ball',
|
||||||
|
(game) async {
|
||||||
|
final drain = Drain.test();
|
||||||
|
final behavior = DrainingBehavior();
|
||||||
|
final ball = Ball.test();
|
||||||
|
await drain.add(behavior);
|
||||||
|
await game.pump(
|
||||||
|
drain,
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await game.ensureAdd(ball);
|
||||||
|
|
||||||
|
behavior.beginContact(ball, _MockContact());
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
expect(game.descendants().whereType<Ball>(), isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -1,54 +1,57 @@
|
|||||||
// ignore_for_file: cascade_invocations
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_bloc/flame_bloc.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_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.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';
|
||||||
|
|
||||||
import '../../../helpers/helpers.dart';
|
class _TestGame extends Forge2DGame {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
images.prefix = '';
|
||||||
|
await images.loadAll([
|
||||||
|
Assets.images.multiball.lit.keyName,
|
||||||
|
Assets.images.multiball.dimmed.keyName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pump(Multiballs child, {GameBloc? gameBloc}) {
|
||||||
|
return ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: gameBloc ?? GameBloc(),
|
||||||
|
children: [child],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final assets = [
|
|
||||||
Assets.images.multiball.lit.keyName,
|
|
||||||
Assets.images.multiball.dimmed.keyName,
|
|
||||||
];
|
|
||||||
late GameBloc gameBloc;
|
|
||||||
|
|
||||||
setUp(() {
|
final flameBlocTester = FlameTester(_TestGame.new);
|
||||||
gameBloc = GameBloc();
|
|
||||||
});
|
|
||||||
|
|
||||||
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
|
||||||
gameBuilder: EmptyPinballTestGame.new,
|
|
||||||
blocBuilder: () => gameBloc,
|
|
||||||
assets: assets,
|
|
||||||
);
|
|
||||||
|
|
||||||
group('Multiballs', () {
|
group('Multiballs', () {
|
||||||
flameBlocTester.testGameWidget(
|
flameBlocTester.testGameWidget(
|
||||||
'loads correctly',
|
'loads correctly',
|
||||||
setUp: (game, tester) async {
|
setUp: (game, tester) async {
|
||||||
final multiballs = Multiballs();
|
final multiballs = Multiballs();
|
||||||
await game.ensureAdd(multiballs);
|
await game.pump(multiballs);
|
||||||
|
expect(game.descendants(), contains(multiballs));
|
||||||
expect(game.contains(multiballs), isTrue);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
group('loads', () {
|
flameBlocTester.test(
|
||||||
flameBlocTester.testGameWidget(
|
'loads four Multiball',
|
||||||
'four Multiball',
|
(game) async {
|
||||||
setUp: (game, tester) async {
|
final multiballs = Multiballs();
|
||||||
final multiballs = Multiballs();
|
await game.pump(multiballs);
|
||||||
await game.ensureAdd(multiballs);
|
expect(
|
||||||
|
multiballs.descendants().whereType<Multiball>().length,
|
||||||
expect(
|
equals(4),
|
||||||
multiballs.descendants().whereType<Multiball>().length,
|
);
|
||||||
equals(4),
|
},
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,65 @@
|
|||||||
// ignore_for_file: cascade_invocations
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_bloc/flame_bloc.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_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.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';
|
||||||
|
|
||||||
import '../../../helpers/helpers.dart';
|
class _TestGame extends Forge2DGame {
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
images.prefix = '';
|
||||||
|
await images.loadAll([
|
||||||
|
Assets.images.multiplier.x2.lit.keyName,
|
||||||
|
Assets.images.multiplier.x2.dimmed.keyName,
|
||||||
|
Assets.images.multiplier.x3.lit.keyName,
|
||||||
|
Assets.images.multiplier.x3.dimmed.keyName,
|
||||||
|
Assets.images.multiplier.x4.lit.keyName,
|
||||||
|
Assets.images.multiplier.x4.dimmed.keyName,
|
||||||
|
Assets.images.multiplier.x5.lit.keyName,
|
||||||
|
Assets.images.multiplier.x5.dimmed.keyName,
|
||||||
|
Assets.images.multiplier.x6.lit.keyName,
|
||||||
|
Assets.images.multiplier.x6.dimmed.keyName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pump(Multipliers child) async {
|
||||||
|
await ensureAdd(
|
||||||
|
FlameBlocProvider<GameBloc, GameState>.value(
|
||||||
|
value: GameBloc(),
|
||||||
|
children: [child],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final assets = [
|
|
||||||
Assets.images.multiplier.x2.lit.keyName,
|
|
||||||
Assets.images.multiplier.x2.dimmed.keyName,
|
|
||||||
Assets.images.multiplier.x3.lit.keyName,
|
|
||||||
Assets.images.multiplier.x3.dimmed.keyName,
|
|
||||||
Assets.images.multiplier.x4.lit.keyName,
|
|
||||||
Assets.images.multiplier.x4.dimmed.keyName,
|
|
||||||
Assets.images.multiplier.x5.lit.keyName,
|
|
||||||
Assets.images.multiplier.x5.dimmed.keyName,
|
|
||||||
Assets.images.multiplier.x6.lit.keyName,
|
|
||||||
Assets.images.multiplier.x6.dimmed.keyName,
|
|
||||||
];
|
|
||||||
|
|
||||||
late GameBloc gameBloc;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
gameBloc = GameBloc();
|
|
||||||
});
|
|
||||||
|
|
||||||
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
final flameTester = FlameTester(_TestGame.new);
|
||||||
gameBuilder: EmptyPinballTestGame.new,
|
|
||||||
blocBuilder: () => gameBloc,
|
|
||||||
assets: assets,
|
|
||||||
);
|
|
||||||
|
|
||||||
group('Multipliers', () {
|
group('Multipliers', () {
|
||||||
flameBlocTester.testGameWidget(
|
flameTester.test(
|
||||||
'loads correctly',
|
'loads correctly',
|
||||||
setUp: (game, tester) async {
|
(game) async {
|
||||||
final multipliersGroup = Multipliers();
|
final component = Multipliers();
|
||||||
await game.ensureAdd(multipliersGroup);
|
await game.pump(component);
|
||||||
|
expect(game.descendants(), contains(component));
|
||||||
expect(game.contains(multipliersGroup), isTrue);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
group('loads', () {
|
flameTester.test(
|
||||||
flameBlocTester.testGameWidget(
|
'loads five Multiplier',
|
||||||
'five Multiplier',
|
(game) async {
|
||||||
setUp: (game, tester) async {
|
final multipliersGroup = Multipliers();
|
||||||
final multipliersGroup = Multipliers();
|
await game.pump(multipliersGroup);
|
||||||
await game.ensureAdd(multipliersGroup);
|
expect(
|
||||||
|
multipliersGroup.descendants().whereType<Multiplier>().length,
|
||||||
expect(
|
equals(5),
|
||||||
multipliersGroup.descendants().whereType<Multiplier>().length,
|
);
|
||||||
equals(5),
|
},
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:flame_test/flame_test.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
class FlameBlocTester<T extends FlameGame, B extends Bloc<dynamic, dynamic>>
|
|
||||||
extends FlameTester<T> {
|
|
||||||
FlameBlocTester({
|
|
||||||
required GameCreateFunction<T> gameBuilder,
|
|
||||||
required B Function() blocBuilder,
|
|
||||||
// TODO(allisonryan0002): find alternative for testGameWidget. Loading
|
|
||||||
// assets in onLoad fails because the game loads after
|
|
||||||
List<String>? assets,
|
|
||||||
List<RepositoryProvider> Function()? repositories,
|
|
||||||
}) : super(
|
|
||||||
gameBuilder,
|
|
||||||
pumpWidget: (gameWidget, tester) async {
|
|
||||||
if (assets != null) {
|
|
||||||
await Future.wait(assets.map(gameWidget.game.images.load));
|
|
||||||
}
|
|
||||||
await tester.pumpWidget(
|
|
||||||
BlocProvider.value(
|
|
||||||
value: blocBuilder(),
|
|
||||||
child: repositories == null
|
|
||||||
? gameWidget
|
|
||||||
: MultiRepositoryProvider(
|
|
||||||
providers: repositories.call(),
|
|
||||||
child: gameWidget,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:mocktail/mocktail.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
|
|
||||||
class FakeContact extends Fake implements Contact {}
|
|
||||||
|
|
||||||
class FakeGameEvent extends Fake implements GameEvent {}
|
|
@ -1,13 +0,0 @@
|
|||||||
import 'package:flame_forge2d/flame_forge2d.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);
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
// ignore_for_file: must_call_super
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flame/input.dart';
|
|
||||||
import 'package:flame_bloc/flame_bloc.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
|
||||||
import 'package:mocktail/mocktail.dart';
|
|
||||||
import 'package:pinball/game/game.dart';
|
|
||||||
import 'package:pinball/l10n/l10n.dart';
|
|
||||||
import 'package:pinball_audio/pinball_audio.dart';
|
|
||||||
import 'package:pinball_theme/pinball_theme.dart';
|
|
||||||
|
|
||||||
class _MockPinballPlayer extends Mock implements PinballPlayer {}
|
|
||||||
|
|
||||||
class _MockAppLocalizations extends Mock implements AppLocalizations {}
|
|
||||||
|
|
||||||
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestGame extends Forge2DGame with FlameBloc {
|
|
||||||
TestGame() {
|
|
||||||
images.prefix = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PinballTestGame extends PinballGame {
|
|
||||||
PinballTestGame({
|
|
||||||
List<String>? assets,
|
|
||||||
PinballPlayer? player,
|
|
||||||
LeaderboardRepository? leaderboardRepository,
|
|
||||||
CharacterTheme? theme,
|
|
||||||
AppLocalizations? l10n,
|
|
||||||
}) : _assets = assets,
|
|
||||||
super(
|
|
||||||
player: player ?? _MockPinballPlayer(),
|
|
||||||
leaderboardRepository:
|
|
||||||
leaderboardRepository ?? _MockLeaderboardRepository(),
|
|
||||||
characterTheme: theme ?? const DashTheme(),
|
|
||||||
l10n: l10n ?? _MockAppLocalizations(),
|
|
||||||
);
|
|
||||||
final List<String>? _assets;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
if (_assets != null) {
|
|
||||||
await images.loadAll(_assets!);
|
|
||||||
}
|
|
||||||
await super.onLoad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DebugPinballTestGame extends DebugPinballGame {
|
|
||||||
DebugPinballTestGame({
|
|
||||||
List<String>? assets,
|
|
||||||
PinballPlayer? player,
|
|
||||||
LeaderboardRepository? leaderboardRepository,
|
|
||||||
CharacterTheme? theme,
|
|
||||||
AppLocalizations? l10n,
|
|
||||||
}) : _assets = assets,
|
|
||||||
super(
|
|
||||||
player: player ?? _MockPinballPlayer(),
|
|
||||||
leaderboardRepository:
|
|
||||||
leaderboardRepository ?? _MockLeaderboardRepository(),
|
|
||||||
characterTheme: theme ?? const DashTheme(),
|
|
||||||
l10n: l10n ?? _MockAppLocalizations(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<String>? _assets;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
if (_assets != null) {
|
|
||||||
await images.loadAll(_assets!);
|
|
||||||
}
|
|
||||||
await super.onLoad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmptyPinballTestGame extends PinballTestGame {
|
|
||||||
EmptyPinballTestGame({
|
|
||||||
List<String>? assets,
|
|
||||||
PinballPlayer? player,
|
|
||||||
CharacterTheme? theme,
|
|
||||||
AppLocalizations? l10n,
|
|
||||||
}) : super(
|
|
||||||
assets: assets,
|
|
||||||
player: player,
|
|
||||||
theme: theme,
|
|
||||||
l10n: l10n ?? _MockAppLocalizations(),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
if (_assets != null) {
|
|
||||||
await images.loadAll(_assets!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmptyKeyboardPinballTestGame extends PinballTestGame
|
|
||||||
with HasKeyboardHandlerComponents {
|
|
||||||
EmptyKeyboardPinballTestGame({
|
|
||||||
List<String>? assets,
|
|
||||||
PinballPlayer? player,
|
|
||||||
CharacterTheme? theme,
|
|
||||||
AppLocalizations? l10n,
|
|
||||||
}) : super(
|
|
||||||
assets: assets,
|
|
||||||
player: player,
|
|
||||||
theme: theme,
|
|
||||||
l10n: l10n ?? _MockAppLocalizations(),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
if (_assets != null) {
|
|
||||||
await images.loadAll(_assets!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
bool tapTextSpan(RichText richText, String text) {
|
|
||||||
final isTapped = !richText.text.visitChildren(
|
|
||||||
(visitor) => _findTextAndTap(visitor, text),
|
|
||||||
);
|
|
||||||
return isTapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _findTextAndTap(InlineSpan visitor, String text) {
|
|
||||||
if (visitor is TextSpan && visitor.text == text) {
|
|
||||||
(visitor.recognizer as TapGestureRecognizer?)?.onTap?.call();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
Loading…
Reference in new issue