feat: multiball asset (#235)

* feat: multiball assets

* feat: added multiball to components

* feat: added controller for multiball

* feat: positioned multiball and changed animation

* feat: added sandbox for multiball

* chore: unused import

* refactor: add rotation to multiball constructor

* test: coverage for multiball

* chore: todos for refactor multiball childrens

* test: removed unused mock

* chore: removed unused imports

* test: removed golden tests

* refactor: changed assets and refactored multiball

* refactor: changed assets and refactored multiball

* test: tests for multiball

* refactor: multiballs group refactored

* chore: names and doc

* refactor: removed duplicated images for multiball

* refactor: changed multiball cubit and state

* refactor: changed multiball and group

* chore: positions of lights

* refactor: changing blink behavior

* test: blink behavior

* refactor: blinking multiball lights

* test: tests for blink behavior

* chore: analysis errors

* test: coverage for blinking

* test: coverage

* test: trying to fix tests

* fix: fixed bloc error on behavior with tests

* refactor: multiball blink

* refactor: blinking behavior to TimerComponent and test coverage

* refactor: modified blinking behavior

* chore: error on merge tests

* test: coverage multiballs

* refactor: cleaned blink behavior

* chore: unused import

* Update packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/multiballs/behaviors/multiballs_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update packages/pinball_components/test/src/components/multiball/multiball_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: changed multiball states enum values

* test: multiball descendant test at pinball

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/293/head
Rui Miguel Alonso 2 years ago committed by GitHub
parent 26acb63460
commit 3a593c2e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,6 +10,7 @@ export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word/google_word.dart';
export 'launcher.dart';
export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart';
export 'sparky_scorch.dart';

@ -0,0 +1 @@
export 'multiballs_behavior.dart';

@ -0,0 +1,28 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiball] when there is a bonus ball.
class MultiballsBehavior extends Component
with
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
newState.bonusHistory.last == GameBonus.dashNest;
return hasChanged && lastBonusIsMultiball;
}
@override
void onNewState(GameState state) {
parent.children.whereType<Multiball>().forEach((multiball) {
multiball.bloc.onAnimate();
});
}
}

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiballs_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
class Multiballs extends Component with ZIndex {
/// {@macro multiballs_component}
Multiballs()
: super(
children: [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
MultiballsBehavior(),
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [Multiballs] without any children.
///
/// This can be used for testing [Multiballs]'s behaviors in isolation.
@visibleForTesting
Multiballs.test();
}

@ -114,6 +114,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.lit.keyName),

@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
];
final characterAreas = [
AndroidAcres(),

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -28,6 +28,7 @@ class $AssetsImagesGen {
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen();
$AssetsImagesMultiballGen get multiball => const $AssetsImagesMultiballGen();
$AssetsImagesMultiplierGen get multiplier =>
const $AssetsImagesMultiplierGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
@ -180,6 +181,18 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png');
}
class $AssetsImagesMultiballGen {
const $AssetsImagesMultiballGen();
/// File path: assets/images/multiball/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiball/dimmed.png');
/// File path: assets/images/multiball/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiball/lit.png');
}
class $AssetsImagesMultiplierGen {
const $AssetsImagesMultiplierGen();

@ -21,6 +21,7 @@ export 'kicker/kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
export 'rocket.dart';

@ -0,0 +1,78 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiball_blinking_behavior}
/// Makes a [Multiball] blink back to [MultiballLightState.lit] when
/// [MultiballLightState.dimmed].
/// {@endtemplate}
class MultiballBlinkingBehavior extends TimerComponent
with ParentIsA<Multiball> {
/// {@macro multiball_blinking_behavior}
MultiballBlinkingBehavior() : super(period: 0.1);
final _maxBlinks = 10;
int _blinksCounter = 0;
bool _isAnimating = false;
void _onNewState(MultiballState state) {
final animationEnabled =
state.animationState == MultiballAnimationState.blinking;
final canBlink = _blinksCounter < _maxBlinks;
if (animationEnabled && canBlink) {
_start();
} else {
_stop();
}
}
void _start() {
if (!_isAnimating) {
_isAnimating = true;
timer
..reset()
..start();
_animate();
}
}
void _animate() {
parent.bloc.onBlink();
_blinksCounter++;
}
void _stop() {
if (_isAnimating) {
_isAnimating = false;
timer.stop();
_blinksCounter = 0;
parent.bloc.onStop();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (!_isAnimating) {
timer.stop();
} else {
if (_blinksCounter < _maxBlinks) {
_animate();
timer
..reset()
..start();
} else {
timer.stop();
}
}
}
}

@ -0,0 +1,37 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'multiball_state.dart';
class MultiballCubit extends Cubit<MultiballState> {
MultiballCubit() : super(const MultiballState.initial());
void onAnimate() {
emit(
state.copyWith(animationState: MultiballAnimationState.blinking),
);
}
void onStop() {
emit(
state.copyWith(animationState: MultiballAnimationState.idle),
);
}
void onBlink() {
switch (state.lightState) {
case MultiballLightState.lit:
emit(
state.copyWith(lightState: MultiballLightState.dimmed),
);
break;
case MultiballLightState.dimmed:
emit(
state.copyWith(lightState: MultiballLightState.lit),
);
break;
}
}
}

@ -0,0 +1,44 @@
// ignore_for_file: comment_references, public_member_api_docs
part of 'multiball_cubit.dart';
/// Indicates the different sprite states for [MultiballSpriteGroupComponent].
enum MultiballLightState {
lit,
dimmed,
}
// Indicates if the blinking animation is running.
enum MultiballAnimationState {
idle,
blinking,
}
class MultiballState extends Equatable {
const MultiballState({
required this.lightState,
required this.animationState,
});
const MultiballState.initial()
: this(
lightState: MultiballLightState.dimmed,
animationState: MultiballAnimationState.idle,
);
final MultiballLightState lightState;
final MultiballAnimationState animationState;
MultiballState copyWith({
MultiballLightState? lightState,
MultiballAnimationState? animationState,
}) {
return MultiballState(
lightState: lightState ?? this.lightState,
animationState: animationState ?? this.animationState,
);
}
@override
List<Object> get props => [lightState, animationState];
}

@ -0,0 +1,138 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import 'package:pinball_components/src/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/multiball_cubit.dart';
/// {@template multiball}
/// A [Component] for the multiball lighting decals on the board.
/// {@endtemplate}
class Multiball extends Component {
/// {@macro multiball}
Multiball._({
required Vector2 position,
double rotation = 0,
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [
MultiballBlinkingBehavior(),
MultiballSpriteGroupComponent(
position: position,
litAssetPath: Assets.images.multiball.lit.keyName,
dimmedAssetPath: Assets.images.multiball.dimmed.keyName,
rotation: rotation,
state: bloc.state.lightState,
),
...?children,
],
);
/// {@macro multiball}
Multiball.a({
Iterable<Component>? children,
}) : this._(
position: Vector2(-23, 7.5),
rotation: -24 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.b({
Iterable<Component>? children,
}) : this._(
position: Vector2(-7.2, -6.2),
rotation: -5 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.c({
Iterable<Component>? children,
}) : this._(
position: Vector2(-0.7, -9.3),
rotation: 2.7 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// {@macro multiball}
Multiball.d({
Iterable<Component>? children,
}) : this._(
position: Vector2(15, 7),
rotation: 24 * math.pi / 180,
bloc: MultiballCubit(),
children: children,
);
/// Creates an [Multiball] without any children.
///
/// This can be used for testing [Multiball]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
Multiball.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 MultiballCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
/// {@template multiball_sprite_group_component}
/// A [SpriteGroupComponent] for the multiball over the board.
/// {@endtemplate}
@visibleForTesting
class MultiballSpriteGroupComponent
extends SpriteGroupComponent<MultiballLightState>
with HasGameRef, ParentIsA<Multiball> {
/// {@macro multiball_sprite_group_component}
MultiballSpriteGroupComponent({
required Vector2 position,
required String litAssetPath,
required String dimmedAssetPath,
required double rotation,
required MultiballLightState state,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
anchor: Anchor.center,
position: position,
angle: rotation,
current: state,
);
final String _litAssetPath;
final String _dimmedAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.lightState);
final sprites = {
MultiballLightState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
MultiballLightState.dimmed:
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -81,6 +81,7 @@ flutter:
- assets/images/google_word/letter5/
- assets/images/google_word/letter6/
- assets/images/signpost/
- assets/images/multiball/
- assets/images/multiplier/x2/
- assets/images/multiplier/x3/
- assets/images/multiplier/x4/

@ -27,6 +27,7 @@ void main() {
addScoreStories(dashbook);
addBackboardStories(dashbook);
addDinoWallStories(dashbook);
addMultiballStories(dashbook);
addMultipliersStories(dashbook);
runApp(dashbook);

@ -0,0 +1,56 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class MultiballGame extends BallGame with KeyboardEvents {
MultiballGame()
: super(
imagesFileNames: [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
],
);
static const description = '''
Shows how the Multiball are rendered.
- Tap anywhere on the screen to spawn a ball into the game.
- Press space bar to animate multiballs.
''';
final List<Multiball> multiballs = [
Multiball.a(),
Multiball.b(),
Multiball.c(),
Multiball.d(),
];
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addAll(multiballs);
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
for (final multiball in multiballs) {
multiball.bloc.onBlink();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}

@ -0,0 +1,11 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/multiball/multiball_game.dart';
void addMultiballStories(Dashbook dashbook) {
dashbook.storiesOf('Multiball').addGame(
title: 'Assets',
description: MultiballGame.description,
gameBuilder: (_) => MultiballGame(),
);
}

@ -10,6 +10,7 @@ export 'flutter_forest/stories.dart';
export 'google_word/stories.dart';
export 'launch_ramp/stories.dart';
export 'layer/stories.dart';
export 'multiball/stories.dart';
export 'multipliers/stories.dart';
export 'plunger/stories.dart';
export 'score/stories.dart';

@ -25,6 +25,8 @@ class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}
class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}
class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}

@ -0,0 +1,158 @@
// ignore_for_file: prefer_const_constructors, 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/multiball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'MultiballBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlink every 0.1 seconds when animation state is animated',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
game.update(0);
verify(bloc.onBlink).called(1);
await tester.pump();
game.update(0.1);
await streamController.close();
verify(bloc.onBlink).called(1);
},
);
flameTester.testGameWidget(
'calls onStop when animation state is stopped',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await streamController.close();
verify(bloc.onStop).called(1);
},
);
flameTester.testGameWidget(
'onTick stops when there is no animation',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
behavior.onTick();
expect(behavior.timer.isRunning(), false);
},
);
flameTester.testGameWidget(
'onTick stops after 10 blinks repetitions',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.dimmed,
),
);
await tester.pump();
for (var i = 0; i < 10; i++) {
behavior.onTick();
}
expect(behavior.timer.isRunning(), false);
},
);
},
);
}

@ -0,0 +1,67 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'MultiballCubit',
() {
blocTest<MultiballCubit, MultiballState>(
'onAnimate emits animationState [animate]',
build: MultiballCubit.new,
act: (bloc) => bloc.onAnimate(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.blinking,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onStop emits animationState [stopped]',
build: MultiballCubit.new,
act: (bloc) => bloc.onStop(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.idle,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onBlink emits lightState [lit, dimmed, lit]',
build: MultiballCubit.new,
act: (bloc) => bloc
..onBlink()
..onBlink()
..onBlink(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.lit,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.dimmed,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
'lightState',
MultiballLightState.lit,
)
],
);
},
);
}

@ -0,0 +1,76 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/pinball_components.dart';
void main() {
group('MultiballState', () {
test('supports value equality', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
equals(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
),
isNotNull,
);
});
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
);
expect(
multiballState.copyWith(),
equals(multiballState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
);
final otherMultiballState = MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
);
expect(multiballState, isNot(equals(otherMultiballState)));
expect(
multiballState.copyWith(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
equals(otherMultiballState),
);
},
);
});
});
}

@ -0,0 +1,90 @@
// 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/multiball/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('Multiball', () {
group('loads correctly', () {
flameTester.test('"a"', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"b"', (game) async {
final multiball = Multiball.b();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"c"', (game) async {
final multiball = Multiball.c();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
flameTester.test('"d"', (game) async {
final multiball = Multiball.d();
await game.ensureAdd(multiball);
expect(game.contains(multiball), isTrue);
});
});
flameTester.test(
'closes bloc when removed',
(game) async {
final bloc = MockMultiballCubit();
whenListen(
bloc,
const Stream<MultiballLightState>.empty(),
initialState: MultiballLightState.dimmed,
);
when(bloc.close).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await game.ensureAdd(multiball);
game.remove(multiball);
await game.ready();
verify(bloc.close).called(1);
},
);
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final multiball = Multiball.a(
children: [component],
);
await game.ensureAdd(multiball);
expect(multiball.children, contains(component));
});
flameTester.test('a MultiballBlinkingBehavior', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(
multiball.children.whereType<MultiballBlinkingBehavior>().single,
isNotNull,
);
});
});
});
}

@ -0,0 +1,136 @@
// 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/components/multiballs/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
group('MultiballsBehavior', () {
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,
);
group('listenWhen', () {
test(
'is true when the bonusHistory has changed '
'with a new GameBonus.dashNest', () {
final previous = GameState.initial();
final state = previous.copyWith(
bonusHistory: [GameBonus.dashNest],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isTrue,
);
});
test(
'is false when the bonusHistory has changed '
'with a bonus different than GameBonus.dashNest', () {
final previous =
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]);
final state = previous.copyWith(
bonusHistory: [...previous.bonusHistory, GameBonus.androidSpaceship],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isFalse,
);
});
test('is false when the bonusHistory state is the same', () {
final previous = GameState.initial();
final state = GameState(
score: 10,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isFalse,
);
});
});
group('onNewState', () {
flameBlocTester.testGameWidget(
"calls 'onAnimate' once for every multiball",
setUp: (game, tester) async {
final behavior = MultiballsBehavior();
final parent = Multiballs.test();
final multiballCubit = MockMultiballCubit();
final otherMultiballCubit = MockMultiballCubit();
final multiballs = [
Multiball.test(
bloc: multiballCubit,
),
Multiball.test(
bloc: otherMultiballCubit,
),
];
whenListen(
multiballCubit,
const Stream<MultiballState>.empty(),
initialState: MultiballState.initial(),
);
when(multiballCubit.onAnimate).thenAnswer((_) async {});
whenListen(
otherMultiballCubit,
const Stream<MultiballState>.empty(),
initialState: MultiballState.initial(),
);
when(otherMultiballCubit.onAnimate).thenAnswer((_) async {});
await parent.addAll(multiballs);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
await tester.pump();
behavior.onNewState(
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]),
);
for (final multiball in multiballs) {
verify(
multiball.bloc.onAnimate,
).called(1);
}
},
);
});
});
}

@ -0,0 +1,54 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
];
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('Multiballs', () {
flameBlocTester.testGameWidget(
'loads correctly',
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.ensureAdd(multiballs);
expect(game.contains(multiballs), isTrue);
},
);
group('loads', () {
flameBlocTester.testGameWidget(
'four Multiball',
setUp: (game, tester) async {
final multiballs = Multiballs();
await game.ensureAdd(multiballs);
expect(
multiballs.descendants().whereType<Multiball>().length,
equals(4),
);
},
);
});
});
}

@ -64,6 +64,8 @@ void main() {
Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.foregroundRailing.keyName,
Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.multiball.lit.keyName,
Assets.images.multiball.dimmed.keyName,
Assets.images.multiplier.x2.lit.keyName,
Assets.images.multiplier.x2.dimmed.keyName,
Assets.images.multiplier.x3.lit.keyName,
@ -178,6 +180,18 @@ void main() {
);
});
flameBlocTester.test(
'has only one Multiballs',
(game) async {
await game.ready();
expect(
game.descendants().whereType<Multiballs>().length,
equals(1),
);
},
);
flameBlocTester.test(
'one GoogleWord',
(game) async {

@ -95,9 +95,7 @@ class MockAndroidBumper extends Mock implements AndroidBumper {}
class MockSparkyBumper extends Mock implements SparkyBumper {}
class MockMultiplier extends Mock implements Multiplier {}
class MockMultipliersGroup extends Mock implements Multipliers {}
class MockMultiballCubit extends Mock implements MultiballCubit {}
class MockMultiplierCubit extends Mock implements MultiplierCubit {}

Loading…
Cancel
Save