From 3a593c2e6e4ddbe33f9c8597349ee08b0c572158 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Mon, 2 May 2022 16:51:35 +0200 Subject: [PATCH] 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> --- lib/game/components/components.dart | 1 + .../multiballs/behaviors/behaviors.dart | 1 + .../behaviors/multiballs_behavior.dart | 28 ++++ .../components/multiballs/multiballs.dart | 30 ++++ lib/game/game_assets.dart | 2 + lib/game/pinball_game.dart | 1 + .../assets/images/multiball/dimmed.png | Bin 0 -> 14979 bytes .../assets/images/multiball/lit.png | Bin 0 -> 15893 bytes .../lib/gen/assets.gen.dart | 13 ++ .../lib/src/components/components.dart | 1 + .../multiball/behaviors/behaviors.dart | 1 + .../multiball_blinking_behavior.dart | 78 +++++++++ .../multiball/cubit/multiball_cubit.dart | 37 ++++ .../multiball/cubit/multiball_state.dart | 44 +++++ .../src/components/multiball/multiball.dart | 138 +++++++++++++++ packages/pinball_components/pubspec.yaml | 1 + .../pinball_components/sandbox/lib/main.dart | 1 + .../lib/stories/multiball/multiball_game.dart | 56 +++++++ .../lib/stories/multiball/stories.dart | 11 ++ .../sandbox/lib/stories/stories.dart | 1 + .../test/helpers/mocks.dart | 2 + .../multiball_blinking_behavior_test.dart | 158 ++++++++++++++++++ .../multiball/cubit/multiball_cubit_test.dart | 67 ++++++++ .../multiball/cubit/multiball_state_test.dart | 76 +++++++++ .../components/multiball/multiball_test.dart | 90 ++++++++++ .../behaviors/multiballs_behavior_test.dart | 136 +++++++++++++++ .../multiballs/multiballs_test.dart | 54 ++++++ test/game/pinball_game_test.dart | 14 ++ test/helpers/mocks.dart | 4 +- 29 files changed, 1043 insertions(+), 3 deletions(-) create mode 100644 lib/game/components/multiballs/behaviors/behaviors.dart create mode 100644 lib/game/components/multiballs/behaviors/multiballs_behavior.dart create mode 100644 lib/game/components/multiballs/multiballs.dart create mode 100644 packages/pinball_components/assets/images/multiball/dimmed.png create mode 100644 packages/pinball_components/assets/images/multiball/lit.png create mode 100644 packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart create mode 100644 packages/pinball_components/lib/src/components/multiball/multiball.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/multiball/stories.dart create mode 100644 packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart create mode 100644 packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart create mode 100644 packages/pinball_components/test/src/components/multiball/multiball_test.dart create mode 100644 test/game/components/multiballs/behaviors/multiballs_behavior_test.dart create mode 100644 test/game/components/multiballs/multiballs_test.dart diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 5af4efc0..aeb5742e 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -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'; diff --git a/lib/game/components/multiballs/behaviors/behaviors.dart b/lib/game/components/multiballs/behaviors/behaviors.dart new file mode 100644 index 00000000..921063dc --- /dev/null +++ b/lib/game/components/multiballs/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiballs_behavior.dart'; diff --git a/lib/game/components/multiballs/behaviors/multiballs_behavior.dart b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart new file mode 100644 index 00000000..8b323ff4 --- /dev/null +++ b/lib/game/components/multiballs/behaviors/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, + ParentIsA, + BlocComponent { + @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().forEach((multiball) { + multiball.bloc.onAnimate(); + }); + } +} diff --git a/lib/game/components/multiballs/multiballs.dart b/lib/game/components/multiballs/multiballs.dart new file mode 100644 index 00000000..04f6525a --- /dev/null +++ b/lib/game/components/multiballs/multiballs.dart @@ -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(); +} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index d8532c50..76e1bddc 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -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), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index cd5a1c7f..41005886 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame final decals = [ GoogleWord(position: Vector2(-4.25, 1.8)), Multipliers(), + Multiballs(), ]; final characterAreas = [ AndroidAcres(), diff --git a/packages/pinball_components/assets/images/multiball/dimmed.png b/packages/pinball_components/assets/images/multiball/dimmed.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d9407af3922c309f63fb63a29a57fe019f0fb8 GIT binary patch literal 14979 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/multiball/lit.png b/packages/pinball_components/assets/images/multiball/lit.png new file mode 100644 index 0000000000000000000000000000000000000000..3444309cc296e31fdec7486a2675567de9f41e83 GIT binary patch literal 15893 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index d526909e..3e2faad1 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -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(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 394f32ed..5b661691 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -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'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart new file mode 100644 index 00000000..052b4a4e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiball_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart new file mode 100644 index 00000000..48c90552 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.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 { + /// {@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 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(); + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart new file mode 100644 index 00000000..9d943c9d --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart @@ -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 { + 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; + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart new file mode 100644 index 00000000..bbc66fd5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart @@ -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 get props => [lightState, animationState]; +} diff --git a/packages/pinball_components/lib/src/components/multiball/multiball.dart b/packages/pinball_components/lib/src/components/multiball/multiball.dart new file mode 100644 index 00000000..ca348604 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/multiball.dart @@ -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? 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? children, + }) : this._( + position: Vector2(-23, 7.5), + rotation: -24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.b({ + Iterable? children, + }) : this._( + position: Vector2(-7.2, -6.2), + rotation: -5 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.c({ + Iterable? children, + }) : this._( + position: Vector2(-0.7, -9.3), + rotation: 2.7 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.d({ + Iterable? 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 + with HasGameRef, ParentIsA { + /// {@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 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; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 1d2232e0..61e62386 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -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/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 25473f02..9fdee65a 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -27,6 +27,7 @@ void main() { addScoreStories(dashbook); addBackboardStories(dashbook); addDinoWallStories(dashbook); + addMultiballStories(dashbook); addMultipliersStories(dashbook); runApp(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart new file mode 100644 index 00000000..83b53785 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart @@ -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 multiballs = [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + ]; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + + await addAll(multiballs); + await traceAllBodies(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space) { + for (final multiball in multiballs) { + multiball.bloc.onBlink(); + } + + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart b/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart new file mode 100644 index 00000000..6993ed92 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart @@ -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(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 89bf5d96..8cdd38b1 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -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'; diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index ab867e3b..99959e03 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.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 {} diff --git a/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart new file mode 100644 index 00000000..2b3885f9 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart @@ -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(); + 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(); + 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(); + 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(); + 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); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart b/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart new file mode 100644 index 00000000..2fcb5ccc --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart @@ -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( + 'onAnimate emits animationState [animate]', + build: MultiballCubit.new, + act: (bloc) => bloc.onAnimate(), + expect: () => [ + isA() + ..having( + (state) => state.animationState, + 'animationState', + MultiballAnimationState.blinking, + ) + ], + ); + + blocTest( + 'onStop emits animationState [stopped]', + build: MultiballCubit.new, + act: (bloc) => bloc.onStop(), + expect: () => [ + isA() + ..having( + (state) => state.animationState, + 'animationState', + MultiballAnimationState.idle, + ) + ], + ); + + blocTest( + 'onBlink emits lightState [lit, dimmed, lit]', + build: MultiballCubit.new, + act: (bloc) => bloc + ..onBlink() + ..onBlink() + ..onBlink(), + expect: () => [ + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.lit, + ), + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.dimmed, + ), + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.lit, + ) + ], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart b/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart new file mode 100644 index 00000000..69789be9 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart @@ -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), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/multiball/multiball_test.dart b/packages/pinball_components/test/src/components/multiball/multiball_test.dart new file mode 100644 index 00000000..9b1e0e2f --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/multiball_test.dart @@ -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.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().single, + isNotNull, + ); + }); + }); + }); +} diff --git a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart new file mode 100644 index 00000000..00049a83 --- /dev/null +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -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.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + 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.empty(), + initialState: MultiballState.initial(), + ); + when(multiballCubit.onAnimate).thenAnswer((_) async {}); + + whenListen( + otherMultiballCubit, + const Stream.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); + } + }, + ); + }); + }); +} diff --git a/test/game/components/multiballs/multiballs_test.dart b/test/game/components/multiballs/multiballs_test.dart new file mode 100644 index 00000000..c1a328b1 --- /dev/null +++ b/test/game/components/multiballs/multiballs_test.dart @@ -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( + 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().length, + equals(4), + ); + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index c2357046..8d76e8f0 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -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().length, + equals(1), + ); + }, + ); + flameBlocTester.test( 'one GoogleWord', (game) async { diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 1d3ad3c7..7a5862e7 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -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 {}