diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..c8a41a4b --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,25 @@ +name: deploy + +on: + push: + branches: + - main + +jobs: + deploy-dev: + runs-on: ubuntu-latest + name: Deploy Development + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter packages get + - run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}" + channelId: live + projectId: pinball-dev + target: ashehwkdkdjruejdnensjsjdne \ No newline at end of file diff --git a/.github/workflows/pinball_ui.yaml b/.github/workflows/pinball_ui.yaml new file mode 100644 index 00000000..98643ffa --- /dev/null +++ b/.github/workflows/pinball_ui.yaml @@ -0,0 +1,23 @@ +name: pinball_ui + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/pinball_ui/**" + - ".github/workflows/pinball_ui.yaml" + + pull_request: + paths: + - "packages/pinball_ui/**" + - ".github/workflows/pinball_ui.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_ui + coverage_excludes: "lib/gen/*.dart" diff --git a/.github/workflows/platform_helper.yaml b/.github/workflows/platform_helper.yaml new file mode 100644 index 00000000..0c1c61e7 --- /dev/null +++ b/.github/workflows/platform_helper.yaml @@ -0,0 +1,23 @@ +name: platform_helper + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + + pull_request: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/platform_helper + coverage_excludes: "lib/gen/*.dart" diff --git a/assets/images/components/key.png b/assets/images/components/key.png new file mode 100644 index 00000000..588c2b89 Binary files /dev/null and b/assets/images/components/key.png differ diff --git a/assets/images/components/space.png b/assets/images/components/space.png new file mode 100644 index 00000000..9949b383 Binary files /dev/null and b/assets/images/components/space.png differ diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres.dart index 752f68f9..da8e4949 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres.dart @@ -6,8 +6,8 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template android_acres} -/// Area positioned on the left side of the board containing the [Spaceship], -/// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. +/// Area positioned on the left side of the board containing the +/// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. /// {@endtemplate} class AndroidAcres extends Blueprint { /// {@macro android_acres} @@ -16,18 +16,23 @@ class AndroidAcres extends Blueprint { components: [ AndroidBumper.a( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], - )..initialPosition = Vector2(-32.52, -9.1), + )..initialPosition = Vector2(-25, 1.3), AndroidBumper.b( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(-32.6, -9.2), + AndroidBumper.cow( children: [ ScoringBehavior(points: 20), ], - )..initialPosition = Vector2(-22.89, -17.35), + )..initialPosition = Vector2(-20.5, -13.8), ], blueprints: [ SpaceshipRamp(), - Spaceship(position: Vector2(-26.5, -28.5)), + AndroidSpaceship(position: Vector2(-26.5, -28.5)), SpaceshipRail(), ], ); diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart deleted file mode 100644 index 666cec5b..00000000 --- a/lib/game/components/board.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template board} -/// The main flat surface of the [PinballGame]. -/// {endtemplate} -class Board extends Component { - /// {@macro board} - // TODO(alestiago): Make Board a Blueprint and sort out priorities. - Board() : super(priority: 1); - - @override - Future onLoad() async { - // TODO(allisonryan0002): add bottom group and flutter forest to pinball - // game directly. Then remove board. - final bottomGroup = _BottomGroup(); - - final flutterForest = FlutterForest(); - - await addAll([ - bottomGroup, - flutterForest, - ]); - } -} - -/// {@template bottom_group} -/// Grouping of the board's bottom [Component]s. -/// -/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s. -/// {@endtemplate} -// TODO(alestiago): Consider renaming once entire Board is defined. -class _BottomGroup extends Component { - /// {@macro bottom_group} - _BottomGroup() : super(priority: RenderPriority.bottomGroup); - - @override - Future onLoad() async { - final rightSide = _BottomGroupSide( - side: BoardSide.right, - ); - final leftSide = _BottomGroupSide( - side: BoardSide.left, - ); - - await addAll([rightSide, leftSide]); - } -} - -/// {@template bottom_group_side} -/// Group with one side of [_BottomGroup]'s symmetric [Component]s. -/// -/// For example, [Flipper]s are symmetric components. -/// {@endtemplate} -class _BottomGroupSide extends Component { - /// {@macro bottom_group_side} - _BottomGroupSide({ - required BoardSide side, - }) : _side = side; - - final BoardSide _side; - - @override - Future onLoad() async { - final direction = _side.direction; - final centerXAdjustment = _side.isLeft ? 0 : -6.5; - - final flipper = ControlledFlipper( - side: _side, - )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); - final baseboard = Baseboard(side: _side) - ..initialPosition = Vector2( - (25.58 * direction) + centerXAdjustment, - 28.69, - ); - final kicker = Kicker( - side: _side, - )..initialPosition = Vector2( - (22.4 * direction) + centerXAdjustment, - 25, - ); - - await addAll([flipper, baseboard, kicker]); - } -} diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart new file mode 100644 index 00000000..921a8e58 --- /dev/null +++ b/lib/game/components/bottom_group.dart @@ -0,0 +1,61 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template bottom_group} +/// Grouping of the board's symmetrical bottom [Component]s. +/// +/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. +/// {@endtemplate} +// TODO(allisonryan0002): Consider renaming. +class BottomGroup extends Component { + /// {@macro bottom_group} + BottomGroup() + : super( + children: [ + _BottomGroupSide(side: BoardSide.right), + _BottomGroupSide(side: BoardSide.left), + ], + priority: RenderPriority.bottomGroup, + ); +} + +/// {@template bottom_group_side} +/// Group with one side of [BottomGroup]'s symmetric [Component]s. +/// +/// For example, [Flipper]s are symmetric components. +/// {@endtemplate} +class _BottomGroupSide extends Component { + /// {@macro bottom_group_side} + _BottomGroupSide({ + required BoardSide side, + }) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + final direction = _side.direction; + final centerXAdjustment = _side.isLeft ? 0 : -6.5; + + final flipper = ControlledFlipper( + side: _side, + )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); + final baseboard = Baseboard(side: _side) + ..initialPosition = Vector2( + (25.58 * direction) + centerXAdjustment, + 28.69, + ); + final kicker = Kicker( + side: _side, + children: [ + ScoringBehavior(points: 5000), + ], + )..initialPosition = Vector2( + (22.4 * direction) + centerXAdjustment, + 25, + ); + + await addAll([flipper, baseboard, kicker]); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 37de1948..afef04f0 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,13 +1,15 @@ export 'android_acres.dart'; -export 'board.dart'; +export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; +export 'dino_desert.dart'; +export 'drain.dart'; export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; +export 'multipliers/multipliers.dart'; export 'scoring_behavior.dart'; export 'sparky_fire_zone.dart'; -export 'wall.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index e76aabe1..ff05ad62 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart new file mode 100644 index 00000000..9e912575 --- /dev/null +++ b/lib/game/components/dino_desert.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template dino_desert} +/// Area located next to the [Launcher] containing the [ChromeDino] and +/// [DinoWalls]. +/// {@endtemplate} +// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is +// fully implemented. +class DinoDesert extends Blueprint { + /// {@macro dino_desert} + DinoDesert() + : super( + components: [ + ChromeDino()..initialPosition = Vector2(12.3, -6.9), + ], + blueprints: [ + DinoWalls(), + ], + ); +} diff --git a/lib/game/components/drain.dart b/lib/game/components/drain.dart new file mode 100644 index 00000000..1dc3e211 --- /dev/null +++ b/lib/game/components/drain.dart @@ -0,0 +1,34 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template drain} +/// Area located at the bottom of the board to detect when a [Ball] is lost. +/// {@endtemplate} +// TODO(allisonryan0002): move to components package when possible. +class Drain extends BodyComponent with ContactCallbacks { + /// {@macro drain} + Drain() : super(renderBody: false); + + @override + Body createBody() { + final shape = EdgeShape() + ..set( + BoardDimensions.bounds.bottomLeft.toVector2(), + BoardDimensions.bounds.bottomRight.toVector2(), + ); + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef(userData: this); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + +// TODO(allisonryan0002): move this to ball.dart when BallLost is removed. + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + other.controller.lost(); + } +} diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 02483159..7508d5c3 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -7,7 +7,7 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template flutter_forest} -/// Area positioned at the top right of the [Board] where the [Ball] can bounce +/// Area positioned at the top right of the board where the [Ball] can bounce /// off [DashNestBumper]s. /// {@endtemplate} class FlutterForest extends Component { @@ -23,17 +23,17 @@ class FlutterForest extends Component { )..initialPosition = Vector2(8.35, -58.3), DashNestBumper.main( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 200000), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(23.3, -46.75), DashAnimatronic()..position = Vector2(20, -66), diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart index 9a9faa9a..63999fe1 100644 --- a/lib/game/components/google_word/google_word.dart +++ b/lib/game/components/google_word/google_word.dart @@ -1,6 +1,7 @@ import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template google_word} @@ -12,12 +13,30 @@ class GoogleWord extends Component { required Vector2 position, }) : super( children: [ - GoogleLetter(0)..initialPosition = position + Vector2(-12.92, 1.82), - GoogleLetter(1)..initialPosition = position + Vector2(-8.33, -0.65), - GoogleLetter(2)..initialPosition = position + Vector2(-2.88, -1.75), - GoogleLetter(3)..initialPosition = position + Vector2(2.88, -1.75), - GoogleLetter(4)..initialPosition = position + Vector2(8.33, -0.65), - GoogleLetter(5)..initialPosition = position + Vector2(12.92, 1.82), + GoogleLetter( + 0, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-12.92, 1.82), + GoogleLetter( + 1, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-8.33, -0.65), + GoogleLetter( + 2, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-2.88, -1.75), + GoogleLetter( + 3, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(2.88, -1.75), + GoogleLetter( + 4, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(8.33, -0.65), + GoogleLetter( + 5, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(12.92, 1.82), GoogleWordBonusBehavior(), ], ); diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index 7aef09d2..959e8da0 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -12,9 +12,9 @@ class Launcher extends Blueprint { Launcher() : super( components: [ - ControlledPlunger(compressionDistance: 14) - ..initialPosition = Vector2(40.7, 38), - RocketSpriteComponent()..position = Vector2(43, 62), + ControlledPlunger(compressionDistance: 10.5) + ..initialPosition = Vector2(41.1, 43), + RocketSpriteComponent()..position = Vector2(43, 62.3), ], blueprints: [LaunchRamp()], ); diff --git a/lib/game/components/multipliers/behaviors/behaviors.dart b/lib/game/components/multipliers/behaviors/behaviors.dart new file mode 100644 index 00000000..70703bba --- /dev/null +++ b/lib/game/components/multipliers/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multipliers_behavior.dart'; diff --git a/lib/game/components/multipliers/behaviors/multipliers_behavior.dart b/lib/game/components/multipliers/behaviors/multipliers_behavior.dart new file mode 100644 index 00000000..33a59a08 --- /dev/null +++ b/lib/game/components/multipliers/behaviors/multipliers_behavior.dart @@ -0,0 +1,25 @@ +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 [Multiplier] when GameState.multiplier changes. +class MultipliersBehavior extends Component + with + HasGameRef, + ParentIsA, + BlocComponent { + @override + bool listenWhen(GameState? previousState, GameState newState) { + return previousState?.multiplier != newState.multiplier; + } + + @override + void onNewState(GameState state) { + final multipliers = parent.children.whereType(); + for (final multiplier in multipliers) { + multiplier.bloc.next(state.multiplier); + } + } +} diff --git a/lib/game/components/multipliers/multipliers.dart b/lib/game/components/multipliers/multipliers.dart new file mode 100644 index 00000000..6a6a1563 --- /dev/null +++ b/lib/game/components/multipliers/multipliers.dart @@ -0,0 +1,44 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template multipliers} +/// A group for the multipliers on the board. +/// {@endtemplate} +class Multipliers extends Component { + /// {@macro multipliers} + Multipliers() + : super( + children: [ + Multiplier.x2( + position: Vector2(-19.5, -2), + angle: -15 * math.pi / 180, + ), + Multiplier.x3( + position: Vector2(13, -9.4), + angle: 15 * math.pi / 180, + ), + Multiplier.x4( + position: Vector2(0, -21.2), + angle: 0, + ), + Multiplier.x5( + position: Vector2(-8.5, -28), + angle: -3 * math.pi / 180, + ), + Multiplier.x6( + position: Vector2(10, -30.7), + angle: 8 * math.pi / 180, + ), + MultipliersBehavior(), + ], + ); + + /// Creates [Multipliers] without any children. + /// + /// This can be used for testing [Multipliers]'s behaviors in isolation. + @visibleForTesting + Multipliers.test(); +} diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart index a23a4fbc..a37c2469 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_fire_zone.dart @@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_fire_zone} -/// Area positioned at the top left of the [Board] where the [Ball] +/// Area positioned at the top left of the board where the [Ball] /// can bounce off [SparkyBumper]s. /// /// When a [Ball] hits [SparkyBumper]s, the bumper animates. @@ -18,17 +18,17 @@ class SparkyFireZone extends Blueprint { components: [ SparkyBumper.a( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.b( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-21.25, -57.9), SparkyBumper.c( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: 20000), ], )..initialPosition = Vector2(-3.3, -52.55), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), @@ -47,7 +47,13 @@ class SparkyFireZone extends Blueprint { class SparkyComputerSensor extends BodyComponent with InitialPosition, ContactCallbacks { /// {@macro sparky_computer_sensor} - SparkyComputerSensor() : super(renderBody: false); + SparkyComputerSensor() + : super( + renderBody: false, + children: [ + ScoringBehavior(points: 200000), + ], + ); @override Body createBody() { diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart deleted file mode 100644 index 2f180d61..00000000 --- a/lib/game/components/wall.dart +++ /dev/null @@ -1,60 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/extensions.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -/// {@template wall} -/// A continuous generic and [BodyType.static] barrier that divides a game area. -/// {@endtemplate} -// TODO(alestiago): Remove [Wall] for [Pathway.straight]. -class Wall extends BodyComponent { - /// {@macro wall} - Wall({ - required this.start, - required this.end, - }); - - /// The [start] of the [Wall]. - final Vector2 start; - - /// The [end] of the [Wall]. - final Vector2 end; - - @override - Body createBody() { - final shape = EdgeShape()..set(start, end); - - final fixtureDef = FixtureDef(shape) - ..restitution = 0.1 - ..friction = 0; - - final bodyDef = BodyDef() - ..userData = this - ..position = Vector2.zero() - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template bottom_wall} -/// [Wall] located at the bottom of the board. -/// -/// {@endtemplate} -class BottomWall extends Wall with ContactCallbacks { - /// {@macro bottom_wall} - BottomWall() - : super( - start: BoardDimensions.bounds.bottomLeft.toVector2(), - end: BoardDimensions.bounds.bottomRight.toVector2(), - ); - - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! ControlledBall) return; - other.controller.lost(); - } -} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index ab4e9860..ab7a6169 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -50,39 +50,42 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.outer.keyName), images.load(components.Assets.images.boundary.outerBottom.keyName), - images.load(components.Assets.images.spaceship.saucer.keyName), - images.load(components.Assets.images.spaceship.bridge.keyName), - images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), + images.load(components.Assets.images.android.spaceship.saucer.keyName), + images + .load(components.Assets.images.android.spaceship.animatronic.keyName), + images.load(components.Assets.images.android.spaceship.lightBeam.keyName), + images.load(components.Assets.images.android.ramp.boardOpening.keyName), images.load( - components.Assets.images.spaceship.ramp.railingForeground.keyName, + components.Assets.images.android.ramp.railingForeground.keyName, ), images.load( - components.Assets.images.spaceship.ramp.railingBackground.keyName, + components.Assets.images.android.ramp.railingBackground.keyName, ), - images.load(components.Assets.images.spaceship.ramp.main.keyName), - images - .load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName), + images.load(components.Assets.images.android.ramp.main.keyName), + images.load(components.Assets.images.android.ramp.arrow.inactive.keyName), images.load( - components.Assets.images.spaceship.ramp.arrow.active1.keyName, + components.Assets.images.android.ramp.arrow.active1.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active2.keyName, + components.Assets.images.android.ramp.arrow.active2.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active3.keyName, + components.Assets.images.android.ramp.arrow.active3.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active4.keyName, + components.Assets.images.android.ramp.arrow.active4.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active5.keyName, + components.Assets.images.android.ramp.arrow.active5.keyName, ), - images.load(components.Assets.images.spaceship.rail.main.keyName), - images.load(components.Assets.images.spaceship.rail.exit.keyName), - images.load(components.Assets.images.androidBumper.a.lit.keyName), - images.load(components.Assets.images.androidBumper.a.dimmed.keyName), - images.load(components.Assets.images.androidBumper.b.lit.keyName), - images.load(components.Assets.images.androidBumper.b.dimmed.keyName), + images.load(components.Assets.images.android.rail.main.keyName), + images.load(components.Assets.images.android.rail.exit.keyName), + images.load(components.Assets.images.android.bumper.a.lit.keyName), + images.load(components.Assets.images.android.bumper.a.dimmed.keyName), + images.load(components.Assets.images.android.bumper.b.lit.keyName), + images.load(components.Assets.images.android.bumper.b.dimmed.keyName), + images.load(components.Assets.images.android.bumper.cow.lit.keyName), + images.load(components.Assets.images.android.bumper.cow.dimmed.keyName), images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.animatronic.keyName), @@ -101,6 +104,16 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.googleWord.letter5.keyName), images.load(components.Assets.images.googleWord.letter6.keyName), images.load(components.Assets.images.backboard.display.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), + images.load(components.Assets.images.multiplier.x3.dimmed.keyName), + images.load(components.Assets.images.multiplier.x4.lit.keyName), + images.load(components.Assets.images.multiplier.x4.dimmed.keyName), + images.load(components.Assets.images.multiplier.x5.lit.keyName), + images.load(components.Assets.images.multiplier.x5.dimmed.keyName), + images.load(components.Assets.images.multiplier.x6.lit.keyName), + images.load(components.Assets.images.multiplier.x6.dimmed.keyName), images.load(dashTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 4b57f1dd..6933e649 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -6,6 +6,7 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; @@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents, - Controls<_GameBallsController> { + Controls<_GameBallsController>, + TapDetector { PinballGame({ required this.characterTheme, required this.audio, @@ -44,20 +46,18 @@ class PinballGame extends Forge2DGame unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); - - // TODO(allisonryan0002): banish Wall and Board classes in later PR. - await add(BottomWall()); + await add(Drain()); + await add(BottomGroup()); unawaited(addFromBlueprint(Boundaries())); - unawaited(addFromBlueprint(LaunchRamp())); final launcher = Launcher(); unawaited(addFromBlueprint(launcher)); - unawaited(add(Board())); + await add(Multipliers()); + await add(FlutterForest()); await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(AndroidAcres()); + await addFromBlueprint(DinoDesert()); unawaited(addFromBlueprint(Slingshots())); - unawaited(addFromBlueprint(DinoWalls())); - await add(ChromeDino()..initialPosition = Vector2(12.3, -6.9)); await add( GoogleWord( position: Vector2( @@ -67,9 +67,64 @@ class PinballGame extends Forge2DGame ), ); - controller.attachTo(launcher.components.whereType().first); + controller.attachTo(launcher.components.whereType().single); await super.onLoad(); } + + BoardSide? focusedBoardSide; + + @override + void onTapDown(TapDownInfo info) { + if (info.raw.kind == PointerDeviceKind.touch) { + final rocket = children.whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.pull(); + } else { + final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; + focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveUp(); + } + } + + super.onTapDown(info); + } + + @override + void onTapUp(TapUpInfo info) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + children.whereType().first.release(); + } else { + _moveFlippersDown(); + } + super.onTapUp(info); + } + + @override + void onTapCancel() { + children.whereType().first.release(); + + _moveFlippersDown(); + super.onTapCancel(); + } + + void _moveFlippersDown() { + if (focusedBoardSide != null) { + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveDown(); + focusedBoardSide = null; + } + } } class _GameBallsController extends ComponentController @@ -116,7 +171,7 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter { DebugPinballGame({ required CharacterTheme characterTheme, required PinballAudio audio, @@ -153,9 +208,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { @override void onTapUp(TapUpInfo info) { - add( - ControlledBall.debug()..initialPosition = info.eventPosition.game, - ); + super.onTapUp(info); + + if (info.raw.kind == PointerDeviceKind.mouse) { + add(ControlledBall.debug()..initialPosition = info.eventPosition.game); + } } } diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 9cfb2d67..e3c44877 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -66,14 +66,14 @@ class _ScoreViewDecoration extends StatelessWidget { @override Widget build(BuildContext context) { const radius = BorderRadius.all(Radius.circular(12)); - const boardWidth = 5.0; + const borderWidth = 5.0; return DecoratedBox( decoration: BoxDecoration( borderRadius: radius, border: Border.all( color: AppColors.white, - width: boardWidth, + width: borderWidth, ), image: DecorationImage( fit: BoxFit.cover, @@ -83,7 +83,7 @@ class _ScoreViewDecoration extends StatelessWidget { ), ), child: Padding( - padding: const EdgeInsets.all(boardWidth - 1), + padding: const EdgeInsets.all(borderWidth - 1), child: ClipRRect( borderRadius: radius, child: child, diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index f90ebb98..3db62a50 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -28,6 +28,7 @@ class PlayButtonOverlay extends StatelessWidget { context: context, barrierDismissible: false, builder: (_) { + // TODO(arturplaczek): remove after merge StarBlocListener final height = MediaQuery.of(context).size.height * 0.5; return Center( diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart index 30135cd2..b8f67c26 100644 --- a/lib/game/view/widgets/round_count_display.dart +++ b/lib/game/view/widgets/round_count_display.dart @@ -21,7 +21,7 @@ class RoundCountDisplay extends StatelessWidget { Text( l10n.rounds, style: AppTextStyle.subtitle1.copyWith( - color: AppColors.orange, + color: AppColors.yellow, ), ), const SizedBox(width: 8), @@ -53,7 +53,7 @@ class RoundIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128); + final color = isActive ? AppColors.yellow : AppColors.yellow.withAlpha(128); const size = 8.0; return Padding( diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart index 288ea05c..40b33c35 100644 --- a/lib/game/view/widgets/score_view.dart +++ b/lib/game/view/widgets/score_view.dart @@ -59,7 +59,7 @@ class _ScoreDisplay extends StatelessWidget { Text( l10n.score.toLowerCase(), style: AppTextStyle.subtitle1.copyWith( - color: AppColors.orange, + color: AppColors.yellow, ), ), const _ScoreText(), diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index f5b935a5..9559fd45 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -47,6 +47,14 @@ class $AssetsImagesComponentsGen { /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); + + /// File path: assets/images/components/key.png + AssetGenImage get key => + const AssetGenImage('assets/images/components/key.png'); + + /// File path: assets/images/components/space.png + AssetGenImage get space => + const AssetGenImage('assets/images/components/space.png'); } class $AssetsImagesScoreGen { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 9655d8be..19b12296 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -8,6 +8,10 @@ "@howToPlay": { "description": "Text displayed on the landing page how to play button" }, + "tipsForFlips": "Tips for flips", + "@tipsForFlips": { + "description": "Text displayed on the landing page how to play button" + }, "launchControls": "Launch Controls", "@launchControls": { "description": "Text displayed on the how to play dialog with the launch controls" @@ -16,6 +20,26 @@ "@flipperControls": { "description": "Text displayed on the how to play dialog with the flipper controls" }, + "tapAndHoldRocket": "Tap & Hold Rocket", + "@tapAndHoldRocket": { + "description": "Text displayed on the how to launch on mobile" + }, + "to": "to", + "@to": { + "description": "Text displayed for the word to" + }, + "launch": "LAUNCH", + "@launch": { + "description": "Text displayed for the word launch" + }, + "tapLeftRightScreen": "Tap left/right screen", + "@tapLeftRightScreen": { + "description": "Text displayed on the how to flip on mobile" + }, + "flip": "FLIP", + "@flip": { + "description": "Text displayed for the word FLIP" + }, "start": "Start", "@start": { "description": "Text displayed on the character selection page start button" @@ -24,6 +48,10 @@ "@select": { "description": "Text displayed on the character selection page select button" }, + "space": "Space", + "@space": { + "description": "Text displayed on space control button" + }, "characterSelectionTitle": "Choose your character!", "@characterSelectionTitle": { "description": "Title text displayed on the character selection page" @@ -80,4 +108,4 @@ "@rounds": { "description": "Text displayed on the scoreboard widget to indicate rounds left" } -} +} \ No newline at end of file diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart deleted file mode 100644 index 49a35474..00000000 --- a/lib/leaderboard/bloc/leaderboard_bloc.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; - -part 'leaderboard_event.dart'; -part 'leaderboard_state.dart'; - -/// {@template leaderboard_bloc} -/// Manages leaderboard events. -/// -/// Uses a [LeaderboardRepository] to request and update players participations. -/// {@endtemplate} -class LeaderboardBloc extends Bloc { - /// {@macro leaderboard_bloc} - LeaderboardBloc(this._leaderboardRepository) - : super(const LeaderboardState.initial()) { - on(_onTop10Fetched); - on(_onLeaderboardEntryAdded); - } - - final LeaderboardRepository _leaderboardRepository; - - Future _onTop10Fetched( - Top10Fetched event, - Emitter emit, - ) async { - emit(state.copyWith(status: LeaderboardStatus.loading)); - try { - final top10Leaderboard = - await _leaderboardRepository.fetchTop10Leaderboard(); - - final leaderboardEntries = []; - top10Leaderboard.asMap().forEach( - (index, value) => leaderboardEntries.add(value.toEntry(index + 1)), - ); - - emit( - state.copyWith( - status: LeaderboardStatus.success, - leaderboard: leaderboardEntries, - ), - ); - } catch (error) { - emit(state.copyWith(status: LeaderboardStatus.error)); - addError(error); - } - } - - Future _onLeaderboardEntryAdded( - LeaderboardEntryAdded event, - Emitter emit, - ) async { - emit(state.copyWith(status: LeaderboardStatus.loading)); - try { - final ranking = - await _leaderboardRepository.addLeaderboardEntry(event.entry); - emit( - state.copyWith( - status: LeaderboardStatus.success, - ranking: ranking, - ), - ); - } catch (error) { - emit(state.copyWith(status: LeaderboardStatus.error)); - addError(error); - } - } -} diff --git a/lib/leaderboard/bloc/leaderboard_event.dart b/lib/leaderboard/bloc/leaderboard_event.dart deleted file mode 100644 index b9e6955a..00000000 --- a/lib/leaderboard/bloc/leaderboard_event.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'leaderboard_bloc.dart'; - -/// {@template leaderboard_event} -/// Represents the events available for [LeaderboardBloc]. -/// {endtemplate} -abstract class LeaderboardEvent extends Equatable { - /// {@macro leaderboard_event} - const LeaderboardEvent(); -} - -/// {@template top_10_fetched} -/// Request the top 10 [LeaderboardEntryData]s. -/// {endtemplate} -class Top10Fetched extends LeaderboardEvent { - /// {@macro top_10_fetched} - const Top10Fetched(); - - @override - List get props => []; -} - -/// {@template leaderboard_entry_added} -/// Writes a new [LeaderboardEntryData]. -/// -/// Should be added when a player finishes a game. -/// {endtemplate} -class LeaderboardEntryAdded extends LeaderboardEvent { - /// {@macro leaderboard_entry_added} - const LeaderboardEntryAdded({required this.entry}); - - /// [LeaderboardEntryData] to be written to the remote storage. - final LeaderboardEntryData entry; - - @override - List get props => [entry]; -} diff --git a/lib/leaderboard/bloc/leaderboard_state.dart b/lib/leaderboard/bloc/leaderboard_state.dart deleted file mode 100644 index 20d68f0d..00000000 --- a/lib/leaderboard/bloc/leaderboard_state.dart +++ /dev/null @@ -1,59 +0,0 @@ -// ignore_for_file: public_member_api_docs - -part of 'leaderboard_bloc.dart'; - -/// Defines the request status. -enum LeaderboardStatus { - /// Request is being loaded. - loading, - - /// Request was processed successfully and received a valid response. - success, - - /// Request was processed unsuccessfully and received an error. - error, -} - -/// {@template leaderboard_state} -/// Represents the state of the leaderboard. -/// {@endtemplate} -class LeaderboardState extends Equatable { - /// {@macro leaderboard_state} - const LeaderboardState({ - required this.status, - required this.ranking, - required this.leaderboard, - }); - - const LeaderboardState.initial() - : status = LeaderboardStatus.loading, - ranking = const LeaderboardRanking( - ranking: 0, - outOf: 0, - ), - leaderboard = const []; - - /// The current [LeaderboardStatus] of the state. - final LeaderboardStatus status; - - /// Rank of the current player. - final LeaderboardRanking ranking; - - /// List of top-ranked players. - final List leaderboard; - - @override - List get props => [status, ranking, leaderboard]; - - LeaderboardState copyWith({ - LeaderboardStatus? status, - LeaderboardRanking? ranking, - List? leaderboard, - }) { - return LeaderboardState( - status: status ?? this.status, - ranking: ranking ?? this.ranking, - leaderboard: leaderboard ?? this.leaderboard, - ); - } -} diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart deleted file mode 100644 index 08765743..00000000 --- a/lib/leaderboard/leaderboard.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'bloc/leaderboard_bloc.dart'; -export 'models/leader_board_entry.dart'; -export 'view/leaderboard_page.dart'; diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart deleted file mode 100644 index b9866111..00000000 --- a/lib/leaderboard/view/leaderboard_page.dart +++ /dev/null @@ -1,306 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/select_character/select_character.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -class LeaderboardPage extends StatelessWidget { - const LeaderboardPage({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - static Route route({required CharacterTheme theme}) { - return MaterialPageRoute( - builder: (_) => LeaderboardPage(theme: theme), - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LeaderboardBloc( - context.read(), - )..add(const Top10Fetched()), - child: LeaderboardView(theme: theme), - ); - } -} - -class LeaderboardView extends StatelessWidget { - const LeaderboardView({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 80), - Text( - l10n.leaderboard, - style: Theme.of(context).textTheme.headline3, - ), - const SizedBox(height: 80), - BlocBuilder( - builder: (context, state) { - switch (state.status) { - case LeaderboardStatus.loading: - return _LeaderboardLoading(theme: theme); - case LeaderboardStatus.success: - return _LeaderboardRanking( - ranking: state.leaderboard, - theme: theme, - ); - case LeaderboardStatus.error: - return _LeaderboardError(theme: theme); - } - }, - ), - const SizedBox(height: 20), - TextButton( - onPressed: () => Navigator.of(context).push( - CharacterSelectionDialog.route(), - ), - child: Text(l10n.retry), - ), - ], - ), - ), - ), - ); - } -} - -class _LeaderboardLoading extends StatelessWidget { - const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return const Center( - child: CircularProgressIndicator(), - ); - } -} - -class _LeaderboardError extends StatelessWidget { - const _LeaderboardError({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: Text( - 'There was en error loading data!', - style: - Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red), - ), - ); - } -} - -class _LeaderboardRanking extends StatelessWidget { - const _LeaderboardRanking({ - Key? key, - required this.ranking, - required this.theme, - }) : super(key: key); - - final List ranking; - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardHeaders(theme: theme), - _LeaderboardList( - ranking: ranking, - theme: theme, - ), - ], - ), - ); - } -} - -class _LeaderboardHeaders extends StatelessWidget { - const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardHeaderItem(title: l10n.rank, theme: theme), - _LeaderboardHeaderItem(title: l10n.character, theme: theme), - _LeaderboardHeaderItem(title: l10n.username, theme: theme), - _LeaderboardHeaderItem(title: l10n.score, theme: theme), - ], - ); - } -} - -class _LeaderboardHeaderItem extends StatelessWidget { - const _LeaderboardHeaderItem({ - Key? key, - required this.title, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final String title; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: theme.ballColor, - ), - child: Text( - title, - style: Theme.of(context).textTheme.headline5, - ), - ), - ); - } -} - -class _LeaderboardList extends StatelessWidget { - const _LeaderboardList({ - Key? key, - required this.ranking, - required this.theme, - }) : super(key: key); - - final List ranking; - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return ListView.builder( - shrinkWrap: true, - itemBuilder: (_, index) => _LeaderBoardCompetitor( - entry: ranking[index], - theme: theme, - ), - itemCount: ranking.length, - ); - } -} - -class _LeaderBoardCompetitor extends StatelessWidget { - const _LeaderBoardCompetitor({ - Key? key, - required this.entry, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - - final LeaderboardEntry entry; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardCompetitorField( - text: entry.rank, - theme: theme, - ), - _LeaderboardCompetitorCharacter( - characterAsset: entry.character, - theme: theme, - ), - _LeaderboardCompetitorField( - text: entry.playerInitials, - theme: theme, - ), - _LeaderboardCompetitorField( - text: entry.score.toString(), - theme: theme, - ), - ], - ); - } -} - -class _LeaderboardCompetitorField extends StatelessWidget { - const _LeaderboardCompetitorField({ - Key? key, - required this.text, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final String text; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: theme.ballColor, - width: 2, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(text), - ), - ), - ); - } -} - -class _LeaderboardCompetitorCharacter extends StatelessWidget { - const _LeaderboardCompetitorCharacter({ - Key? key, - required this.characterAsset, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final AssetGenImage characterAsset; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: theme.ballColor, - width: 2, - ), - ), - child: SizedBox( - height: 30, - child: characterAsset.image(), - ), - ), - ); - } -} diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 0e83db8d..863722e6 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -6,6 +6,7 @@ import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; class CharacterSelectionDialog extends StatelessWidget { const CharacterSelectionDialog({Key? key}) : super(key: key); @@ -32,25 +33,31 @@ class CharacterSelectionView extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return PixelatedDecoration( + header: Text( + l10n.characterSelectionTitle, + style: Theme.of(context).textTheme.headline3, + ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 80), - Text( - l10n.characterSelectionTitle, - style: Theme.of(context).textTheme.headline3, - ), - const SizedBox(height: 80), const _CharacterSelectionGridView(), const SizedBox(height: 20), TextButton( onPressed: () { Navigator.of(context).pop(); + // TODO(arturplaczek): remove after merge StarBlocListener + final height = MediaQuery.of(context).size.height * 0.5; showDialog( context: context, - builder: (_) => const HowToPlayDialog(), + builder: (_) => Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: HowToPlayDialog(), + ), + ), ); }, child: Text(l10n.start), diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index aed7a3e3..1665d35d 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -1,27 +1,114 @@ // ignore_for_file: public_member_api_docs +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; +import 'package:platform_helper/platform_helper.dart'; + +@visibleForTesting +enum Control { + left, + right, + down, + a, + d, + s, + space, +} + +extension on Control { + bool get isArrow => isDown || isRight || isLeft; + + bool get isDown => this == Control.down; + + bool get isRight => this == Control.right; + + bool get isLeft => this == Control.left; + + bool get isSpace => this == Control.space; + + String getCharacter(BuildContext context) { + switch (this) { + case Control.a: + return 'A'; + case Control.d: + return 'D'; + case Control.down: + return '>'; // Will be rotated + case Control.left: + return '<'; + case Control.right: + return '>'; + case Control.s: + return 'S'; + case Control.space: + return context.l10n.space; + } + } +} -class HowToPlayDialog extends StatelessWidget { - const HowToPlayDialog({Key? key}) : super(key: key); +class HowToPlayDialog extends StatefulWidget { + HowToPlayDialog({ + Key? key, + @visibleForTesting PlatformHelper? platformHelper, + }) : platformHelper = platformHelper ?? PlatformHelper(), + super(key: key); + + final PlatformHelper platformHelper; + + @override + State createState() => _HowToPlayDialogState(); +} + +class _HowToPlayDialogState extends State { + late Timer closeTimer; + @override + void initState() { + super.initState(); + closeTimer = Timer(const Duration(seconds: 3), () { + if (mounted) { + Navigator.of(context).maybePop(); + } + }); + } + + @override + void dispose() { + closeTimer.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { - final l10n = context.l10n; - const spacing = SizedBox(height: 16); + final isMobile = widget.platformHelper.isMobile; + return PixelatedDecoration( + header: const _HowToPlayHeader(), + body: isMobile ? const _MobileBody() : const _DesktopBody(), + ); + } +} + +class _MobileBody extends StatelessWidget { + const _MobileBody({Key? key}) : super(key: key); - return Dialog( + @override + Widget build(BuildContext context) { + final paddingWidth = MediaQuery.of(context).size.width * 0.15; + final paddingHeight = MediaQuery.of(context).size.height * 0.075; + return FittedBox( child: Padding( - padding: const EdgeInsets.all(20), + padding: EdgeInsets.symmetric( + horizontal: paddingWidth, + ), child: Column( - mainAxisSize: MainAxisSize.min, children: [ - Text(l10n.howToPlay), - spacing, - const _LaunchControls(), - spacing, - const _FlipperControls(), + const _MobileLaunchControls(), + SizedBox(height: paddingHeight), + const _MobileFlipperControls(), ], ), ), @@ -29,8 +116,121 @@ class HowToPlayDialog extends StatelessWidget { } } -class _LaunchControls extends StatelessWidget { - const _LaunchControls({Key? key}) : super(key: key); +class _MobileLaunchControls extends StatelessWidget { + const _MobileLaunchControls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const textStyle = AppTextStyle.subtitle3; + return Column( + children: [ + Text( + l10n.tapAndHoldRocket, + style: textStyle, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${l10n.to} ', + style: textStyle, + ), + TextSpan( + text: l10n.launch, + style: textStyle.copyWith( + color: AppColors.blue, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _MobileFlipperControls extends StatelessWidget { + const _MobileFlipperControls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const textStyle = AppTextStyle.subtitle3; + return Column( + children: [ + Text( + l10n.tapLeftRightScreen, + style: textStyle, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${l10n.to} ', + style: textStyle, + ), + TextSpan( + text: l10n.flip, + style: textStyle.copyWith( + color: AppColors.orange, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _DesktopBody extends StatelessWidget { + const _DesktopBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + const spacing = SizedBox(height: 16); + return ListView( + children: const [ + spacing, + _DesktopLaunchControls(), + spacing, + _DesktopFlipperControls(), + ], + ); + } +} + +class _HowToPlayHeader extends StatelessWidget { + const _HowToPlayHeader({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const headerTextStyle = AppTextStyle.title; + + return FittedBox( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.howToPlay, + style: headerTextStyle.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + l10n.tipsForFlips, + style: headerTextStyle, + ), + ], + ), + ); + } +} + +class _DesktopLaunchControls extends StatelessWidget { + const _DesktopLaunchControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -39,17 +239,18 @@ class _LaunchControls extends StatelessWidget { return Column( children: [ - Text(l10n.launchControls), + Text( + l10n.launchControls, + style: AppTextStyle.headline4, + ), const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + Wrap( children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down), + KeyButton(control: Control.down), spacing, - KeyIndicator.fromKeyName(keyName: 'SPACE'), + KeyButton(control: Control.space), spacing, - KeyIndicator.fromKeyName(keyName: 'S'), + KeyButton(control: Control.s), ], ) ], @@ -57,8 +258,8 @@ class _LaunchControls extends StatelessWidget { } } -class _FlipperControls extends StatelessWidget { - const _FlipperControls({Key? key}) : super(key: key); +class _DesktopFlipperControls extends StatelessWidget { + const _DesktopFlipperControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -67,7 +268,10 @@ class _FlipperControls extends StatelessWidget { return Column( children: [ - Text(l10n.flipperControls), + Text( + l10n.flipperControls, + style: AppTextStyle.subtitle2, + ), const SizedBox(height: 10), Column( children: [ @@ -75,19 +279,17 @@ class _FlipperControls extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), + KeyButton(control: Control.left), rowSpacing, - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right), + KeyButton(control: Control.right), ], ), const SizedBox(height: 8), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + Wrap( children: const [ - KeyIndicator.fromKeyName(keyName: 'A'), + KeyButton(control: Control.a), rowSpacing, - KeyIndicator.fromKeyName(keyName: 'D'), + KeyButton(control: Control.d), ], ) ], @@ -97,65 +299,46 @@ class _FlipperControls extends StatelessWidget { } } -// TODO(allisonryan0002): remove visibility when adding final UI. @visibleForTesting -class KeyIndicator extends StatelessWidget { - const KeyIndicator._({ +class KeyButton extends StatelessWidget { + const KeyButton({ Key? key, - required String keyName, - required IconData keyIcon, - required bool fromIcon, - }) : _keyName = keyName, - _keyIcon = keyIcon, - _fromIcon = fromIcon, + required Control control, + }) : _control = control, super(key: key); - const KeyIndicator.fromKeyName({Key? key, required String keyName}) - : this._( - key: key, - keyName: keyName, - keyIcon: Icons.keyboard_arrow_down, - fromIcon: false, - ); - - const KeyIndicator.fromIcon({Key? key, required IconData keyIcon}) - : this._( - key: key, - keyName: '', - keyIcon: keyIcon, - fromIcon: true, - ); - - final String _keyName; - - final IconData _keyIcon; - - final bool _fromIcon; + final Control _control; @override Widget build(BuildContext context) { - const iconPadding = EdgeInsets.all(15); - const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22); - final boarderColor = Colors.blue.withOpacity(0.5); - final color = Colors.blue.withOpacity(0.7); - + final textStyle = + _control.isArrow ? AppTextStyle.headline1 : AppTextStyle.headline3; + const height = 60.0; + final width = _control.isSpace ? height * 2.83 : height; return DecoratedBox( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: boarderColor, - width: 3, + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage( + _control.isSpace + ? Assets.images.components.space.keyName + : Assets.images.components.key.keyName, + ), ), ), - child: _fromIcon - ? Padding( - padding: iconPadding, - child: Icon(_keyIcon, color: color), - ) - : Padding( - padding: textPadding, - child: Text(_keyName, style: TextStyle(color: color)), + child: SizedBox( + width: width, + height: height, + child: Center( + child: RotatedBox( + quarterTurns: _control.isDown ? 1 : 0, + child: Text( + _control.getCharacter(context), + style: textStyle.copyWith(color: AppColors.white), ), + ), + ), + ), ); } } diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index 2d3899a6..a12d3edc 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -7,7 +7,9 @@ abstract class AppColors { static const Color darkBlue = Color(0xFF0C32A4); - static const Color orange = Color(0xFFFFEE02); + static const Color yellow = Color(0xFFFFEE02); + + static const Color orange = Color(0xFFE5AB05); static const Color blue = Color(0xFF4B94F6); diff --git a/lib/theme/app_text_style.dart b/lib/theme/app_text_style.dart index 8104ca11..084936e9 100644 --- a/lib/theme/app_text_style.dart +++ b/lib/theme/app_text_style.dart @@ -27,6 +27,35 @@ abstract class AppTextStyle { fontFamily: _primaryFontFamily, ); + static const headline4 = TextStyle( + color: AppColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const title = TextStyle( + color: AppColors.darkBlue, + fontSize: 20, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle3 = TextStyle( + color: AppColors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle2 = TextStyle( + color: AppColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + static const subtitle1 = TextStyle( fontSize: 10, fontFamily: _primaryFontFamily, diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 9d8b2434..c522584c 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -1,91 +1,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; -/// {@template leaderboard_exception} -/// Base exception for leaderboard repository failures. -/// {@endtemplate} -abstract class LeaderboardException implements Exception { - /// {@macro leaderboard_exception} - const LeaderboardException(this.error, this.stackTrace); - - /// The error that was caught. - final Object error; - - /// The Stacktrace associated with the [error]. - final StackTrace stackTrace; -} - -/// {@template leaderboard_deserialization_exception} -/// Exception thrown when leaderboard data cannot be deserialized in the -/// expected way. -/// {@endtemplate} -class LeaderboardDeserializationException extends LeaderboardException { - /// {@macro leaderboard_deserialization_exception} - const LeaderboardDeserializationException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template fetch_top_10_leaderboard_exception} -/// Exception thrown when failure occurs while fetching top 10 leaderboard. -/// {@endtemplate} -class FetchTop10LeaderboardException extends LeaderboardException { - /// {@macro fetch_top_10_leaderboard_exception} - const FetchTop10LeaderboardException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template add_leaderboard_entry_exception} -/// Exception thrown when failure occurs while adding entry to leaderboard. -/// {@endtemplate} -class AddLeaderboardEntryException extends LeaderboardException { - /// {@macro add_leaderboard_entry_exception} - const AddLeaderboardEntryException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template fetch_player_ranking_exception} -/// Exception thrown when failure occurs while fetching player ranking. -/// {@endtemplate} -class FetchPlayerRankingException extends LeaderboardException { - /// {@macro fetch_player_ranking_exception} - const FetchPlayerRankingException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template fetch_prohibited_initials_exception} -/// Exception thrown when failure occurs while fetching prohibited initials. -/// {@endtemplate} -class FetchProhibitedInitialsException extends LeaderboardException { - /// {@macro fetch_prohibited_initials_exception} - const FetchProhibitedInitialsException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - /// {@template leaderboard_repository} /// Repository to access leaderboard data in Firebase Cloud Firestore. /// {@endtemplate} @@ -97,73 +12,40 @@ class LeaderboardRepository { final FirebaseFirestore _firebaseFirestore; + static const _leaderboardLimit = 10; + static const _leaderboardCollectionName = 'leaderboard'; + static const _scoreFieldName = 'score'; + /// Acquires top 10 [LeaderboardEntryData]s. Future> fetchTop10Leaderboard() async { - final leaderboardEntries = []; - late List documents; - try { final querySnapshot = await _firebaseFirestore - .collection('leaderboard') - .orderBy('score', descending: true) - .limit(10) + .collection(_leaderboardCollectionName) + .orderBy(_scoreFieldName, descending: true) + .limit(_leaderboardLimit) .get(); - documents = querySnapshot.docs; + final documents = querySnapshot.docs; + return documents.toLeaderboard(); + } on LeaderboardDeserializationException { + rethrow; } on Exception catch (error, stackTrace) { throw FetchTop10LeaderboardException(error, stackTrace); } - - for (final document in documents) { - final data = document.data() as Map?; - if (data != null) { - try { - leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); - } catch (error, stackTrace) { - throw LeaderboardDeserializationException(error, stackTrace); - } - } - } - - return leaderboardEntries; } - /// Adds player's score entry to the leaderboard and gets their - /// [LeaderboardRanking]. - Future addLeaderboardEntry( + /// Adds player's score entry to the leaderboard if it is within the top-10 + Future addLeaderboardEntry( LeaderboardEntryData entry, ) async { - late DocumentReference entryReference; - try { - entryReference = await _firebaseFirestore - .collection('leaderboard') - .add(entry.toJson()); - } on Exception catch (error, stackTrace) { - throw AddLeaderboardEntryException(error, stackTrace); - } - - try { - final querySnapshot = await _firebaseFirestore - .collection('leaderboard') - .orderBy('score', descending: true) - .get(); - - // TODO(allisonryan0002): see if we can find a more performant solution. - final documents = querySnapshot.docs; - final ranking = documents.indexWhere( - (document) => document.id == entryReference.id, - ) + - 1; - - if (ranking > 0) { - return LeaderboardRanking(ranking: ranking, outOf: documents.length); - } else { - throw FetchPlayerRankingException( - 'Player score could not be found and ranking cannot be provided.', - StackTrace.current, - ); + final leaderboard = await _fetchLeaderboardSortedByScore(); + if (leaderboard.length < 10) { + await _saveScore(entry); + } else { + final tenthPositionScore = leaderboard[9].score; + if (entry.score > tenthPositionScore) { + await _saveScore(entry); + await _deleteScoresUnder(tenthPositionScore); } - } on Exception catch (error, stackTrace) { - throw FetchPlayerRankingException(error, stackTrace); } } @@ -174,7 +56,6 @@ class LeaderboardRepository { if (!initialsRegex.hasMatch(initials)) { return false; } - try { final document = await _firebaseFirestore .collection('prohibitedInitials') @@ -187,4 +68,61 @@ class LeaderboardRepository { throw FetchProhibitedInitialsException(error, stackTrace); } } + + Future> _fetchLeaderboardSortedByScore() async { + try { + final querySnapshot = await _firebaseFirestore + .collection(_leaderboardCollectionName) + .orderBy(_scoreFieldName, descending: true) + .get(); + final documents = querySnapshot.docs; + return documents.toLeaderboard(); + } on Exception catch (error, stackTrace) { + throw FetchLeaderboardException(error, stackTrace); + } + } + + Future _saveScore(LeaderboardEntryData entry) { + try { + return _firebaseFirestore + .collection(_leaderboardCollectionName) + .add(entry.toJson()); + } on Exception catch (error, stackTrace) { + throw AddLeaderboardEntryException(error, stackTrace); + } + } + + Future _deleteScoresUnder(int score) async { + try { + final querySnapshot = await _firebaseFirestore + .collection(_leaderboardCollectionName) + .where(_scoreFieldName, isLessThanOrEqualTo: score) + .get(); + final documents = querySnapshot.docs; + for (final document in documents) { + await document.reference.delete(); + } + } on LeaderboardDeserializationException { + rethrow; + } on Exception catch (error, stackTrace) { + throw DeleteLeaderboardException(error, stackTrace); + } + } +} + +extension on List { + List toLeaderboard() { + final leaderboardEntries = []; + for (final document in this) { + final data = document.data() as Map?; + if (data != null) { + try { + leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); + } catch (error, stackTrace) { + throw LeaderboardDeserializationException(error, stackTrace); + } + } + } + return leaderboardEntries; + } } diff --git a/packages/leaderboard_repository/lib/src/models/exceptions.dart b/packages/leaderboard_repository/lib/src/models/exceptions.dart new file mode 100644 index 00000000..f709a27e --- /dev/null +++ b/packages/leaderboard_repository/lib/src/models/exceptions.dart @@ -0,0 +1,69 @@ +/// {@template leaderboard_exception} +/// Base exception for leaderboard repository failures. +/// {@endtemplate} +abstract class LeaderboardException implements Exception { + /// {@macro leaderboard_exception} + const LeaderboardException(this.error, this.stackTrace); + + /// The error that was caught. + final Object error; + + /// The Stacktrace associated with the [error]. + final StackTrace stackTrace; +} + +/// {@template leaderboard_deserialization_exception} +/// Exception thrown when leaderboard data cannot be deserialized in the +/// expected way. +/// {@endtemplate} +class LeaderboardDeserializationException extends LeaderboardException { + /// {@macro leaderboard_deserialization_exception} + const LeaderboardDeserializationException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template fetch_top_10_leaderboard_exception} +/// Exception thrown when failure occurs while fetching top 10 leaderboard. +/// {@endtemplate} +class FetchTop10LeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const FetchTop10LeaderboardException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template fetch_leaderboard_exception} +/// Exception thrown when failure occurs while fetching the leaderboard. +/// {@endtemplate} +class FetchLeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const FetchLeaderboardException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template delete_leaderboard_exception} +/// Exception thrown when failure occurs while deleting the leaderboard under +/// the tenth position. +/// {@endtemplate} +class DeleteLeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const DeleteLeaderboardException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template add_leaderboard_entry_exception} +/// Exception thrown when failure occurs while adding entry to leaderboard. +/// {@endtemplate} +class AddLeaderboardEntryException extends LeaderboardException { + /// {@macro add_leaderboard_entry_exception} + const AddLeaderboardEntryException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template fetch_prohibited_initials_exception} +/// Exception thrown when failure occurs while fetching prohibited initials. +/// {@endtemplate} +class FetchProhibitedInitialsException extends LeaderboardException { + /// {@macro fetch_prohibited_initials_exception} + const FetchProhibitedInitialsException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart deleted file mode 100644 index 4a322e00..00000000 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; - -/// {@template leaderboard_ranking} -/// Contains [ranking] for a single [LeaderboardEntryData] and the number of -/// players the [ranking] is [outOf]. -/// {@endtemplate} -class LeaderboardRanking extends Equatable { - /// {@macro leaderboard_ranking} - const LeaderboardRanking({required this.ranking, required this.outOf}); - - /// Place ranking by score for a [LeaderboardEntryData]. - final int ranking; - - /// Number of [LeaderboardEntryData]s at the time of score entry. - final int outOf; - - @override - List get props => [ranking, outOf]; -} diff --git a/packages/leaderboard_repository/lib/src/models/models.dart b/packages/leaderboard_repository/lib/src/models/models.dart index e10a743b..a612f3ac 100644 --- a/packages/leaderboard_repository/lib/src/models/models.dart +++ b/packages/leaderboard_repository/lib/src/models/models.dart @@ -1,2 +1,2 @@ +export 'exceptions.dart'; export 'leaderboard_entry_data.dart'; -export 'leaderboard_ranking.dart'; diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index 9d31983f..af3c5fa3 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -153,7 +153,6 @@ void main() { character: CharacterType.dash, ); const entryDocumentId = 'id$entryScore'; - final ranking = LeaderboardRanking(ranking: 3, outOf: 4); setUp(() { leaderboardRepository = LeaderboardRepository(firestore); @@ -165,13 +164,12 @@ void main() { final queryDocumentSnapshot = MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', - 'username': 'user$score', + 'playerInitials': 'AAA', 'score': score }); when(() => queryDocumentSnapshot.id).thenReturn('id$score'); return queryDocumentSnapshot; }).toList(); - when(() => firestore.collection('leaderboard')) .thenAnswer((_) => collectionReference); when(() => collectionReference.add(any())) @@ -184,19 +182,29 @@ void main() { }); test( - 'adds leaderboard entry and returns player ranking when ' - 'firestore operations succeed', () async { - final rankingResult = - await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); - - expect(rankingResult, equals(ranking)); + 'throws FetchLeaderboardException ' + 'when querying the leaderboard fails', () { + when(() => firestore.collection('leaderboard')).thenThrow(Exception()); + expect( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + throwsA(isA()), + ); }); test( - 'throws AddLeaderboardEntryException when Exception occurs ' - 'when trying to add entry to firestore', () async { - when(() => firestore.collection('leaderboard')).thenThrow(Exception()); + 'saves the new score if the existing leaderboard ' + 'has less than 10 scores', () async { + await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); + verify( + () => collectionReference.add(leaderboardEntry.toJson()), + ).called(1); + }); + test( + 'throws AddLeaderboardEntryException ' + 'when adding a new entry fails', () async { + when(() => collectionReference.add(leaderboardEntry.toJson())) + .thenThrow(Exception('oops')); expect( () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), throwsA(isA()), @@ -204,26 +212,160 @@ void main() { }); test( - 'throws FetchPlayerRankingException when Exception occurs ' - 'when trying to retrieve information from firestore', () async { - when(() => collectionReference.orderBy('score', descending: true)) - .thenThrow(Exception()); + 'does nothing if there are more than 10 scores in the leaderboard ' + 'and the new score is smaller than the top 10', () async { + final leaderboardScores = [ + 10000, + 9500, + 9000, + 8500, + 8000, + 7500, + 7000, + 6500, + 6000, + 5500, + 5000 + ]; + final queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + return queryDocumentSnapshot; + }).toList(); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); - expect( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - throwsA(isA()), + await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); + verifyNever( + () => collectionReference.add(leaderboardEntry.toJson()), ); }); test( - 'throws FetchPlayerRankingException when score cannot be found ' - 'in firestore leaderboard data', () async { - when(() => documentReference.id).thenReturn('nonexistentDocumentId'); - + 'throws DeleteLeaderboardException ' + 'when deleting scores outside the top 10 fails', () async { + final deleteQuery = MockQuery(); + final deleteQuerySnapshot = MockQuerySnapshot(); + final newScore = LeaderboardEntryData( + playerInitials: 'ABC', + score: 15000, + character: CharacterType.android, + ); + final leaderboardScores = [ + 10000, + 9500, + 9000, + 8500, + 8000, + 7500, + 7000, + 6500, + 6000, + 5500, + 5000, + ]; + final deleteDocumentSnapshots = [5500, 5000].map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when(deleteQuery.get).thenAnswer((_) async => deleteQuerySnapshot); + when(() => deleteQuerySnapshot.docs) + .thenReturn(deleteDocumentSnapshots); + final queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when( + () => collectionReference.where('score', isLessThanOrEqualTo: 5500), + ).thenAnswer((_) => deleteQuery); + when(() => documentReference.delete()).thenThrow(Exception('oops')); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); expect( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - throwsA(isA()), + () => leaderboardRepository.addLeaderboardEntry(newScore), + throwsA(isA()), + ); + }); + + test( + 'saves the new score when there are more than 10 scores in the ' + 'leaderboard and the new score is higher than the lowest top 10, and ' + 'deletes the scores that are not in the top 10 anymore', () async { + final deleteQuery = MockQuery(); + final deleteQuerySnapshot = MockQuerySnapshot(); + final newScore = LeaderboardEntryData( + playerInitials: 'ABC', + score: 15000, + character: CharacterType.android, ); + final leaderboardScores = [ + 10000, + 9500, + 9000, + 8500, + 8000, + 7500, + 7000, + 6500, + 6000, + 5500, + 5000, + ]; + final deleteDocumentSnapshots = [5500, 5000].map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when(deleteQuery.get).thenAnswer((_) async => deleteQuerySnapshot); + when(() => deleteQuerySnapshot.docs) + .thenReturn(deleteDocumentSnapshots); + final queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when( + () => collectionReference.where('score', isLessThanOrEqualTo: 5500), + ).thenAnswer((_) => deleteQuery); + when(() => documentReference.delete()) + .thenAnswer((_) async => Future.value()); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); + await leaderboardRepository.addLeaderboardEntry(newScore); + verify(() => collectionReference.add(newScore.toJson())).called(1); + verify(() => documentReference.delete()).called(2); }); }); diff --git a/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart b/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart deleted file mode 100644 index 577251e4..00000000 --- a/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:test/test.dart'; - -void main() { - group('LeaderboardRanking', () { - test('can be instantiated', () { - const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1); - - expect(leaderboardRanking, isNotNull); - }); - - test('supports value equality.', () { - const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1); - const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1); - - expect(leaderboardRanking, equals(leaderboardRanking2)); - }); - }); -} diff --git a/packages/pinball_components/assets/images/android_bumper/a/dimmed.png b/packages/pinball_components/assets/images/android/bumper/a/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/android_bumper/a/dimmed.png rename to packages/pinball_components/assets/images/android/bumper/a/dimmed.png diff --git a/packages/pinball_components/assets/images/android_bumper/a/lit.png b/packages/pinball_components/assets/images/android/bumper/a/lit.png similarity index 100% rename from packages/pinball_components/assets/images/android_bumper/a/lit.png rename to packages/pinball_components/assets/images/android/bumper/a/lit.png diff --git a/packages/pinball_components/assets/images/android_bumper/b/dimmed.png b/packages/pinball_components/assets/images/android/bumper/b/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/android_bumper/b/dimmed.png rename to packages/pinball_components/assets/images/android/bumper/b/dimmed.png diff --git a/packages/pinball_components/assets/images/android_bumper/b/lit.png b/packages/pinball_components/assets/images/android/bumper/b/lit.png similarity index 100% rename from packages/pinball_components/assets/images/android_bumper/b/lit.png rename to packages/pinball_components/assets/images/android/bumper/b/lit.png diff --git a/packages/pinball_components/assets/images/android/bumper/cow/dimmed.png b/packages/pinball_components/assets/images/android/bumper/cow/dimmed.png new file mode 100644 index 00000000..6a8bb146 Binary files /dev/null and b/packages/pinball_components/assets/images/android/bumper/cow/dimmed.png differ diff --git a/packages/pinball_components/assets/images/android/bumper/cow/lit.png b/packages/pinball_components/assets/images/android/bumper/cow/lit.png new file mode 100644 index 00000000..4909708b Binary files /dev/null and b/packages/pinball_components/assets/images/android/bumper/cow/lit.png differ diff --git a/packages/pinball_components/assets/images/spaceship/rail/exit.png b/packages/pinball_components/assets/images/android/rail/exit.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/rail/exit.png rename to packages/pinball_components/assets/images/android/rail/exit.png diff --git a/packages/pinball_components/assets/images/spaceship/rail/main.png b/packages/pinball_components/assets/images/android/rail/main.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/rail/main.png rename to packages/pinball_components/assets/images/android/rail/main.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png b/packages/pinball_components/assets/images/android/ramp/arrow/active1.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active1.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png b/packages/pinball_components/assets/images/android/ramp/arrow/active2.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active2.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png b/packages/pinball_components/assets/images/android/ramp/arrow/active3.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active3.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png b/packages/pinball_components/assets/images/android/ramp/arrow/active4.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active4.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png b/packages/pinball_components/assets/images/android/ramp/arrow/active5.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active5.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png b/packages/pinball_components/assets/images/android/ramp/arrow/inactive.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png rename to packages/pinball_components/assets/images/android/ramp/arrow/inactive.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/board-opening.png b/packages/pinball_components/assets/images/android/ramp/board-opening.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/board-opening.png rename to packages/pinball_components/assets/images/android/ramp/board-opening.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/main.png b/packages/pinball_components/assets/images/android/ramp/main.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/main.png rename to packages/pinball_components/assets/images/android/ramp/main.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/railing-background.png b/packages/pinball_components/assets/images/android/ramp/railing-background.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/railing-background.png rename to packages/pinball_components/assets/images/android/ramp/railing-background.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png b/packages/pinball_components/assets/images/android/ramp/railing-foreground.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png rename to packages/pinball_components/assets/images/android/ramp/railing-foreground.png diff --git a/packages/pinball_components/assets/images/android/spaceship/animatronic.png b/packages/pinball_components/assets/images/android/spaceship/animatronic.png new file mode 100644 index 00000000..d4b165f3 Binary files /dev/null and b/packages/pinball_components/assets/images/android/spaceship/animatronic.png differ diff --git a/packages/pinball_components/assets/images/android/spaceship/light-beam.png b/packages/pinball_components/assets/images/android/spaceship/light-beam.png new file mode 100644 index 00000000..eb33725d Binary files /dev/null and b/packages/pinball_components/assets/images/android/spaceship/light-beam.png differ diff --git a/packages/pinball_components/assets/images/android/spaceship/saucer.png b/packages/pinball_components/assets/images/android/spaceship/saucer.png new file mode 100644 index 00000000..6c77525a Binary files /dev/null and b/packages/pinball_components/assets/images/android/spaceship/saucer.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x2/dimmed.png b/packages/pinball_components/assets/images/multiplier/x2/dimmed.png new file mode 100644 index 00000000..7cc9fc4f Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x2/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x2/lit.png b/packages/pinball_components/assets/images/multiplier/x2/lit.png new file mode 100644 index 00000000..be2b3f08 Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x2/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x3/dimmed.png b/packages/pinball_components/assets/images/multiplier/x3/dimmed.png new file mode 100644 index 00000000..460b1a0e Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x3/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x3/lit.png b/packages/pinball_components/assets/images/multiplier/x3/lit.png new file mode 100644 index 00000000..7fdedbbe Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x3/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x4/dimmed.png b/packages/pinball_components/assets/images/multiplier/x4/dimmed.png new file mode 100644 index 00000000..e8a6256e Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x4/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x4/lit.png b/packages/pinball_components/assets/images/multiplier/x4/lit.png new file mode 100644 index 00000000..5beceabb Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x4/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x5/dimmed.png b/packages/pinball_components/assets/images/multiplier/x5/dimmed.png new file mode 100644 index 00000000..96e018e4 Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x5/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x5/lit.png b/packages/pinball_components/assets/images/multiplier/x5/lit.png new file mode 100644 index 00000000..23fd3aab Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x5/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x6/dimmed.png b/packages/pinball_components/assets/images/multiplier/x6/dimmed.png new file mode 100644 index 00000000..d518e1eb Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x6/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x6/lit.png b/packages/pinball_components/assets/images/multiplier/x6/lit.png new file mode 100644 index 00000000..54244bab Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x6/lit.png differ diff --git a/packages/pinball_components/assets/images/plunger/rocket.png b/packages/pinball_components/assets/images/plunger/rocket.png index ee5eef5b..bef65ea1 100644 Binary files a/packages/pinball_components/assets/images/plunger/rocket.png and b/packages/pinball_components/assets/images/plunger/rocket.png differ diff --git a/packages/pinball_components/assets/images/spaceship/bridge.png b/packages/pinball_components/assets/images/spaceship/bridge.png deleted file mode 100644 index 6ebb143e..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/bridge.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/spaceship/saucer.png b/packages/pinball_components/assets/images/spaceship/saucer.png deleted file mode 100644 index 4cd65522..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/saucer.png and /dev/null differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 1a272d31..3c7b2454 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -10,8 +10,7 @@ import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); - $AssetsImagesAndroidBumperGen get androidBumper => - const $AssetsImagesAndroidBumperGen(); + $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); @@ -24,20 +23,23 @@ class $AssetsImagesGen { $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesMultiplierGen get multiplier => + const $AssetsImagesMultiplierGen(); $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(); } -class $AssetsImagesAndroidBumperGen { - const $AssetsImagesAndroidBumperGen(); +class $AssetsImagesAndroidGen { + const $AssetsImagesAndroidGen(); - $AssetsImagesAndroidBumperAGen get a => - const $AssetsImagesAndroidBumperAGen(); - $AssetsImagesAndroidBumperBGen get b => - const $AssetsImagesAndroidBumperBGen(); + $AssetsImagesAndroidBumperGen get bumper => + const $AssetsImagesAndroidBumperGen(); + $AssetsImagesAndroidRailGen get rail => const $AssetsImagesAndroidRailGen(); + $AssetsImagesAndroidRampGen get ramp => const $AssetsImagesAndroidRampGen(); + $AssetsImagesAndroidSpaceshipGen get spaceship => + const $AssetsImagesAndroidSpaceshipGen(); } class $AssetsImagesBackboardGen { @@ -188,6 +190,16 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesMultiplierGen { + const $AssetsImagesMultiplierGen(); + + $AssetsImagesMultiplierX2Gen get x2 => const $AssetsImagesMultiplierX2Gen(); + $AssetsImagesMultiplierX3Gen get x3 => const $AssetsImagesMultiplierX3Gen(); + $AssetsImagesMultiplierX4Gen get x4 => const $AssetsImagesMultiplierX4Gen(); + $AssetsImagesMultiplierX5Gen get x5 => const $AssetsImagesMultiplierX5Gen(); + $AssetsImagesMultiplierX6Gen get x6 => const $AssetsImagesMultiplierX6Gen(); +} + class $AssetsImagesPlungerGen { const $AssetsImagesPlungerGen(); @@ -232,56 +244,79 @@ class $AssetsImagesSlingshotGen { const AssetGenImage('assets/images/slingshot/upper.png'); } -class $AssetsImagesSpaceshipGen { - const $AssetsImagesSpaceshipGen(); - - /// File path: assets/images/spaceship/bridge.png - AssetGenImage get bridge => - const AssetGenImage('assets/images/spaceship/bridge.png'); - - $AssetsImagesSpaceshipRailGen get rail => - const $AssetsImagesSpaceshipRailGen(); - $AssetsImagesSpaceshipRampGen get ramp => - const $AssetsImagesSpaceshipRampGen(); - - /// File path: assets/images/spaceship/saucer.png - AssetGenImage get saucer => - const AssetGenImage('assets/images/spaceship/saucer.png'); -} - class $AssetsImagesSparkyGen { const $AssetsImagesSparkyGen(); + /// File path: assets/images/sparky/animatronic.png AssetGenImage get animatronic => const AssetGenImage('assets/images/sparky/animatronic.png'); + $AssetsImagesSparkyBumperGen get bumper => const $AssetsImagesSparkyBumperGen(); $AssetsImagesSparkyComputerGen get computer => const $AssetsImagesSparkyComputerGen(); } -class $AssetsImagesAndroidBumperAGen { - const $AssetsImagesAndroidBumperAGen(); +class $AssetsImagesAndroidBumperGen { + const $AssetsImagesAndroidBumperGen(); - /// File path: assets/images/android_bumper/a/dimmed.png - AssetGenImage get dimmed => - const AssetGenImage('assets/images/android_bumper/a/dimmed.png'); + $AssetsImagesAndroidBumperAGen get a => + const $AssetsImagesAndroidBumperAGen(); + $AssetsImagesAndroidBumperBGen get b => + const $AssetsImagesAndroidBumperBGen(); + $AssetsImagesAndroidBumperCowGen get cow => + const $AssetsImagesAndroidBumperCowGen(); +} - /// File path: assets/images/android_bumper/a/lit.png - AssetGenImage get lit => - const AssetGenImage('assets/images/android_bumper/a/lit.png'); +class $AssetsImagesAndroidRailGen { + const $AssetsImagesAndroidRailGen(); + + /// File path: assets/images/android/rail/exit.png + AssetGenImage get exit => + const AssetGenImage('assets/images/android/rail/exit.png'); + + /// File path: assets/images/android/rail/main.png + AssetGenImage get main => + const AssetGenImage('assets/images/android/rail/main.png'); } -class $AssetsImagesAndroidBumperBGen { - const $AssetsImagesAndroidBumperBGen(); +class $AssetsImagesAndroidRampGen { + const $AssetsImagesAndroidRampGen(); - /// File path: assets/images/android_bumper/b/dimmed.png - AssetGenImage get dimmed => - const AssetGenImage('assets/images/android_bumper/b/dimmed.png'); + $AssetsImagesAndroidRampArrowGen get arrow => + const $AssetsImagesAndroidRampArrowGen(); - /// File path: assets/images/android_bumper/b/lit.png - AssetGenImage get lit => - const AssetGenImage('assets/images/android_bumper/b/lit.png'); + /// File path: assets/images/android/ramp/board-opening.png + AssetGenImage get boardOpening => + const AssetGenImage('assets/images/android/ramp/board-opening.png'); + + /// File path: assets/images/android/ramp/main.png + AssetGenImage get main => + const AssetGenImage('assets/images/android/ramp/main.png'); + + /// File path: assets/images/android/ramp/railing-background.png + AssetGenImage get railingBackground => + const AssetGenImage('assets/images/android/ramp/railing-background.png'); + + /// File path: assets/images/android/ramp/railing-foreground.png + AssetGenImage get railingForeground => + const AssetGenImage('assets/images/android/ramp/railing-foreground.png'); +} + +class $AssetsImagesAndroidSpaceshipGen { + const $AssetsImagesAndroidSpaceshipGen(); + + /// File path: assets/images/android/spaceship/animatronic.png + AssetGenImage get animatronic => + const AssetGenImage('assets/images/android/spaceship/animatronic.png'); + + /// File path: assets/images/android/spaceship/light-beam.png + AssetGenImage get lightBeam => + const AssetGenImage('assets/images/android/spaceship/light-beam.png'); + + /// File path: assets/images/android/spaceship/saucer.png + AssetGenImage get saucer => + const AssetGenImage('assets/images/android/spaceship/saucer.png'); } class $AssetsImagesDashBumperGen { @@ -305,39 +340,64 @@ class $AssetsImagesDinoAnimatronicGen { const AssetGenImage('assets/images/dino/animatronic/mouth.png'); } -class $AssetsImagesSpaceshipRailGen { - const $AssetsImagesSpaceshipRailGen(); +class $AssetsImagesMultiplierX2Gen { + const $AssetsImagesMultiplierX2Gen(); - /// File path: assets/images/spaceship/rail/exit.png - AssetGenImage get exit => - const AssetGenImage('assets/images/spaceship/rail/exit.png'); + /// File path: assets/images/multiplier/x2/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x2/dimmed.png'); - /// File path: assets/images/spaceship/rail/main.png - AssetGenImage get main => - const AssetGenImage('assets/images/spaceship/rail/main.png'); + /// File path: assets/images/multiplier/x2/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x2/lit.png'); } -class $AssetsImagesSpaceshipRampGen { - const $AssetsImagesSpaceshipRampGen(); +class $AssetsImagesMultiplierX3Gen { + const $AssetsImagesMultiplierX3Gen(); - $AssetsImagesSpaceshipRampArrowGen get arrow => - const $AssetsImagesSpaceshipRampArrowGen(); + /// File path: assets/images/multiplier/x3/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x3/dimmed.png'); - /// File path: assets/images/spaceship/ramp/board-opening.png - AssetGenImage get boardOpening => - const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); + /// File path: assets/images/multiplier/x3/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x3/lit.png'); +} - /// File path: assets/images/spaceship/ramp/main.png - AssetGenImage get main => - const AssetGenImage('assets/images/spaceship/ramp/main.png'); +class $AssetsImagesMultiplierX4Gen { + const $AssetsImagesMultiplierX4Gen(); + + /// File path: assets/images/multiplier/x4/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x4/dimmed.png'); + + /// File path: assets/images/multiplier/x4/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x4/lit.png'); +} + +class $AssetsImagesMultiplierX5Gen { + const $AssetsImagesMultiplierX5Gen(); + + /// File path: assets/images/multiplier/x5/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x5/dimmed.png'); + + /// File path: assets/images/multiplier/x5/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x5/lit.png'); +} - /// File path: assets/images/spaceship/ramp/railing-background.png - AssetGenImage get railingBackground => const AssetGenImage( - 'assets/images/spaceship/ramp/railing-background.png'); +class $AssetsImagesMultiplierX6Gen { + const $AssetsImagesMultiplierX6Gen(); - /// File path: assets/images/spaceship/ramp/railing-foreground.png - AssetGenImage get railingForeground => const AssetGenImage( - 'assets/images/spaceship/ramp/railing-foreground.png'); + /// File path: assets/images/multiplier/x6/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x6/dimmed.png'); + + /// File path: assets/images/multiplier/x6/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x6/lit.png'); } class $AssetsImagesSparkyBumperGen { @@ -360,6 +420,70 @@ class $AssetsImagesSparkyComputerGen { const AssetGenImage('assets/images/sparky/computer/top.png'); } +class $AssetsImagesAndroidBumperAGen { + const $AssetsImagesAndroidBumperAGen(); + + /// File path: assets/images/android/bumper/a/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/android/bumper/a/dimmed.png'); + + /// File path: assets/images/android/bumper/a/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/android/bumper/a/lit.png'); +} + +class $AssetsImagesAndroidBumperBGen { + const $AssetsImagesAndroidBumperBGen(); + + /// File path: assets/images/android/bumper/b/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/android/bumper/b/dimmed.png'); + + /// File path: assets/images/android/bumper/b/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/android/bumper/b/lit.png'); +} + +class $AssetsImagesAndroidBumperCowGen { + const $AssetsImagesAndroidBumperCowGen(); + + /// File path: assets/images/android/bumper/cow/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/android/bumper/cow/dimmed.png'); + + /// File path: assets/images/android/bumper/cow/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/android/bumper/cow/lit.png'); +} + +class $AssetsImagesAndroidRampArrowGen { + const $AssetsImagesAndroidRampArrowGen(); + + /// File path: assets/images/android/ramp/arrow/active1.png + AssetGenImage get active1 => + const AssetGenImage('assets/images/android/ramp/arrow/active1.png'); + + /// File path: assets/images/android/ramp/arrow/active2.png + AssetGenImage get active2 => + const AssetGenImage('assets/images/android/ramp/arrow/active2.png'); + + /// File path: assets/images/android/ramp/arrow/active3.png + AssetGenImage get active3 => + const AssetGenImage('assets/images/android/ramp/arrow/active3.png'); + + /// File path: assets/images/android/ramp/arrow/active4.png + AssetGenImage get active4 => + const AssetGenImage('assets/images/android/ramp/arrow/active4.png'); + + /// File path: assets/images/android/ramp/arrow/active5.png + AssetGenImage get active5 => + const AssetGenImage('assets/images/android/ramp/arrow/active5.png'); + + /// File path: assets/images/android/ramp/arrow/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/android/ramp/arrow/inactive.png'); +} + class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); @@ -396,34 +520,6 @@ class $AssetsImagesDashBumperMainGen { const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); } -class $AssetsImagesSpaceshipRampArrowGen { - const $AssetsImagesSpaceshipRampArrowGen(); - - /// File path: assets/images/spaceship/ramp/arrow/active1.png - AssetGenImage get active1 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active1.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active2.png - AssetGenImage get active2 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active2.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active3.png - AssetGenImage get active3 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active3.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active4.png - AssetGenImage get active4 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active4.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active5.png - AssetGenImage get active5 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active5.png'); - - /// File path: assets/images/spaceship/ramp/arrow/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/spaceship/ramp/arrow/inactive.png'); -} - class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); diff --git a/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart b/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart index ad954975..e1a3857e 100644 --- a/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart +++ b/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart @@ -10,7 +10,7 @@ import 'package:pinball_flame/pinball_flame.dart'; export 'cubit/android_bumper_cubit.dart'; /// {@template android_bumper} -/// Bumper for area under the [Spaceship]. +/// Bumper for area under the [AndroidSpaceship]. /// {@endtemplate} class AndroidBumper extends BodyComponent with InitialPosition { /// {@macro android_bumper} @@ -19,6 +19,7 @@ class AndroidBumper extends BodyComponent with InitialPosition { required double minorRadius, required String litAssetPath, required String dimmedAssetPath, + required Vector2 spritePosition, Iterable? children, required this.bloc, }) : _majorRadius = majorRadius, @@ -32,6 +33,7 @@ class AndroidBumper extends BodyComponent with InitialPosition { _AndroidBumperSpriteGroupComponent( dimmedAssetPath: dimmedAssetPath, litAssetPath: litAssetPath, + position: spritePosition, state: bloc.state, ), ...?children, @@ -44,8 +46,9 @@ class AndroidBumper extends BodyComponent with InitialPosition { }) : this._( majorRadius: 3.52, minorRadius: 2.97, - litAssetPath: Assets.images.androidBumper.a.lit.keyName, - dimmedAssetPath: Assets.images.androidBumper.a.dimmed.keyName, + litAssetPath: Assets.images.android.bumper.a.lit.keyName, + dimmedAssetPath: Assets.images.android.bumper.a.dimmed.keyName, + spritePosition: Vector2(0, -0.1), bloc: AndroidBumperCubit(), children: children, ); @@ -56,8 +59,22 @@ class AndroidBumper extends BodyComponent with InitialPosition { }) : this._( majorRadius: 3.19, minorRadius: 2.79, - litAssetPath: Assets.images.androidBumper.b.lit.keyName, - dimmedAssetPath: Assets.images.androidBumper.b.dimmed.keyName, + litAssetPath: Assets.images.android.bumper.b.lit.keyName, + dimmedAssetPath: Assets.images.android.bumper.b.dimmed.keyName, + spritePosition: Vector2(0, -0.1), + bloc: AndroidBumperCubit(), + children: children, + ); + + /// {@macro android_bumper} + AndroidBumper.cow({ + Iterable? children, + }) : this._( + majorRadius: 3.4, + minorRadius: 2.9, + litAssetPath: Assets.images.android.bumper.cow.lit.keyName, + dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName, + spritePosition: Vector2(0, -0.68), bloc: AndroidBumperCubit(), children: children, ); @@ -113,12 +130,13 @@ class _AndroidBumperSpriteGroupComponent _AndroidBumperSpriteGroupComponent({ required String litAssetPath, required String dimmedAssetPath, + required Vector2 position, required AndroidBumperState state, }) : _litAssetPath = litAssetPath, _dimmedAssetPath = dimmedAssetPath, super( anchor: Anchor.center, - position: Vector2(0, -0.1), + position: position, current: state, ); diff --git a/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart b/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart index 3d3fd4b1..3e75f890 100644 --- a/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart +++ b/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart @@ -5,7 +5,7 @@ import 'package:bloc/bloc.dart'; part 'android_bumper_state.dart'; class AndroidBumperCubit extends Cubit { - AndroidBumperCubit() : super(AndroidBumperState.dimmed); + AndroidBumperCubit() : super(AndroidBumperState.lit); void onBallContacted() { emit(AndroidBumperState.dimmed); diff --git a/packages/pinball_components/lib/src/components/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship.dart new file mode 100644 index 00000000..1dcf6780 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship.dart @@ -0,0 +1,209 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; + +class AndroidSpaceship extends Blueprint { + AndroidSpaceship({required Vector2 position}) + : super( + components: [ + _SpaceshipSaucer()..initialPosition = position, + _SpaceshipSaucerSpriteAnimationComponent()..position = position, + _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), + _AndroidHead()..initialPosition = position + Vector2(0.5, 0.25), + _SpaceshipHole( + outsideLayer: Layer.spaceshipExitRail, + outsidePriority: RenderPriority.ballOnSpaceshipRail, + )..initialPosition = position - Vector2(5.3, -5.4), + _SpaceshipHole( + outsideLayer: Layer.board, + outsidePriority: RenderPriority.ballOnBoard, + )..initialPosition = position - Vector2(-7.5, -1.1), + ], + ); +} + +class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { + _SpaceshipSaucer() : super(renderBody: false) { + layer = Layer.spaceship; + } + + @override + Body createBody() { + final shape = _SpaceshipSaucerShape(); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + angle: -1.7, + ); + + return world.createBody(bodyDef)..createFixtureFromShape(shape); + } +} + +class _SpaceshipSaucerShape extends ChainShape { + _SpaceshipSaucerShape() { + const minorRadius = 9.75; + const majorRadius = 11.9; + + createChain( + [ + for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180) + Vector2( + minorRadius * math.cos(angle), + majorRadius * math.sin(angle), + ), + ], + ); + } +} + +class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef { + _SpaceshipSaucerSpriteAnimationComponent() + : super( + anchor: Anchor.center, + priority: RenderPriority.spaceshipSaucer, + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.saucer.keyName, + ); + + const amountPerRow = 5; + const amountPerColumn = 3; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ), + ); + } +} + +// TODO(allisonryan0002): add pulsing behavior. +class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef { + _LightBeamSpriteComponent() + : super( + anchor: Anchor.center, + priority: RenderPriority.spaceshipLightBeam, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.android.spaceship.lightBeam.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _AndroidHead extends BodyComponent with InitialPosition, Layered { + _AndroidHead() + : super( + priority: RenderPriority.androidHead, + children: [_AndroidHeadSpriteAnimationComponent()], + renderBody: false, + ) { + layer = Layer.spaceship; + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 3.1, + minorRadius: 2, + )..rotate(1.4); + // TODO(allisonryan0002): use bumping behavior. + final fixtureDef = FixtureDef( + shape, + restitution: 0.1, + ); + final bodyDef = BodyDef(position: initialPosition); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef { + _AndroidHeadSpriteAnimationComponent() + : super( + anchor: Anchor.center, + position: Vector2(-0.24, -2.6), + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.animatronic.keyName, + ); + + const amountPerRow = 18; + const amountPerColumn = 4; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ), + ); + } +} + +class _SpaceshipHole extends LayerSensor { + _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) + : super( + insideLayer: Layer.spaceship, + outsideLayer: outsideLayer, + orientation: LayerEntranceOrientation.down, + insidePriority: RenderPriority.ballOnSpaceship, + outsidePriority: outsidePriority, + ) { + layer = Layer.spaceship; + } + + @override + Shape get shape { + return ArcShape( + center: Vector2(0, -3.2), + arcRadius: 5, + angle: 1, + rotation: -2, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/bumping_behavior.dart b/packages/pinball_components/lib/src/components/bumping_behavior.dart index 654f96b4..af0d07c3 100644 --- a/packages/pinball_components/lib/src/components/bumping_behavior.dart +++ b/packages/pinball_components/lib/src/components/bumping_behavior.dart @@ -1,4 +1,5 @@ import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template bumping_behavior} @@ -11,15 +12,22 @@ class BumpingBehavior extends ContactBehavior { /// Determines how strong the bump is. final double _strength; + /// This is used to recoginze the current state of a contact manifold in world + /// coordinates. + @visibleForTesting + final WorldManifold worldManifold = WorldManifold(); + @override void postSolve(Object other, Contact contact, ContactImpulse impulse) { super.postSolve(other, contact, impulse); if (other is! BodyComponent) return; + contact.getWorldManifold(worldManifold); other.body.applyLinearImpulse( - contact.manifold.localPoint - ..normalize() - ..multiply(Vector2.all(other.body.mass * _strength)), + worldManifold.normal + ..multiply( + Vector2.all(other.body.mass * _strength), + ), ); } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index c6c5c802..2f0e4031 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,4 +1,5 @@ export 'android_bumper/android_bumper.dart'; +export 'android_spaceship.dart'; export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; @@ -19,6 +20,7 @@ export 'kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; export 'layer_sensor.dart'; +export 'multiplier/multiplier.dart'; export 'plunger.dart'; export 'render_priority.dart'; export 'rocket.dart'; @@ -26,7 +28,6 @@ export 'score_text.dart'; export 'shapes/shapes.dart'; export 'signpost.dart'; export 'slingshot.dart'; -export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; export 'sparky_animatronic.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index dccd7ce7..bb982e96 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -55,7 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { ); final joint = _FlipperJoint(jointDef); world.createJoint(joint); - unawaited(mounted.whenComplete(joint.unlock)); } List _createFixtureDefs() { @@ -132,6 +131,15 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { return body; } + + @override + void onMount() { + super.onMount(); + + gameRef.ready().whenComplete( + () => body.joints.whereType<_FlipperJoint>().first.unlock(), + ); + } } class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { @@ -215,11 +223,8 @@ class _FlipperJoint extends RevoluteJoint { /// The joint is locked when initialized in order to force the [Flipper] /// at its resting position. void lock() { - const angle = _halfSweepingAngle; - setLimits( - angle * side.direction, - angle * side.direction, - ); + final angle = _halfSweepingAngle * side.direction; + setLimits(angle, angle); } /// Unlocks the [Flipper] from its resting position. diff --git a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart index 63207e01..a865acf8 100644 --- a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart +++ b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart @@ -13,12 +13,14 @@ export 'cubit/google_letter_cubit.dart'; class GoogleLetter extends BodyComponent with InitialPosition { /// {@macro google_letter} GoogleLetter( - int index, - ) : bloc = GoogleLetterCubit(), + int index, { + Iterable? children, + }) : bloc = GoogleLetterCubit(), super( children: [ GoogleLetterBallContactBehavior(), - _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]) + _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]), + ...?children, ], ); diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index 12cd638d..527ffde4 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -16,9 +16,13 @@ class Kicker extends BodyComponent with InitialPosition { /// {@macro kicker} Kicker({ required BoardSide side, + Iterable? children, }) : _side = side, super( - children: [_KickerSpriteComponent(side: side)], + children: [ + _KickerSpriteComponent(side: side), + ...?children, + ], renderBody: false, ); diff --git a/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_cubit.dart b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_cubit.dart new file mode 100644 index 00000000..1d265b2e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_cubit.dart @@ -0,0 +1,25 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'multiplier_state.dart'; + +class MultiplierCubit extends Cubit { + MultiplierCubit(MultiplierValue multiplierValue) + : super(MultiplierState.initial(multiplierValue)); + + /// Event added when the game's current multiplier changes. + void next(int multiplier) { + if (state.value.equals(multiplier)) { + if (state.spriteState == MultiplierSpriteState.dimmed) { + emit(state.copyWith(spriteState: MultiplierSpriteState.lit)); + } + } else { + if (state.spriteState == MultiplierSpriteState.lit) { + emit(state.copyWith(spriteState: MultiplierSpriteState.dimmed)); + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_state.dart b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_state.dart new file mode 100644 index 00000000..e3adde70 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_state.dart @@ -0,0 +1,56 @@ +// ignore_for_file: public_member_api_docs + +part of 'multiplier_cubit.dart'; + +enum MultiplierSpriteState { + lit, + dimmed, +} + +class MultiplierState extends Equatable { + const MultiplierState({ + required this.value, + required this.spriteState, + }); + + const MultiplierState.initial(MultiplierValue multiplierValue) + : this( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ); + + /// Current value for the [Multiplier] + final MultiplierValue value; + + /// The [MultiplierSpriteGroupComponent] current sprite state + final MultiplierSpriteState spriteState; + + MultiplierState copyWith({ + MultiplierSpriteState? spriteState, + }) { + return MultiplierState( + value: value, + spriteState: spriteState ?? this.spriteState, + ); + } + + @override + List get props => [value, spriteState]; +} + +extension MultiplierValueX on MultiplierValue { + bool equals(int value) { + switch (this) { + case MultiplierValue.x2: + return value == 2; + case MultiplierValue.x3: + return value == 3; + case MultiplierValue.x4: + return value == 4; + case MultiplierValue.x5: + return value == 5; + case MultiplierValue.x6: + return value == 6; + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiplier/multiplier.dart b/packages/pinball_components/lib/src/components/multiplier/multiplier.dart new file mode 100644 index 00000000..54d02857 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiplier/multiplier.dart @@ -0,0 +1,204 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/src/components/multiplier/cubit/multiplier_cubit.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/multiplier_cubit.dart'; + +/// {@template multiplier} +/// Backlit multiplier decal displayed on the board. +/// {@endtemplate} +class Multiplier extends Component { + /// {@macro multiplier} + Multiplier._({ + required MultiplierValue value, + required Vector2 position, + required double angle, + required this.bloc, + }) : _value = value, + _position = position, + _angle = angle, + super(); + + /// {@macro multiplier} + Multiplier.x2({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x2, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x2), + ); + + /// {@macro multiplier} + Multiplier.x3({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x3, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x3), + ); + + /// {@macro multiplier} + Multiplier.x4({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x4, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x4), + ); + + /// {@macro multiplier} + Multiplier.x5({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x5, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x5), + ); + + /// {@macro multiplier} + Multiplier.x6({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x6, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x6), + ); + + /// Creates a [Multiplier] without any children. + /// + /// This can be used for testing [Multiplier]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Multiplier.test({ + required MultiplierValue value, + required this.bloc, + }) : _value = value, + _position = Vector2.zero(), + _angle = 0; + +// TODO(ruimiguel): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + final MultiplierCubit bloc; + + final MultiplierValue _value; + final Vector2 _position; + final double _angle; + late final MultiplierSpriteGroupComponent _sprite; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Future onLoad() async { + await super.onLoad(); + _sprite = MultiplierSpriteGroupComponent( + position: _position, + litAssetPath: _value.litAssetPath, + dimmedAssetPath: _value.dimmedAssetPath, + angle: _angle, + current: bloc.state, + ); + await add(_sprite); + } +} + +/// Available multiplier values. +enum MultiplierValue { + x2, + x3, + x4, + x5, + x6, +} + +extension on MultiplierValue { + String get litAssetPath { + switch (this) { + case MultiplierValue.x2: + return Assets.images.multiplier.x2.lit.keyName; + case MultiplierValue.x3: + return Assets.images.multiplier.x3.lit.keyName; + case MultiplierValue.x4: + return Assets.images.multiplier.x4.lit.keyName; + case MultiplierValue.x5: + return Assets.images.multiplier.x5.lit.keyName; + case MultiplierValue.x6: + return Assets.images.multiplier.x6.lit.keyName; + } + } + + String get dimmedAssetPath { + switch (this) { + case MultiplierValue.x2: + return Assets.images.multiplier.x2.dimmed.keyName; + case MultiplierValue.x3: + return Assets.images.multiplier.x3.dimmed.keyName; + case MultiplierValue.x4: + return Assets.images.multiplier.x4.dimmed.keyName; + case MultiplierValue.x5: + return Assets.images.multiplier.x5.dimmed.keyName; + case MultiplierValue.x6: + return Assets.images.multiplier.x6.dimmed.keyName; + } + } +} + +/// {@template multiplier_sprite_group_component} +/// A [SpriteGroupComponent] for a [Multiplier] with lit and dimmed states. +/// {@endtemplate} +@visibleForTesting +class MultiplierSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + /// {@macro multiplier_sprite_group_component} + MultiplierSpriteGroupComponent({ + required Vector2 position, + required String litAssetPath, + required String dimmedAssetPath, + required double angle, + required MultiplierState current, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + position: position, + angle: angle, + current: current.spriteState, + ); + + final String _litAssetPath; + final String _dimmedAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state.spriteState); + + final sprites = { + MultiplierSpriteState.lit: + Sprite(gameRef.images.fromCache(_litAssetPath)), + MultiplierSpriteState.dimmed: + Sprite(gameRef.images.fromCache(_dimmedAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index 735a5490..81de9dd5 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -82,7 +82,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void release() { - final velocity = (initialPosition.y - body.position.y) * 5; + final velocity = (initialPosition.y - body.position.y) * 7; body.linearVelocity = Vector2(0, velocity); _spriteComponent.release(); } @@ -221,7 +221,7 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { plunger.body, anchor.body, plunger.body.position + anchor.body.position, - Vector2(18.6, BoardDimensions.bounds.height), + Vector2(16, BoardDimensions.bounds.height), ); enableLimit = true; lowerTranslation = double.negativeInfinity; diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart index 395ca49c..3e7d5a29 100644 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -20,14 +20,14 @@ abstract class RenderPriority { static const int ballOnSpaceshipRamp = _above + spaceshipRampBackgroundRailing; - /// Render priority for the [Ball] while it's on the [Spaceship]. + /// Render priority for the [Ball] while it's on the [AndroidSpaceship]. static const int ballOnSpaceship = _above + spaceshipSaucer; /// Render priority for the [Ball] while it's on the [SpaceshipRail]. static const int ballOnSpaceshipRail = _above + spaceshipRail; /// Render priority for the [Ball] while it's on the [LaunchRamp]. - static const int ballOnLaunchRamp = _above + launchRamp; + static const int ballOnLaunchRamp = launchRamp; // Background @@ -51,11 +51,11 @@ abstract class RenderPriority { static const int launchRamp = _above + outerBoundary; - static const int launchRampForegroundRailing = _below + ballOnBoard; + static const int launchRampForegroundRailing = ballOnBoard; static const int plunger = _above + launchRamp; - static const int rocket = _above + bottomBoundary; + static const int rocket = _below + bottomBoundary; // Dino Land @@ -91,7 +91,7 @@ abstract class RenderPriority { static const int spaceshipSaucer = _above + ballOnSpaceshipRail; - static const int spaceshipSaucerWall = _above + spaceshipSaucer; + static const int spaceshipLightBeam = _below + spaceshipSaucer; static const int androidHead = _above + spaceshipSaucer; diff --git a/packages/pinball_components/lib/src/components/rocket.dart b/packages/pinball_components/lib/src/components/rocket.dart index 3f9161ca..6ba0b10c 100644 --- a/packages/pinball_components/lib/src/components/rocket.dart +++ b/packages/pinball_components/lib/src/components/rocket.dart @@ -6,19 +6,22 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; /// A [SpriteComponent] for the rocket over [Plunger]. /// {@endtemplate} class RocketSpriteComponent extends SpriteComponent with HasGameRef { - // TODO(ruimiguel): change this priority to be over launcher ramp and bottom - // wall. /// {@macro rocket_sprite_component} - RocketSpriteComponent() : super(priority: RenderPriority.rocket); + RocketSpriteComponent() + : super( + priority: RenderPriority.rocket, + anchor: Anchor.center, + ); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.plunger.rocket.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.plunger.rocket.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart deleted file mode 100644 index a52df81d..00000000 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template spaceship} -/// A [Blueprint] which creates the spaceship feature. -/// {@endtemplate} -class Spaceship extends Blueprint { - /// {@macro spaceship} - Spaceship({required Vector2 position}) - : super( - components: [ - SpaceshipSaucer()..initialPosition = position, - _SpaceshipEntrance()..initialPosition = position, - AndroidHead()..initialPosition = position, - _SpaceshipHole( - outsideLayer: Layer.spaceshipExitRail, - outsidePriority: RenderPriority.ballOnSpaceshipRail, - )..initialPosition = position - Vector2(5.2, -4.8), - _SpaceshipHole( - outsideLayer: Layer.board, - outsidePriority: RenderPriority.ballOnBoard, - )..initialPosition = position - Vector2(-7.2, -0.8), - SpaceshipWall()..initialPosition = position, - ], - ); - - /// Total size of the spaceship. - static final size = Vector2(25, 19); -} - -/// {@template spaceship_saucer} -/// A [BodyComponent] for the base, or the saucer of the spaceship -/// {@endtemplate} -class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_saucer} - SpaceshipSaucer() - : super( - priority: RenderPriority.spaceshipSaucer, - renderBody: false, - children: [ - _SpaceshipSaucerSpriteComponent(), - ], - ) { - layer = Layer.spaceship; - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 3; - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef { - _SpaceshipSaucerSpriteComponent() - : super( - anchor: Anchor.center, - // TODO(alestiago): Refactor to use sprite orignial size instead. - size: Spaceship.size, - ); - - @override - Future onLoad() async { - await super.onLoad(); - - // TODO(alestiago): Use cached sprite. - sprite = await gameRef.loadSprite( - Assets.images.spaceship.saucer.keyName, - ); - } -} - -/// {@template spaceship_bridge} -/// A [BodyComponent] that provides both the collision and the rotation -/// animation for the bridge. -/// {@endtemplate} -class AndroidHead extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_bridge} - AndroidHead() - : super( - priority: RenderPriority.androidHead, - children: [_AndroidHeadSpriteAnimation()], - renderBody: false, - ) { - layer = Layer.spaceship; - } - - @override - Body createBody() { - final circleShape = CircleShape()..radius = 2; - - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(circleShape)..restitution = 0.4, - ); - } -} - -class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent - with HasGameRef { - @override - Future onLoad() async { - await super.onLoad(); - - final image = await gameRef.images.load( - Assets.images.spaceship.bridge.keyName, - ); - size = Vector2(8.2, 10); - position = Vector2(0, -2); - anchor = Anchor.center; - - final data = SpriteAnimationData.sequenced( - amount: 72, - amountPerRow: 24, - stepTime: 0.05, - textureSize: size * 10, - ); - animation = SpriteAnimation.fromFrameData(image, data); - } -} - -class _SpaceshipEntrance extends LayerSensor { - _SpaceshipEntrance() - : super( - insideLayer: Layer.spaceship, - orientation: LayerEntranceOrientation.up, - insidePriority: RenderPriority.ballOnSpaceship, - ) { - layer = Layer.spaceship; - } - - @override - Shape get shape { - final radius = Spaceship.size.y / 2; - return PolygonShape() - ..setAsEdge( - Vector2( - radius * cos(20 * pi / 180), - radius * sin(20 * pi / 180), - )..rotate(90 * pi / 180), - Vector2( - radius * cos(340 * pi / 180), - radius * sin(340 * pi / 180), - )..rotate(90 * pi / 180), - ); - } -} - -class _SpaceshipHole extends LayerSensor { - _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) - : super( - insideLayer: Layer.spaceship, - outsideLayer: outsideLayer, - orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnSpaceship, - outsidePriority: outsidePriority, - ) { - layer = Layer.spaceship; - } - - @override - Shape get shape { - return ArcShape( - center: Vector2(0, -3.2), - arcRadius: 5, - angle: 1, - rotation: -2, - ); - } -} - -/// {@template spaceship_wall_shape} -/// The [ChainShape] that defines the shape of the [SpaceshipWall]. -/// {@endtemplate} -class _SpaceshipWallShape extends ChainShape { - /// {@macro spaceship_wall_shape} - _SpaceshipWallShape() { - final minorRadius = (Spaceship.size.y - 2) / 2; - final majorRadius = (Spaceship.size.x - 2) / 2; - - createChain( - [ - // TODO(alestiago): Try converting this logic to radian. - for (var angle = 20; angle <= 340; angle++) - Vector2( - minorRadius * cos(angle * pi / 180), - majorRadius * sin(angle * pi / 180), - ), - ], - ); - } -} - -/// {@template spaceship_wall} -/// A [BodyComponent] that provides the collision for the wall -/// surrounding the spaceship. -/// -/// It has a small opening to allow the [Ball] to get inside the spaceship -/// saucer. -/// -/// It also contains the [SpriteComponent] for the lower wall -/// {@endtemplate} -class SpaceshipWall extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_wall} - SpaceshipWall() - : super( - priority: RenderPriority.spaceshipSaucerWall, - renderBody: false, - ) { - layer = Layer.spaceship; - } - - @override - Body createBody() { - final shape = _SpaceshipWallShape(); - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - angle: -1.7, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index 91540c62..df9fc16c 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_rail} -/// A [Blueprint] for the rail exiting the [Spaceship]. +/// A [Blueprint] for the rail exiting the [AndroidSpaceship]. /// {@endtemplate} class SpaceshipRail extends Blueprint { /// {@macro spaceship_rail} @@ -116,7 +116,7 @@ class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef { final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.rail.main.keyName, + Assets.images.android.rail.main.keyName, ), ); this.sprite = sprite; @@ -139,7 +139,7 @@ class _SpaceshipRailExitSpriteComponent extends SpriteComponent final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.rail.exit.keyName, + Assets.images.android.rail.exit.keyName, ), ); this.sprite = sprite; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index c9a1d574..6a034daa 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_ramp} -/// A [Blueprint] which creates the ramp leading into the [Spaceship]. +/// A [Blueprint] which creates the ramp leading into the [AndroidSpaceship]. /// {@endtemplate} class SpaceshipRamp extends Blueprint { /// {@macro spaceship_ramp} @@ -73,17 +73,17 @@ extension on SpaceshipRampArrowSpriteState { String get path { switch (this) { case SpaceshipRampArrowSpriteState.inactive: - return Assets.images.spaceship.ramp.arrow.inactive.keyName; + return Assets.images.android.ramp.arrow.inactive.keyName; case SpaceshipRampArrowSpriteState.active1: - return Assets.images.spaceship.ramp.arrow.active1.keyName; + return Assets.images.android.ramp.arrow.active1.keyName; case SpaceshipRampArrowSpriteState.active2: - return Assets.images.spaceship.ramp.arrow.active2.keyName; + return Assets.images.android.ramp.arrow.active2.keyName; case SpaceshipRampArrowSpriteState.active3: - return Assets.images.spaceship.ramp.arrow.active3.keyName; + return Assets.images.android.ramp.arrow.active3.keyName; case SpaceshipRampArrowSpriteState.active4: - return Assets.images.spaceship.ramp.arrow.active4.keyName; + return Assets.images.android.ramp.arrow.active4.keyName; case SpaceshipRampArrowSpriteState.active5: - return Assets.images.spaceship.ramp.arrow.active5.keyName; + return Assets.images.android.ramp.arrow.active5.keyName; } } @@ -161,7 +161,7 @@ class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.railingBackground.keyName, + Assets.images.android.ramp.railingBackground.keyName, ), ); this.sprite = sprite; @@ -182,7 +182,7 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.main.keyName, + Assets.images.android.ramp.main.keyName, ), ); this.sprite = sprite; @@ -234,7 +234,7 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.boardOpening.keyName, + Assets.images.android.ramp.boardOpening.keyName, ), ); this.sprite = sprite; @@ -304,7 +304,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingForeground.keyName, ), ); this.sprite = sprite; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index c260b626..0769f484 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -45,7 +45,6 @@ flutter: - asset: fonts/PixeloidMono-1G8ae.ttf assets: - - assets/images/ - assets/images/ball/ - assets/images/baseboard/ - assets/images/boundary/ @@ -57,15 +56,16 @@ flutter: - assets/images/dash/bumper/a/ - assets/images/dash/bumper/b/ - assets/images/dash/bumper/main/ - - assets/images/spaceship/ - - assets/images/spaceship/rail/ - - assets/images/spaceship/ramp/ - - assets/images/spaceship/ramp/arrow/ + - assets/images/android/spaceship/ + - assets/images/android/rail/ + - assets/images/android/ramp/ + - assets/images/android/ramp/arrow/ + - assets/images/android/bumper/a/ + - assets/images/android/bumper/b/ + - assets/images/android/bumper/cow/ - assets/images/kicker/ - assets/images/plunger/ - assets/images/slingshot/ - - assets/images/android_bumper/a/ - - assets/images/android_bumper/b/ - assets/images/sparky/ - assets/images/sparky/computer/ - assets/images/sparky/bumper/a/ @@ -74,6 +74,11 @@ flutter: - assets/images/backboard/ - assets/images/google_word/ - assets/images/signpost/ + - assets/images/multiplier/x2/ + - assets/images/multiplier/x3/ + - assets/images/multiplier/x4/ + - assets/images/multiplier/x5/ + - assets/images/multiplier/x6/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 40396b88..1f0077fb 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,7 +6,6 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; -import 'package:sandbox/stories/kicker/stories.dart'; import 'package:sandbox/stories/stories.dart'; void main() { @@ -15,11 +14,9 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); - addFlipperStories(dashbook); - addBaseboardStories(dashbook); addChromeDinoStories(dashbook); - addDashNestBumperStories(dashbook); - addKickerStories(dashbook); + addFlutterForestStories(dashbook); + addBottomGroupStories(dashbook); addPlungerStories(dashbook); addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); @@ -30,6 +27,7 @@ void main() { addScoreTextStories(dashbook); addBackboardStories(dashbook); addDinoWallStories(dashbook); + addMultipliersStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart index 4dcd1cb8..32638c2d 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart @@ -9,8 +9,8 @@ class AndroidBumperAGame extends BallGame { : super( color: const Color(0xFF0000FF), imagesFileNames: [ - Assets.images.androidBumper.a.lit.keyName, - Assets.images.androidBumper.a.dimmed.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, ], ); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart index e504fe1e..bfd4206c 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart @@ -9,8 +9,8 @@ class AndroidBumperBGame extends BallGame { : super( color: const Color(0xFF0000FF), imagesFileNames: [ - Assets.images.androidBumper.b.lit.keyName, - Assets.images.androidBumper.b.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, ], ); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_cow_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_cow_game.dart new file mode 100644 index 00000000..ac1bc6fe --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_cow_game.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AndroidBumperCowGame extends BallGame { + AndroidBumperCowGame() + : super( + imagesFileNames: [ + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ], + ); + + static const description = ''' + Shows how a AndroidBumper.cow is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await add( + AndroidBumper.cow()..priority = 1, + ); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart new file mode 100644 index 00000000..076b2d2b --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AndroidSpaceshipGame extends BallGame { + AndroidSpaceshipGame() + : super( + ballPriority: RenderPriority.ballOnSpaceship, + ballLayer: Layer.spaceship, + imagesFileNames: [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + ], + ); + + static const description = ''' + Shows how the AndroidSpaceship is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a Ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await addFromBlueprint( + AndroidSpaceship(position: Vector2.zero()), + ); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_game.dart deleted file mode 100644 index ad897dd4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_game.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; -import 'package:sandbox/common/common.dart'; - -class SpaceshipGame extends AssetsGame with TapDetector { - static const description = ''' - Shows how a Spaceship works. - - - Tap anywhere on the screen to spawn a Ball into the game. -'''; - - @override - Future onLoad() async { - await super.onLoad(); - - camera.followVector2(Vector2.zero()); - await addFromBlueprint( - Spaceship(position: Vector2.zero()), - ); - await ready(); - } - - @override - void onTapUp(TapUpInfo info) { - add( - Ball(baseColor: Colors.blue) - ..initialPosition = info.eventPosition.game - ..layer = Layer.spaceshipEntranceRamp, - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index 4bd067fa..87bac14d 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -13,8 +13,8 @@ class SpaceshipRailGame extends BallGame { ballPriority: RenderPriority.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, imagesFileNames: [ - Assets.images.spaceship.rail.main.keyName, - Assets.images.spaceship.rail.exit.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, ], ); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index 1817f40a..43944a37 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -14,16 +14,16 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { ballPriority: RenderPriority.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, imagesFileNames: [ - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, ], ); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart index 92ddd5d5..ec4a783e 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart @@ -2,7 +2,8 @@ import 'package:dashbook/dashbook.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart'; -import 'package:sandbox/stories/android_acres/spaceship_game.dart'; +import 'package:sandbox/stories/android_acres/android_bumper_cow_game.dart'; +import 'package:sandbox/stories/android_acres/android_spaceship_game.dart'; import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart'; import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart'; @@ -19,9 +20,14 @@ void addAndroidAcresStories(Dashbook dashbook) { gameBuilder: (_) => AndroidBumperBGame(), ) ..addGame( - title: 'Spaceship', - description: SpaceshipGame.description, - gameBuilder: (_) => SpaceshipGame(), + title: 'Android Bumper Cow', + description: AndroidBumperCowGame.description, + gameBuilder: (_) => AndroidBumperCowGame(), + ) + ..addGame( + title: 'Android Spaceship', + description: AndroidSpaceshipGame.description, + gameBuilder: (_) => AndroidSpaceshipGame(), ) ..addGame( title: 'Spaceship Rail', diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart deleted file mode 100644 index b07e3a73..00000000 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/baseboard/baseboard_game.dart'; - -void addBaseboardStories(Dashbook dashbook) { - dashbook.storiesOf('Baseboard').addGame( - title: 'Traced', - description: BaseboardGame.description, - gameBuilder: (_) => BaseboardGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart new file mode 100644 index 00000000..d0cc7322 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart @@ -0,0 +1 @@ +export 'stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart new file mode 100644 index 00000000..7712ca79 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart @@ -0,0 +1,24 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/bottom_group/baseboard_game.dart'; +import 'package:sandbox/stories/bottom_group/flipper_game.dart'; +import 'package:sandbox/stories/bottom_group/kicker_game.dart'; + +void addBottomGroupStories(Dashbook dashbook) { + dashbook.storiesOf('Bottom Group') + ..addGame( + title: 'Flipper', + description: FlipperGame.description, + gameBuilder: (_) => FlipperGame(), + ) + ..addGame( + title: 'Kicker', + description: KickerGame.description, + gameBuilder: (_) => KickerGame(), + ) + ..addGame( + title: 'Baseboard', + description: BaseboardGame.description, + gameBuilder: (_) => BaseboardGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart deleted file mode 100644 index 2ef2a4b6..00000000 --- a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/flipper/flipper_game.dart'; - -void addFlipperStories(Dashbook dashbook) { - dashbook.storiesOf('Flipper').addGame( - title: 'Traced', - description: FlipperGame.description, - gameBuilder: (_) => FlipperGame(), - ); -} 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 ef9c1ffb..dd557a27 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -5,7 +5,7 @@ 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) { +void addFlutterForestStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') ..addGame( title: 'Signpost', diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart deleted file mode 100644 index cfebb7e4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/kicker/kicker_game.dart'; - -void addKickerStories(Dashbook dashbook) { - dashbook.storiesOf('Kickers').addGame( - title: 'Traced', - description: KickerGame.description, - gameBuilder: (_) => KickerGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/multipliers/multipliers_game.dart b/packages/pinball_components/sandbox/lib/stories/multipliers/multipliers_game.dart new file mode 100644 index 00000000..ae641623 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multipliers/multipliers_game.dart @@ -0,0 +1,97 @@ +import 'dart:math' as math; +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 MultipliersGame extends BallGame with KeyboardEvents { + MultipliersGame() + : super( + imagesFileNames: [ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ], + ); + + static const description = ''' + Shows how the Multipliers are rendered. + + - Tap anywhere on the screen to spawn a ball into the game. + - Press digits 2 to 6 for toggle state multipliers 2 to 6. +'''; + + final List multipliers = [ + Multiplier.x2( + position: Vector2(-20, 0), + angle: -15 * math.pi / 180, + ), + Multiplier.x3( + position: Vector2(20, -5), + angle: 15 * math.pi / 180, + ), + Multiplier.x4( + position: Vector2(0, -15), + angle: 0, + ), + Multiplier.x5( + position: Vector2(-10, -25), + angle: -3 * math.pi / 180, + ), + Multiplier.x6( + position: Vector2(10, -35), + angle: 8 * math.pi / 180, + ), + ]; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + + await addAll(multipliers); + await traceAllBodies(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyDownEvent) { + var currentMultiplier = 1; + + if (event.logicalKey == LogicalKeyboardKey.digit2) { + currentMultiplier = 2; + } + if (event.logicalKey == LogicalKeyboardKey.digit3) { + currentMultiplier = 3; + } + if (event.logicalKey == LogicalKeyboardKey.digit4) { + currentMultiplier = 4; + } + if (event.logicalKey == LogicalKeyboardKey.digit5) { + currentMultiplier = 5; + } + if (event.logicalKey == LogicalKeyboardKey.digit6) { + currentMultiplier = 6; + } + + for (final multiplier in multipliers) { + multiplier.bloc.next(currentMultiplier); + } + + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/multipliers/stories.dart b/packages/pinball_components/sandbox/lib/stories/multipliers/stories.dart new file mode 100644 index 00000000..48b6da6d --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multipliers/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/multipliers/multipliers_game.dart'; + +void addMultipliersStories(Dashbook dashbook) { + dashbook.storiesOf('Multipliers').addGame( + title: 'Multipliers', + description: MultipliersGame.description, + gameBuilder: (_) => MultipliersGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index df51fc0f..d5e410b4 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,16 +1,16 @@ export 'android_acres/stories.dart'; export 'backboard/stories.dart'; export 'ball/stories.dart'; -export 'baseboard/stories.dart'; +export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; export 'dino_wall/stories.dart'; export 'effects/stories.dart'; -export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'multipliers/stories.dart'; export 'plunger/stories.dart'; export 'score_text/stories.dart'; export 'slingshot/stories.dart'; diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index 8d61da32..d2500fbe 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + bloc: + dependency: transitive + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.3" boolean_selector: dependency: transitive description: @@ -171,7 +178,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.3" json_annotation: dependency: transitive description: @@ -199,7 +206,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.3" meta: dependency: transitive description: @@ -220,7 +227,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" path_provider_linux: dependency: transitive description: @@ -351,7 +358,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -386,7 +393,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.8" typed_data: dependency: transitive description: @@ -456,7 +463,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.1" very_good_analysis: dependency: "direct dev" description: diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 2230becb..33c5670d 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -24,3 +24,5 @@ class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} + +class MockMultiplierCubit extends Mock implements MultiplierCubit {} diff --git a/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart b/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart index abc51a28..a5256b79 100644 --- a/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart +++ b/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart @@ -13,10 +13,12 @@ import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.androidBumper.a.lit.keyName, - Assets.images.androidBumper.a.dimmed.keyName, - Assets.images.androidBumper.b.lit.keyName, - Assets.images.androidBumper.b.dimmed.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); @@ -33,6 +35,12 @@ void main() { expect(game.contains(androidBumper), isTrue); }); + flameTester.test('"cow" loads correctly', (game) async { + final androidBumper = AndroidBumper.cow(); + await game.ensureAdd(androidBumper); + expect(game.contains(androidBumper), isTrue); + }); + // TODO(alestiago): Consider refactoring once the following is merged: // https://github.com/flame-engine/flame/pull/1538 // ignore: public_member_api_docs diff --git a/packages/pinball_components/test/src/components/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship_test.dart new file mode 100644 index 00000000..92219a64 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship_test.dart @@ -0,0 +1,66 @@ +// 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 'package:pinball_flame/pinball_flame.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('AndroidSpaceship', () { + group('Spaceship', () { + final assets = [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + await game.addFromBlueprint(AndroidSpaceship(position: Vector2.zero())); + await game.ready(); + }); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game + .addFromBlueprint(AndroidSpaceship(position: Vector2.zero())); + game.camera.followVector2(Vector2.zero()); + await game.ready(); + await tester.pump(); + }, + verify: (game, tester) async { + final animationDuration = game + .descendants() + .whereType() + .last + .animation! + .totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_spaceship/start.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_spaceship/middle.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_spaceship/end.png'), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/bumping_behavior_test.dart b/packages/pinball_components/test/src/components/bumping_behavior_test.dart index d346a0ae..dd0493f7 100644 --- a/packages/pinball_components/test/src/components/bumping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/bumping_behavior_test.dart @@ -7,22 +7,16 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/src/components/bumping_behavior.dart'; import '../../helpers/helpers.dart'; -import 'layer_test.dart'; -class MockContactImpulse extends Mock implements ContactImpulse {} +class _MockContact extends Mock implements Contact {} -class MockManifold extends Mock implements Manifold {} +class _MockContactImpulse extends Mock implements ContactImpulse {} -class TestHeavyBodyComponent extends BodyComponent { +class _TestBodyComponent extends BodyComponent { @override - Body createBody() { - final shape = CircleShape(); - return world.createBody( - BodyDef( - type: BodyType.dynamic, - ), - )..createFixtureFromShape(shape, 20); - } + Body createBody() => world.createBody( + BodyDef(type: BodyType.dynamic), + )..createFixtureFromShape(CircleShape(), 1); } void main() { @@ -32,7 +26,7 @@ void main() { group('BumpingBehavior', () { flameTester.test('can be added', (game) async { final behavior = BumpingBehavior(strength: 0); - final component = TestBodyComponent(); + final component = _TestBodyComponent(); await component.add(behavior); await game.ensureAdd(component); }); @@ -40,16 +34,18 @@ void main() { flameTester.testGameWidget( 'the bump is greater when the strengh is greater', setUp: (game, tester) async { - final component1 = TestBodyComponent(); - final behavior1 = BumpingBehavior(strength: 1); + final component1 = _TestBodyComponent(); + final behavior1 = BumpingBehavior(strength: 1) + ..worldManifold.normal.setFrom(Vector2.all(1)); await component1.add(behavior1); - final component2 = TestBodyComponent(); - final behavior2 = BumpingBehavior(strength: 2); + final component2 = _TestBodyComponent(); + final behavior2 = BumpingBehavior(strength: 2) + ..worldManifold.normal.setFrom(Vector2.all(1)); await component2.add(behavior2); - final dummy1 = TestHeavyBodyComponent(); - final dummy2 = TestHeavyBodyComponent(); + final dummy1 = _TestBodyComponent(); + final dummy2 = _TestBodyComponent(); await game.ensureAddAll([ component1, @@ -58,14 +54,8 @@ void main() { dummy2, ]); - expect(dummy1.body.inverseMass, greaterThan(0)); - expect(dummy2.body.inverseMass, greaterThan(0)); - - final contact = MockContact(); - final manifold = MockManifold(); - final contactImpulse = MockContactImpulse(); - when(() => manifold.localPoint).thenReturn(Vector2.all(1)); - when(() => contact.manifold).thenReturn(manifold); + final contact = _MockContact(); + final contactImpulse = _MockContactImpulse(); behavior1.postSolve(dummy1, contact, contactImpulse); behavior2.postSolve(dummy2, contact, contactImpulse); diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png new file mode 100644 index 00000000..c2a0631a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png new file mode 100644 index 00000000..c6651abd Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png new file mode 100644 index 00000000..25e8863a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x2-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x2-dimmed.png new file mode 100644 index 00000000..ca2d8bf1 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x2-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x2-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x2-lit.png new file mode 100644 index 00000000..94001e27 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x2-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x3-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x3-dimmed.png new file mode 100644 index 00000000..4727ea3e Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x3-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x3-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x3-lit.png new file mode 100644 index 00000000..f2f84178 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x3-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x4-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x4-dimmed.png new file mode 100644 index 00000000..76c84994 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x4-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x4-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x4-lit.png new file mode 100644 index 00000000..b4918e62 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x4-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x5-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x5-dimmed.png new file mode 100644 index 00000000..2bbbf1ef Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x5-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x5-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x5-lit.png new file mode 100644 index 00000000..5e750af8 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x5-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x6-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x6-dimmed.png new file mode 100644 index 00000000..aff09619 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x6-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x6-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x6-lit.png new file mode 100644 index 00000000..7e5edc10 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x6-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/rocket.png b/packages/pinball_components/test/src/components/golden/rocket.png index 9511f3d5..62ba4e61 100644 Binary files a/packages/pinball_components/test/src/components/golden/rocket.png and b/packages/pinball_components/test/src/components/golden/rocket.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship.png b/packages/pinball_components/test/src/components/golden/spaceship.png deleted file mode 100644 index d43db8c7..00000000 Binary files a/packages/pinball_components/test/src/components/golden/spaceship.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart index 624168b9..7ad0e64b 100644 --- a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart @@ -1,6 +1,7 @@ // 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'; @@ -80,6 +81,16 @@ void main() { }, ); + flameTester.test('adds new children', (game) async { + final component = Component(); + final googleLetter = GoogleLetter( + 1, + children: [component], + ); + await game.ensureAdd(googleLetter); + expect(googleLetter.children, contains(component)); + }); + test('throws error when index out of range', () { expect(() => GoogleLetter(-1), throwsA(isA())); expect(() => GoogleLetter(6), throwsA(isA())); diff --git a/packages/pinball_components/test/src/components/kicker_test.dart b/packages/pinball_components/test/src/components/kicker_test.dart index 8c48a1fb..aebf9380 100644 --- a/packages/pinball_components/test/src/components/kicker_test.dart +++ b/packages/pinball_components/test/src/components/kicker_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -44,6 +45,16 @@ void main() { }, ); + flameTester.test('adds new children', (game) async { + final component = Component(); + final kicker = Kicker( + side: BoardSide.left, + children: [component], + ); + await game.ensureAdd(kicker); + expect(kicker.children, contains(component)); + }); + flameTester.test( 'body is static', (game) async { diff --git a/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_cubit_test.dart b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_cubit_test.dart new file mode 100644 index 00000000..35ed652e --- /dev/null +++ b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_cubit_test.dart @@ -0,0 +1,118 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'MultiplierCubit', + () { + blocTest( + "emits [lit] when 'next' on x2 dimmed with x2 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + act: (bloc) => bloc.next(2), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x3 dimmed with x3 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x3), + act: (bloc) => bloc.next(3), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x4 dimmed with x4 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x4), + act: (bloc) => bloc.next(4), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x5 dimmed with x5 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x5), + act: (bloc) => bloc.next(5), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x6 dimmed with x6 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x6), + act: (bloc) => bloc.next(6), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [dimmed] when 'next' on lit with different multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + seed: () => MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + act: (bloc) => bloc.next(3), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.dimmed, + ), + ], + ); + + blocTest( + "emits nothing when 'next' on lit with same multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + seed: () => MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + act: (bloc) => bloc.next(2), + expect: () => [], + ); + + blocTest( + "emits nothing when 'next' on dimmed with different multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + act: (bloc) => bloc.next(3), + expect: () => [], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_state_test.dart b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_state_test.dart new file mode 100644 index 00000000..9789d7c5 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_state_test.dart @@ -0,0 +1,75 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/pinball_components.dart'; + +void main() { + group('MultiplierState', () { + test('supports value equality', () { + expect( + MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + equals( + MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + isNotNull, + ); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + const multiplierState = MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ); + expect( + multiplierState.copyWith(), + equals(multiplierState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const multiplierState = MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ); + final otherMultiplierState = MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.dimmed, + ); + expect(multiplierState, isNot(equals(otherMultiplierState))); + + expect( + multiplierState.copyWith( + spriteState: MultiplierSpriteState.dimmed, + ), + equals(otherMultiplierState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart new file mode 100644 index 00000000..edc2735f --- /dev/null +++ b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart @@ -0,0 +1,517 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +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 '../../../helpers/helpers.dart'; + +void main() { + final bloc = MockMultiplierCubit(); + + group('Multiplier', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('"x2" loads correctly', (game) async { + final multiplier = Multiplier.x2( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x3" loads correctly', (game) async { + final multiplier = Multiplier.x3( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x4" loads correctly', (game) async { + final multiplier = Multiplier.x4( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x5" loads correctly', (game) async { + final multiplier = Multiplier.x5( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x6" loads correctly', (game) async { + final multiplier = Multiplier.x6( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + group('renders correctly', () { + group('x2', () { + const multiplierValue = MultiplierValue.x2; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x2-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x2-dimmed.png'), + ); + }, + ); + }); + + group('x3', () { + const multiplierValue = MultiplierValue.x3; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x3-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x3-dimmed.png'), + ); + }, + ); + }); + + group('x4', () { + const multiplierValue = MultiplierValue.x4; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x4-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x4-dimmed.png'), + ); + }, + ); + }); + + group('x5', () { + const multiplierValue = MultiplierValue.x5; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x5-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x5-dimmed.png'), + ); + }, + ); + }); + + group('x6', () { + const multiplierValue = MultiplierValue.x6; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x6-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x6-dimmed.png'), + ); + }, + ); + }); + }); + + flameTester.test('closes bloc when removed', (game) async { + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + when(bloc.close).thenAnswer((_) async {}); + final multiplier = Multiplier.test(value: MultiplierValue.x2, bloc: bloc); + + await game.ensureAdd(multiplier); + game.remove(multiplier); + await game.ready(); + + verify(bloc.close).called(1); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/rocket_test.dart b/packages/pinball_components/test/src/components/rocket_test.dart index 87cfe515..5bc9b136 100644 --- a/packages/pinball_components/test/src/components/rocket_test.dart +++ b/packages/pinball_components/test/src/components/rocket_test.dart @@ -8,14 +8,24 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; void main() { - group('RocketSpriteComponent', () { - final tester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.plunger.rocket.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); - tester.testGameWidget( + group('RocketSpriteComponent', () { + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - game.camera.followVector2(Vector2.zero()); + await game.images.loadAll(assets); await game.ensureAdd(RocketSpriteComponent()); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 8; + + await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_components/test/src/components/spaceship_rail_test.dart b/packages/pinball_components/test/src/components/spaceship_rail_test.dart index bc5a7f75..a24b0a17 100644 --- a/packages/pinball_components/test/src/components/spaceship_rail_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_rail_test.dart @@ -12,8 +12,8 @@ void main() { group('SpaceshipRail', () { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.spaceship.rail.main.keyName, - Assets.images.spaceship.rail.exit.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart index a65ba18b..1f5a231a 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart @@ -11,16 +11,16 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart deleted file mode 100644 index c9a90746..00000000 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ /dev/null @@ -1,56 +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:mocktail/mocktail.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('Spaceship', () { - late Filter filterData; - late Fixture fixture; - late Body body; - late Ball ball; - late Forge2DGame game; - - setUp(() { - filterData = MockFilter(); - - fixture = MockFixture(); - when(() => fixture.filterData).thenReturn(filterData); - - body = MockBody(); - when(() => body.fixtures).thenReturn([fixture]); - - game = MockGame(); - - ball = MockBall(); - when(() => ball.gameRef).thenReturn(game); - when(() => ball.body).thenReturn(body); - }); - - group('Spaceship', () { - final tester = FlameTester(TestGame.new); - - tester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - final position = Vector2(30, -30); - await game.addFromBlueprint(Spaceship(position: position)); - game.camera.followVector2(position); - await game.ready(); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/spaceship.png'), - ); - }, - ); - }); - }); -} diff --git a/packages/pinball_flame/lib/src/contact_behavior.dart b/packages/pinball_flame/lib/src/contact_behavior.dart index 79112398..ff715b12 100644 --- a/packages/pinball_flame/lib/src/contact_behavior.dart +++ b/packages/pinball_flame/lib/src/contact_behavior.dart @@ -1,95 +1,105 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Appends a new [ContactCallbacks] to the parent. /// /// This is a convenience class for adding a [ContactCallbacks] to the parent. -/// In constract with just adding a [ContactCallbacks] to the parent's body -/// userData, this class respects the previous [ContactCallbacks] in the -/// parent's body userData, if any. Hence, it avoids overriding any previous -/// [ContactCallbacks] in the parent. +/// In contrast with just assigning a [ContactCallbacks] to a userData, this +/// class respects the previous userData. /// -/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup], -/// and resetting the parent's userData accordingly. +/// It does so by grouping the userData in a [_UserData], and resetting the +/// parent's userData accordingly. // TODO(alestiago): Make use of generics to infer the type of the contact. // https://github.com/VGVentures/pinball/pull/234#discussion_r859182267 -// TODO(alestiago): Consider if there is a need to support adjusting a fixture's -// userData. class ContactBehavior extends Component with ContactCallbacks, ParentIsA { + final _fixturesUserData = {}; + + /// Specifies which fixtures should be considered for contact. + /// + /// Fixtures are identifiable by their userData. + /// + /// If no fixtures are specified, the [ContactCallbacks] is applied to the + /// entire body, hence all fixtures are considered. + void applyTo(Iterable userData) => _fixturesUserData.addAll(userData); + @override - @mustCallSuper Future onLoad() async { - final userData = parent.body.userData; - if (userData is _ContactCallbacksGroup) { - userData.addContactCallbacks(this); - } else if (userData is ContactCallbacks) { - final contactCallbacksGroup = _ContactCallbacksGroup() - ..addContactCallbacks(userData) - ..addContactCallbacks(this); - parent.body.userData = contactCallbacksGroup; + if (_fixturesUserData.isNotEmpty) { + for (final fixture in _targetedFixtures) { + fixture.userData = _UserData.fromFixture(fixture)..add(this); + } } else { - parent.body.userData = this; + parent.body.userData = _UserData.fromBody(parent.body)..add(this); } } + + Iterable get _targetedFixtures => + parent.body.fixtures.where((fixture) { + if (_fixturesUserData.contains(fixture.userData)) return true; + + final userData = fixture.userData; + if (userData is _UserData) { + return _fixturesUserData.contains(userData.value); + } + + return false; + }); } -class _ContactCallbacksGroup implements ContactCallbacks { - final List _contactCallbacks = []; +class _UserData with ContactCallbacks { + _UserData._(Object? userData) : _userData = [userData]; + + factory _UserData._fromUserData(Object? userData) { + if (userData is _UserData) return userData; + return _UserData._(userData); + } + + factory _UserData.fromFixture(Fixture fixture) => + _UserData._fromUserData(fixture.userData); + + factory _UserData.fromBody(Body body) => + _UserData._fromUserData(body.userData); + + final List _userData; + + Iterable get _contactCallbacks => + _userData.whereType(); + + Object? get value => _userData.first; + + void add(Object? userData) => _userData.add(userData); @override - @mustCallSuper void beginContact(Object other, Contact contact) { - onBeginContact?.call(other, contact); + super.beginContact(other, contact); for (final callback in _contactCallbacks) { callback.beginContact(other, contact); } } @override - @mustCallSuper void endContact(Object other, Contact contact) { - onEndContact?.call(other, contact); + super.endContact(other, contact); for (final callback in _contactCallbacks) { callback.endContact(other, contact); } } @override - @mustCallSuper void preSolve(Object other, Contact contact, Manifold oldManifold) { - onPreSolve?.call(other, contact, oldManifold); + super.preSolve(other, contact, oldManifold); for (final callback in _contactCallbacks) { callback.preSolve(other, contact, oldManifold); } } @override - @mustCallSuper void postSolve(Object other, Contact contact, ContactImpulse impulse) { - onPostSolve?.call(other, contact, impulse); + super.postSolve(other, contact, impulse); for (final callback in _contactCallbacks) { callback.postSolve(other, contact, impulse); } } - - void addContactCallbacks(ContactCallbacks callback) { - _contactCallbacks.add(callback); - } - - @override - void Function(Object other, Contact contact)? onBeginContact; - - @override - void Function(Object other, Contact contact)? onEndContact; - - @override - void Function(Object other, Contact contact, ContactImpulse impulse)? - onPostSolve; - - @override - void Function(Object other, Contact contact, Manifold oldManifold)? - onPreSolve; } diff --git a/packages/pinball_flame/test/src/contact_behavior_test.dart b/packages/pinball_flame/test/src/contact_behavior_test.dart index 630156ed..cf7fe35a 100644 --- a/packages/pinball_flame/test/src/contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/contact_behavior_test.dart @@ -58,28 +58,78 @@ void main() { late Contact contact; late Manifold manifold; late ContactImpulse contactImpulse; + late FixtureDef fixtureDef; setUp(() { other = Object(); contact = _MockContact(); manifold = _MockManifold(); contactImpulse = _MockContactImpulse(); + fixtureDef = FixtureDef(CircleShape()); }); flameTester.test( - 'should add a new ContactCallbacks to the parent', + "should add a new ContactCallbacks to the parent's body userData " + 'when not applied to fixtures', (game) async { final parent = _TestBodyComponent(); final contactBehavior = ContactBehavior(); await parent.add(contactBehavior); await game.ensureAdd(parent); - expect(parent.body.userData, contactBehavior); + expect(parent.body.userData, isA()); }, ); flameTester.test( - "should respect the previous ContactCallbacks in the parent's userData", + 'should add a new ContactCallbacks to the targeted fixture ', + (game) async { + final parent = _TestBodyComponent(); + + await game.ensureAdd(parent); + final fixture1 = + parent.body.createFixture(fixtureDef..userData = 'foo'); + final fixture2 = parent.body.createFixture(fixtureDef..userData = null); + final contactBehavior = ContactBehavior() + ..applyTo( + [fixture1.userData!], + ); + + await parent.ensureAdd(contactBehavior); + + expect(parent.body.userData, isNull); + expect(fixture1.userData, isA()); + expect(fixture2.userData, isNull); + }, + ); + + flameTester.test( + 'should add a new ContactCallbacks to the targeted fixtures ', + (game) async { + final parent = _TestBodyComponent(); + + await game.ensureAdd(parent); + final fixture1 = + parent.body.createFixture(fixtureDef..userData = 'foo'); + final fixture2 = + parent.body.createFixture(fixtureDef..userData = 'boo'); + final contactBehavior = ContactBehavior() + ..applyTo([ + fixture1.userData!, + fixture2.userData!, + ]); + + await parent.ensureAdd(contactBehavior); + + expect(parent.body.userData, isNull); + expect(fixture1.userData, isA()); + expect(fixture2.userData, isA()); + }, + ); + + flameTester.test( + "should respect the previous ContactCallbacks in the parent's userData " + 'when not applied to fixtures', (game) async { final parent = _TestBodyComponent(); await game.ensureAdd(parent); @@ -113,41 +163,94 @@ void main() { }, ); - flameTester.test('can group multiple ContactBehaviors and keep listening', - (game) async { - final parent = _TestBodyComponent(); - await game.ensureAdd(parent); - - final contactBehavior1 = _TestContactBehavior(); - final contactBehavior2 = _TestContactBehavior(); - final contactBehavior3 = _TestContactBehavior(); - await parent.ensureAddAll([ - contactBehavior1, - contactBehavior2, - contactBehavior3, - ]); - - final contactCallbacks = parent.body.userData! as ContactCallbacks; - - contactCallbacks.beginContact(other, contact); - expect(contactBehavior1.beginContactCallsCount, equals(1)); - expect(contactBehavior2.beginContactCallsCount, equals(1)); - expect(contactBehavior3.beginContactCallsCount, equals(1)); - - contactCallbacks.endContact(other, contact); - expect(contactBehavior1.endContactCallsCount, equals(1)); - expect(contactBehavior2.endContactCallsCount, equals(1)); - expect(contactBehavior3.endContactCallsCount, equals(1)); - - contactCallbacks.preSolve(other, contact, manifold); - expect(contactBehavior1.preSolveContactCallsCount, equals(1)); - expect(contactBehavior2.preSolveContactCallsCount, equals(1)); - expect(contactBehavior3.preSolveContactCallsCount, equals(1)); - - contactCallbacks.postSolve(other, contact, contactImpulse); - expect(contactBehavior1.postSolveContactCallsCount, equals(1)); - expect(contactBehavior2.postSolveContactCallsCount, equals(1)); - expect(contactBehavior3.postSolveContactCallsCount, equals(1)); - }); + flameTester.test( + 'can group multiple ContactBehaviors and keep listening', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final contactBehavior1 = _TestContactBehavior(); + final contactBehavior2 = _TestContactBehavior(); + final contactBehavior3 = _TestContactBehavior(); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = parent.body.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }, + ); + + flameTester.test( + 'can group multiple ContactBehaviors and keep listening ' + 'when applied to a fixture', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final fixture = parent.body.createFixture(fixtureDef..userData = 'foo'); + + final contactBehavior1 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + final contactBehavior2 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + final contactBehavior3 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = fixture.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }, + ); }); } diff --git a/packages/pinball_ui/.gitignore b/packages/pinball_ui/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_ui/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/pinball_ui/README.md b/packages/pinball_ui/README.md new file mode 100644 index 00000000..cabc194a --- /dev/null +++ b/packages/pinball_ui/README.md @@ -0,0 +1,11 @@ +# pinball_ui + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +UI Toolkit for the Pinball Flutter Application + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/pinball_ui/analysis_options.yaml b/packages/pinball_ui/analysis_options.yaml new file mode 100644 index 00000000..f8155aa6 --- /dev/null +++ b/packages/pinball_ui/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart diff --git a/packages/pinball_ui/assets/images/dialog/background.png b/packages/pinball_ui/assets/images/dialog/background.png new file mode 100644 index 00000000..0aad300f Binary files /dev/null and b/packages/pinball_ui/assets/images/dialog/background.png differ diff --git a/packages/pinball_ui/lib/gen/assets.gen.dart b/packages/pinball_ui/lib/gen/assets.gen.dart new file mode 100644 index 00000000..41c45ece --- /dev/null +++ b/packages/pinball_ui/lib/gen/assets.gen.dart @@ -0,0 +1,78 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// ignore_for_file: directives_ordering,unnecessary_import + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + $AssetsImagesDialogGen get dialog => const $AssetsImagesDialogGen(); +} + +class $AssetsImagesDialogGen { + const $AssetsImagesDialogGen(); + + /// File path: assets/images/dialog/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/dialog/background.png'); +} + +class Assets { + Assets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) + : super(assetName, package: 'pinball_ui'); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/packages/pinball_ui/lib/gen/gen.dart b/packages/pinball_ui/lib/gen/gen.dart new file mode 100644 index 00000000..e7ad4c54 --- /dev/null +++ b/packages/pinball_ui/lib/gen/gen.dart @@ -0,0 +1 @@ +export 'assets.gen.dart'; diff --git a/packages/pinball_ui/lib/pinball_ui.dart b/packages/pinball_ui/lib/pinball_ui.dart new file mode 100644 index 00000000..b46adf95 --- /dev/null +++ b/packages/pinball_ui/lib/pinball_ui.dart @@ -0,0 +1,3 @@ +library pinball_ui; + +export 'src/dialog/dialog.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/dialog.dart b/packages/pinball_ui/lib/src/dialog/dialog.dart new file mode 100644 index 00000000..7a224272 --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/dialog.dart @@ -0,0 +1 @@ +export 'pixelated_decoration.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart b/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart new file mode 100644 index 00000000..542a00db --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; + +/// {@template pixelated_decoration} +/// Widget with pixelated background and layout defined for dialog displays. +/// {@endtemplate} +class PixelatedDecoration extends StatelessWidget { + /// {@macro pixelated_decoration} + const PixelatedDecoration({ + Key? key, + required Widget header, + required Widget body, + }) : _header = header, + _body = body, + super(key: key); + + final Widget _header; + final Widget _body; + + @override + Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(12)); + + return Material( + borderRadius: radius, + child: Padding( + padding: const EdgeInsets.all(5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage(Assets.images.dialog.background.keyName), + ), + ), + child: ClipRRect( + borderRadius: radius, + child: Column( + children: [ + Expanded( + child: Center( + child: _header, + ), + ), + Expanded( + flex: 4, + child: _body, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/pinball_ui/pubspec.yaml b/packages/pinball_ui/pubspec.yaml new file mode 100644 index 00000000..79c65338 --- /dev/null +++ b/packages/pinball_ui/pubspec.yaml @@ -0,0 +1,29 @@ +name: pinball_ui +description: UI Toolkit for the Pinball Flutter Application +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + test: ^1.19.2 + very_good_analysis: ^2.4.0 + +flutter: + uses-material-design: true + generate: true + + assets: + - assets/images/dialog/ + +flutter_gen: + line_length: 80 + assets: + package_parameter_enabled: true diff --git a/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart b/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart new file mode 100644 index 00000000..772f2570 --- /dev/null +++ b/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart @@ -0,0 +1,26 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PixelatedDecoration', () { + testWidgets('renders header and body', (tester) async { + const headerText = 'header'; + const bodyText = 'body'; + + await tester.pumpWidget( + MaterialApp( + home: PixelatedDecoration( + header: Text(headerText), + body: Text(bodyText), + ), + ), + ); + + expect(find.text(headerText), findsOneWidget); + expect(find.text(bodyText), findsOneWidget); + }); + }); +} diff --git a/packages/platform_helper/.gitignore b/packages/platform_helper/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/platform_helper/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/platform_helper/README.md b/packages/platform_helper/README.md new file mode 100644 index 00000000..7a96e658 --- /dev/null +++ b/packages/platform_helper/README.md @@ -0,0 +1,11 @@ +# platform_helper + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Platform helper for Pinball application. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/platform_helper/analysis_options.yaml b/packages/platform_helper/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/platform_helper/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/platform_helper/lib/platform_helper.dart b/packages/platform_helper/lib/platform_helper.dart new file mode 100644 index 00000000..4b6d7f48 --- /dev/null +++ b/packages/platform_helper/lib/platform_helper.dart @@ -0,0 +1,3 @@ +library platform_helper; + +export 'src/platform_helper.dart'; diff --git a/packages/platform_helper/lib/src/platform_helper.dart b/packages/platform_helper/lib/src/platform_helper.dart new file mode 100644 index 00000000..638d1ab6 --- /dev/null +++ b/packages/platform_helper/lib/src/platform_helper.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; + +/// {@template platform_helper} +/// Returns whether the current platform is running on a mobile device. +/// {@endtemplate} +class PlatformHelper { + /// {@macro platform_helper} + bool get isMobile { + return defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android; + } +} diff --git a/packages/platform_helper/pubspec.yaml b/packages/platform_helper/pubspec.yaml new file mode 100644 index 00000000..edff346a --- /dev/null +++ b/packages/platform_helper/pubspec.yaml @@ -0,0 +1,16 @@ +name: platform_helper +description: Platform helper for Pinball application. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/packages/platform_helper/test/src/platform_helper_test.dart b/packages/platform_helper/test/src/platform_helper_test.dart new file mode 100644 index 00000000..69bec3a8 --- /dev/null +++ b/packages/platform_helper/test/src/platform_helper_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_helper/platform_helper.dart'; + +void main() { + group('PlatformHelper', () { + test('can be instantiated', () { + expect(PlatformHelper(), isNotNull); + }); + + group('isMobile', () { + tearDown(() async { + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is iOS', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is android', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test( + 'returns false when defaultTargetPlatform is niether iOS nor android', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + expect(PlatformHelper().isMobile, isFalse); + debugDefaultTargetPlatformOverride = null; + }, + ); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 9ee8ae6c..ab39378a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -499,6 +499,13 @@ packages: relative: true source: path version: "1.0.0+1" + pinball_ui: + dependency: "direct main" + description: + path: "packages/pinball_ui" + relative: true + source: path + version: "1.0.0+1" platform: dependency: transitive description: @@ -506,6 +513,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_helper: + dependency: "direct main" + description: + path: "packages/platform_helper" + relative: true + source: path + version: "1.0.0+1" plugin_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 48c570c3..51c85cd5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,10 @@ dependencies: path: packages/pinball_flame pinball_theme: path: packages/pinball_theme + pinball_ui: + path: packages/pinball_ui + platform_helper: + path: packages/platform_helper dev_dependencies: bloc_test: ^9.0.2 diff --git a/test/game/components/android_acres_test.dart b/test/game/components/android_acres_test.dart index 419524c6..aef6a812 100644 --- a/test/game/components/android_acres_test.dart +++ b/test/game/components/android_acres_test.dart @@ -11,22 +11,27 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, - Assets.images.spaceship.rail.main.keyName, - Assets.images.spaceship.rail.exit.keyName, - Assets.images.androidBumper.a.lit.keyName, - Assets.images.androidBumper.a.dimmed.keyName, - Assets.images.androidBumper.b.lit.keyName, - Assets.images.androidBumper.b.dimmed.keyName, + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, ]; final flameTester = FlameTester( () => EmptyPinballTestGame(assets: assets), @@ -46,7 +51,7 @@ void main() { 'a Spaceship', (game) async { expect( - AndroidAcres().blueprints.whereType().single, + AndroidAcres().blueprints.whereType().single, isNotNull, ); }, @@ -73,7 +78,7 @@ void main() { ); flameTester.test( - 'two AndroidBumper', + 'three AndroidBumper', (game) async { final androidZone = AndroidAcres(); await game.addFromBlueprint(androidZone); @@ -81,7 +86,7 @@ void main() { expect( game.descendants().whereType().length, - equals(2), + equals(3), ); }, ); diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart deleted file mode 100644 index 63b7251b..00000000 --- a/test/game/components/board_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -// 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.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); - - group('Board', () { - flameTester.test( - 'loads correctly', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - expect(game.contains(board), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'one left flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final leftFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isLeft, - ); - expect(leftFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'one right flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - final rightFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isRight, - ); - expect(rightFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'two Baseboards', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final baseboards = board.descendants().whereType(); - expect(baseboards.length, equals(2)); - }, - ); - - flameTester.test( - 'two Kickers', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final kickers = board.descendants().whereType(); - expect(kickers.length, equals(2)); - }, - ); - - flameTester.test( - 'one FlutterForest', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final flutterForest = board.descendants().whereType(); - expect(flutterForest.length, equals(1)); - }, - ); - }); - }); -} diff --git a/test/game/components/bottom_group_test.dart b/test/game/components/bottom_group_test.dart new file mode 100644 index 00000000..3254f155 --- /dev/null +++ b/test/game/components/bottom_group_test.dart @@ -0,0 +1,86 @@ +// 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.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('BottomGroup', () { + flameTester.test( + 'loads correctly', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + expect(game.contains(bottomGroup), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'one left flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isLeft, + ); + expect(leftFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'one right flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final rightFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isRight, + ); + expect(rightFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'two Baseboards', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final basebottomGroups = + bottomGroup.descendants().whereType(); + expect(basebottomGroups.length, equals(2)); + }, + ); + + flameTester.test( + 'two Kickers', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final kickers = bottomGroup.descendants().whereType(); + expect(kickers.length, equals(2)); + }, + ); + }); + }); +} diff --git a/test/game/components/drain_test.dart b/test/game/components/drain_test.dart new file mode 100644 index 00000000..f1875a56 --- /dev/null +++ b/test/game/components/drain_test.dart @@ -0,0 +1,60 @@ +// 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:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Drain', () { + flameTester.test( + 'loads correctly', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(game.contains(drain), isTrue); + }, + ); + + flameTester.test( + 'body is static', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.bodyType, equals(BodyType.static)); + }, + ); + + flameTester.test( + 'is sensor', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.fixtures.first.isSensor, isTrue); + }, + ); + + test( + 'calls lost on contact with ball', + () async { + final drain = Drain(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + + drain.beginContact(ball, MockContact()); + + verify(controller.lost).called(1); + }, + ); + }); +} diff --git a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart new file mode 100644 index 00000000..a4f3502c --- /dev/null +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -0,0 +1,133 @@ +// 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:mockingjay/mockingjay.dart'; +import 'package:pinball/game/components/multipliers/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.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]; + + group('MultipliersBehavior', () { + late GameBloc gameBloc; + + setUp(() { + registerFallbackValue(MockComponent()); + 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 multiplier has changed', () { + final state = GameState( + score: 10, + multiplier: 2, + rounds: 0, + bonusHistory: const [], + ); + + final previous = GameState.initial(); + expect( + MultipliersBehavior().listenWhen(previous, state), + isTrue, + ); + }); + + test('is false when the multiplier state is the same', () { + final state = GameState( + score: 10, + multiplier: 1, + rounds: 0, + bonusHistory: const [], + ); + + final previous = GameState.initial(); + expect( + MultipliersBehavior().listenWhen(previous, state), + isFalse, + ); + }); + }); + + group('onNewState', () { + flameBlocTester.testGameWidget( + "calls 'next' once per each multiplier when GameBloc emit state", + setUp: (game, tester) async { + final behavior = MultipliersBehavior(); + final parent = Multipliers.test(); + final multiplierX2Cubit = MockMultiplierCubit(); + final multiplierX3Cubit = MockMultiplierCubit(); + final multipliers = [ + Multiplier.test( + value: MultiplierValue.x2, + bloc: multiplierX2Cubit, + ), + Multiplier.test( + value: MultiplierValue.x3, + bloc: multiplierX3Cubit, + ), + ]; + + whenListen( + multiplierX2Cubit, + const Stream.empty(), + initialState: MultiplierState.initial(MultiplierValue.x2), + ); + when(() => multiplierX2Cubit.next(any())).thenAnswer((_) async {}); + + whenListen( + multiplierX3Cubit, + const Stream.empty(), + initialState: MultiplierState.initial(MultiplierValue.x2), + ); + when(() => multiplierX3Cubit.next(any())).thenAnswer((_) async {}); + + await parent.addAll(multipliers); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + await tester.pump(); + + behavior.onNewState( + GameState.initial().copyWith(multiplier: 2), + ); + + for (final multiplier in multipliers) { + verify( + () => multiplier.bloc.next(any()), + ).called(1); + } + }, + ); + }); + }); +} diff --git a/test/game/components/multipliers/multipliers_test.dart b/test/game/components/multipliers/multipliers_test.dart new file mode 100644 index 00000000..6b2d95a6 --- /dev/null +++ b/test/game/components/multipliers/multipliers_test.dart @@ -0,0 +1,63 @@ +// 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.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + group('Multipliers', () { + flameBlocTester.testGameWidget( + 'loads correctly', + setUp: (game, tester) async { + final multipliersGroup = Multipliers(); + await game.ensureAdd(multipliersGroup); + + expect(game.contains(multipliersGroup), isTrue); + }, + ); + + group('loads', () { + flameBlocTester.testGameWidget( + 'five Multiplier', + setUp: (game, tester) async { + final multipliersGroup = Multipliers(); + await game.ensureAdd(multipliersGroup); + + expect( + multipliersGroup.descendants().whereType().length, + equals(5), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart deleted file mode 100644 index 16f7ce34..00000000 --- a/test/game/components/wall_test.dart +++ /dev/null @@ -1,165 +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/game/game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); - - group('Wall', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(game.contains(wall), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - game.contains(wall); - - expect(wall.body.position, Vector2.zero()); - }, - ); - - flameTester.test( - 'is static', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.restitution, greaterThan(0)); - }, - ); - - flameTester.test( - 'has no friction', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.friction, equals(0)); - }, - ); - }); - }); - - group( - 'BottomWall', - () { - group('removes ball on contact', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); - - flameBlocTester.testGameWidget( - 'when ball is launch', - setUp: (game, tester) async { - final ball = ControlledBall.launch( - characterTheme: game.characterTheme, - ); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'when ball is bonus', - setUp: (game, tester) async { - final ball = ControlledBall.bonus( - characterTheme: game.characterTheme, - ); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'when ball is debug', - setUp: (game, tester) async { - final ball = ControlledBall.debug(); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - }); - }, - ); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 2fdbe6c4..3a531cac 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,8 +1,10 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -13,10 +15,12 @@ import '../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.androidBumper.a.lit.keyName, - Assets.images.androidBumper.a.dimmed.keyName, - Assets.images.androidBumper.b.lit.keyName, - Assets.images.androidBumper.b.dimmed.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, Assets.images.backboard.backboardScores.keyName, Assets.images.backboard.backboardGameOver.keyName, Assets.images.backboard.display.keyName, @@ -51,6 +55,16 @@ void main() { Assets.images.launchRamp.ramp.keyName, Assets.images.launchRamp.foregroundRailing.keyName, Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, Assets.images.plunger.plunger.keyName, Assets.images.plunger.rocket.keyName, Assets.images.signpost.inactive.keyName, @@ -59,20 +73,21 @@ void main() { Assets.images.signpost.active3.keyName, Assets.images.slingshot.upper.keyName, Assets.images.slingshot.lower.keyName, - Assets.images.spaceship.saucer.keyName, - Assets.images.spaceship.bridge.keyName, - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, - Assets.images.spaceship.rail.main.keyName, - Assets.images.spaceship.rail.exit.keyName, + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, Assets.images.sparky.bumper.a.active.keyName, Assets.images.sparky.bumper.a.inactive.keyName, Assets.images.sparky.bumper.b.active.keyName, @@ -91,6 +106,17 @@ void main() { Assets.images.sparky.bumper.c.inactive.keyName, ]; + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + final flameTester = FlameTester( () => PinballTestGame(assets: assets), ); @@ -98,44 +124,57 @@ void main() { () => DebugPinballTestGame(assets: assets), ); + final flameBlocTester = FlameBlocTester( + gameBuilder: () => PinballTestGame(assets: assets), + blocBuilder: () => gameBloc, + ); + group('PinballGame', () { group('components', () { // TODO(alestiago): tests that Blueprints get added once the Blueprint // class is removed. - flameTester.test( - 'has only one BottomWall', + flameBlocTester.test( + 'has only one Drain', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, ); - flameTester.test( - 'has only one Plunger', + flameBlocTester.test( + 'has only one BottomGroup', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, ); - flameTester.test( - 'has one Board', + flameBlocTester.test( + 'has only one Plunger', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, ); - flameTester.test( + flameBlocTester.test('has one FlutterForest', (game) async { + await game.ready(); + expect( + game.children.whereType().length, + equals(1), + ); + }); + + flameBlocTester.test( 'one GoogleWord', (game) async { await game.ready(); @@ -229,6 +268,181 @@ void main() { ); }); }); + + group('flipper control', () { + flameTester.test('tap down moves left flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap down moves right flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(game.canvasSize); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.right, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap up moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + + flameTester.test('tap cancel moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + game.onTapCancel(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + }); + + group('plunger control', () { + flameTester.test('tap down moves plunger down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + }); + + flameTester.test('tap up releases plunger', (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + + flameTester.test('tap cancel releases plunger', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + game.onTapCancel(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + }); }); group('DebugPinballGame', () { @@ -238,8 +452,12 @@ void main() { final eventPosition = MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final raw = MockTapUpDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); + final tapUpEvent = MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + when(() => tapUpEvent.raw).thenReturn(raw); final previousBalls = game.descendants().whereType().toList(); diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index dfa28869..8f5f7f13 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -108,7 +108,7 @@ void main() { expect( find.byWidgetPredicate( - (widget) => widget is Container && widget.color == AppColors.orange, + (widget) => widget is Container && widget.color == AppColors.yellow, ), findsOneWidget, ); @@ -125,7 +125,7 @@ void main() { find.byWidgetPredicate( (widget) => widget is Container && - widget.color == AppColors.orange.withAlpha(128), + widget.color == AppColors.yellow.withAlpha(128), ), findsOneWidget, ); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index b58dc619..1578c3e0 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,12 +2,11 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -15,9 +14,7 @@ import 'package:pinball_components/pinball_components.dart'; class MockPinballGame extends Mock implements PinballGame {} -class MockWall extends Mock implements Wall {} - -class MockBottomWall extends Mock implements BottomWall {} +class MockDrain extends Mock implements Drain {} class MockBody extends Mock implements Body {} @@ -37,8 +34,6 @@ class MockGameState extends Mock implements GameState {} class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} -class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} - class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @@ -55,14 +50,22 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class MockTapDownInfo extends Mock implements TapDownInfo {} + +class MockTapDownDetails extends Mock implements TapDownDetails {} + class MockTapUpInfo extends Mock implements TapUpInfo {} +class MockTapUpDetails extends Mock implements TapUpDetails {} + class MockEventPosition extends Mock implements EventPosition {} class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} +class MockComponent extends Mock implements Component {} + class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} @@ -85,3 +88,9 @@ class MockGameFlowController extends Mock implements GameFlowController {} 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 MockMultiplierCubit extends Mock implements MultiplierCubit {} diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart deleted file mode 100644 index 2b217704..00000000 --- a/test/leaderboard/bloc/leaderboard_bloc_test.dart +++ /dev/null @@ -1,203 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LeaderboardBloc', () { - late LeaderboardRepository leaderboardRepository; - - setUp(() { - leaderboardRepository = MockLeaderboardRepository(); - }); - - test('initial state has state loading no ranking and empty leaderboard', - () { - final bloc = LeaderboardBloc(leaderboardRepository); - expect(bloc.state.status, equals(LeaderboardStatus.loading)); - expect(bloc.state.ranking.ranking, equals(0)); - expect(bloc.state.ranking.outOf, equals(0)); - expect(bloc.state.leaderboard.isEmpty, isTrue); - }); - - group('Top10Fetched', () { - const top10Scores = [ - 2500, - 2200, - 2200, - 2000, - 1800, - 1400, - 1300, - 1000, - 600, - 300, - 100, - ]; - - final top10Leaderboard = top10Scores - .map( - (score) => LeaderboardEntryData( - playerInitials: 'user$score', - score: score, - character: CharacterType.dash, - ), - ) - .toList(); - - blocTest( - 'emits [loading, success] statuses ' - 'when fetchTop10Leaderboard succeeds', - setUp: () { - when(() => leaderboardRepository.fetchTop10Leaderboard()).thenAnswer( - (_) async => top10Leaderboard, - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(Top10Fetched()), - expect: () => [ - LeaderboardState.initial(), - isA() - ..having( - (element) => element.status, - 'status', - equals(LeaderboardStatus.success), - ) - ..having( - (element) => element.leaderboard.length, - 'leaderboard', - equals(top10Leaderboard.length), - ) - ], - verify: (_) => - verify(() => leaderboardRepository.fetchTop10Leaderboard()) - .called(1), - ); - - blocTest( - 'emits [loading, error] statuses ' - 'when fetchTop10Leaderboard fails', - setUp: () { - when(() => leaderboardRepository.fetchTop10Leaderboard()).thenThrow( - Exception(), - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(Top10Fetched()), - expect: () => [ - LeaderboardState.initial(), - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), - ], - verify: (_) => - verify(() => leaderboardRepository.fetchTop10Leaderboard()) - .called(1), - errors: () => [isA()], - ); - }); - - group('LeaderboardEntryAdded', () { - final leaderboardEntry = LeaderboardEntryData( - playerInitials: 'ABC', - score: 1500, - character: CharacterType.dash, - ); - - final ranking = LeaderboardRanking(ranking: 3, outOf: 4); - - blocTest( - 'emits [loading, success] statuses ' - 'when addLeaderboardEntry succeeds', - setUp: () { - when( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).thenAnswer( - (_) async => ranking, - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), - expect: () => [ - LeaderboardState.initial(), - isA() - ..having( - (element) => element.status, - 'status', - equals(LeaderboardStatus.success), - ) - ..having( - (element) => element.ranking, - 'ranking', - equals(ranking), - ) - ], - verify: (_) => verify( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).called(1), - ); - - blocTest( - 'emits [loading, error] statuses ' - 'when addLeaderboardEntry fails', - setUp: () { - when( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).thenThrow( - Exception(), - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), - expect: () => [ - LeaderboardState.initial(), - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), - ], - verify: (_) => verify( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).called(1), - errors: () => [isA()], - ); - }); - }); - - group('CharacterTypeX', () { - test('converts CharacterType.android to AndroidTheme', () { - expect(CharacterType.android.toTheme, equals(AndroidTheme())); - }); - - test('converts CharacterType.dash to DashTheme', () { - expect(CharacterType.dash.toTheme, equals(DashTheme())); - }); - - test('converts CharacterType.dino to DinoTheme', () { - expect(CharacterType.dino.toTheme, equals(DinoTheme())); - }); - - test('converts CharacterType.sparky to SparkyTheme', () { - expect(CharacterType.sparky.toTheme, equals(SparkyTheme())); - }); - }); - - group('CharacterThemeX', () { - test('converts AndroidTheme to CharacterType.android', () { - expect(AndroidTheme().toType, equals(CharacterType.android)); - }); - - test('converts DashTheme to CharacterType.dash', () { - expect(DashTheme().toType, equals(CharacterType.dash)); - }); - - test('converts DinoTheme to CharacterType.dino', () { - expect(DinoTheme().toType, equals(CharacterType.dino)); - }); - - test('converts SparkyTheme to CharacterType.sparky', () { - expect(SparkyTheme().toType, equals(CharacterType.sparky)); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_event_test.dart b/test/leaderboard/bloc/leaderboard_event_test.dart deleted file mode 100644 index 33199ca1..00000000 --- a/test/leaderboard/bloc/leaderboard_event_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; - -void main() { - group('GameEvent', () { - group('Top10Fetched', () { - test('can be instantiated', () { - expect(const Top10Fetched(), isNotNull); - }); - - test('supports value equality', () { - expect( - Top10Fetched(), - equals(const Top10Fetched()), - ); - }); - }); - - group('LeaderboardEntryAdded', () { - const leaderboardEntry = LeaderboardEntryData( - playerInitials: 'ABC', - score: 1500, - character: CharacterType.dash, - ); - - test('can be instantiated', () { - expect(const LeaderboardEntryAdded(entry: leaderboardEntry), isNotNull); - }); - - test('supports value equality', () { - expect( - LeaderboardEntryAdded(entry: leaderboardEntry), - equals(const LeaderboardEntryAdded(entry: leaderboardEntry)), - ); - }); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart deleted file mode 100644 index 1b5d41d9..00000000 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('LeaderboardState', () { - test('supports value equality', () { - expect( - LeaderboardState.initial(), - equals( - LeaderboardState.initial(), - ), - ); - }); - - group('constructor', () { - test('can be instantiated', () { - expect( - LeaderboardState.initial(), - isNotNull, - ); - }); - }); - - group('copyWith', () { - final leaderboardEntry = LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 1500, - character: DashTheme().leaderboardIcon, - ); - - test( - 'copies correctly ' - 'when no argument specified', - () { - const leaderboardState = LeaderboardState.initial(); - expect( - leaderboardState.copyWith(), - equals(leaderboardState), - ); - }, - ); - - test( - 'copies correctly ' - 'when all arguments specified', - () { - const leaderboardState = LeaderboardState.initial(); - final otherLeaderboardState = LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: [leaderboardEntry], - ); - expect(leaderboardState, isNot(equals(otherLeaderboardState))); - - expect( - leaderboardState.copyWith( - status: otherLeaderboardState.status, - ranking: otherLeaderboardState.ranking, - leaderboard: otherLeaderboardState.leaderboard, - ), - equals(otherLeaderboardState), - ); - }, - ); - }); - }); -} diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart deleted file mode 100644 index daacb4a7..00000000 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LeaderboardPage', () { - testWidgets('renders LeaderboardView', (tester) async { - await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), - ), - ); - - expect(find.byType(LeaderboardView), findsOneWidget); - }); - - testWidgets('route returns a valid navigation route', (tester) async { - await expectNavigatesToRoute( - tester, - LeaderboardPage.route( - theme: DashTheme(), - ), - ); - }); - }); - - group('LeaderboardView', () { - late LeaderboardBloc leaderboardBloc; - - setUp(() { - leaderboardBloc = MockLeaderboardBloc(); - }); - - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.text(l10n.leaderboard), findsOneWidget); - expect(find.text(l10n.retry), findsOneWidget); - }); - - testWidgets('renders loading view when bloc emits [loading]', - (tester) async { - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - expect(find.text('There was en error loading data!'), findsNothing); - expect(find.byType(ListView), findsNothing); - }); - - testWidgets('renders error view when bloc emits [error]', (tester) async { - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial().copyWith( - status: LeaderboardStatus.error, - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('There was en error loading data!'), findsOneWidget); - expect(find.byType(ListView), findsNothing); - }); - - testWidgets('renders success view when bloc emits [success]', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: [ - LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 10000, - character: DashTheme().leaderboardIcon, - ), - ], - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('There was en error loading data!'), findsNothing); - expect(find.text(l10n.rank), findsOneWidget); - expect(find.text(l10n.character), findsOneWidget); - expect(find.text(l10n.username), findsOneWidget); - expect(find.text(l10n.score), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); - }); - - testWidgets('navigates to CharacterSelectionPage when retry is tapped', - (tester) async { - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - - await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), - ), - navigator: navigator, - ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - - verify(() => navigator.push(any())).called(1); - }); - }); -} diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 0dda92d7..dc5d70ea 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: prefer_const_constructors +import 'dart:async'; + import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -84,17 +86,68 @@ void main() { .called(1); }); - testWidgets('displays how to play dialog when start is tapped', + group('HowToPlayDialog', () { + testWidgets( + 'is displayed for 3 seconds when start is tapped', (tester) async { - await tester.pumpApp( - CharacterSelectionView(), - characterThemeCubit: characterThemeCubit, + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionDialog.route()); + }, + child: Text('Tap me'), + ); + }, + ), + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.pump(Duration(seconds: 3)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }, ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); + testWidgets( + 'can be dismissed manually before 3 seconds have passed', + (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionDialog.route()); + }, + child: Text('Tap me'), + ); + }, + ), + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.tapAt(Offset(1, 1)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }, + ); }); }); diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart index 082f102e..1de4c2ad 100644 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -2,38 +2,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/start_game/start_game.dart'; +import 'package:platform_helper/platform_helper.dart'; import '../../helpers/helpers.dart'; +class MockPlatformHelper extends Mock implements PlatformHelper {} + void main() { group('HowToPlayDialog', () { - testWidgets('displays dialog', (tester) async { - await tester.pumpApp(HowToPlayDialog()); + late AppLocalizations l10n; + late PlatformHelper platformHelper; - expect(find.byType(Dialog), findsOneWidget); + setUp(() async { + l10n = await AppLocalizations.delegate.load(Locale('en')); + platformHelper = MockPlatformHelper(); }); - }); - group('KeyIndicator', () { - testWidgets('fromKeyName renders correctly', (tester) async { - const keyName = 'A'; + testWidgets( + 'can be instantiated without passing in a platform helper', + (tester) async { + await tester.pumpApp(HowToPlayDialog()); + expect(find.byType(HowToPlayDialog), findsOneWidget); + }, + ); + testWidgets('displays content for desktop', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => false); await tester.pumpApp( - KeyIndicator.fromKeyName(keyName: keyName), + HowToPlayDialog( + platformHelper: platformHelper, + ), ); - - expect(find.text(keyName), findsOneWidget); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.launchControls), findsOneWidget); + expect(find.text(l10n.flipperControls), findsOneWidget); + expect(find.byType(KeyButton), findsNWidgets(7)); }); - testWidgets('fromIcon renders correctly', (tester) async { - const keyIcon = Icons.keyboard_arrow_down; + testWidgets('displays content for mobile', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => true); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); + expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); + }); + }); + group('KeyButton', () { + testWidgets('renders correctly', (tester) async { await tester.pumpApp( - KeyIndicator.fromIcon(keyIcon: keyIcon), + KeyButton(control: Control.a), ); - expect(find.byIcon(keyIcon), findsOneWidget); + expect(find.text('A'), findsOneWidget); }); }); } diff --git a/web/index.html b/web/index.html index 37e17170..e1544bf2 100644 --- a/web/index.html +++ b/web/index.html @@ -56,6 +56,13 @@ I/O Pinball Machine - Flutter + +