mirror of https://github.com/flutter/pinball.git
parent
403d997f17
commit
518e183ad8
@ -1,2 +1,3 @@
|
|||||||
export 'ball.dart';
|
export 'ball.dart';
|
||||||
export 'score_points.dart';
|
export 'score_points.dart';
|
||||||
|
export 'wall.dart';
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
|
||||||
|
class Wall extends BodyComponent {
|
||||||
|
Wall(this.start, this.end);
|
||||||
|
|
||||||
|
final Vector2 start;
|
||||||
|
final Vector2 end;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EdgeShape()..set(start, end);
|
||||||
|
|
||||||
|
final fixtureDef = FixtureDef(shape)
|
||||||
|
..restitution = 0.0
|
||||||
|
..friction = 0.3;
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()
|
||||||
|
..userData = this
|
||||||
|
..position = Vector2.zero()
|
||||||
|
..type = BodyType.static;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export 'bloc/game_bloc.dart';
|
export 'bloc/game_bloc.dart';
|
||||||
export 'components/components.dart';
|
export 'components/components.dart';
|
||||||
export 'pinball_game.dart';
|
export 'pinball_game.dart';
|
||||||
export 'view/pinball_game_page.dart';
|
export 'view/view.dart';
|
||||||
|
@ -1,10 +1,49 @@
|
|||||||
|
// ignore_for_file: avoid_renaming_method_parameters
|
||||||
|
|
||||||
import 'package:flame_bloc/flame_bloc.dart';
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
import 'package:flame_forge2d/forge2d_game.dart';
|
import 'package:flame_forge2d/forge2d_game.dart';
|
||||||
import 'package:pinball/game/game.dart';
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
class BallWallContactCallback extends ContactCallback<Ball, Wall> {
|
||||||
|
@override
|
||||||
|
void begin(Ball ball, Wall wall, Contact contact) {
|
||||||
|
ball.shouldRemove = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void end(_, __, ___) {}
|
||||||
|
}
|
||||||
|
|
||||||
class PinballGame extends Forge2DGame with FlameBloc {
|
class PinballGame extends Forge2DGame with FlameBloc {
|
||||||
|
void resetBall() {
|
||||||
|
add(Ball(position: ballStartingPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final ballStartingPosition = screenToWorld(
|
||||||
|
Vector2(
|
||||||
|
camera.viewport.effectiveSize.x / 2,
|
||||||
|
camera.viewport.effectiveSize.y - 20,
|
||||||
|
),
|
||||||
|
) -
|
||||||
|
Vector2(
|
||||||
|
0,
|
||||||
|
-20,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
addContactCallback(BallScorePointsCallback());
|
addContactCallback(BallScorePointsCallback());
|
||||||
|
|
||||||
|
final topLeft = Vector2.zero();
|
||||||
|
final bottomRight = screenToWorld(camera.viewport.effectiveSize);
|
||||||
|
final bottomLeft = Vector2(topLeft.x, bottomRight.y);
|
||||||
|
|
||||||
|
await add(
|
||||||
|
Wall(bottomRight, bottomLeft),
|
||||||
|
);
|
||||||
|
addContactCallback(BallWallContactCallback());
|
||||||
|
|
||||||
|
resetBall();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:pinball/game/game.dart';
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
class PinballGamePage extends StatelessWidget {
|
class PinballGamePage extends StatelessWidget {
|
||||||
const PinballGamePage({Key? key}) : super(key: key);
|
const PinballGamePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
static Route route() {
|
static Route route() {
|
||||||
return MaterialPageRoute<void>(builder: (_) => const PinballGamePage());
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (_) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => GameBloc(),
|
||||||
|
child: const PinballGamePage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GameWidget(game: PinballGame());
|
return const PinballGameView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
class PinballGameView extends StatelessWidget {
|
||||||
|
const PinballGameView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<GameBloc, GameState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.isGameOver) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return const Dialog(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
child: Center(
|
||||||
|
child: Text('Game Over'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: GameWidget<PinballGame>(game: PinballGame()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export 'pinball_game_page.dart';
|
||||||
|
export 'pinball_game_view.dart';
|
@ -0,0 +1,81 @@
|
|||||||
|
// 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('Wall', () {
|
||||||
|
final flameTester = FlameTester(PinballGame.new);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
final wall = Wall(Vector2.zero(), Vector2(100, 0));
|
||||||
|
await game.ensureAdd(wall);
|
||||||
|
|
||||||
|
expect(game.contains(wall), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('body', () {
|
||||||
|
flameTester.test(
|
||||||
|
'positions correctly',
|
||||||
|
(game) async {
|
||||||
|
final wall = Wall(Vector2.zero(), Vector2(100, 0));
|
||||||
|
await game.ensureAdd(wall);
|
||||||
|
game.contains(wall);
|
||||||
|
|
||||||
|
expect(wall.body.position, Vector2.zero());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'is static',
|
||||||
|
(game) async {
|
||||||
|
final wall = Wall(Vector2.zero(), Vector2(100, 0));
|
||||||
|
await game.ensureAdd(wall);
|
||||||
|
|
||||||
|
expect(wall.body.bodyType, equals(BodyType.static));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('first fixture', () {
|
||||||
|
flameTester.test(
|
||||||
|
'exists',
|
||||||
|
(game) async {
|
||||||
|
final wall = Wall(Vector2.zero(), Vector2(100, 0));
|
||||||
|
await game.ensureAdd(wall);
|
||||||
|
|
||||||
|
expect(wall.body.fixtures[0], isA<Fixture>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has restitution equals 0',
|
||||||
|
(game) async {
|
||||||
|
final wall = Wall(Vector2.zero(), Vector2(100, 0));
|
||||||
|
await game.ensureAdd(wall);
|
||||||
|
|
||||||
|
final fixture = wall.body.fixtures[0];
|
||||||
|
expect(fixture.restitution, equals(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'has friction',
|
||||||
|
(game) async {
|
||||||
|
final wall = Wall(Vector2.zero(), Vector2(100, 0));
|
||||||
|
await game.ensureAdd(wall);
|
||||||
|
|
||||||
|
final fixture = wall.body.fixtures[0];
|
||||||
|
expect(fixture.friction, greaterThan(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,9 +1,109 @@
|
|||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockingjay/mockingjay.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
class MockPinballGame extends Mock implements PinballGame {}
|
||||||
|
|
||||||
|
class MockWall extends Mock implements Wall {}
|
||||||
|
|
||||||
|
class MockBall extends Mock implements Ball {}
|
||||||
|
|
||||||
|
class MockContact extends Mock implements Contact {}
|
||||||
|
|
||||||
|
class MockGameBloc extends Mock implements GameBloc {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('PinballGame', () {
|
|
||||||
// TODO(alestiago): test if [PinballGame] registers
|
// TODO(alestiago): test if [PinballGame] registers
|
||||||
// [BallScorePointsCallback] once the following issue is resolved:
|
// [BallScorePointsCallback] once the following issue is resolved:
|
||||||
// https://github.com/flame-engine/flame/issues/1416
|
// https://github.com/flame-engine/flame/issues/1416
|
||||||
|
group('PinballGame', () {
|
||||||
|
group('BallWallContactCallback', () {
|
||||||
|
test('removes the ball on begin contact', () {
|
||||||
|
final game = MockPinballGame();
|
||||||
|
final wall = MockWall();
|
||||||
|
final ball = MockBall();
|
||||||
|
|
||||||
|
when(() => ball.gameRef).thenReturn(game);
|
||||||
|
|
||||||
|
BallWallContactCallback()
|
||||||
|
// Remove once https://github.com/flame-engine/flame/pull/1415
|
||||||
|
// is merged
|
||||||
|
..end(MockBall(), MockWall(), MockContact())
|
||||||
|
..begin(ball, wall, MockContact());
|
||||||
|
|
||||||
|
verify(() => ball.shouldRemove = true).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('resetting a ball', () {
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = MockGameBloc();
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
const Stream<GameState>.empty(),
|
||||||
|
initialState: const GameState.initial(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FlameTester(
|
||||||
|
PinballGame.new,
|
||||||
|
pumpWidget: (gameWidget, tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
BlocProvider.value(
|
||||||
|
value: gameBloc,
|
||||||
|
child: gameWidget,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..widgetTest('adds BallLost to GameBloc', (game, tester) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
game.children.whereType<Ball>().first.removeFromParent();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
verify(() => gameBloc.add(const BallLost())).called(1);
|
||||||
|
})
|
||||||
|
..widgetTest(
|
||||||
|
'resets the ball if the game is not over',
|
||||||
|
(game, tester) async {
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
game.children.whereType<Ball>().first.removeFromParent();
|
||||||
|
await game.ready(); // Making sure that all additions are done
|
||||||
|
|
||||||
|
expect(
|
||||||
|
game.children.whereType<Ball>().length,
|
||||||
|
equals(1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..widgetTest(
|
||||||
|
'no ball is added on game over',
|
||||||
|
(game, tester) async {
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
const Stream<GameState>.empty(),
|
||||||
|
initialState: const GameState(score: 10, balls: 1),
|
||||||
|
);
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
game.children.whereType<Ball>().first.removeFromParent();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
game.children.whereType<Ball>().length,
|
||||||
|
equals(0),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PinballGameView', () {
|
||||||
|
testWidgets('renders', (tester) async {
|
||||||
|
final gameBloc = MockGameBloc();
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(const GameState.initial()),
|
||||||
|
initialState: const GameState.initial(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'renders a game over dialog when the user has lost',
|
||||||
|
(tester) async {
|
||||||
|
final gameBloc = MockGameBloc();
|
||||||
|
const state = GameState(score: 0, balls: 0);
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: state,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.text('Game Over'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue