Merge branch 'main' into refactor/flipper-position-and-angle

pull/161/head
Allison Ryan 4 years ago committed by GitHub
commit 425074634f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,27 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'assets_manager_state.dart';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
/// Loads the assets
Future<void> load() async {
final all = state.loadables.map((loadable) async {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -0,0 +1,41 @@
part of 'assets_manager_cubit.dart';
/// {@template assets_manager_state}
/// State used to load the game assets
/// {@endtemplate}
class AssetsManagerState extends Equatable {
/// {@macro assets_manager_state}
const AssetsManagerState({
required this.loadables,
required this.loaded,
});
/// {@macro assets_manager_state}
const AssetsManagerState.initial({
required List<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
/// List of futures to load
final List<Future> loadables;
/// List of loaded futures
final List<Future> loaded;
/// Returns a value between 0 and 1 to indicate the loading progress
double get progress => loaded.length / loadables.length;
/// Returns a copy of this instance with the given parameters
/// updated
AssetsManagerState copyWith({
List<Future>? loadables,
List<Future>? loaded,
}) {
return AssetsManagerState(
loadables: loadables ?? this.loadables,
loaded: loaded ?? this.loaded,
);
}
@override
List<Object> get props => [loaded, loadables];
}

@ -5,4 +5,5 @@ export 'controlled_flipper.dart';
export 'flutter_forest.dart';
export 'plunger.dart';
export 'score_points.dart';
export 'sparky_fire_zone.dart';
export 'wall.dart';

@ -136,12 +136,10 @@ class PlungerAnchor extends JointAnchor {
@override
Body createBody() {
final shape = CircleShape()..radius = 0.5;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
return world.createBody(bodyDef);
}
}

@ -0,0 +1,44 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
// TODO(ruimiguel): create and add SparkyFireZone component here in other PR.
// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done
// ignore: public_member_api_docs
class ControlledSparkyBumper extends SparkyBumper
with Controls<_SparkyBumperController> {
// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done
// ignore: public_member_api_docs
ControlledSparkyBumper() : super.a() {
controller = _SparkyBumperController(this);
}
}
/// {@template sparky_bumper_controller}
/// Controls a [SparkyBumper].
/// {@endtemplate}
class _SparkyBumperController extends ComponentController<SparkyBumper>
with HasGameRef<PinballGame> {
/// {@macro sparky_bumper_controller}
_SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper)
: super(controlledSparkyBumper);
/// Flag for activated state of the [SparkyBumper].
///
/// Used to toggle [SparkyBumper]s' state between activated and deactivated.
bool isActivated = false;
/// Registers when a [SparkyBumper] is hit by a [Ball].
void hit() {
if (isActivated) {
component.deactivate();
} else {
component.activate();
}
isActivated = !isActivated;
}
}

@ -1,3 +1,4 @@
export 'assets_manager/cubit/assets_manager_cubit.dart';
export 'bloc/game_bloc.dart';
export 'components/components.dart';
export 'game_assets.dart';

@ -4,9 +4,9 @@ import 'package:pinball_components/pinball_components.dart' as components;
/// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame {
/// Pre load the initial assets of the game.
Future<void> preLoadAssets() async {
await Future.wait([
/// Returns a list of assets to be loaded
List<Future> preLoadAssets() {
return [
images.load(components.Assets.images.ball.keyName),
images.load(components.Assets.images.flutterSignPost.keyName),
images.load(components.Assets.images.flipper.left.keyName),
@ -47,6 +47,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.chromeDino.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName),
images.load(Assets.images.components.background.path),
]);
];
}
}

@ -9,16 +9,41 @@ import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGamePage extends StatelessWidget {
const PinballGamePage({Key? key, required this.theme}) : super(key: key);
const PinballGamePage({
Key? key,
required this.theme,
required this.game,
}) : super(key: key);
final PinballTheme theme;
final PinballGame game;
static Route route({required PinballTheme theme}) {
static Route route({
required PinballTheme theme,
bool isDebugMode = kDebugMode,
}) {
return MaterialPageRoute<void>(
builder: (_) {
return BlocProvider(
create: (_) => GameBloc(),
child: PinballGamePage(theme: theme),
builder: (context) {
final audio = context.read<PinballAudio>();
final game = isDebugMode
? DebugPinballGame(theme: theme, audio: audio)
: PinballGame(theme: theme, audio: audio);
final pinballAudio = context.read<PinballAudio>();
final loadables = [
...game.preLoadAssets(),
pinballAudio.load(),
];
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => GameBloc()),
BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
),
],
child: PinballGamePage(theme: theme, game: game),
);
},
);
@ -26,51 +51,19 @@ class PinballGamePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PinballGameView(theme: theme);
return PinballGameView(theme: theme, game: game);
}
}
class PinballGameView extends StatefulWidget {
class PinballGameView extends StatelessWidget {
const PinballGameView({
Key? key,
required this.theme,
bool isDebugMode = kDebugMode,
}) : _isDebugMode = isDebugMode,
super(key: key);
required this.game,
}) : super(key: key);
final PinballTheme theme;
final bool _isDebugMode;
@override
State<PinballGameView> createState() => _PinballGameViewState();
}
class _PinballGameViewState extends State<PinballGameView> {
late PinballGame _game;
@override
void initState() {
super.initState();
final audio = context.read<PinballAudio>();
_game = widget._isDebugMode
? DebugPinballGame(theme: widget.theme, audio: audio)
: PinballGame(theme: widget.theme, audio: audio);
// TODO(erickzanardo): Revisit this when we start to have more assets
// this could expose a Stream (maybe even a cubit?) so we could show the
// the loading progress with some fancy widgets.
_fetchAssets();
}
Future<void> _fetchAssets() async {
final pinballAudio = context.read<PinballAudio>();
await Future.wait([
_game.preLoadAssets(),
pinballAudio.load(),
]);
}
final PinballGame game;
@override
Widget build(BuildContext context) {
@ -84,24 +77,51 @@ class _PinballGameViewState extends State<PinballGameView> {
builder: (_) {
return GameOverDialog(
score: state.score,
theme: widget.theme.characterTheme,
theme: theme.characterTheme,
);
},
);
}
},
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game),
),
const Positioned(
top: 8,
left: 8,
child: GameHud(),
child: _GameView(game: game),
);
}
}
class _GameView extends StatelessWidget {
const _GameView({
Key? key,
required PinballGame game,
}) : _game = game,
super(key: key);
final PinballGame _game;
@override
Widget build(BuildContext context) {
final loadingProgress = context.watch<AssetsManagerCubit>().state.progress;
if (loadingProgress != 1) {
return Scaffold(
body: Center(
child: Text(
loadingProgress.toString(),
),
],
),
),
);
}
return Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game),
),
const Positioned(
top: 8,
left: 8,
child: GameHud(),
),
],
);
}
}

@ -3,6 +3,8 @@
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
@ -15,6 +17,7 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png');

@ -22,36 +22,31 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
layer = Layer.board;
}
/// The size of the [Ball]
static final Vector2 size = Vector2.all(4.5);
/// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13);
/// The base [Color] used to tint this [Ball]
/// The base [Color] used to tint this [Ball].
final Color baseColor;
double _boostTimer = 0;
static const _boostDuration = 2.0;
late SpriteComponent _spriteComponent;
final _BallSpriteComponent _spriteComponent = _BallSpriteComponent();
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(Assets.images.ball.keyName);
final tint = baseColor.withOpacity(0.5);
renderBody = false;
await add(
_spriteComponent = SpriteComponent(
sprite: sprite,
size: size * 1.15,
anchor: Anchor.center,
)..tint(tint),
_spriteComponent..tint(baseColor.withOpacity(0.5)),
);
}
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this
@ -70,7 +65,7 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
/// Allows the [Ball] to be affected by forces.
///
/// If previously [stop]ed, the previous ball's velocity is not kept.
/// If previously [stop]ped, the previous ball's velocity is not kept.
void resume() {
body.setType(BodyType.dynamic);
}
@ -114,3 +109,16 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
_spriteComponent.scale = Vector2.all(scaleFactor);
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.ball.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}

@ -82,23 +82,8 @@ class Baseboard extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
(_side.isLeft)
? Assets.images.baseboard.left.keyName
: Assets.images.baseboard.right.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(27.5, 17.9),
anchor: Anchor.center,
position: Vector2(_side.isLeft ? 0.4 : -0.4, 0),
),
);
renderBody = false;
await add(_BaseboardSpriteComponent(side: _side));
}
@override
@ -115,3 +100,23 @@ class Baseboard extends BodyComponent with InitialPosition {
return body;
}
}
class _BaseboardSpriteComponent extends SpriteComponent with HasGameRef {
_BaseboardSpriteComponent({required BoardSide side}) : _side = side;
final BoardSide _side;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
(_side.isLeft)
? Assets.images.baseboard.left.keyName
: Assets.images.baseboard.right.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
position = Vector2(0.4 * -_side.direction, 0);
anchor = Anchor.center;
}
}

@ -63,23 +63,22 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_BottomBoundarySpriteComponent());
}
}
Future<void> _loadSprite() async {
class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.boundary.bottom.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
position: Vector2(-5.4, 57.4),
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-5.4, 57.4);
}
}
@ -135,22 +134,21 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_OuterBoundarySpriteComponent());
}
}
Future<void> _loadSprite() async {
class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.boundary.outer.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
position: Vector2(-0.2, -1.4),
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-0.2, -1.4);
}
}

@ -81,13 +81,10 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
@ -103,21 +100,22 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_DinoTopWallSpriteComponent());
}
}
Future<void> _loadSprite() async {
class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.dino.dinoLandTop.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(10.6, 27.7),
anchor: Anchor.center,
position: Vector2(27, -28.2),
);
await add(spriteComponent);
this.sprite = sprite;
size = sprite.originalSize / 10;
position = Vector2(22, -41.8);
}
}
@ -182,8 +180,6 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
@ -204,19 +200,21 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_DinoBottomWallSpriteComponent());
}
}
Future<void> _loadSprite() async {
class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.dino.dinoLandBottom.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(15.6, 54.8),
anchor: Anchor.center,
)..position = Vector2(31.7, 18);
await add(spriteComponent);
this.sprite = sprite;
size = sprite.originalSize / 10;
position = Vector2(23.8, -9.5);
}
}

@ -41,22 +41,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
body.linearVelocity = Vector2(0, _speed);
}
/// Loads the sprite that renders with the [Flipper].
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
(side.isLeft)
? Assets.images.flipper.left.keyName
: Assets.images.flipper.right.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
);
await add(spriteComponent);
}
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async {
final anchor = _FlipperAnchor(flipper: this);
@ -128,10 +112,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
await super.onLoad();
renderBody = false;
await Future.wait<void>([
_loadSprite(),
_anchorToJoint(),
]);
await _anchorToJoint();
await add(_FlipperSpriteComponent(side: side));
}
@override
@ -147,6 +129,25 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
}
}
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
_FlipperSpriteComponent({required BoardSide side}) : _side = side;
final BoardSide _side;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
(_side.isLeft)
? Assets.images.flipper.left.keyName
: Assets.images.flipper.right.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///

@ -1,33 +1,17 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template flutter_sign_post}
/// A sign, found in the FlutterForest.
/// A sign, found in the Flutter Forest.
/// {@endtemplate}
// TODO(alestiago): Revisit doc comment if FlutterForest is moved to package.
class FlutterSignPost extends BodyComponent with InitialPosition {
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.flutterSignPost.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.bottomCenter,
position: Vector2(0.65, 0.45),
);
await add(spriteComponent);
}
@override
Future<void> onLoad() async {
await super.onLoad();
paint = Paint()
..color = Colors.blue.withOpacity(0.5)
..style = PaintingStyle.fill;
await _loadSprite();
renderBody = false;
await add(_FlutterSignPostSpriteComponent());
}
@override
@ -39,3 +23,18 @@ class FlutterSignPost extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _FlutterSignPostSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.flutterSignPost.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.bottomCenter;
position = Vector2(0.65, 0.45);
}
}

@ -18,6 +18,9 @@ class Kicker extends BodyComponent with InitialPosition {
required BoardSide side,
}) : _side = side;
/// The size of the [Kicker] body.
static final Vector2 size = Vector2(4.4, 15);
/// Whether the [Kicker] is on the left or right side of the board.
///
/// A [Kicker] with [BoardSide.left] propels the [Ball] to the right,
@ -25,9 +28,6 @@ class Kicker extends BodyComponent with InitialPosition {
/// left.
final BoardSide _side;
/// The size of the [Kicker] body.
static final Vector2 size = Vector2(4.4, 15);
List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
final direction = _side.direction;
@ -122,21 +122,28 @@ class Kicker extends BodyComponent with InitialPosition {
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_KickerSpriteComponent(side: _side));
}
}
class _KickerSpriteComponent extends SpriteComponent with HasGameRef {
_KickerSpriteComponent({required BoardSide side}) : _side = side;
final BoardSide _side;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
(_side.isLeft)
? Assets.images.kicker.left.keyName
: Assets.images.kicker.right.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(8.7, 19),
anchor: Anchor.center,
position: Vector2(0.7 * -_side.direction, -2.2),
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(0.7 * -_side.direction, -2.2);
}
}

@ -115,23 +115,24 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_LaunchRampBaseSpriteComponent());
}
}
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.launchRamp.ramp.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
position: Vector2(25.65, 0),
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(25.65, 0);
}
}
@ -192,23 +193,25 @@ class _LaunchRampForegroundRailing extends BodyComponent
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_LaunchRampForegroundRailingSpriteComponent());
}
}
class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.launchRamp.foregroundRailing.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
position: Vector2(22.8, 0),
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(22.8, 0);
}
}

@ -102,27 +102,9 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
final sprite = await gameRef.images.load(
Assets.images.spaceship.bridge.keyName,
);
await add(
SpriteAnimationComponent.fromFrameData(
sprite,
SpriteAnimationData.sequenced(
amount: 72,
amountPerRow: 24,
stepTime: 0.05,
textureSize: Vector2(82, 100),
),
size: Vector2(8.2, 10),
position: Vector2(0, -2),
anchor: Anchor.center,
),
);
await add(_AndroidHeadSpriteAnimation());
}
@override
@ -141,6 +123,29 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
}
}
class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final image = await gameRef.images.load(
Assets.images.spaceship.bridge.keyName,
);
size = Vector2(8.2, 10);
position = Vector2(0, -2);
anchor = Anchor.center;
final data = SpriteAnimationData.sequenced(
amount: 72,
amountPerRow: 24,
stepTime: 0.05,
textureSize: size * 10,
);
animation = SpriteAnimation.fromFrameData(image, data);
}
}
/// {@template spaceship_entrance}
/// A sensor [BodyComponent] used to detect when the ball enters the
/// the spaceship area in order to modify its filter data so the ball
@ -228,8 +233,11 @@ class _SpaceshipWallShape extends ChainShape {
/// {@template spaceship_wall}
/// A [BodyComponent] that provides the collision for the wall
/// surrounding the spaceship, with a small opening to allow the
/// [Ball] to get inside the spaceship saucer.
/// surrounding the spaceship.
///
/// It has a small opening to allow the [Ball] to get inside the spaceship
/// saucer.
///
/// It also contains the [SpriteComponent] for the lower wall
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {

@ -139,21 +139,23 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
await add(_SpaceshipRailRampSpriteComponent());
}
}
class _SpaceshipRailRampSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.rail.main.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(17.5, 55.7),
anchor: Anchor.center,
position: Vector2(-29.4, -5.7),
);
await add(spriteComponent);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-29.4, -5.7);
}
}

@ -96,8 +96,6 @@ class _SpaceshipRampBackground extends BodyComponent
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
@ -111,35 +109,40 @@ class _SpaceshipRampBackground extends BodyComponent
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
}
Future<void> _loadSprites() async {
final spriteRamp = await gameRef.loadSprite(
Assets.images.spaceship.ramp.main.keyName,
);
renderBody = false;
final spriteRampComponent = SpriteComponent(
sprite: spriteRamp,
size: Vector2(38.1, 33.8),
anchor: Anchor.center,
position: Vector2(-12.2, -53.5),
);
await add(_SpaceshipRampBackgroundRailingSpriteComponent());
await add(_SpaceshipRampBackgroundRampSpriteComponent());
}
}
final spriteRailingBg = await gameRef.loadSprite(
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.ramp.railingBackground.keyName,
);
final spriteRailingBgComponent = SpriteComponent(
sprite: spriteRailingBg,
size: Vector2(38.3, 35.1),
anchor: Anchor.center,
position: spriteRampComponent.position + Vector2(0, -1),
);
this.sprite = sprite;
size = Vector2(38.3, 35.1);
anchor = Anchor.center;
position = Vector2(-12.2, -54.5);
}
}
await addAll([
spriteRailingBgComponent,
spriteRampComponent,
]);
class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.ramp.main.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-12.2, -53.5);
}
}
@ -196,21 +199,22 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
await add(_SpaceshipRampForegroundRalingSpriteComponent());
}
}
Future<void> _loadSprites() async {
final spriteRailingFg = await gameRef.loadSprite(
class _SpaceshipRampForegroundRalingSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.ramp.railingForeground.keyName,
);
final spriteRailingFgComponent = SpriteComponent(
sprite: spriteRailingFg,
size: Vector2(26.1, 28.3),
anchor: Anchor.center,
position: Vector2(-12.2, -52.5),
);
await add(spriteRailingFgComponent);
this.sprite = sprite;
size = Vector2(26.1, 28.3);
anchor = Anchor.center;
position = Vector2(-12.2, -52.5);
}
}

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BasicBallGame extends BasicGame with TapDetector {
class BasicBallGame extends BasicGame with TapDetector, Traceable {
BasicBallGame({required this.color});
static const info = '''
@ -19,5 +19,6 @@ class BasicBallGame extends BasicGame with TapDetector {
add(
Ball(baseColor: color)..initialPosition = info.eventPosition.game,
);
traceAllBodies();
}
}

@ -12,7 +12,7 @@ void addBallStories(Dashbook dashbook) {
(context) => GameWidget(
game: BasicBallGame(
color: context.colorProperty('color', Colors.blue),
),
)..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('ball/basic.dart'),
info: BasicBallGame.info,

@ -0,0 +1,35 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group('AssetsManagerCubit', () {
final completer1 = Completer<void>();
final completer2 = Completer<void>();
final future1 = completer1.future;
final future2 = completer2.future;
blocTest<AssetsManagerCubit, AssetsManagerState>(
'emits the loaded on the order that they load',
build: () => AssetsManagerCubit([future1, future2]),
act: (cubit) {
cubit.load();
completer2.complete();
completer1.complete();
},
expect: () => [
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2],
),
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2, future1],
),
],
);
});
}

@ -0,0 +1,145 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group('AssetsManagerState', () {
test('can be instantiated', () {
expect(
AssetsManagerState(loadables: const [], loaded: const []),
isNotNull,
);
});
test('has the correct initial state', () {
final future = Future<void>.value();
expect(
AssetsManagerState.initial(loadables: [future]),
equals(
AssetsManagerState(
loadables: [future],
loaded: const [],
),
),
);
});
group('progress', () {
final future1 = Future<void>.value();
final future2 = Future<void>.value();
test('returns 0 when no future is loaded', () {
expect(
AssetsManagerState(
loadables: [future1, future2],
loaded: const [],
).progress,
equals(0),
);
});
test('returns the correct value when some of the futures are loaded', () {
expect(
AssetsManagerState(
loadables: [future1, future2],
loaded: [future1],
).progress,
equals(0.5),
);
});
test('returns the 1 when all futures are loaded', () {
expect(
AssetsManagerState(
loadables: [future1, future2],
loaded: [future1, future2],
).progress,
equals(1),
);
});
});
group('copyWith', () {
final future = Future<void>.value();
test('returns a copy with the updated loadables', () {
expect(
AssetsManagerState(
loadables: const [],
loaded: const [],
).copyWith(loadables: [future]),
equals(
AssetsManagerState(
loadables: [future],
loaded: const [],
),
),
);
});
test('returns a copy with the updated loaded', () {
expect(
AssetsManagerState(
loadables: const [],
loaded: const [],
).copyWith(loaded: [future]),
equals(
AssetsManagerState(
loadables: const [],
loaded: [future],
),
),
);
});
});
test('supports value comparison', () {
final future1 = Future<void>.value();
final future2 = Future<void>.value();
expect(
AssetsManagerState(
loadables: const [],
loaded: const [],
),
equals(
AssetsManagerState(
loadables: const [],
loaded: const [],
),
),
);
expect(
AssetsManagerState(
loadables: [future1],
loaded: const [],
),
isNot(
equals(
AssetsManagerState(
loadables: [future2],
loaded: const [],
),
),
),
);
expect(
AssetsManagerState(
loadables: const [],
loaded: [future1],
),
isNot(
equals(
AssetsManagerState(
loadables: const [],
loaded: [future2],
),
),
),
);
});
});
}

@ -0,0 +1,45 @@
// ignore_for_file: cascade_invocations
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() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('SparkyFireZone', () {
group('bumpers', () {
late ControlledSparkyBumper controlledSparkyBumper;
flameTester.testGameWidget(
'activate when deactivated bumper is hit',
setUp: (game, tester) async {
controlledSparkyBumper = ControlledSparkyBumper();
await game.ensureAdd(controlledSparkyBumper);
controlledSparkyBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledSparkyBumper.controller.isActivated, isTrue);
},
);
flameTester.testGameWidget(
'deactivate when activated bumper is hit',
setUp: (game, tester) async {
controlledSparkyBumper = ControlledSparkyBumper();
await game.ensureAdd(controlledSparkyBumper);
controlledSparkyBumper.controller.hit();
controlledSparkyBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledSparkyBumper.controller.isActivated, isFalse);
},
);
});
});
}

@ -11,6 +11,7 @@ import '../../helpers/helpers.dart';
void main() {
const theme = PinballTheme(characterTheme: DashTheme());
final game = PinballGameTest();
group('PinballGamePage', () {
testWidgets('renders PinballGameView', (tester) async {
@ -22,37 +23,107 @@ void main() {
);
await tester.pumpApp(
PinballGamePage(theme: theme),
PinballGamePage(theme: theme, game: game),
gameBloc: gameBloc,
);
expect(find.byType(PinballGameView), findsOneWidget);
});
testWidgets('route returns a valid navigation route', (tester) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context)
.push<void>(PinballGamePage.route(theme: theme));
},
child: const Text('Tap me'),
);
},
testWidgets(
'renders the loading indicator while the assets load',
(tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
final assetsManagerCubit = MockAssetsManagerCubit();
final initialAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: const [],
);
whenListen(
assetsManagerCubit,
Stream.value(initialAssetsState),
initialState: initialAssetsState,
);
await tester.pumpApp(
PinballGamePage(theme: theme, game: game),
gameBloc: gameBloc,
assetsManagerCubit: assetsManagerCubit,
);
expect(find.text('0.0'), findsOneWidget);
final loadedAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: [Future<void>.value()],
);
whenListen(
assetsManagerCubit,
Stream.value(loadedAssetsState),
initialState: loadedAssetsState,
);
await tester.pump();
expect(find.byType(PinballGameView), findsOneWidget);
},
);
group('route', () {
Future<void> pumpRoute({
required WidgetTester tester,
required bool isDebugMode,
}) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
PinballGamePage.route(
theme: theme,
isDebugMode: isDebugMode,
),
);
},
child: const Text('Tap me'),
);
},
),
),
),
);
);
await tester.tap(find.text('Tap me'));
await tester.tap(find.text('Tap me'));
// We can't use pumpAndSettle here because the page renders a Flame game
// which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation
// We can't use pumpAndSettle here because the page renders a Flame game
// which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation
}
expect(find.byType(PinballGamePage), findsOneWidget);
testWidgets('route creates the correct non debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: false);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('route creates the correct debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: true);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
});
});
@ -66,7 +137,7 @@ void main() {
);
await tester.pumpApp(
PinballGameView(theme: theme),
PinballGameView(theme: theme, game: game),
gameBloc: gameBloc,
);
@ -99,7 +170,7 @@ void main() {
);
await tester.pumpApp(
const PinballGameView(theme: theme),
PinballGameView(theme: theme, game: game),
gameBloc: gameBloc,
);
await tester.pump();
@ -107,45 +178,5 @@ void main() {
expect(find.byType(GameOverDialog), findsOneWidget);
},
);
testWidgets('renders the real game when not in debug mode', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(
const PinballGameView(theme: theme, isDebugMode: false),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('renders the debug game when on debug mode', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(
const PinballGameView(theme: theme),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
});
}

@ -74,3 +74,5 @@ class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {}
class MockPinballAudio extends Mock implements PinballAudio {}
class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}

@ -5,6 +5,7 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@ -26,11 +27,31 @@ PinballAudio _buildDefaultPinballAudio() {
return audio;
}
MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() {
final cubit = MockAssetsManagerCubit();
final state = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: [
Future<void>.value(),
],
);
whenListen(
cubit,
Stream.value(state),
initialState: state,
);
return cubit;
}
extension PumpApp on WidgetTester {
Future<void> pumpApp(
Widget widget, {
MockNavigator? navigator,
GameBloc? gameBloc,
AssetsManagerCubit? assetsManagerCubit,
ThemeCubit? themeCubit,
LeaderboardRepository? leaderboardRepository,
PinballAudio? pinballAudio,
@ -54,6 +75,9 @@ extension PumpApp on WidgetTester {
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
BlocProvider.value(
value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(),
),
],
child: MaterialApp(
localizationsDelegates: const [

Loading…
Cancel
Save