Merge branch 'main' into fix/render-with-high-quality

pull/334/head
Alejandro Santiago 3 years ago committed by GitHub
commit 2a9cfe1a4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,7 @@ service cloud.firestore {
}
function isAuthedUser(auth) {
return request.auth.uid != null; && auth.token.firebase.sign_in_provider == "anonymous"
return request.auth.uid != null && auth.token.firebase.sign_in_provider == "anonymous"
}
// Leaderboard can be read if it doesn't contain any prohibited initials

@ -36,12 +36,14 @@ class DinoDesert extends Component {
}
class _BarrierBehindDino extends BodyComponent {
_BarrierBehindDino() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
Vector2(25, -14.2),
Vector2(25, -7.7),
Vector2(25.3, -14.2),
Vector2(25.3, -7.7),
);
return world.createBody(BodyDef())..createFixtureFromShape(shape);

@ -131,6 +131,10 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flapper.backSupport.keyName),
images.load(components.Assets.images.flapper.frontSupport.keyName),
images.load(components.Assets.images.flapper.flap.keyName),
images.load(components.Assets.images.skillShot.decal.keyName),
images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName),

@ -7,6 +7,7 @@ import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -57,6 +58,11 @@ class PinballGame extends PinballForge2DGame
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
];
final characterAreas = [
AndroidAcres(),

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -35,6 +35,7 @@ class $AssetsImagesGen {
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSkillShotGen get skillShot => const $AssetsImagesSkillShotGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
}
@ -272,6 +273,26 @@ class $AssetsImagesSignpostGen {
const AssetGenImage('assets/images/signpost/inactive.png');
}
class $AssetsImagesSkillShotGen {
const $AssetsImagesSkillShotGen();
/// File path: assets/images/skill_shot/decal.png
AssetGenImage get decal =>
const AssetGenImage('assets/images/skill_shot/decal.png');
/// File path: assets/images/skill_shot/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/skill_shot/dimmed.png');
/// File path: assets/images/skill_shot/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/skill_shot/lit.png');
/// File path: assets/images/skill_shot/pin.png
AssetGenImage get pin =>
const AssetGenImage('assets/images/skill_shot/pin.png');
}
class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen();

@ -7,7 +7,7 @@ import 'package:pinball_components/pinball_components.dart';
part 'chrome_dino_state.dart';
class ChromeDinoCubit extends Cubit<ChromeDinoState> {
ChromeDinoCubit() : super(const ChromeDinoState.inital());
ChromeDinoCubit() : super(const ChromeDinoState.initial());
void onOpenMouth() {
emit(state.copyWith(isMouthOpen: true));

@ -14,7 +14,7 @@ class ChromeDinoState extends Equatable {
this.ball,
});
const ChromeDinoState.inital()
const ChromeDinoState.initial()
: this(
status: ChromeDinoStatus.idle,
isMouthOpen: false,

@ -21,7 +21,7 @@ export 'joint_anchor.dart';
export 'kicker/kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'layer_sensor.dart';
export 'layer_sensor/layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
@ -29,6 +29,7 @@ export 'rocket.dart';
export 'score_component.dart';
export 'shapes/shapes.dart';
export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';
export 'slingshot.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp/spaceship_ramp.dart';

@ -1,90 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// By default the base [layer] is set to [Layer.board] and the
/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent
with InitialPosition, Layered, ContactCallbacks {
/// {@macro layer_sensor}
LayerSensor({
required Layer insideLayer,
Layer? outsideLayer,
required int insideZIndex,
int? outsideZIndex,
required this.orientation,
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_insideZIndex = insideZIndex,
_outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
super(renderBody: false) {
layer = Layer.opening;
}
final Layer _insideLayer;
final Layer _outsideLayer;
final int _insideZIndex;
final int _outsideZIndex;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != _insideLayer) {
final isBallEnteringOpening =
(orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
other
..layer = _insideLayer
..zIndex = _insideZIndex;
}
} else {
other
..layer = _outsideLayer
..zIndex = _outsideZIndex;
}
}
}

@ -0,0 +1,2 @@
export 'behaviors.dart';
export 'layer_filtering_behavior.dart';

@ -0,0 +1,31 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class LayerFilteringBehavior extends ContactBehavior<LayerSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != parent.insideLayer) {
final isBallEnteringOpening =
(parent.orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(parent.orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
other
..layer = parent.insideLayer
..zIndex = parent.insideZIndex;
}
} else {
other
..layer = parent.outsideLayer
..zIndex = parent.outsideZIndex;
}
}
}

@ -0,0 +1,66 @@
// ignore_for_file: avoid_renaming_method_parameters, public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// By default the base [layer] is set to [Layer.board] and the
/// [outsideZIndex] is set to [ZIndexes.ballOnBoard].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
/// {@macro layer_sensor}
LayerSensor({
required this.insideLayer,
Layer? outsideLayer,
required this.insideZIndex,
int? outsideZIndex,
required this.orientation,
}) : outsideLayer = outsideLayer ?? Layer.board,
outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
super(
renderBody: false,
children: [LayerFilteringBehavior()],
) {
layer = Layer.opening;
}
final Layer insideLayer;
final Layer outsideLayer;
final int insideZIndex;
final int outsideZIndex;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -0,0 +1,2 @@
export 'skill_shot_ball_contact_behavior.dart';
export 'skill_shot_blinking_behavior.dart';

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class SkillShotBallContactBehavior extends ContactBehavior<SkillShot> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
parent.firstChild<SpriteAnimationComponent>()?.playing = true;
}
}

@ -0,0 +1,44 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template skill_shot_blinking_behavior}
/// Makes a [SkillShot] blink between [SkillShotSpriteState.lit] and
/// [SkillShotSpriteState.dimmed] for a set amount of blinks.
/// {@endtemplate}
class SkillShotBlinkingBehavior extends TimerComponent
with ParentIsA<SkillShot> {
/// {@macro skill_shot_blinking_behavior}
SkillShotBlinkingBehavior() : super(period: 0.15);
final _maxBlinks = 4;
int _blinks = 0;
void _onNewState(SkillShotState state) {
if (state.isBlinking) {
timer
..reset()
..start();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (_blinks != _maxBlinks * 2) {
parent.bloc.switched();
_blinks++;
} else {
_blinks = 0;
timer.stop();
parent.bloc.onBlinkingFinished();
}
}
}

@ -0,0 +1,39 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'skill_shot_state.dart';
class SkillShotCubit extends Cubit<SkillShotState> {
SkillShotCubit() : super(const SkillShotState.initial());
void onBallContacted() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
}
void switched() {
switch (state.spriteState) {
case SkillShotSpriteState.lit:
emit(state.copyWith(spriteState: SkillShotSpriteState.dimmed));
break;
case SkillShotSpriteState.dimmed:
emit(state.copyWith(spriteState: SkillShotSpriteState.lit));
break;
}
}
void onBlinkingFinished() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
);
}
}

@ -0,0 +1,37 @@
// ignore_for_file: public_member_api_docs
part of 'skill_shot_cubit.dart';
enum SkillShotSpriteState {
lit,
dimmed,
}
class SkillShotState extends Equatable {
const SkillShotState({
required this.spriteState,
required this.isBlinking,
});
const SkillShotState.initial()
: this(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
final SkillShotSpriteState spriteState;
final bool isBlinking;
SkillShotState copyWith({
SkillShotSpriteState? spriteState,
bool? isBlinking,
}) =>
SkillShotState(
spriteState: spriteState ?? this.spriteState,
isBlinking: isBlinking ?? this.isBlinking,
);
@override
List<Object?> get props => [spriteState, isBlinking];
}

@ -0,0 +1,169 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/skill_shot_cubit.dart';
/// {@template skill_shot}
/// Rollover awarding extra points.
/// {@endtemplate}
class SkillShot extends BodyComponent with ZIndex {
/// {@macro skill_shot}
SkillShot({Iterable<Component>? children})
: this._(
children: children,
bloc: SkillShotCubit(),
);
SkillShot._({
Iterable<Component>? children,
required this.bloc,
}) : super(
renderBody: false,
children: [
SkillShotBallContactBehavior(),
SkillShotBlinkingBehavior(),
_RolloverDecalSpriteComponent(),
PinSpriteAnimationComponent(),
_TextDecalSpriteGroupComponent(state: bloc.state.spriteState),
...?children,
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [SkillShot] without any children.
///
/// This can be used for testing [SkillShot]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
SkillShot.test({
required this.bloc,
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SkillShotCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
0.1,
3.7,
Vector2(-31.9, 9.1),
0.11,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
class _RolloverDecalSpriteComponent extends SpriteComponent with HasGameRef {
_RolloverDecalSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-31.9, 9.1),
angle: 0.11,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.skillShot.decal.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template pin_sprite_animation_component}
/// Animation for pin in [SkillShot] rollover.
/// {@endtemplate}
@visibleForTesting
class PinSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
/// {@macro pin_sprite_animation_component}
PinSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-31.9, 9.1),
angle: 0,
playing: false,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.skillShot.pin.keyName,
);
const amountPerRow = 3;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
)..onComplete = () {
animation?.reset();
playing = false;
};
}
}
class _TextDecalSpriteGroupComponent
extends SpriteGroupComponent<SkillShotSpriteState>
with HasGameRef, ParentIsA<SkillShot> {
_TextDecalSpriteGroupComponent({
required SkillShotSpriteState state,
}) : super(
anchor: Anchor.center,
position: Vector2(-35.55, 3.59),
current: state,
);
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.spriteState);
final sprites = {
SkillShotSpriteState.lit: Sprite(
gameRef.images.fromCache(Assets.images.skillShot.lit.keyName),
),
SkillShotSpriteState.dimmed: Sprite(
gameRef.images.fromCache(Assets.images.skillShot.dimmed.keyName),
),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -89,6 +89,7 @@ flutter:
- assets/images/score/
- assets/images/backbox/
- assets/images/flapper/
- assets/images/skill_shot/
flutter_gen:
line_length: 80

@ -36,7 +36,7 @@ void main() {
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
initialState: const ChromeDinoState.initial(),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -58,7 +58,7 @@ void main() {
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
initialState: const ChromeDinoState.initial(),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -91,7 +91,7 @@ void main() {
bloc,
const Stream<ChromeDinoState>.empty(),
initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: true),
const ChromeDinoState.initial().copyWith(isMouthOpen: true),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -120,7 +120,7 @@ void main() {
bloc,
const Stream<ChromeDinoState>.empty(),
initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: false),
const ChromeDinoState.initial().copyWith(isMouthOpen: false),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -148,7 +148,7 @@ void main() {
bloc,
const Stream<ChromeDinoState>.empty(),
initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: false),
const ChromeDinoState.initial().copyWith(isMouthOpen: false),
);
final chromeDino = ChromeDino.test(bloc: bloc);

@ -79,7 +79,7 @@ void main() {
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
initialState: const ChromeDinoState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final chromeDino = ChromeDino.test(bloc: bloc);

@ -57,7 +57,7 @@ void main() {
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onChomp emits nothing when the ball is already in the mouth',
build: ChromeDinoCubit.new,
seed: () => const ChromeDinoState.inital().copyWith(ball: ball),
seed: () => const ChromeDinoState.initial().copyWith(ball: ball),
act: (bloc) => bloc.onChomp(ball),
expect: () => <ChromeDinoState>[],
);

@ -36,7 +36,7 @@ void main() {
status: ChromeDinoStatus.idle,
isMouthOpen: false,
);
expect(ChromeDinoState.inital(), equals(initialState));
expect(ChromeDinoState.initial(), equals(initialState));
});
});

@ -0,0 +1,136 @@
// 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:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _TestLayerSensor extends LayerSensor {
_TestLayerSensor({
required LayerEntranceOrientation orientation,
required int insideZIndex,
required Layer insideLayer,
}) : super(
insideLayer: insideLayer,
insideZIndex: insideZIndex,
orientation: orientation,
);
@override
Shape get shape => PolygonShape()..setAsBoxXY(1, 1);
}
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'LayerSensorBehavior',
() {
test('can be instantiated', () {
expect(
LayerFilteringBehavior(),
isA<LayerFilteringBehavior>(),
);
});
flameTester.test(
'loads',
(game) async {
final behavior = LayerFilteringBehavior();
final parent = _TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insideZIndex: 1,
insideLayer: Layer.spaceshipEntranceRamp,
);
await parent.add(behavior);
await game.ensureAdd(parent);
expect(game.contains(parent), isTrue);
},
);
group('beginContact', () {
late Ball ball;
late Body body;
late int insideZIndex;
late Layer insideLayer;
setUp(() {
ball = _MockBall();
body = _MockBody();
insideZIndex = 1;
insideLayer = Layer.spaceshipEntranceRamp;
when(() => ball.body).thenReturn(body);
when(() => ball.layer).thenReturn(Layer.board);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits a downward oriented LayerSensor',
(game) async {
final parent = _TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insideZIndex: 1,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
final behavior = LayerFilteringBehavior();
await parent.add(behavior);
await game.ensureAdd(parent);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insideZIndex).called(1);
when(() => ball.layer).thenReturn(insideLayer);
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits an upward oriented LayerSensor',
(game) async {
final parent = _TestLayerSensor(
orientation: LayerEntranceOrientation.up,
insideZIndex: 1,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
final behavior = LayerFilteringBehavior();
await parent.add(behavior);
await game.ensureAdd(parent);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = 1).called(1);
when(() => ball.layer).thenReturn(insideLayer);
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
});
},
);
}

@ -2,16 +2,10 @@
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_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart';
import '../../helpers/helpers.dart';
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
import '../../../helpers/helpers.dart';
class TestLayerSensor extends LayerSensor {
TestLayerSensor({
@ -112,68 +106,22 @@ void main() {
);
});
});
});
group('beginContact', () {
late Ball ball;
late Body body;
late int insideZIndex;
late Layer insideLayer;
setUp(() {
ball = _MockBall();
body = _MockBody();
insideZIndex = 1;
insideLayer = Layer.spaceshipEntranceRamp;
when(() => ball.body).thenReturn(body);
when(() => ball.layer).thenReturn(Layer.board);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits a downward oriented LayerSensor',
'adds a LayerFilteringBehavior',
(game) async {
final sensor = TestLayerSensor(
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insideZIndex: insidePriority,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insideZIndex).called(1);
when(() => ball.layer).thenReturn(insideLayer);
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits an upward oriented LayerSensor',
(game) async {
final sensor = TestLayerSensor(
orientation: LayerEntranceOrientation.up,
insideZIndex: insidePriority,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insidePriority).called(1);
when(() => ball.layer).thenReturn(insideLayer);
insideLayer: Layer.spaceshipEntranceRamp,
);
await game.ensureAdd(layerSensor);
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
expect(
layerSensor.children.whereType<LayerFilteringBehavior>().length,
equals(1),
);
},
);
});
}

@ -0,0 +1,62 @@
// 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_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SkillShotBallContactBehavior',
() {
test('can be instantiated', () {
expect(
SkillShotBallContactBehavior(),
isA<SkillShotBallContactBehavior>(),
);
});
flameTester.testGameWidget(
'beginContact animates pin and calls onBallContacted '
'when contacts with a ball',
setUp: (game, tester) async {
await game.images.load(Assets.images.skillShot.pin.keyName);
final behavior = SkillShotBallContactBehavior();
final bloc = _MockSkillShotCubit();
whenListen(
bloc,
const Stream<SkillShotState>.empty(),
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.addAll([behavior, PinSpriteAnimationComponent()]);
await game.ensureAdd(skillShot);
behavior.beginContact(_MockBall(), _MockContact());
await tester.pump();
expect(
skillShot.firstChild<PinSpriteAnimationComponent>()!.playing,
isTrue,
);
verify(skillShot.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -0,0 +1,125 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SkillShotBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls switched after 0.15 seconds when isBlinking and lit',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
await tester.pump();
game.update(0.15);
await streamController.close();
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls switched after 0.15 seconds when isBlinking and dimmed',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: true,
),
);
await tester.pump();
game.update(0.15);
await streamController.close();
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls onBlinkingFinished after all blinks complete',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
for (var i = 0; i <= 8; i++) {
if (i.isEven) {
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
} else {
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: true,
),
);
}
await tester.pump();
game.update(0.15);
}
await streamController.close();
verify(bloc.onBlinkingFinished).called(1);
},
);
},
);
}

@ -0,0 +1,66 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'SkillShotCubit',
() {
blocTest<SkillShotCubit, SkillShotState>(
'onBallContacted emits lit and true',
build: SkillShotCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [
SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
],
);
blocTest<SkillShotCubit, SkillShotState>(
'switched emits lit when dimmed',
build: SkillShotCubit.new,
act: (bloc) => bloc.switched(),
expect: () => [
isA<SkillShotState>().having(
(state) => state.spriteState,
'spriteState',
SkillShotSpriteState.lit,
)
],
);
blocTest<SkillShotCubit, SkillShotState>(
'switched emits dimmed when lit',
build: SkillShotCubit.new,
seed: () => SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: false,
),
act: (bloc) => bloc.switched(),
expect: () => [
isA<SkillShotState>().having(
(state) => state.spriteState,
'spriteState',
SkillShotSpriteState.dimmed,
)
],
);
blocTest<SkillShotCubit, SkillShotState>(
'onBlinkingFinished emits dimmed and false',
build: SkillShotCubit.new,
act: (bloc) => bloc.onBlinkingFinished(),
expect: () => [
SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
],
);
},
);
}

@ -0,0 +1,84 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('SkillShotState', () {
test('supports value equality', () {
expect(
SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
equals(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
isNotNull,
);
});
test('initial is idle with mouth closed', () {
const initialState = SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
expect(SkillShotState.initial(), equals(initialState));
});
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified',
() {
const chromeDinoState = SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
);
expect(
chromeDinoState.copyWith(),
equals(chromeDinoState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
const chromeDinoState = SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
);
final otherSkillShotState = SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
expect(chromeDinoState, isNot(equals(otherSkillShotState)));
expect(
chromeDinoState.copyWith(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
equals(otherSkillShotState),
);
},
);
});
});
}

@ -0,0 +1,99 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.skillShot.decal.keyName,
Assets.images.skillShot.pin.keyName,
Assets.images.skillShot.lit.keyName,
Assets.images.skillShot.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('SkillShot', () {
flameTester.test('loads correctly', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(game.contains(skillShot), isTrue);
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSkillShotCubit();
whenListen(
bloc,
const Stream<SkillShotState>.empty(),
initialState: const SkillShotState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final skillShot = SkillShot.test(bloc: bloc);
await game.ensureAdd(skillShot);
game.remove(skillShot);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final skillShot = SkillShot(
children: [component],
);
await game.ensureAdd(skillShot);
expect(skillShot.children, contains(component));
});
flameTester.test('a SkillShotBallContactBehavior', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(
skillShot.children.whereType<SkillShotBallContactBehavior>().single,
isNotNull,
);
});
flameTester.test('a SkillShotBlinkingBehavior', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(
skillShot.children.whereType<SkillShotBlinkingBehavior>().single,
isNotNull,
);
});
});
flameTester.test(
'pin stops animating after animation completes',
(game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
final pinSpriteAnimationComponent =
skillShot.firstChild<PinSpriteAnimationComponent>()!;
pinSpriteAnimationComponent.playing = true;
game.update(
pinSpriteAnimationComponent.animation!.totalDuration() + 0.1,
);
expect(pinSpriteAnimationComponent.playing, isFalse);
},
);
});
}

@ -139,6 +139,10 @@ void main() {
Assets.images.flapper.flap.keyName,
Assets.images.flapper.backSupport.keyName,
Assets.images.flapper.frontSupport.keyName,
Assets.images.skillShot.decal.keyName,
Assets.images.skillShot.pin.keyName,
Assets.images.skillShot.lit.keyName,
Assets.images.skillShot.dimmed.keyName,
];
late GameBloc gameBloc;
@ -198,13 +202,16 @@ void main() {
},
);
flameBlocTester.test('has one FlutterForest', (game) async {
flameBlocTester.test(
'has one FlutterForest',
(game) async {
await game.ready();
expect(
game.descendants().whereType<FlutterForest>().length,
equals(1),
);
});
},
);
flameBlocTester.test(
'has only one Multiballs',
@ -229,6 +236,17 @@ void main() {
},
);
flameBlocTester.test(
'one SkillShot',
(game) async {
await game.ready();
expect(
game.descendants().whereType<SkillShot>().length,
equals(1),
);
},
);
group('controller', () {
group('listenWhen', () {
flameTester.testGameWidget(

Loading…
Cancel
Save