diff --git a/.gitignore b/.gitignore index a7531405..2d9c4dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,3 @@ app.*.map.json test/.test_runner.dart web/__/firebase/init.js - -# Application exceptions -!/packages/pinball_components/assets/images/flutter_sign_post.png diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index 3c5f5a1f..5149ad81 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -25,7 +25,7 @@ class FlutterForest extends Component await super.onLoad(); gameRef.addContactCallback(_DashNestBumperBallContactCallback()); - final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, -58.3); + final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3); final bigNest = _BigDashNestBumper() ..initialPosition = Vector2(18.55, -59.35); @@ -36,7 +36,7 @@ class FlutterForest extends Component final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66); await addAll([ - signPost, + signpost, smallLeftNest, smallRightNest, bigNest, diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 533c7bd1..1e966a42 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -9,7 +9,10 @@ extension PinballGameAssetsX on PinballGame { return [ images.load(components.Assets.images.ball.ball.keyName), images.load(components.Assets.images.ball.flameEffect.keyName), - images.load(components.Assets.images.flutterSignPost.keyName), + images.load(components.Assets.images.signpost.inactive.keyName), + images.load(components.Assets.images.signpost.active1.keyName), + images.load(components.Assets.images.signpost.active2.keyName), + images.load(components.Assets.images.signpost.active3.keyName), images.load(components.Assets.images.flipper.left.keyName), images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.baseboard.left.keyName), diff --git a/packages/pinball_components/assets/images/flutter_sign_post.png b/packages/pinball_components/assets/images/flutter_sign_post.png deleted file mode 100644 index 28a3facb..00000000 Binary files a/packages/pinball_components/assets/images/flutter_sign_post.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/signpost/active1.png b/packages/pinball_components/assets/images/signpost/active1.png new file mode 100644 index 00000000..1addb228 Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/active1.png differ diff --git a/packages/pinball_components/assets/images/signpost/active2.png b/packages/pinball_components/assets/images/signpost/active2.png new file mode 100644 index 00000000..081a936c Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/active2.png differ diff --git a/packages/pinball_components/assets/images/signpost/active3.png b/packages/pinball_components/assets/images/signpost/active3.png new file mode 100644 index 00000000..8d781dfb Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/active3.png differ diff --git a/packages/pinball_components/assets/images/signpost/inactive.png b/packages/pinball_components/assets/images/signpost/inactive.png new file mode 100644 index 00000000..6043454b Binary files /dev/null and b/packages/pinball_components/assets/images/signpost/inactive.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 836f9495..ca4c3f3c 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -21,17 +21,13 @@ class $AssetsImagesGen { $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); - - /// File path: assets/images/flutter_sign_post.png - AssetGenImage get flutterSignPost => - const AssetGenImage('assets/images/flutter_sign_post.png'); - $AssetsImagesGoogleWordGen get googleWord => const $AssetsImagesGoogleWordGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); + $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); @@ -209,6 +205,26 @@ class $AssetsImagesPlungerGen { const AssetGenImage('assets/images/plunger/rocket.png'); } +class $AssetsImagesSignpostGen { + const $AssetsImagesSignpostGen(); + + /// File path: assets/images/signpost/active1.png + AssetGenImage get active1 => + const AssetGenImage('assets/images/signpost/active1.png'); + + /// File path: assets/images/signpost/active2.png + AssetGenImage get active2 => + const AssetGenImage('assets/images/signpost/active2.png'); + + /// File path: assets/images/signpost/active3.png + AssetGenImage get active3 => + const AssetGenImage('assets/images/signpost/active3.png'); + + /// File path: assets/images/signpost/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/signpost/inactive.png'); +} + class $AssetsImagesSlingshotGen { const $AssetsImagesSlingshotGen(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 846482f2..9ea39d69 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -12,7 +12,6 @@ export 'dash_nest_bumper.dart'; export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; -export 'flutter_sign_post.dart'; export 'google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; @@ -25,6 +24,7 @@ export 'render_priority.dart'; export 'rocket.dart'; export 'score_text.dart'; export 'shapes/shapes.dart'; +export 'signpost.dart'; export 'slingshot.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; diff --git a/packages/pinball_components/lib/src/components/flutter_sign_post.dart b/packages/pinball_components/lib/src/components/flutter_sign_post.dart deleted file mode 100644 index 6e85d694..00000000 --- a/packages/pinball_components/lib/src/components/flutter_sign_post.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template flutter_sign_post} -/// A sign, found in the Flutter Forest. -/// {@endtemplate} -class FlutterSignPost extends BodyComponent with InitialPosition { - /// {@macro flutter_sign_post} - FlutterSignPost() - : super( - priority: RenderPriority.flutterSignPost, - children: [_FlutterSignPostSpriteComponent()], - ) { - renderBody = false; - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 0.25; - final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef( - position: initialPosition, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -class _FlutterSignPostSpriteComponent extends SpriteComponent with HasGameRef { - @override - Future onLoad() async { - await super.onLoad(); - - final sprite = await gameRef.loadSprite( - Assets.images.flutterSignPost.keyName, - ); - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.bottomCenter; - position = Vector2(0.65, 0.45); - } -} diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart index 030d45c9..1850369c 100644 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -67,7 +67,7 @@ abstract class RenderPriority { // Flutter Forest - static const int flutterSignPost = _above + launchRampForegroundRailing; + static const int signpost = _above + launchRampForegroundRailing; static const int dashBumper = _above + ballOnBoard; diff --git a/packages/pinball_components/lib/src/components/signpost.dart b/packages/pinball_components/lib/src/components/signpost.dart new file mode 100644 index 00000000..665c2cbb --- /dev/null +++ b/packages/pinball_components/lib/src/components/signpost.dart @@ -0,0 +1,102 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// Represents the [Signpost]'s current [Sprite] state. +@visibleForTesting +enum SignpostSpriteState { + /// Signpost with no active dashes. + inactive, + + /// Signpost with a single sign of active dashes. + active1, + + /// Signpost with two signs of active dashes. + active2, + + /// Signpost with all signs of active dashes. + active3, +} + +extension on SignpostSpriteState { + String get path { + switch (this) { + case SignpostSpriteState.inactive: + return Assets.images.signpost.inactive.keyName; + case SignpostSpriteState.active1: + return Assets.images.signpost.active1.keyName; + case SignpostSpriteState.active2: + return Assets.images.signpost.active2.keyName; + case SignpostSpriteState.active3: + return Assets.images.signpost.active3.keyName; + } + } + + SignpostSpriteState get next { + return SignpostSpriteState + .values[(index + 1) % SignpostSpriteState.values.length]; + } +} + +/// {@template signpost} +/// A sign, found in the Flutter Forest. +/// +/// Lights up a new sign whenever all three [DashNestBumper]s are hit. +/// {@endtemplate} +class Signpost extends BodyComponent with InitialPosition { + /// {@macro signpost} + Signpost() + : super( + priority: RenderPriority.signpost, + children: [_SignpostSpriteComponent()], + ) { + renderBody = false; + } + + /// Forwards the sprite to the next [SignpostSpriteState]. + /// + /// If the current state is the last one it cycles back to the initial state. + void progress() => firstChild<_SignpostSpriteComponent>()!.progress(); + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.25; + final fixtureDef = FixtureDef(shape); + final bodyDef = BodyDef( + position: initialPosition, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _SignpostSpriteComponent extends SpriteGroupComponent + with HasGameRef { + _SignpostSpriteComponent() + : super( + anchor: Anchor.bottomCenter, + position: Vector2(0.65, 0.45), + ); + + void progress() => current = current?.next; + + @override + Future onLoad() async { + await super.onLoad(); + + final sprites = {}; + this.sprites = sprites; + for (final spriteState in SignpostSpriteState.values) { + // TODO(allisonryan0002): Support caching + // https://github.com/VGVentures/pinball/pull/204 + // sprites[spriteState] = Sprite( + // gameRef.images.fromCache(spriteState.path), + // ); + sprites[spriteState] = await gameRef.loadSprite(spriteState.path); + } + + current = SignpostSpriteState.inactive; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index da4446c1..26d919da 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -64,6 +64,7 @@ flutter: - assets/images/sparky/bumper/c/ - assets/images/backboard/ - assets/images/google_word/ + - assets/images/signpost/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart similarity index 57% rename from packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart rename to packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart index 3efb83fe..b7c11cf2 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart @@ -1,22 +1,30 @@ import 'dart:async'; -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class FlutterSignPostGame extends BasicBallGame with Traceable { +class SignpostGame extends BasicBallGame with Traceable, TapDetector { static const info = ''' - Shows how a FlutterSignPost is rendered. + Shows how a Signpost is rendered. - Activate the "trace" parameter to overlay the body. + - Tap to progress the sprite. '''; @override Future onLoad() async { await super.onLoad(); + camera.followVector2(Vector2.zero()); - await add(FlutterSignPost()..priority = 1); + await add(Signpost()..priority = 1); await traceAllBodies(); } + + @override + void onTap() { + super.onTap(); + firstChild()!.progress(); + } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart index a625d174..a563a09a 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -2,20 +2,19 @@ import 'package:dashbook/dashbook.dart'; import 'package:flame/game.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart'; -import 'package:sandbox/stories/flutter_forest/flutter_sign_post_game.dart'; +import 'package:sandbox/stories/flutter_forest/signpost_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; void addDashNestBumperStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') ..add( - 'Flutter Sign Post', + 'Signpost', (context) => GameWidget( - game: FlutterSignPostGame() - ..trace = context.boolProperty('Trace', true), + game: SignpostGame()..trace = context.boolProperty('Trace', true), ), - codeLink: buildSourceLink('flutter_forest/flutter_sign_post.dart'), - info: FlutterSignPostGame.info, + codeLink: buildSourceLink('flutter_forest/signpost.dart'), + info: SignpostGame.info, ) ..add( 'Big Dash Nest Bumper', diff --git a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart b/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart index 7e6d035f..f1c17fe9 100644 --- a/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/zoom/basic_zoom_game.dart @@ -14,7 +14,7 @@ class BasicCameraZoomGame extends BasicGame with TapDetector { @override Future onLoad() async { - final sprite = await loadSprite(Assets.images.flutterSignPost.keyName); + final sprite = await loadSprite(Assets.images.signpost.inactive.keyName); await add( SpriteComponent( diff --git a/packages/pinball_components/test/src/components/camera_zoom_test.dart b/packages/pinball_components/test/src/components/camera_zoom_test.dart index 00f43847..a7f64eca 100644 --- a/packages/pinball_components/test/src/components/camera_zoom_test.dart +++ b/packages/pinball_components/test/src/components/camera_zoom_test.dart @@ -17,7 +17,7 @@ void main() { game.camera.followVector2(Vector2.zero()); game.camera.zoom = 10; final sprite = await game.loadSprite( - Assets.images.flutterSignPost.keyName, + Assets.images.signpost.inactive.keyName, ); await game.add( diff --git a/packages/pinball_components/test/src/components/flutter_sign_post_test.dart b/packages/pinball_components/test/src/components/flutter_sign_post_test.dart deleted file mode 100644 index 0dee4482..00000000 --- a/packages/pinball_components/test/src/components/flutter_sign_post_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); - - group('FlutterSignPost', () { - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.ensureAdd(FlutterSignPost()); - game.camera.followVector2(Vector2.zero()); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/flutter-sign-post.png'), - ); - }, - ); - - flameTester.test( - 'loads correctly', - (game) async { - final flutterSignPost = FlutterSignPost(); - await game.ready(); - await game.ensureAdd(flutterSignPost); - - expect(game.contains(flutterSignPost), isTrue); - }, - ); - }); -} diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png index be784ada..1d3daa81 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png index 3809f0d0..f0312ae5 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png index a6215d65..5fd65077 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png differ diff --git a/packages/pinball_components/test/src/components/golden/flutter-sign-post.png b/packages/pinball_components/test/src/components/golden/flutter-sign-post.png deleted file mode 100644 index 68388670..00000000 Binary files a/packages/pinball_components/test/src/components/golden/flutter-sign-post.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active1.png b/packages/pinball_components/test/src/components/golden/signpost/active1.png new file mode 100644 index 00000000..f11af5a8 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/active1.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active2.png b/packages/pinball_components/test/src/components/golden/signpost/active2.png new file mode 100644 index 00000000..6ddf8786 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/active2.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active3.png b/packages/pinball_components/test/src/components/golden/signpost/active3.png new file mode 100644 index 00000000..5e9b0005 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/active3.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/inactive.png b/packages/pinball_components/test/src/components/golden/signpost/inactive.png new file mode 100644 index 00000000..7ed00fba Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/signpost/inactive.png differ diff --git a/packages/pinball_components/test/src/components/signpost_test.dart b/packages/pinball_components/test/src/components/signpost_test.dart new file mode 100644 index 00000000..33844994 --- /dev/null +++ b/packages/pinball_components/test/src/components/signpost_test.dart @@ -0,0 +1,141 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Signpost', () { + flameTester.test( + 'loads correctly', + (game) async { + final signpost = Signpost(); + await game.ready(); + await game.ensureAdd(signpost); + + expect(game.contains(signpost), isTrue); + }, + ); + + group('renders correctly', () { + flameTester.testGameWidget( + 'inactive sprite', + setUp: (game, tester) async { + final signpost = Signpost(); + await game.ensureAdd(signpost); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.inactive, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/inactive.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active1 sprite', + setUp: (game, tester) async { + final signpost = Signpost(); + await game.ensureAdd(signpost); + signpost.progress(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.active1, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/active1.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active2 sprite', + setUp: (game, tester) async { + final signpost = Signpost(); + await game.ensureAdd(signpost); + signpost + ..progress() + ..progress(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.active2, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/active2.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'active3 sprite', + setUp: (game, tester) async { + final signpost = Signpost(); + await game.ensureAdd(signpost); + signpost + ..progress() + ..progress() + ..progress(); + + expect( + signpost.firstChild()!.current, + SignpostSpriteState.active3, + ); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/signpost/active3.png'), + ); + }, + ); + }); + + flameTester.test( + 'progress correctly cycles through all sprites', + (game) async { + final signpost = Signpost(); + await game.ready(); + await game.ensureAdd(signpost); + + final spriteComponent = signpost.firstChild()!; + + expect(spriteComponent.current, SignpostSpriteState.inactive); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.active1); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.active2); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.active3); + signpost.progress(); + expect(spriteComponent.current, SignpostSpriteState.inactive); + }, + ); + }); +} diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 2089b7b7..a318d342 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -27,13 +27,13 @@ void main() { group('loads', () { flameTester.test( - 'a FlutterSignPost', + 'a Signpost', (game) async { final flutterForest = FlutterForest(); await game.ensureAdd(flutterForest); expect( - flutterForest.descendants().whereType().length, + flutterForest.descendants().whereType().length, equals(1), ); },