feat: `SpaceshipRamp` shot logic (#296)

* feat: spaceship ramp added cubit and behavior to sensor

* refactor: changed spaceshipt ramp sensor, cubit and behavior names

* refactor: added behaviors to AndroidAcres

* refactor: connect rampsensors with android acres bonus

* refactor: move ramp sensor to spaceship ramp children

* test: fixed some tests for ramp

* chore: removed unused imports

* chore: removed unused import

* Update lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* refactor: search sensors from parent children

* refactor: moved ramp sensor cubit to spaceship ramp

* refactor: modified ramp behaviors

* refactor: fixed ramp behaviors tests

* refactor: changed ramp behaviors

* chore: analysis errors

* test: fixed ramp contact test

* test: coverage for spaceshipramp

* Update packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* refactor: fixed test when removing children from spaceship test constructor

* refactor: moved arrow state to cubit inside ramp instead of propagate from behavior

* refactor: sandbox for spaceshipramp modified

* refactor: removed arrow value from spaceship ramp state to sprite logic

* test: golden tests for ramp arrow

* test: coverage

* test: coverage

* refactor: changed name for RampBallAscendingContactBehavior

* refactor: added ScoringBehavior on shot and bonus behaviors

* feat: cancel subscription on ramp behavior remove

* chore: unused import

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
pull/338/head
Rui Miguel Alonso 3 years ago committed by GitHub
parent 03f60fbffe
commit 79624f07f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,7 +15,16 @@ class AndroidAcres extends Component {
AndroidAcres() AndroidAcres()
: super( : super(
children: [ children: [
SpaceshipRamp(), SpaceshipRamp(
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
),
],
),
SpaceshipRail(), SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic( AndroidAnimatronic(

@ -1 +1,3 @@
export 'android_spaceship_bonus_behavior.dart'; export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart';
export 'ramp_shot_behavior.dart';

@ -0,0 +1,62 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate}
class RampBonusBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_bonus_behavior}
RampBonusBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampBonusBehavior].
///
/// This can be used for testing [RampBonusBehavior] in isolation.
@visibleForTesting
RampBonusBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (achievedOneMillionPoints) {
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_shot_behavior}
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate}
class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_shot_behavior}
RampShotBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampShotBehavior].
///
/// This can be used for testing [RampShotBehavior] in isolation.
@visibleForTesting
RampShotBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) {
gameRef.read<GameBloc>().add(const MultiplierIncreased());
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -31,7 +31,7 @@ export 'shapes/shapes.dart';
export 'signpost/signpost.dart'; export 'signpost/signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart'; export 'sparky_computer.dart';

@ -0,0 +1,24 @@
// 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';
/// {@template ramp_ball_ascending_contact_behavior}
/// Detects an ascending [Ball] that enters into the [SpaceshipRamp].
///
/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of
/// the [SpaceshipRamp].
/// {@endtemplate}
class RampBallAscendingContactBehavior
extends ContactBehavior<RampScoringSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.body.linearVelocity.y < 0) {
parent.parent.bloc.onAscendingBallEntered();
}
}
}

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'spaceship_ramp_state.dart';
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
void onAscendingBallEntered() {
emit(
state.copyWith(hits: state.hits + 1),
);
}
}

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
part of 'spaceship_ramp_cubit.dart';
class SpaceshipRampState extends Equatable {
const SpaceshipRampState({
required this.hits,
}) : assert(hits >= 0, "Hits can't be negative");
const SpaceshipRampState.initial() : this(hits: 0);
final int hits;
SpaceshipRampState copyWith({
int? hits,
}) {
return SpaceshipRampState(
hits: hits ?? this.hits,
);
}
@override
List<Object?> get props => [hits];
}

@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/spaceship_ramp_cubit.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// Ramp leading into the [AndroidSpaceship]. /// Ramp leading into the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRamp extends Component { class SpaceshipRamp extends Component {
/// {@macro spaceship_ramp} /// {@macro spaceship_ramp}
SpaceshipRamp() SpaceshipRamp({
: super( Iterable<Component>? children,
}) : this._(
children: children,
bloc: SpaceshipRampCubit(),
);
SpaceshipRamp._({
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [ children: [
// TODO(ruimiguel): refactor RampScoringSensor and
// _SpaceshipRampOpening to be in only one sensor if possible.
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening( _SpaceshipRampOpening(
outsidePriority: ZIndexes.ballOnBoard, outsidePriority: ZIndexes.ballOnBoard,
rotation: math.pi, rotation: math.pi,
@ -34,60 +53,30 @@ class SpaceshipRamp extends Component {
_SpaceshipRampForegroundRailing(), _SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20),
_SpaceshipRampBackgroundRailingSpriteComponent(), _SpaceshipRampBackgroundRailingSpriteComponent(),
_SpaceshipRampArrowSpriteComponent(), SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
),
...?children,
], ],
); );
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. /// Creates a [SpaceshipRamp] without any children.
/// ///
/// If the current state is the last one it cycles back to the initial state. /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation.
void progress() => @visibleForTesting
firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); SpaceshipRamp.test({
} required this.bloc,
}) : super();
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState { // TODO(alestiago): Consider refactoring once the following is merged:
String get path { // https://github.com/flame-engine/flame/pull/1538
switch (this) { // ignore: public_member_api_docs
case SpaceshipRampArrowSpriteState.inactive: final SpaceshipRampCubit bloc;
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
SpaceshipRampArrowSpriteState get next { @override
return SpaceshipRampArrowSpriteState void onRemove() {
.values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; bloc.close();
super.onRemove();
} }
} }
@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
/// ///
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class _SpaceshipRampArrowSpriteComponent @visibleForTesting
extends SpriteGroupComponent<SpaceshipRampArrowSpriteState> class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent<int>
with HasGameRef, ZIndex { with HasGameRef, ParentIsA<SpaceshipRamp>, ZIndex {
/// {@macro spaceship_ramp_arrow_sprite_component} /// {@macro spaceship_ramp_arrow_sprite_component}
_SpaceshipRampArrowSpriteComponent() SpaceshipRampArrowSpriteComponent({
: super( required int current,
}) : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-3.9, -56.5), position: Vector2(-3.9, -56.5),
current: current,
) { ) {
zIndex = ZIndexes.spaceshipRampArrow; zIndex = ZIndexes.spaceshipRampArrow;
} }
/// Changes arrow image to the next [Sprite].
void progress() => current = current?.next;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprites = <SpaceshipRampArrowSpriteState, Sprite>{}; parent.bloc.stream.listen((state) {
current = state.hits % SpaceshipRampArrowSpriteState.values.length;
});
final sprites = <int, Sprite>{};
this.sprites = sprites; this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) { for (final spriteState in SpaceshipRampArrowSpriteState.values) {
sprites[spriteState] = Sprite( sprites[spriteState.index] = Sprite(
gameRef.images.fromCache(spriteState.path), gameRef.images.fromCache(spriteState.path),
); );
} }
current = SpaceshipRampArrowSpriteState.inactive; current = 0;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
}
class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) {
@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor {
); );
} }
} }
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
///
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -54,7 +54,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
) { ) {
if (event is RawKeyDownEvent && if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) { event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.progress(); _spaceshipRamp.bloc.onAscendingBallEntered();
return KeyEventResult.handled; return KeyEventResult.handled;
} }

@ -0,0 +1,117 @@
// 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/spaceship_ramp/behavior/behavior.dart';
import '../../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group(
'RampBallAscendingContactBehavior',
() {
test('can be instantiated', () {
expect(
RampBallAscendingContactBehavior(),
isA<RampBallAscendingContactBehavior>(),
);
});
group('beginContact', () {
late Ball ball;
late Body body;
setUp(() {
ball = _MockBall();
body = _MockBody();
when(() => ball.body).thenReturn(body);
});
flameTester.test(
"calls 'onAscendingBallEntered' when a ball enters into the ramp",
(game) async {
final behavior = RampBallAscendingContactBehavior();
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
await spaceshipRamp.add(rampSensor);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
behavior.beginContact(ball, _MockContact());
verify(bloc.onAscendingBallEntered).called(1);
},
);
flameTester.test(
"doesn't call 'onAscendingBallEntered' when a ball goes out the ramp",
(game) async {
final behavior = RampBallAscendingContactBehavior();
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
await spaceshipRamp.add(rampSensor);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
behavior.beginContact(ball, _MockContact());
verifyNever(bloc.onAscendingBallEntered);
},
);
});
},
);
}

@ -0,0 +1,25 @@
// 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('SpaceshipRampCubit', () {
group('onAscendingBallEntered', () {
blocTest<SpaceshipRampCubit, SpaceshipRampState>(
'emits hits incremented and arrow goes to the next value',
build: SpaceshipRampCubit.new,
act: (bloc) => bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered(),
expect: () => [
SpaceshipRampState(hits: 1),
SpaceshipRampState(hits: 2),
SpaceshipRampState(hits: 3),
],
);
});
});
}

@ -0,0 +1,78 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
void main() {
group('SpaceshipRampState', () {
test('supports value equality', () {
expect(
SpaceshipRampState(hits: 0),
equals(
SpaceshipRampState(hits: 0),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
SpaceshipRampState(hits: 0),
isNotNull,
);
});
});
test(
'throws AssertionError '
'when hits is negative',
() {
expect(
() => SpaceshipRampState(hits: -1),
throwsAssertionError,
);
},
);
group('copyWith', () {
test(
'throws AssertionError '
'when hits is decreased',
() {
const rampState = SpaceshipRampState(hits: 0);
expect(
() => rampState.copyWith(hits: rampState.hits - 1),
throwsAssertionError,
);
},
);
test(
'copies correctly '
'when no argument specified',
() {
const rampState = SpaceshipRampState(hits: 0);
expect(
rampState.copyWith(),
equals(rampState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
const rampState = SpaceshipRampState(hits: 0);
final otherRampState = SpaceshipRampState(hits: rampState.hits + 1);
expect(rampState, isNot(equals(otherRampState)));
expect(
rampState.copyWith(hits: rampState.hits + 1),
equals(otherRampState),
);
},
);
});
});
}

@ -1,12 +1,16 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.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:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -25,28 +29,35 @@ void main() {
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
group('SpaceshipRamp', () { group('SpaceshipRamp', () {
flameTester.test('loads correctly', (game) async { flameTester.test(
final component = SpaceshipRamp(); 'loads correctly',
await game.ensureAdd(component); (game) async {
expect(game.contains(component), isTrue); final spaceshipRamp = SpaceshipRamp();
}); await game.ensureAdd(spaceshipRamp);
expect(game.children, contains(spaceshipRamp));
},
);
group('renders correctly', () { group('renders correctly', () {
const goldenFilePath = 'golden/spaceship_ramp/'; const goldenFilePath = '../golden/spaceship_ramp/';
final centerForSpaceshipRamp = Vector2(-13, -55); final centerForSpaceshipRamp = Vector2(-13, -55);
flameTester.testGameWidget( flameTester.testGameWidget(
'inactive sprite', 'inactive sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.inactive, SpaceshipRampArrowSpriteState.inactive,
); );
@ -64,15 +75,21 @@ void main() {
'active1 sprite', 'active1 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component.progress(); ramp.bloc.onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active1, SpaceshipRampArrowSpriteState.active1,
); );
@ -90,17 +107,23 @@ void main() {
'active2 sprite', 'active2 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active2, SpaceshipRampArrowSpriteState.active2,
); );
@ -118,18 +141,24 @@ void main() {
'active3 sprite', 'active3 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active3, SpaceshipRampArrowSpriteState.active3,
); );
@ -147,19 +176,25 @@ void main() {
'active4 sprite', 'active4 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active4, SpaceshipRampArrowSpriteState.active4,
); );
@ -177,20 +212,26 @@ void main() {
'active5 sprite', 'active5 sprite',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.images.loadAll(assets); await game.images.loadAll(assets);
final component = SpaceshipRamp(); final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]); final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas); await game.ensureAdd(canvas);
component ramp.bloc
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress() ..onAscendingBallEntered()
..progress(); ..onAscendingBallEntered();
await game.ready();
await tester.pump(); await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect( expect(
component.children.whereType<SpriteGroupComponent>().first.current, SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active5, SpaceshipRampArrowSpriteState.active5,
); );
@ -204,5 +245,34 @@ void main() {
}, },
); );
}); });
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final ramp = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ramp);
game.remove(ramp);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final ramp = SpaceshipRamp(children: [component]);
await game.ensureAdd(ramp);
expect(ramp.children, contains(component));
});
});
}); });
} }

@ -0,0 +1,152 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
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/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.oneMillion.keyName,
];
group('RampBonusBehavior', () {
const shotPoints = Points.oneMillion;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget(
'when hits are multiples of 10 times adds a ScoringBehavior',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
expect(scores.length, 1);
},
);
flameBlocTester.testGameWidget(
"when hits are not multiple of 10 times doesn't add any ScoringBehavior",
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
expect(scores.length, 0);
},
);
flameBlocTester.testGameWidget(
'closes subscription when removed',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampBonusBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -0,0 +1,156 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
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/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.fiveThousand.keyName,
];
group('RampShotBehavior', () {
const shotPoints = Points.fiveThousand;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget(
'when hits are not multiple of 10 times '
'increases multiplier and adds a ScoringBehavior',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
verify(() => gameBloc.add(MultiplierIncreased())).called(1);
expect(scores.length, 1);
},
);
flameBlocTester.testGameWidget(
'when hits multiple of 10 times '
"doesn't increase multiplier, neither ScoringBehavior",
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.children.whereType<ScoringBehavior>();
await game.ready();
verifyNever(() => gameBloc.add(MultiplierIncreased()));
expect(scores.length, 0);
},
);
flameBlocTester.testGameWidget(
'closes subscription when removed',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampShotBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -513,8 +513,11 @@ void main() {
game.onTapUp(0, tapUpEvent); game.onTapUp(0, tapUpEvent);
await game.ready(); await game.ready();
final currentBalls =
game.descendants().whereType<ControlledBall>().toList();
expect( expect(
game.descendants().whereType<ControlledBall>().length, currentBalls.length,
equals(previousBalls.length + 1), equals(previousBalls.length + 1),
); );
}, },

Loading…
Cancel
Save