mirror of https://github.com/flutter/pinball.git
feat: implemented `SkillShot` (#337)
* feat: add skill shot * fix: unused import * refactor: switched method * style: parameter orderpull/344/head
parent
0595e82649
commit
155e316ba1
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue